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

Staging #21

Merged
merged 13 commits into from
Apr 4, 2024
59 changes: 59 additions & 0 deletions .github/workflows/publish-beta.workflow.yml
Original file line number Diff line number Diff line change
@@ -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 }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
31 changes: 28 additions & 3 deletions src/key/eth-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ 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(
private mnemonic: string,
private pvtKey: string,
private walletType: 'mnemonic' | 'pvtKey',
private options: WalletOptions,
private provider?: Provider,
) {}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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,
};
});
}
Expand All @@ -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);
Expand All @@ -104,6 +127,7 @@ export class EthWallet {
throw new Error(`Address ${signerAddress} not found in wallet`);
}
const { ethWallet } = account;

return ethWallet.signTransaction(transaction);
}

Expand All @@ -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),
Expand Down
19 changes: 16 additions & 3 deletions src/key/wallet-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Expand Down
29 changes: 19 additions & 10 deletions src/keychain/keychain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,17 @@ export class KeyChain {
public static async getSigner<T extends string>(
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<T>;
Expand All @@ -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 });
}
}

Expand Down Expand Up @@ -285,11 +294,11 @@ export class KeyChain {
const addresses: Record<string, string> = {};
const pubKeys: Record<string, string> = {};
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) {
Expand Down
1 change: 1 addition & 0 deletions src/types/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
25 changes: 23 additions & 2 deletions tests/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading