UNPKG

@alephium/web3-wallet

Version:
229 lines (196 loc) 8.18 kB
/* Copyright 2018 - 2022 The Alephium Authors This file is part of the alephium project. The library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with the library. If not, see <http://www.gnu.org/licenses/>. */ import { isGroupedAccount, publicKeyFromPrivateKey, utils } from '@alephium/web3' import { Account, KeyType, ExplorerProvider, NodeProvider } from '@alephium/web3' import { addressFromPublicKey } from '@alephium/web3' import { Address } from '@alephium/web3' import { groupOfAddress } from '@alephium/web3' import { groupOfPrivateKey, SignerProviderWithCachedAccounts, TOTAL_NUMBER_OF_GROUPS, web3 } from '@alephium/web3' import { generateMnemonic as _generateMnemonic, mnemonicToSeedSync } from '@scure/bip39' import { wordlist } from '@scure/bip39/wordlists/english' import { HDKey } from '@scure/bip32' import { PrivateKeyWallet } from './privatekey-wallet' export function generateMnemonic(wordLength?: 12 | 24): string { return _generateMnemonic(wordlist, wordLength === 12 ? 128 : 256) } export function deriveHDWalletPrivateKey( mnemonic: string, keyType: KeyType, _fromAddressIndex?: number, passphrase?: string ): string { const seed = mnemonicToSeedSync(mnemonic, passphrase) const masterKey = HDKey.fromMasterSeed(seed) const fromAddressIndex = _fromAddressIndex ?? 0 const child = masterKey.derive(getHDWalletPath(keyType, fromAddressIndex)) if (!child.privateKey) throw new Error('Missing private key') return utils.binToHex(child.privateKey) } export function deriveSecp256K1PrivateKey(mnemonic: string, fromAddressIndex?: number, passphrase?: string): string { return deriveHDWalletPrivateKey(mnemonic, 'default', fromAddressIndex, passphrase) } export function deriveSchnorrPrivateKey(mnemonic: string, fromAddressIndex?: number, passphrase?: string): string { return deriveHDWalletPrivateKey(mnemonic, 'bip340-schnorr', fromAddressIndex, passphrase) } export function deriveHDWalletPrivateKeyForGroup( mnemonic: string, targetGroup: number, keyType: KeyType, _fromAddressIndex?: number, passphrase?: string ): [string, number] { if (targetGroup < 0 || targetGroup > TOTAL_NUMBER_OF_GROUPS) { throw Error(`Invalid target group for HD wallet derivation: ${targetGroup}`) } const fromAddressIndex = _fromAddressIndex ?? 0 const privateKey = deriveHDWalletPrivateKey(mnemonic, keyType, fromAddressIndex, passphrase) if (groupOfPrivateKey(privateKey, keyType) === targetGroup) { return [privateKey, fromAddressIndex] } else { return deriveHDWalletPrivateKeyForGroup(mnemonic, targetGroup, keyType, fromAddressIndex + 1, passphrase) } } export function deriveSecp256K1PrivateKeyForGroup( mnemonic: string, targetGroup: number, _fromAddressIndex?: number, passphrase?: string ): [string, number] { return deriveHDWalletPrivateKeyForGroup(mnemonic, targetGroup, 'default', _fromAddressIndex, passphrase) } export function deriveSchnorrPrivateKeyForGroup( mnemonic: string, targetGroup: number, _fromAddressIndex?: number, passphrase?: string ): [string, number] { return deriveHDWalletPrivateKeyForGroup(mnemonic, targetGroup, 'bip340-schnorr', _fromAddressIndex, passphrase) } export function getHDWalletPath(keyType: KeyType, addressIndex: number): string { if (addressIndex < 0 || !Number.isInteger(addressIndex) || addressIndex.toString().includes('e')) { throw new Error('Invalid address index path level') } // Being explicit: we always use coinType 1234 no matter the network. const coinType = "1234'" // eslint-disable-next-line const keyTypeNum = (() => { switch (keyType) { case 'default': return 0 case 'bip340-schnorr': return 1 case 'gl-secp256k1': return 2 case 'gl-secp256r1': return 3 case 'gl-ed25519': return 4 case 'gl-webauthn': return 5 default: throw new Error(`Unsupported key type: ${keyType}`) } })() return `m/44'/${coinType}/${keyTypeNum}'/0/${addressIndex}` } export function getSecp259K1Path(addressIndex: number): string { return getHDWalletPath('default', addressIndex) } export function getSchnorrPath(addressIndex: number): string { return getHDWalletPath('bip340-schnorr', addressIndex) } export function getGlSecp256K1Path(addressIndex: number): string { return getHDWalletPath('gl-secp256k1', addressIndex) } export function getGlSecp256R1Path(addressIndex: number): string { return getHDWalletPath('gl-secp256r1', addressIndex) } export function getGlEd25519Path(addressIndex: number): string { return getHDWalletPath('gl-ed25519', addressIndex) } export function getGlWebauthnPath(addressIndex: number): string { return getHDWalletPath('gl-webauthn', addressIndex) } export type HDWalletAccount = Account & { addressIndex: number } // In-memory HDWallet for simple use cases. Advanced wallet better used the derivation functions above. export class HDWallet extends SignerProviderWithCachedAccounts<HDWalletAccount> { private readonly mnemonic: string readonly keyType: KeyType private readonly passphrase?: string readonly nodeProvider: NodeProvider readonly explorerProvider: ExplorerProvider | undefined constructor({ mnemonic, keyType, nodeProvider, explorerProvider, passphrase }: { mnemonic: string keyType?: KeyType nodeProvider?: NodeProvider explorerProvider?: ExplorerProvider passphrase?: string }) { super() this.mnemonic = mnemonic this.keyType = keyType ?? 'default' if (this.keyType !== 'default' && this.keyType !== 'bip340-schnorr' && this.keyType !== 'gl-secp256k1') { throw new Error(`Invalid key type ${keyType}`) } this.passphrase = passphrase this.nodeProvider = nodeProvider ?? web3.getCurrentNodeProvider() this.explorerProvider = explorerProvider ?? web3.getCurrentExplorerProvider() } private getNextFromAddressIndex(targetGroup?: number): number { let usedAddressIndex = -1 for (const account of this._accounts.values()) { const accountGroup = isGroupedAccount(account) ? account.group : groupOfAddress(account.address) if ((targetGroup === undefined || accountGroup == targetGroup) && account.addressIndex > usedAddressIndex) { usedAddressIndex = account.addressIndex } } return usedAddressIndex + 1 } deriveAndAddNewAccount(targetGroup?: number): HDWalletAccount { const fromAddressIndex = this.getNextFromAddressIndex(targetGroup) let priKey: string let addressIndex: number = fromAddressIndex if (targetGroup !== undefined) { const [_priKey, _addressIndex] = deriveHDWalletPrivateKeyForGroup( this.mnemonic, targetGroup, this.keyType, fromAddressIndex, this.passphrase ) priKey = _priKey addressIndex = _addressIndex } else { priKey = deriveHDWalletPrivateKey(this.mnemonic, this.keyType, fromAddressIndex, this.passphrase) } const publicKey = publicKeyFromPrivateKey(priKey, this.keyType) const address = addressFromPublicKey(publicKey, this.keyType) const group = groupOfAddress(address) const account = { keyType: this.keyType, address, group, publicKey, addressIndex } this._accounts.set(account.address, account) return account } async signRaw(signerAddress: Address, hexString: string): Promise<string> { const account = await this.getAccount(signerAddress) const privateKey = deriveHDWalletPrivateKey(this.mnemonic, account.keyType, account.addressIndex, this.passphrase) return PrivateKeyWallet.sign(privateKey, hexString, this.keyType) } }