Skip to content

Commit

Permalink
Merge pull request #2038 from aeternity/mnemonic-sync
Browse files Browse the repository at this point in the history
AccountMnemonicFactory: init by seed and sync methods
  • Loading branch information
davidyuk authored Feb 24, 2025
2 parents 3b1964e + fe74643 commit 9b9d8d3
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 29 deletions.
78 changes: 52 additions & 26 deletions src/account/MnemonicFactory.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
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';
import { ArgumentError } from '../utils/errors.js';

export const ED25519_CURVE = Buffer.from('ed25519 seed');
const HARDENED_OFFSET = 0x80000000;
Expand Down Expand Up @@ -42,54 +42,80 @@ 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;
#mnemonicOrWalletOrSeed: string | Wallet | Uint8Array;

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

#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.#mnemonicOrWalletOrSeed = {
secretKey: encode(walletKey.secretKey, Encoding.Bytearray),
chainCode: encode(walletKey.chainCode, Encoding.Bytearray),
};
return this.#mnemonicOrWalletOrSeed;
};

if (ArrayBuffer.isView(this.#mnemonicOrWalletOrSeed)) {
if (this.#mnemonicOrWalletOrSeed.length !== 64) {
throw new ArgumentError('seed length', 64, this.#mnemonicOrWalletOrSeed.length);
}
return setWalletBySeed(this.#mnemonicOrWalletOrSeed);
}
if (typeof this.#mnemonicOrWalletOrSeed === 'object') return this.#mnemonicOrWalletOrSeed;
return sync
? setWalletBySeed(mnemonicToSeedSync(this.#mnemonicOrWalletOrSeed))
: mnemonicToSeed(this.#mnemonicOrWalletOrSeed).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);
}
}
2 changes: 1 addition & 1 deletion src/aepp-wallet-communication/connection/BrowserRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default class BrowserRuntimeConnection extends BrowserConnection {
/**
* @param options - Options
*/
constructor({ port, ...options }: { port: Runtime.Port; debug: boolean }) {
constructor({ port, ...options }: { port: Runtime.Port; debug?: boolean }) {
super(options);
this.port = port;
}
Expand Down
41 changes: 39 additions & 2 deletions test/unit/mnemonic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,64 @@ import { Node, Encoded, AccountMnemonicFactory, MemoryAccount } from '../../src'

const mnemonic = 'eye quarter chapter suit cruel scrub verify stuff volume control learn dust';

const seed = new Uint8Array([
26, 43, 123, 108, 82, 100, 153, 240, 181, 30, 143, 186, 96, 84, 133, 187, 20, 179, 152, 54, 114,
118, 104, 243, 147, 193, 110, 110, 179, 195, 207, 131, 230, 174, 67, 145, 148, 16, 229, 126, 115,
211, 147, 77, 150, 171, 211, 227, 217, 151, 80, 229, 196, 192, 209, 44, 71, 40, 106, 234, 223, 20,
163, 59,
]);

const wallet = {
secretKey: 'ba_I1lro/ANfEKuBUal0Glo++D5abkcFLIIihTDLcC8l3My1PuP',
chainCode: 'ba_XZL45EKIQiLe9v/pkY37Bn3GiqLXZ5v2hIya6llA0QOlYf6i',
} 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.eql(wallet);
});

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

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

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

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

it('initializes an account by seed', async () => {
const factory = new AccountMnemonicFactory(seed);
const account = await factory.initialize(42);
expect(account).to.be.an.instanceOf(MemoryAccount);
expect(account.address).to.equal('ak_2HteeujaJzutKeFZiAmYTzcagSoRErSXpBFV179xYgqT4teakv');
});

it('initializes an account by wallet', async () => {
const factory = new AccountMnemonicFactory(wallet);
const account = await factory.initialize(42);
expect(account).to.be.an.instanceOf(MemoryAccount);
expect(account.address).to.equal('ak_2HteeujaJzutKeFZiAmYTzcagSoRErSXpBFV179xYgqT4teakv');
});

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

Expand Down

0 comments on commit 9b9d8d3

Please sign in to comment.