Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AccountMnemonicFactory: init by seed and sync methods #2038

Merged
merged 3 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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