Skip to content

Commit

Permalink
Merge pull request #56 from leapwallet/update/btc-signer
Browse files Browse the repository at this point in the history
Update/btc signer
  • Loading branch information
baryon2 authored Jan 2, 2025
2 parents 40c124d + 97278a6 commit e612216
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 26 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@leapwallet/leap-keychain",
"version": "0.3.3-beta.3",
"version": "0.3.4-beta.0",
"description": "A javascript library for crypto key management",
"scripts": {
"test:coverage": "nyc mocha",
Expand All @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions src/key/btc-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { base64, hex } from '@scure/base';
import Container from 'typedi';
import { secp256k1Token } from '../crypto/ecc/secp256k1';
import { P2Ret } from '@scure/btc-signer/payment';
import { signSync } from '@noble/secp256k1';
export type BTCWalletOptions = WalletOptions & { network: typeof NETWORK };

export abstract class BtcWallet {
Expand Down Expand Up @@ -50,6 +51,21 @@ export abstract class BtcWallet {
if (!account.privateKey) throw new Error('Private key not found');
tx.signIdx(account.privateKey, idx);
}

signECDSA(address: string, hash: Uint8Array) {
const accounts = this.getAccountsWithPrivKey();
const account = accounts.find((account) => account.address === address);
if (!account) throw new Error(`No account found for ${address}`);
const [signature, recoveryParam] = signSync(hash, account.privateKey, {
canonical: true,
recovered: true,
der: false,
});
return {
signature,
recoveryParam,
};
}
}

export class BtcWalletHD extends BtcWallet {
Expand Down
14 changes: 14 additions & 0 deletions src/key/eth-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 8 additions & 6 deletions src/key/wallet-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
9 changes: 9 additions & 0 deletions src/utils/encode-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ export function encodeSecp256k1Pubkey(pubkey: Uint8Array): Pubkey {
value: base64js.fromByteArray(pubkey),
};
}

export function compressSignature(recoveryParam: number, signature: Uint8Array) {
if (!(recoveryParam === 0 || recoveryParam === 1 || recoveryParam === 2 || recoveryParam === 3)) {
throw new Error('recoveryParam must be equal to 0, 1, 2, or 3');
}

let headerByte = recoveryParam + 27 + 4;
return Buffer.concat([Uint8Array.of(headerByte), Uint8Array.from(signature)]).toString('base64');
}
20 changes: 20 additions & 0 deletions test/btc-wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { BtcWalletHD, BtcWalletPk } from '../src/key/btc-wallet';
import expect from 'expect.js';
import { addresses, btcPrivatekey, mnemonic, sbtcPrivatekey } from './mockdata';
import { NETWORK, TEST_NETWORK } from '@scure/btc-signer';
import { base64 } from '@scure/base';
import { compressSignature } from '../src/utils/encode-signature';

beforeEach(() => {
setBip39(Bip39);
Expand Down Expand Up @@ -68,4 +70,22 @@ describe('generate btc wallet', () => {
if (!accounts[0]) throw new Error('No accounts found');
expect(accounts[0].address).to.be(addresses.signet);
});

it('signEcdsa: generates correct signature', () => {
const wallet = BtcWalletHD.generateWalletFromMnemonic(mnemonic, {
addressPrefix: 'bc1q',
paths: ["m/84'/0'/0'/0/0"],
network: NETWORK,
});
const [account] = wallet.getAccounts();
if (!account) throw new Error();

const testHash = 'lZ93LI3uk73n7jGU4os1GIWkEz/4vf//AhBR2m5M/9A=';

const fixture = 'IBU1VH1HFZKtulCFAukOm3JP8QO4ldrqxVohbhY5Qt8YFAxG85AanlP4qPjnOfDlkWGUUTan1gAVad1KcG2FifQ=';

const { signature, recoveryParam } = wallet.signECDSA(account.address, base64.decode(testHash));
const base64Signature = compressSignature(recoveryParam, signature);
expect(base64Signature).to.equal(fixture);
});
});
78 changes: 61 additions & 17 deletions test/mockdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand All @@ -27,50 +28,78 @@ 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,
},
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',
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit e612216

Please sign in to comment.