Skip to content

Commit

Permalink
feat(account): add sync versions of AccountMnemonicFactory methods
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Jan 16, 2025
1 parent a6808c6 commit 5abddc7
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 26 deletions.
66 changes: 42 additions & 24 deletions src/account/MnemonicFactory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { mnemonicToSeed } from '@scure/bip39';
import { mnemonicToSeed, mnemonicToSeedSync } from '@scure/bip39';
import tweetnaclAuth from 'tweetnacl-auth';
import AccountBaseFactory from './BaseFactory.js';
import AccountMemory from './Memory.js';
import { encode, Encoding, Encoded, decode } from '../utils/encoder.js';
import { concatBuffers } from '../utils/other.js';
import { UnexpectedTsError } from '../utils/errors.js';

export const ED25519_CURVE = Buffer.from('ed25519 seed');
const HARDENED_OFFSET = 0x80000000;
Expand Down Expand Up @@ -42,54 +41,73 @@ interface Wallet {
* A factory class that generates instances of AccountMemory based on provided mnemonic phrase.
*/
export default class AccountMnemonicFactory extends AccountBaseFactory {
readonly #mnemonic: string | undefined;

#wallet: Wallet | undefined;
#mnemonicOrWallet: string | Wallet;

/**
* @param mnemonicOrWallet - BIP39-compatible mnemonic phrase or a wallet derived from mnemonic
*/
constructor(mnemonicOrWallet: string | Wallet) {
super();
if (typeof mnemonicOrWallet === 'string') this.#mnemonic = mnemonicOrWallet;
else this.#wallet = mnemonicOrWallet;
this.#mnemonicOrWallet = mnemonicOrWallet;
}

#getWallet(sync: true): Wallet;
#getWallet(sync: false): Wallet | Promise<Wallet>;
#getWallet(sync: boolean): Wallet | Promise<Wallet> {
const setWalletBySeed = (seed: Uint8Array): Wallet => {
const masterKey = deriveKey(seed, ED25519_CURVE);
const walletKey = derivePathFromKey(masterKey, [44, 457]);
this.#mnemonicOrWallet = {
secretKey: encode(walletKey.secretKey, Encoding.Bytearray),
chainCode: encode(walletKey.chainCode, Encoding.Bytearray),
};
return this.#mnemonicOrWallet;
};

if (typeof this.#mnemonicOrWallet === 'object') return this.#mnemonicOrWallet;
return sync
? setWalletBySeed(mnemonicToSeedSync(this.#mnemonicOrWallet))
: mnemonicToSeed(this.#mnemonicOrWallet).then(setWalletBySeed);
}

/**
* Get a wallet to initialize AccountMnemonicFactory instead mnemonic phrase.
* In comparison with mnemonic, the wallet can be used to derive aeternity accounts only.
*/
async getWallet(): Promise<Wallet> {
if (this.#wallet != null) return this.#wallet;
if (this.#mnemonic == null)
throw new UnexpectedTsError(
'AccountMnemonicFactory should be initialized with mnemonic or wallet',
);
const seed = await mnemonicToSeed(this.#mnemonic);
const masterKey = deriveKey(seed, ED25519_CURVE);
const walletKey = derivePathFromKey(masterKey, [44, 457]);
this.#wallet = {
secretKey: encode(walletKey.secretKey, Encoding.Bytearray),
chainCode: encode(walletKey.chainCode, Encoding.Bytearray),
};
return this.#wallet;
return this.#getWallet(false);
}

async #getAccountSecretKey(accountIndex: number): Promise<Encoded.AccountSecretKey> {
const wallet = await this.getWallet();
/**
* The same as `getWallet` but synchronous.
*/
getWalletSync(): Wallet {
return this.#getWallet(true);
}

#getAccountByWallet(accountIndex: number, wallet: Wallet): AccountMemory {
const walletKey = {
secretKey: decode(wallet.secretKey),
chainCode: decode(wallet.chainCode),
};
const raw = derivePathFromKey(walletKey, [accountIndex, 0, 0]).secretKey;
return encode(raw, Encoding.AccountSecretKey);
return new AccountMemory(encode(raw, Encoding.AccountSecretKey));
}

/**
* Get an instance of AccountMemory for a given account index.
* @param accountIndex - Index of account
*/
async initialize(accountIndex: number): Promise<AccountMemory> {
return new AccountMemory(await this.#getAccountSecretKey(accountIndex));
const wallet = await this.getWallet();
return this.#getAccountByWallet(accountIndex, wallet);
}

/**
* The same as `initialize` but synchronous.
*/
initializeSync(accountIndex: number): AccountMemory {
const wallet = this.getWalletSync();
return this.#getAccountByWallet(accountIndex, wallet);
}
}
22 changes: 20 additions & 2 deletions test/unit/mnemonic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@ const wallet = {
} as const;

describe('Account mnemonic factory', () => {
it('derives wallet', async () => {
it('derives wallet by mnemonic', async () => {
const factory = new AccountMnemonicFactory(mnemonic);
expect(await factory.getWallet()).to.be.eql(wallet);
});

it('initializes an account', async () => {
it('derives wallet by wallet', async () => {
const factory = new AccountMnemonicFactory(wallet);
expect(await factory.getWallet()).to.be.eql(wallet);
});

it('derives wallet in sync', async () => {
const factory = new AccountMnemonicFactory(mnemonic);
expect(factory.getWalletSync()).to.be.eql(wallet);
});

it('initializes an account by mnemonic', async () => {
const factory = new AccountMnemonicFactory(mnemonic);
const account = await factory.initialize(42);
expect(account).to.be.instanceOf(MemoryAccount);
Expand All @@ -25,6 +35,14 @@ describe('Account mnemonic factory', () => {
it('initializes an account by wallet', async () => {
const factory = new AccountMnemonicFactory(wallet);
const account = await factory.initialize(42);
expect(account).to.be.instanceOf(MemoryAccount);
expect(account.address).to.be.equal('ak_2HteeujaJzutKeFZiAmYTzcagSoRErSXpBFV179xYgqT4teakv');
});

it('initializes an account in sync', async () => {
const factory = new AccountMnemonicFactory(mnemonic);
const account = factory.initializeSync(42);
expect(account).to.be.instanceOf(MemoryAccount);
expect(account.address).to.be.equal('ak_2HteeujaJzutKeFZiAmYTzcagSoRErSXpBFV179xYgqT4teakv');
});

Expand Down

0 comments on commit 5abddc7

Please sign in to comment.