diff --git a/.github/workflows/publish-beta.workflow.yml b/.github/workflows/publish-beta.workflow.yml new file mode 100644 index 0000000..4215720 --- /dev/null +++ b/.github/workflows/publish-beta.workflow.yml @@ -0,0 +1,59 @@ +on: + push: + branches: [staging] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 16 + - run: yarn + - run: yarn lint:check + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 16 + - run: yarn + - run: yarn test:coverage + + - name: Comment Test Coverage + uses: raulanatol/jest-coverage-comment-action@main + with: + github-token: ${{ secrets.NODE_AUTH_TOKEN }} + use-existing-reports: true + publish-npm-registry: + if: github.ref == 'refs/heads/staging' + needs: [lint, test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + registry-url: 'https://registry.npmjs.org' + - run: yarn + - run: yarn build + - run: yarn publish --tag beta + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + publish-github-registry: + if: github.ref == 'refs/heads/staging' + needs: [lint, test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + registry-url: 'https://npm.pkg.github.com' + scope: '@leapwallet' + - run: yarn + - run: yarn build + - run: yarn publish --tag beta --non-interactive + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index 10f2cc4..8a1d994 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@leapwallet/leap-keychain", - "version": "0.2.2", + "version": "0.2.3-beta.2", "description": "A javascript library for crypto key management", "scripts": { "test": "jest", diff --git a/src/key/eth-wallet.ts b/src/key/eth-wallet.ts index 4140289..343e1f3 100644 --- a/src/key/eth-wallet.ts +++ b/src/key/eth-wallet.ts @@ -11,7 +11,9 @@ import { encodeSecp256k1Signature } from '../utils/encode-signature'; import { HDNode } from '@ethersproject/hdnode'; import { bip39Token, getBip39 } from '../crypto/bip39/bip39-token'; import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import { TransactionRequest, Provider } from '@ethersproject/abstract-provider'; import Container from 'typedi'; +import { pubkeyToAddress } from './wallet'; export class EthWallet { private constructor( @@ -19,6 +21,7 @@ export class EthWallet { private pvtKey: string, private walletType: 'mnemonic' | 'pvtKey', private options: WalletOptions, + private provider?: Provider, ) {} /** @@ -32,6 +35,10 @@ export class EthWallet { return new EthWallet(mnemonic, '', 'mnemonic', options); } + setProvider(provider: Provider) { + this.provider = provider; + } + /** * Generates a wallet from a private key. * @param {string} pvtKey - The private key to generate the wallet from. @@ -66,13 +73,18 @@ export class EthWallet { this.walletType === 'mnemonic' ? HDNode.fromSeed(seed).derivePath(path) : new Wallet(this.pvtKey); const ethAddr = EthereumUtilsAddress.fromString(hdWallet.address).toBuffer(); - const bech32Address = bech32.encode(this.options.addressPrefix, bech32.toWords(ethAddr)); - const ethWallet = new Wallet(hdWallet.privateKey); + + const ethWallet = new Wallet(hdWallet.privateKey, this.provider); + const pubkey = fromHex(ethWallet._signingKey().compressedPublicKey.replace('0x', '')); + + const bech32Address = this.options.pubKeyBech32Address + ? pubkeyToAddress(this.options.addressPrefix, pubkey) + : bech32.encode(this.options.addressPrefix, bech32.toWords(ethAddr)); return { algo: 'ethsecp256k1', address: bech32Address, ethWallet: ethWallet, - pubkey: fromHex(ethWallet._signingKey().compressedPublicKey.replace('0x', '')), + pubkey, }; }); } @@ -87,6 +99,17 @@ export class EthWallet { return ethWallet._signingKey().signDigest(signBytes); } + public async sendTransaction(transaction: TransactionRequest) { + const accounts = this.getAccountsWithPrivKey(); + const account = accounts[0]; + if (!account) throw new Error('Account not found'); + // if (account === undefined) { + // throw new Error(`Address ${signerAddress} not found in wallet`); + // } + const { ethWallet } = account; + return await ethWallet.sendTransaction(transaction); + } + signMessage(signerAddress: string, message: Uint8Array) { const accounts = this.getAccountsWithPrivKey(); const account = accounts.find(({ address }) => address === signerAddress); @@ -104,6 +127,7 @@ export class EthWallet { throw new Error(`Address ${signerAddress} not found in wallet`); } const { ethWallet } = account; + return ethWallet.signTransaction(transaction); } @@ -117,6 +141,7 @@ export class EthWallet { const rawSignature = this.sign(signerAddress, keccak256(Buffer.from(hash))); const splitSignature = bytes.splitSignature(rawSignature); const signature = bytes.arrayify(bytes.concat([splitSignature.r, splitSignature.s])); + return { signed: signDoc, signature: encodeSecp256k1Signature(account.pubkey, signature), diff --git a/src/key/wallet-utils.ts b/src/key/wallet-utils.ts index 5262964..b3d7101 100644 --- a/src/key/wallet-utils.ts +++ b/src/key/wallet-utils.ts @@ -5,13 +5,26 @@ import { Wallet } from './wallet'; import * as base64js from 'base64-js'; import { bip39Token } from '../crypto/bip39/bip39-token'; -export function generateWalletFromMnemonic(mnemonic: string, hdPath: string, addressPrefix: string) { +export function generateWalletFromMnemonic( + mnemonic: string, + { + hdPath, + addressPrefix, + ethWallet, + pubKeyBech32Address, + }: { + hdPath: string; + addressPrefix: string; + ethWallet: boolean; + pubKeyBech32Address?: boolean; + }, +) { const bip39 = Container.get(bip39Token); bip39.mnemonicToEntropy(mnemonic); const hdPathParams = hdPath.split('/'); const coinType = hdPathParams[2]; - if (coinType?.replace("'", '') === '60') { - return EthWallet.generateWalletFromMnemonic(mnemonic, { paths: [hdPath], addressPrefix }); + if (coinType?.replace("'", '') === '60' || ethWallet) { + return EthWallet.generateWalletFromMnemonic(mnemonic, { paths: [hdPath], addressPrefix, pubKeyBech32Address }); } return Wallet.generateWallet(mnemonic, { paths: [hdPath], addressPrefix }); } diff --git a/src/keychain/keychain.ts b/src/keychain/keychain.ts index c9e72f0..af1cf33 100644 --- a/src/keychain/keychain.ts +++ b/src/keychain/keychain.ts @@ -206,8 +206,17 @@ export class KeyChain { public static async getSigner( walletId: string, password: string, - addressPrefix: string, - coinType: string, + { + addressPrefix, + coinType, + ethWallet, + pubKeyBech32Address, + }: { + addressPrefix: string; + coinType: string; + ethWallet?: boolean; + pubKeyBech32Address?: boolean; + }, ) { const storage = Container.get(storageToken); const keychain = (await storage.get(KEYCHAIN)) as unknown as Keystore; @@ -222,14 +231,14 @@ export class KeyChain { throw new Error('Wallet type not supported'); } if (walletData.walletType === WALLETTYPE.PRIVATE_KEY) { - if (coinType === '60') { + if (coinType === '60' || ethWallet) { const hdPath = getHDPath(coinType, walletData.addressIndex.toString()); - return EthWallet.generateWalletFromPvtKey(secret, { paths: [hdPath], addressPrefix }); + return EthWallet.generateWalletFromPvtKey(secret, { paths: [hdPath], addressPrefix, pubKeyBech32Address }); } return PvtKeyWallet.generateWallet(secret, addressPrefix); } else { const hdPath = getHDPath(coinType, walletData.addressIndex.toString()); - return generateWalletFromMnemonic(secret, hdPath, addressPrefix); + return generateWalletFromMnemonic(secret, { hdPath, addressPrefix, ethWallet: !!ethWallet, pubKeyBech32Address }); } } @@ -285,11 +294,11 @@ export class KeyChain { const addresses: Record = {}; const pubKeys: Record = {}; for (const chainInfo of chainsData) { - const wallet = generateWalletFromMnemonic( - mnemonic, - getHDPath(chainInfo.coinType, addressIndex.toString()), - chainInfo.addressPrefix, - ); + const wallet = generateWalletFromMnemonic(mnemonic, { + hdPath: getHDPath(chainInfo.coinType, addressIndex.toString()), + addressPrefix: chainInfo.addressPrefix, + ethWallet: false, + }); const [account] = wallet.getAccounts(); if (account?.address && account?.pubkey) { diff --git a/src/types/wallet.ts b/src/types/wallet.ts index a8cc33f..abf1cb5 100644 --- a/src/types/wallet.ts +++ b/src/types/wallet.ts @@ -4,6 +4,7 @@ import { PvtKeyWallet, Wallet } from '../key/wallet'; export type WalletOptions = { paths: string[]; addressPrefix: string; + pubKeyBech32Address?: boolean; }; export type LeapSigner = EthWallet | Wallet | PvtKeyWallet; diff --git a/tests/wallet.test.ts b/tests/wallet.test.ts index 3fb70c8..0b54ee4 100644 --- a/tests/wallet.test.ts +++ b/tests/wallet.test.ts @@ -72,18 +72,39 @@ describe('generateMnemonic', () => { ).toThrow('Invalid private key'); }); test('generateWalletFromMnemonic', () => { - const wallet = generateWalletFromMnemonic(mnemonic, "m/44'/118'/0'/0/1", 'cosmos'); + const wallet = generateWalletFromMnemonic(mnemonic, { + hdPath: "m/44'/118'/0'/0/1", + addressPrefix: 'cosmos', + ethWallet: false, + }); const accounts = wallet.getAccounts(); expect(accounts[0]?.address).toBe(referenceWallets.ref2.addresses.cosmos); }); + test('generateWalletFromMnemonic for cointype=60', () => { + const wallet = generateWalletFromMnemonic(mnemonic, { + hdPath: "m/44'/60'/0'/0/1", + addressPrefix: 'evmos', + ethWallet: false, + }); + const accounts = wallet.getAccounts(); + expect(accounts[0]?.address).toBe(referenceWallets.ref2.addresses.evmos); + }); test('generateWalletsFromMnemonic', async () => { const wallet = generateWalletsFromMnemonic(mnemonic, ["m/44'/118'/0'/0/0", "m/44'/118'/0'/0/1"], 'cosmos'); const accounts = wallet.getAccounts(); expect(accounts[0]?.address).toBe(referenceWallets.ref1.addresses.cosmos); expect(accounts[1]?.address).toBe(referenceWallets.ref2.addresses.cosmos); }); + test('generateWalletsFromMnemonic for cointype=60', async () => { + const wallet = generateWalletsFromMnemonic(mnemonic, ["m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1"], 'evmos'); + const accounts = wallet.getAccounts(); + expect(accounts[0]?.address).toBe(referenceWallets.ref1.addresses.evmos); + expect(accounts[1]?.address).toBe(referenceWallets.ref2.addresses.evmos); + }); test('generateWalletFromMnemonic throws error if mnemonic is invalid', () => { - expect(() => generateWalletFromMnemonic('', "m/44'/118'/0'/0/0", 'cosmos')).toThrow('Invalid mnemonic'); + expect(() => + generateWalletFromMnemonic('', { hdPath: "m/44'/118'/0'/0/0", addressPrefix: 'cosmos', ethWallet: false }), + ).toThrow('Invalid mnemonic'); }); test('generateWalletsFromMnemonic throws error if mnemonic is invalid', () => { expect(() => generateWalletsFromMnemonic('', ["m/44'/118'/0'/0/0"], 'cosmos')).toThrow('Invalid mnemonic');