diff --git a/package.json b/package.json index b71fbd3..3f41bc0 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,7 @@ "doc:build": "yarn doc", "doc:watch": "yarn doc --watch", "prepublish": "yarn build", - "build": "tsc -p tsconfig-browser.json && tsc -p tsconfig-node.json", - "test:coverage": "nyc" + "build": "tsc -p tsconfig-browser.json && tsc -p tsconfig-node.json" }, "types": "dist/browser/index.d.ts", "license": "MIT", diff --git a/src/key/eth-wallet.ts b/src/key/eth-wallet.ts index be2eaf9..595e9c3 100644 --- a/src/key/eth-wallet.ts +++ b/src/key/eth-wallet.ts @@ -14,6 +14,7 @@ import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; import { TransactionRequest, Provider } from '@ethersproject/abstract-provider'; import Container from 'typedi'; import { pubkeyToAddress } from './wallet'; +import { hex } from '@scure/base'; export class EthWallet { private constructor( @@ -84,11 +85,24 @@ export class EthWallet { algo: 'ethsecp256k1', address: bech32Address, ethWallet: ethWallet, + hexAddress: `0x${hex.encode(ethAddr)}`, pubkey, }; }); } + public getAccountWithHexAddress() { + const accounts = this.getAccountsWithPrivKey(); + return accounts.map((account) => { + return { + algo: account.algo, + address: account.hexAddress, + pubkey: account.pubkey, + bech32Address: account.address, + }; + }); + } + public sign(signerAddress: string, signBytes: string | Uint8Array) { const accounts = this.getAccountsWithPrivKey(); const account = accounts.find(({ address }) => address === signerAddress); diff --git a/src/key/wallet-utils.ts b/src/key/wallet-utils.ts index ff76f8f..b242629 100644 --- a/src/key/wallet-utils.ts +++ b/src/key/wallet-utils.ts @@ -101,24 +101,26 @@ export function generateWalletFromPrivateKey( hdPath: string, prefix: string, btcNetwork?: typeof NETWORK, + ethWallet?: boolean, + pubKeyBech32Address?: boolean, ) { const hdPathParams = hdPath.split('/'); const coinType = hdPathParams[2]; - let wallet; - if (coinType === "60'") { - wallet = EthWallet.generateWalletFromPvtKey(privateKey, { + + if (coinType === "60'" || ethWallet) { + return EthWallet.generateWalletFromPvtKey(privateKey, { paths: [hdPath], addressPrefix: prefix, + pubKeyBech32Address, }); } else if (coinType === "0'" || coinType === "1'") { if (!btcNetwork) throw new Error('Unable to generate key. Please provide btc network in chain info config'); - wallet = new BtcWalletPk(privateKey, { + return new BtcWalletPk(privateKey, { paths: [hdPath], addressPrefix: prefix, network: btcNetwork, }); } else { - wallet = PvtKeyWallet.generateWallet(privateKey, prefix); + return PvtKeyWallet.generateWallet(privateKey, prefix); } - return wallet; } diff --git a/test/mockdata.ts b/test/mockdata.ts index f1fe4e4..dcf4ea8 100644 --- a/test/mockdata.ts +++ b/test/mockdata.ts @@ -6,13 +6,14 @@ export const btcPrivatekey = 'a29da5e16cf8a7a4fe90dc9287c3338e6726b8bd4c1e9a8ab4 export const sbtcPrivatekey = 'acc42e2d3fb504915180bae5d001e818f3304b970b2a145ebc252faf2fbd8867'; export const addresses = { - secret: 'secret18qya73yjlt28hhc3yefu3j97kgpyjj9rfd46d6', + bitcoin: 'bc1qd5xpfp9zp8q696pu3sz7ej2wrk2wn634dlnhfa', cosmos: 'cosmos1uput06d0xac525sdmtf4h5d8dy9d8x3u07smz9', - osmosis: 'osmo1uput06d0xac525sdmtf4h5d8dy9d8x3u89rt5h', - juno: 'juno1uput06d0xac525sdmtf4h5d8dy9d8x3uevnq9e', evmos: 'evmos1reqqp5jum0lgyjh33nyvus8dlyzspk9aw50w3r', injective: 'inj1reqqp5jum0lgyjh33nyvus8dlyzspk9axufyen', - bitcoin: 'bc1qd5xpfp9zp8q696pu3sz7ej2wrk2wn634dlnhfa', + juno: 'juno1uput06d0xac525sdmtf4h5d8dy9d8x3uevnq9e', + osmosis: 'osmo1uput06d0xac525sdmtf4h5d8dy9d8x3u89rt5h', + secret: 'secret18qya73yjlt28hhc3yefu3j97kgpyjj9rfd46d6', + sei: 'sei1nqcal4r4qgfj9hhazrfpu72fx0ccdv35rk53ms', signet: 'tb1qp63mr992cpwspcjnysdfhstj6jaqykrs64yywn', }; @@ -27,18 +28,20 @@ export const referenceWallets = { juno: 'juno1nqcal4r4qgfj9hhazrfpu72fx0ccdv35cgxu6d', osmosis: 'osmo1nqcal4r4qgfj9hhazrfpu72fx0ccdv35xpkhtr', secret: 'secret1gcf3qag3zf0k9sd759ttuuq287p00g4kewjdwc', + sei: 'sei1nqcal4r4qgfj9hhazrfpu72fx0ccdv35rk53ms', signet: 'tb1qp63mr992cpwspcjnysdfhstj6jaqykrs64yywn', }, colorIndex: 0, name: 'testwallet', pubKeys: { + bitcoin: 'AjQmZKXn3epwg10mUNuHUF1SxWLS+06AA1v4um9x8//2', cosmos: 'AwxYytPNgUq91tLoRiGBP6MGEcsghnVTeMcLKcoPSfjW', evmos: 'A71glojh4VpiwcFufLabwSTfkLB6lnJ3i6EAp7oOac0h', injective: 'A71glojh4VpiwcFufLabwSTfkLB6lnJ3i6EAp7oOac0h', juno: 'AwxYytPNgUq91tLoRiGBP6MGEcsghnVTeMcLKcoPSfjW', osmosis: 'AwxYytPNgUq91tLoRiGBP6MGEcsghnVTeMcLKcoPSfjW', secret: 'AnQhTZmbQZXa9MY3KhYdEE1OabdLBtEAbG/wgj0SzBEV', - bitcoin: 'AjQmZKXn3epwg10mUNuHUF1SxWLS+06AA1v4um9x8//2', + sei: 'AwxYytPNgUq91tLoRiGBP6MGEcsghnVTeMcLKcoPSfjW', signet: 'AuLI5ATf2xgkFbHojyMsSQ2qbnvZoVyfu9JACxiZz8ji', }, walletType: 0, @@ -46,31 +49,57 @@ export const referenceWallets = { ref2: { addressIndex: 1, addresses: { + bitcoin: 'bc1qpx6cas6wg4gtpcmfke626va4kpx9m4s5cx03ff', cosmos: 'cosmos1rjtukzmqtlh2u20atc9pjefk55y0h6j2y7atrj', evmos: 'evmos1wdlne45wt60w68pu08u2w9cyzamxglz79npune', injective: 'inj1wdlne45wt60w68pu08u2w9cyzamxglz7dm8kmf', juno: 'juno1rjtukzmqtlh2u20atc9pjefk55y0h6j2jv7syw', osmosis: 'osmo1rjtukzmqtlh2u20atc9pjefk55y0h6j2v9wm4q', secret: 'secret1v7pzm2xdytc75dxx893fnd4te80qh6nw2k9czh', - bitcoin: 'bc1qpx6cas6wg4gtpcmfke626va4kpx9m4s5cx03ff', + sei: 'sei1rjtukzmqtlh2u20atc9pjefk55y0h6j2fjva9n', signet: 'tb1qh9d6qr7twdk7dh5tsw7mqxl5yvxskrcs80gppq', }, colorIndex: 1, name: 'Wallet 2', pubKeys: { + bitcoin: 'A3hQ226XkTCvpEXC3KqxLEZANyNjyMmJhIm8rDwX/EKE', cosmos: 'AqYABZ4+Zqqbx7zZfctmtRQs882J15WfRz3Go9QggsIA', evmos: 'AiuOnXAg+hvUimpEDchQfP9NmiKxgw1WaXpnQL/dvDMS', injective: 'AiuOnXAg+hvUimpEDchQfP9NmiKxgw1WaXpnQL/dvDMS', juno: 'AqYABZ4+Zqqbx7zZfctmtRQs882J15WfRz3Go9QggsIA', osmosis: 'AqYABZ4+Zqqbx7zZfctmtRQs882J15WfRz3Go9QggsIA', secret: 'AlvxQlaPKJI+25bX8I6TD6EaGbZl1f6Ngu/E9nO4KZCn', - bitcoin: 'A3hQ226XkTCvpEXC3KqxLEZANyNjyMmJhIm8rDwX/EKE', + sei: 'AqYABZ4+Zqqbx7zZfctmtRQs882J15WfRz3Go9QggsIA', signet: 'A0Ygge2Va8zePPPtwVyb8dt5Lf5fH9SvPxUdVylD5+cC', }, walletType: 0, }, }; +export const evmAddresses = [ + { + addressIndex: 0, + address: '0xaaa7bc446be72afcdd6d6041fda037b8cda9b493', + }, + { + addressIndex: 1, + address: '0x737f3cd68e5e9eed1c3c79f8a717041776647c5e', + }, +]; + +export const altEvmAddresses = { + sei: { + '0': { + coinType: '118', + address: '0x84a07314cd082feaacbc49487eede93cba01ea00', + }, + '1': { + coinType: '118', + address: '0x5b6a94c44843091692650719d95fee49081919fa', + }, + }, +}; + export const addressPrefixes = { cosmos: 'cosmos', secret: 'secret', @@ -80,12 +109,14 @@ export const addressPrefixes = { injective: 'inj', bitcoin: 'bc1q', signet: 'tb1q', + sei: 'sei', }; export const coinTypes = { cosmos: 118, juno: 118, osmosis: 118, + sei: 118, secret: 529, evmos: 60, injective: 60, @@ -95,8 +126,21 @@ export const coinTypes = { export const chainInfos: Record< string, - { addressPrefix: string; coinType: number; useBip84?: boolean; btcNetwork?: typeof NETWORK } + { + addressPrefix: string; + coinType: number; + useBip84?: boolean; + btcNetwork?: typeof NETWORK; + ethWallet?: boolean; + pubKeyBech32Address?: boolean; + } > = { + bitcoin: { + addressPrefix: addressPrefixes.bitcoin, + coinType: coinTypes.bitcoin, + useBip84: true, + btcNetwork: NETWORK, + }, cosmos: { addressPrefix: addressPrefixes.cosmos, coinType: coinTypes.cosmos, @@ -113,19 +157,19 @@ export const chainInfos: Record< addressPrefix: addressPrefixes.juno, coinType: coinTypes.juno, }, - secret: { - addressPrefix: addressPrefixes.secret, - coinType: coinTypes.secret, - }, osmosis: { addressPrefix: addressPrefixes.osmosis, coinType: coinTypes.osmosis, }, - bitcoin: { - addressPrefix: addressPrefixes.bitcoin, - coinType: coinTypes.bitcoin, - useBip84: true, - btcNetwork: NETWORK, + secret: { + addressPrefix: addressPrefixes.secret, + coinType: coinTypes.secret, + }, + sei: { + addressPrefix: addressPrefixes.sei, + coinType: coinTypes.sei, + ethWallet: true, + pubKeyBech32Address: true, }, signet: { addressPrefix: addressPrefixes.signet, diff --git a/test/wallet-utils.test.ts b/test/wallet-utils.test.ts new file mode 100644 index 0000000..99d7c04 --- /dev/null +++ b/test/wallet-utils.test.ts @@ -0,0 +1,127 @@ +import { mnemonic, chainInfos, referenceWallets, privateKey, altEvmAddresses } from './mockdata'; +import { generateWalletFromMnemonic, generateWalletFromPrivateKey } from '../src/key/wallet-utils'; +import { EthWallet, getFullHDPath } from '../src'; +import { NETWORK } from '@scure/btc-signer'; +import expect from 'expect.js'; + +function getWalletOptions(addressIndex: number): Record< + string, + { + hdPath: string; + addressPrefix: string; + btcNetwork?: typeof NETWORK; + ethWallet: boolean; + pubKeyBech32Address?: boolean; + } +> { + return Object.entries(chainInfos).reduce((acc, [key, value]) => { + const purpose = value.useBip84 ? '84' : '44'; + return { + ...acc, + [key]: { + hdPath: getFullHDPath(purpose, value.coinType.toString(), addressIndex.toString()), + addressPrefix: value.addressPrefix, + btcNetwork: value.btcNetwork, + ethWallet: !!value.ethWallet, + pubKeyBech32Address: !!value.pubKeyBech32Address, + }, + }; + }, {}); +} + +describe('wallet utils', () => { + it('Generates correct wallets from mnemonic', () => { + const addressIndex = 0; + const walletOptions = getWalletOptions(addressIndex); + const accounts = Object.entries(walletOptions) + .filter(([key]) => key !== 'ethereum') + .map(([key, walletOption]) => { + const wallet = generateWalletFromMnemonic(mnemonic, walletOption); + const [account] = wallet.getAccounts(); + if (!account) throw new Error(); + return { + address: account.address, + pubkey: account.pubkey, + chain: key, + }; + }); + + accounts.sort((a, b) => (a.chain > b.chain ? 1 : -1)); + const addresses = accounts.reduce((acc, account) => { + return { ...acc, [account.chain]: account.address }; + }, {}); + + const { ref1 } = referenceWallets; + expect(addresses).to.eql(ref1.addresses); + }); + it('Generates correct wallets from private key', () => { + const addressIndex = 0; + const walletOptions = getWalletOptions(addressIndex); + + const accounts = Object.entries(walletOptions) + .filter(([key]) => chainInfos[key]?.coinType === 118) + .map(([key, walletOption]) => { + const { hdPath, addressPrefix, btcNetwork, ethWallet, pubKeyBech32Address } = walletOption; + + const wallet = generateWalletFromPrivateKey( + privateKey, + hdPath, + addressPrefix, + btcNetwork, + ethWallet, + pubKeyBech32Address, + ); + const [account] = wallet.getAccounts(); + if (!account) throw new Error(); + return { + address: account.address, + pubkey: account.pubkey, + chain: key, + }; + }); + + accounts.sort((a, b) => (a.chain > b.chain ? 1 : -1)); + const addresses = accounts.reduce((acc, account) => { + return { ...acc, [account.chain]: account.address }; + }, {}); + + const { ref1 } = referenceWallets; + for (let [key, value] of Object.entries(addresses)) { + //@ts-expect-error: index error + expect(value).to.eql(ref1.addresses[key]); + } + }); + it('Generates correct alternate addresses from mnemonic', () => { + const walletOptions = getWalletOptions(0); + const option = walletOptions.sei; + if (option === undefined) throw new Error(); + + const wallet = generateWalletFromMnemonic(mnemonic, option); + const [account] = wallet.getAccounts(); + const [hexAddressAccount] = (wallet as EthWallet).getAccountWithHexAddress(); + + if (!account || !hexAddressAccount) throw new Error(); + + expect(account.address).to.equal(referenceWallets.ref1.addresses.sei); + expect(hexAddressAccount.address).to.equal(altEvmAddresses.sei[0].address); + }); + it('Generates correct alternate addresses from private key', () => { + const walletOptions = getWalletOptions(0); + const option = walletOptions.sei; + if (!option) throw new Error(); + const { hdPath, addressPrefix, ethWallet, pubKeyBech32Address } = option; + const wallet = generateWalletFromPrivateKey( + privateKey, + hdPath, + addressPrefix, + undefined, + ethWallet, + pubKeyBech32Address, + ); + const [account] = wallet.getAccounts(); + const [hexAddressAccount] = (wallet as EthWallet).getAccountWithHexAddress(); + if (!account || !hexAddressAccount) throw new Error(); + expect(account.address).to.equal(referenceWallets.ref1.addresses.sei); + expect(hexAddressAccount.address).to.equal(altEvmAddresses.sei[0].address); + }); +});