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

Update/btc signer #56

Merged
merged 4 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading