From 817d6b4b91933ca819777296b40c9fbcbbcb0a25 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 8 Mar 2023 15:33:50 +0100 Subject: [PATCH 1/9] Added BlockCypher Testnet to network types and extended each BlockCypher functions with the BlockCypher Testnet apiUrls --- api/btc.ts | 113 +++++++++++++++++++++++++++------------------- package-lock.json | 4 +- types/network.ts | 2 +- 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/api/btc.ts b/api/btc.ts index 77d3e527..a67d5a3d 100644 --- a/api/btc.ts +++ b/api/btc.ts @@ -11,17 +11,35 @@ import { BtcTransactionsDataResponse } from 'types/api/blockcypher/wallet'; import { BtcTransactionData } from 'types/api/blockcypher/wallet'; import { parseBtcTransactionData } from './helper'; +export async function fetchBtcTransactionData( + tx_hash: string, + network: NetworkType +): Promise { + const apiUrl = { + Mainnet: `https://api.blockcypher.com/v1/btc/main/txs/${tx_hash}`, + Testnet: `https://api.blockcypher.com/v1/btc/test3/txs/${tx_hash}`, + Regtest: `https://api.blockcypher.com/v1/bcy/test/txs/${tx_hash}`, + }; + + return axios + .get(apiUrl[network], { headers: { 'Access-Control-Allow-Origin': '*' } }) + .then((response) => { + const rawTx: string = response.data.hex; + return rawTx; + }); +} + export async function fetchBtcAddressUnspent( btcAddress: string, network: NetworkType ): Promise> { - const btcApiBaseUrl = `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}?unspentOnly=true&limit=50`; - const btcApiBaseUrlTestnet = `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}?unspentOnly=true&limit=50`; - let apiUrl = btcApiBaseUrl; - if (network === 'Testnet') { - apiUrl = btcApiBaseUrlTestnet; - } - return axios.get(apiUrl, { timeout: 45000 }).then((response) => { + const apiUrl = { + Mainnet: `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}?unspentOnly=true&limit=50`, + Testnet: `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}?unspentOnly=true&limit=50`, + Regtest: `https://api.blockcypher.com/v1/bcy/test/assrs/${btcAddress}?unspentOnly=true&limit=50`, + }; + + return axios.get(apiUrl[network], { timeout: 45000 }).then((response) => { const confirmed = response.data.txrefs ? (response.data.txrefs as Array) : []; @@ -37,14 +55,16 @@ export async function fetchPoolBtcAddressBalance( btcAddress: string, network: NetworkType ): Promise { - const btcApiBaseUrl = 'https://api.blockcypher.com/v1/btc/main/addrs/'; - const btcApiBaseUrlTestnet = 'https://api.blockcypher.com/v1/btc/test3/addrs/'; - let apiUrl = `${btcApiBaseUrl}${btcAddress}`; - if (network === 'Testnet') { - apiUrl = `${btcApiBaseUrlTestnet}${btcAddress}`; - } + const apiUrl = { + Mainnet: `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}`, + Testnet: `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}`, + Regtest: `https://api.blockcypher.com/v1/bcy/test/addrs/${btcAddress}`, + }; + return axios - .get(apiUrl, { headers: { 'Access-Control-Allow-Origin': '*' } }) + .get(apiUrl[network], { + headers: { 'Access-Control-Allow-Origin': '*' }, + }) .then((response) => { const btcPoolData: BtcBalance = { balance: response.data.final_balance, @@ -57,17 +77,17 @@ export async function broadcastRawBtcTransaction( rawTx: string, network: NetworkType ): Promise { - const btcApiBaseUrl = 'https://api.blockcypher.com/v1/btc/main/txs/push'; - const btcApiBaseUrlTestnet = 'https://api.blockcypher.com/v1/btc/test3/txs/push'; - let apiUrl = btcApiBaseUrl; - if (network === 'Testnet') { - apiUrl = btcApiBaseUrlTestnet; - } + const apiUrl = { + Mainnet: 'https://api.blockcypher.com/v1/btc/main/txs/push', + Testnet: 'https://api.blockcypher.com/v1/btc/test3/txs/push', + Regtest: 'https://api.blockcypher.com/v1/bcy/test/txs/push', + }; + const data = { tx: rawTx, }; return axios - .post(apiUrl, data, { timeout: 45000 }) + .post(apiUrl[network], data, { timeout: 45000 }) .then((response) => { return response.data; }); @@ -77,31 +97,32 @@ export async function fetchBtcTransactionsData( btcAddress: string, network: NetworkType ): Promise { - const btcApiBaseUrl = `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`; - const btcApiBaseUrlTestnet = `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`; - let apiUrl = btcApiBaseUrl; - if (network === 'Testnet') { - apiUrl = btcApiBaseUrlTestnet; - } + const apiUrl = { + Mainnet: `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`, + Testnet: `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`, + Regtest: `https://api.blockcypher.com/v1/bcy/test/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`, + }; - return axios.get(apiUrl, { timeout: 45000 }).then((response) => { - const transactions: BtcTransactionData[] = []; - response.data.txs.forEach((tx) => { - transactions.push(parseBtcTransactionData(tx, btcAddress)); - }); + return axios + .get(apiUrl[network], { timeout: 45000 }) + .then((response) => { + const transactions: BtcTransactionData[] = []; + response.data.txs.forEach((tx) => { + transactions.push(parseBtcTransactionData(tx, btcAddress)); + }); - const addressData: BtcAddressData = { - address: response.data.address, - totalReceived: response.data.total_received, - totalSent: response.data.total_sent, - balance: response.data.balance, - unconfirmedBalance: response.data.unconfirmed_balance, - finalBalance: response.data.final_balance, - nTx: response.data.n_tx, - unconfirmedTx: response.data.unconfirmed_tx, - finalNTx: response.data.final_n_tx, - transactions: transactions, - }; - return addressData; - }); + const addressData: BtcAddressData = { + address: response.data.address, + totalReceived: response.data.total_received, + totalSent: response.data.total_sent, + balance: response.data.balance, + unconfirmedBalance: response.data.unconfirmed_balance, + finalBalance: response.data.final_balance, + nTx: response.data.n_tx, + unconfirmedTx: response.data.unconfirmed_tx, + finalNTx: response.data.final_n_tx, + transactions: transactions, + }; + return addressData; + }); } diff --git a/package-lock.json b/package-lock.json index 35d1c530..632d7307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@secretkeylabs/xverse-core", - "version": "0.3.0", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@secretkeylabs/xverse-core", - "version": "0.3.0", + "version": "0.5.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/types/network.ts b/types/network.ts index 857c40db..2a5fc3da 100644 --- a/types/network.ts +++ b/types/network.ts @@ -1,6 +1,6 @@ export { StacksNetwork, StacksMainnet, StacksTestnet } from '@stacks/network'; -export type NetworkType = 'Mainnet' | 'Testnet'; +export type NetworkType = 'Mainnet' | 'Testnet' | 'Regtest'; export type SettingsNetwork = { type: NetworkType; From 3b0e1e9e737138e68c4d60c23029372927d82718 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Sun, 19 Mar 2023 07:42:10 +0100 Subject: [PATCH 2/9] Added BlockCypher option --- api/btc.ts | 19 ++++++++++++------- types/network.ts | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/api/btc.ts b/api/btc.ts index a67d5a3d..693bb7f3 100644 --- a/api/btc.ts +++ b/api/btc.ts @@ -16,14 +16,18 @@ export async function fetchBtcTransactionData( network: NetworkType ): Promise { const apiUrl = { - Mainnet: `https://api.blockcypher.com/v1/btc/main/txs/${tx_hash}`, - Testnet: `https://api.blockcypher.com/v1/btc/test3/txs/${tx_hash}`, - Regtest: `https://api.blockcypher.com/v1/bcy/test/txs/${tx_hash}`, + Mainnet: `https://api.blockcypher.com/v1/btc/main/txs/${tx_hash}?includeHex=true`, + Testnet: `https://api.blockcypher.com/v1/btc/test3/txs/${tx_hash}?includeHex=true`, + BlockCypher: `https://api.blockcypher.com/v1/bcy/test/txs/${tx_hash}?includeHex=true`, }; + console.log('xverse-core/btc.ts/fetchBtcTransactionData | tx_hash: ', tx_hash); + console.log('xverse-core/btc.ts/fetchBtcTransactionData | network: ', network); + console.log('xverse-core/btc.ts/fetchBtcTransactionData | url: ', apiUrl[network]) return axios .get(apiUrl[network], { headers: { 'Access-Control-Allow-Origin': '*' } }) .then((response) => { + console.log('xverse-core/btc.ts/fetchBtcTransactionData | BtcTransactionData: ', response); const rawTx: string = response.data.hex; return rawTx; }); @@ -36,7 +40,7 @@ export async function fetchBtcAddressUnspent( const apiUrl = { Mainnet: `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}?unspentOnly=true&limit=50`, Testnet: `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}?unspentOnly=true&limit=50`, - Regtest: `https://api.blockcypher.com/v1/bcy/test/assrs/${btcAddress}?unspentOnly=true&limit=50`, + BlockCypher: `https://api.blockcypher.com/v1/bcy/test/assrs/${btcAddress}?unspentOnly=true&limit=50`, }; return axios.get(apiUrl[network], { timeout: 45000 }).then((response) => { @@ -58,7 +62,7 @@ export async function fetchPoolBtcAddressBalance( const apiUrl = { Mainnet: `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}`, Testnet: `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}`, - Regtest: `https://api.blockcypher.com/v1/bcy/test/addrs/${btcAddress}`, + BlockCypher: `https://api.blockcypher.com/v1/bcy/test/addrs/${btcAddress}`, }; return axios @@ -80,12 +84,13 @@ export async function broadcastRawBtcTransaction( const apiUrl = { Mainnet: 'https://api.blockcypher.com/v1/btc/main/txs/push', Testnet: 'https://api.blockcypher.com/v1/btc/test3/txs/push', - Regtest: 'https://api.blockcypher.com/v1/bcy/test/txs/push', + BlockCypher: 'https://api.blockcypher.com/v1/bcy/test/txs/push', }; const data = { tx: rawTx, }; + console.log('BTCTRANSACTION DATA: ', data) return axios .post(apiUrl[network], data, { timeout: 45000 }) .then((response) => { @@ -100,7 +105,7 @@ export async function fetchBtcTransactionsData( const apiUrl = { Mainnet: `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`, Testnet: `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`, - Regtest: `https://api.blockcypher.com/v1/bcy/test/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`, + BlockCypher: `https://api.blockcypher.com/v1/bcy/test/addrs/${btcAddress}/full?includeHex=true&txlimit=3000&limit=50`, }; return axios diff --git a/types/network.ts b/types/network.ts index 2a5fc3da..43204670 100644 --- a/types/network.ts +++ b/types/network.ts @@ -1,6 +1,6 @@ export { StacksNetwork, StacksMainnet, StacksTestnet } from '@stacks/network'; -export type NetworkType = 'Mainnet' | 'Testnet' | 'Regtest'; +export type NetworkType = 'Mainnet' | 'Testnet' | 'BlockCypher'; export type SettingsNetwork = { type: NetworkType; From 81074eb72fcd08d2f45a71188aca374b27a08436 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 22 Mar 2023 15:55:46 +0100 Subject: [PATCH 3/9] Changed nested segwit addresses to native segwit ones for demo purposes --- wallet/index.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/wallet/index.ts b/wallet/index.ts index 88251dff..d75c8d5f 100644 --- a/wallet/index.ts +++ b/wallet/index.ts @@ -86,14 +86,22 @@ export async function walletFromSeedPhrase({ const btcChild = master.derivePath(getBitcoinDerivationPath({ index, network })); const keyPair = ECPair.fromPrivateKey(btcChild.privateKey!); - const segwitBtcAddress = payments.p2sh({ - redeem: payments.p2wpkh({ - pubkey: keyPair.publicKey, - network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, - }), + + // Current nested segwit address they support + // const segwitBtcAddress = payments.p2wpkh({ + // redeem: payments.p2sh({ + // pubkey: keyPair.publicKey, + // network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, + // }), + // pubkey: keyPair.publicKey, + // network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, + // }); + + const segwitBtcAddress = payments.p2wpkh({ pubkey: keyPair.publicKey, network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, }); + const btcAddress = segwitBtcAddress.address!; const btcPublicKey = keyPair.publicKey.toString('hex'); return { @@ -106,6 +114,7 @@ export async function walletFromSeedPhrase({ }; } + function getBitcoinDerivationPath({ index, network }: { index: BigInt; network: NetworkType }) { return network === 'Mainnet' ? `${BTC_PATH_WITHOUT_INDEX}${index.toString()}` From 53bf4024acb761d99b3e45c07a8629db7e988c33 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 28 Mar 2023 12:03:30 +0200 Subject: [PATCH 4/9] Added dlcBtcAddress to use native segwit address --- account/index.ts | 4 +++- types/account.ts | 3 ++- types/wallet.ts | 1 + wallet/index.ts | 24 +++++++++++++----------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/account/index.ts b/account/index.ts index c25e9607..3f8b70f2 100644 --- a/account/index.ts +++ b/account/index.ts @@ -67,6 +67,7 @@ export async function restoreWalletWithAccounts( id: index, stxAddress: response.stxAddress, btcAddress: response.btcAddress, + dlcBtcAddress: response.dlcBtcAddress, masterPubKey: response.masterPubKey, stxPublicKey: response.stxPublicKey, btcPublicKey: response.btcPublicKey, @@ -94,7 +95,7 @@ export async function createWalletAccount( walletAccounts: Account[], ): Promise { const accountIndex = walletAccounts.length; - const { stxAddress, btcAddress, masterPubKey, stxPublicKey, btcPublicKey } = + const { stxAddress, btcAddress, dlcBtcAddress, masterPubKey, stxPublicKey, btcPublicKey } = await walletFromSeedPhrase({ mnemonic: seedPhrase, index: BigInt(accountIndex), @@ -107,6 +108,7 @@ export async function createWalletAccount( id: accountIndex, stxAddress, btcAddress, + dlcBtcAddress, masterPubKey, stxPublicKey, btcPublicKey, diff --git a/types/account.ts b/types/account.ts index f7dda937..df0d8692 100644 --- a/types/account.ts +++ b/types/account.ts @@ -2,8 +2,9 @@ export interface Account { id: number; stxAddress: string; btcAddress: string; + dlcBtcAddress: string; masterPubKey: string; stxPublicKey: string; btcPublicKey: string; bnsName?: string; - } \ No newline at end of file + } diff --git a/types/wallet.ts b/types/wallet.ts index 25f30c0d..3559265b 100644 --- a/types/wallet.ts +++ b/types/wallet.ts @@ -1,6 +1,7 @@ export interface BaseWallet { stxAddress: string; btcAddress: string; + dlcBtcAddress: string; masterPubKey: string; stxPublicKey: string; btcPublicKey: string; diff --git a/wallet/index.ts b/wallet/index.ts index d75c8d5f..54193dcf 100644 --- a/wallet/index.ts +++ b/wallet/index.ts @@ -88,25 +88,27 @@ export async function walletFromSeedPhrase({ const keyPair = ECPair.fromPrivateKey(btcChild.privateKey!); // Current nested segwit address they support - // const segwitBtcAddress = payments.p2wpkh({ - // redeem: payments.p2sh({ - // pubkey: keyPair.publicKey, - // network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, - // }), - // pubkey: keyPair.publicKey, - // network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, - // }); - - const segwitBtcAddress = payments.p2wpkh({ + const nestedSegwitBtcAddress = payments.p2sh({ + redeem: payments.p2wpkh({ + pubkey: keyPair.publicKey, + network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, + }), pubkey: keyPair.publicKey, network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, }); - const btcAddress = segwitBtcAddress.address!; + const nativeSegwitBtcAddress = payments.p2wpkh({ + pubkey: keyPair.publicKey, + network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, + }); + + const btcAddress = nestedSegwitBtcAddress.address!; + const dlcBtcAddress = nativeSegwitBtcAddress.address!; const btcPublicKey = keyPair.publicKey.toString('hex'); return { stxAddress, btcAddress, + dlcBtcAddress, masterPubKey, stxPublicKey, btcPublicKey, From cb8dab9b2093e0f92f0cf55a0bfb7ad9fd5b48d9 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 28 Mar 2023 15:30:17 +0200 Subject: [PATCH 5/9] Updated package-lock.json --- .DS_Store | Bin 0 -> 6148 bytes package-lock.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9498e50bca478ef0f2ba7e5825a2b5c01a58d1cb GIT binary patch literal 6148 zcmeH~F^&@<^#q<|Dyh64V5D0F8{w$Au;FvJKz4lIXp9kT>kyg=4u>tuyyIXzgm zT8trHk9M-;bv4;Kdpj(L56e57Pcby>?XbdxW;LK71*E`CfmP2(KmYgizvlm0i&7~d z1>Q^n8+M1?mM@iO>&xqT{g_o>H#!-YGd%qSF!7^!Ll5JA@da6vt& Date: Wed, 29 Mar 2023 13:55:06 +0200 Subject: [PATCH 6/9] Removed unnecessary console logs --- api/btc.ts | 87 ++++++++++++++++++++------------- types/api/blockcypher/wallet.ts | 12 +++-- wallet/index.ts | 73 ++++++++++++++++++++------- 3 files changed, 117 insertions(+), 55 deletions(-) diff --git a/api/btc.ts b/api/btc.ts index 1949175a..ca710e9f 100644 --- a/api/btc.ts +++ b/api/btc.ts @@ -10,6 +10,7 @@ import { BtcOrdinal, BtcBalance, BtcTransactionDataResponse, + BtcTransactionDataHexIncluded, } from '../types/api/blockcypher/wallet'; import { NetworkType } from '../types/network'; import { parseBtcTransactionData, parseOrdinalsBtcTransactions } from './helper'; @@ -42,16 +43,14 @@ export async function fetchPoolBtcAddressBalance( btcAddress: string, network: NetworkType ): Promise { - const apiUrl = { - Mainnet: `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}`, - Testnet: `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}`, - BlockCypher: `https://api.blockcypher.com/v1/bcy/test/addrs/${btcAddress}`, - }; - + const btcApiBaseUrl = 'https://api.blockcypher.com/v1/btc/main/addrs/'; + const btcApiBaseUrlTestnet = 'https://api.blockcypher.com/v1/btc/test3/addrs/'; + let apiUrl = `${btcApiBaseUrl}${btcAddress}`; + if (network === 'Testnet') { + apiUrl = `${btcApiBaseUrlTestnet}${btcAddress}`; + } return axios - .get(apiUrl[network], { - headers: { 'Access-Control-Allow-Origin': '*' }, - }) + .get(apiUrl, { headers: { 'Access-Control-Allow-Origin': '*' } }) .then((response) => { const btcPoolData: BtcBalance = { balance: response.data.final_balance, @@ -105,31 +104,53 @@ export async function getBtcWalletData( }); } -export async function fetchBtcTransactionData(txHash: string, btcAddress: string, ordinalsAddress: string): Promise { +export async function fetchBtcTransactionData( + txHash: string, + btcAddress: string, + ordinalsAddress: string +): Promise { const txDataApiUrl = `https://api.blockcypher.com/v1/btc/main/txs/${txHash}`; const response = await axios.get(txDataApiUrl); return parseBtcTransactionData(response.data, btcAddress, ordinalsAddress); } -export async function fetchBtcOrdinalTransactions( - ordinalsAddress: string, +export async function fetchBtcTransactionRawData( + tx_hash: string, network: NetworkType -) { - const btcApiBaseUrl = `https://api.blockcypher.com/v1/btc/main/addrs/${ordinalsAddress}/full?txlimit=3000`; - const btcApiBaseUrlTestnet = `https://api.blockcypher.com/v1/btc/test3/addrs/${ordinalsAddress}/full?txlimit=3000`; - let apiUrl = btcApiBaseUrl; - if (network === 'Testnet') { - apiUrl = btcApiBaseUrlTestnet; - } - return axios.get(apiUrl, { timeout: 45000 }).then((response) => { - const transactions: BtcTransactionData[] = []; - response.data.txs.forEach((tx) => { - transactions.push(parseOrdinalsBtcTransactions(tx, ordinalsAddress)); - }); - return transactions.filter((tx) => tx.incoming); +): Promise { + const apiUrl = { + Mainnet: `https://api.blockcypher.com/v1/btc/main/txs/${tx_hash}?includeHex=true`, + Testnet: `https://api.blockcypher.com/v1/btc/test3/txs/${tx_hash}?includeHex=true`, + BlockCypher: `https://api.blockcypher.com/v1/bcy/test/txs/${tx_hash}?includeHex=true`, + }; + + return axios + .get(apiUrl[network], { + headers: { 'Access-Control-Allow-Origin': '*' }, + }) + .then((response) => { + console.log('xverse-core/btc.ts/fetchBtcTransactionData | BtcTransactionData: ', response); + const rawTx = response.data.hex; + return rawTx; }); } +export async function fetchBtcOrdinalTransactions(ordinalsAddress: string, network: NetworkType) { + const btcApiBaseUrl = `https://api.blockcypher.com/v1/btc/main/addrs/${ordinalsAddress}/full?txlimit=3000`; + const btcApiBaseUrlTestnet = `https://api.blockcypher.com/v1/btc/test3/addrs/${ordinalsAddress}/full?txlimit=3000`; + let apiUrl = btcApiBaseUrl; + if (network === 'Testnet') { + apiUrl = btcApiBaseUrlTestnet; + } + return axios.get(apiUrl, { timeout: 45000 }).then((response) => { + const transactions: BtcTransactionData[] = []; + response.data.txs.forEach((tx) => { + transactions.push(parseOrdinalsBtcTransactions(tx, ordinalsAddress)); + }); + return transactions.filter((tx) => tx.incoming); + }); +} + export async function fetchBtcPaymentTransactions( btcAddress: string, ordinalsAddress: string, @@ -154,7 +175,7 @@ export async function fetchBtcTransactionsData( btcAddress: string, ordinalsAddress: string, network: NetworkType, - withOrdinals: boolean, + withOrdinals: boolean ): Promise { const btcApiBaseUrl = `https://api.blockcypher.com/v1/btc/main/addrs/${btcAddress}/full?txlimit=3000`; const btcApiBaseUrlTestnet = `https://api.blockcypher.com/v1/btc/test3/addrs/${btcAddress}/full?txlimit=3000`; @@ -163,13 +184,13 @@ export async function fetchBtcTransactionsData( apiUrl = btcApiBaseUrlTestnet; } if (withOrdinals) { - const ordinalsTransactions = await fetchBtcOrdinalTransactions(ordinalsAddress, network); - const paymentTransactions = await fetchBtcPaymentTransactions( - btcAddress, - ordinalsAddress, - network - ); - return [...new Set([...paymentTransactions, ...ordinalsTransactions])]; + const ordinalsTransactions = await fetchBtcOrdinalTransactions(ordinalsAddress, network); + const paymentTransactions = await fetchBtcPaymentTransactions( + btcAddress, + ordinalsAddress, + network + ); + return [...new Set([...paymentTransactions, ...ordinalsTransactions])]; } const paymentTransactions = await fetchBtcPaymentTransactions( btcAddress, diff --git a/types/api/blockcypher/wallet.ts b/types/api/blockcypher/wallet.ts index 420b3a23..285b75b9 100644 --- a/types/api/blockcypher/wallet.ts +++ b/types/api/blockcypher/wallet.ts @@ -1,4 +1,4 @@ -import { TransactionData } from "../xverse/transaction"; +import { TransactionData } from '../xverse/transaction'; export type BtcUtxoDataResponse = { tx_hash: string; @@ -53,7 +53,6 @@ export interface BtcTransactionDataResponse { outputs: Output[]; } - export interface BtcTransactionData extends TransactionData { blockHash: string; blockHeight: string; @@ -79,6 +78,10 @@ export interface BtcTransactionData extends TransactionData { isOrdinal: boolean; } +export interface BtcTransactionDataHexIncluded extends BtcTransactionData { + hex: string; +} + export interface Input { addresses: string[]; output_index: number; @@ -91,7 +94,6 @@ export interface Output { value: number; } - export interface BtcTransactionBroadcastResponse { tx: { hash: string; @@ -151,5 +153,5 @@ export interface BtcTransactionsDataResponse { export interface BtcOrdinal { id: string; utxo: BtcUtxoDataResponse; - confirmationTime: string, -} \ No newline at end of file + confirmationTime: string; +} diff --git a/wallet/index.ts b/wallet/index.ts index 7ae999f4..69f97402 100644 --- a/wallet/index.ts +++ b/wallet/index.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import * as bip39 from 'bip39'; -import { hashMessage } from "@stacks/encryption"; +import { hashMessage } from '@stacks/encryption'; import { BTC_PATH_WITHOUT_INDEX, BTC_TESTNET_PATH_WITHOUT_INDEX, @@ -10,7 +10,7 @@ import { STX_PATH_WITHOUT_INDEX, BTC_WRAPPED_SEGWIT_PATH_PURPOSE, BTC_SEGWIT_PATH_PURPOSE, - BTC_TAPROOT_PATH_PURPOSE + BTC_TAPROOT_PATH_PURPOSE, } from '../constant'; import { ChainID, @@ -28,10 +28,10 @@ import { ecPairToHexString } from './helper'; import { Keychain } from 'types/api/xverse/wallet'; import { BaseWallet } from 'types/wallet'; import { deriveWalletConfigKey } from '../gaia'; -import {validate, Network as btcAddressNetwork} from 'bitcoin-address-validation'; +import { validate, Network as btcAddressNetwork } from 'bitcoin-address-validation'; import * as btc from 'micro-btc-signer'; import { hex } from '@scure/base'; -import * as secp256k1 from '@noble/secp256k1' +import * as secp256k1 from '@noble/secp256k1'; import { getBtcNetwork } from '../transactions/btcNetwork'; export const derivationPaths = { @@ -83,7 +83,7 @@ export async function walletFromSeedPhrase({ network === 'Mainnet' ? ChainID.Mainnet : ChainID.Testnet, index ); - + const { address, privateKey } = deriveStxAddressKeychain(rootNode); const stxAddress = address; @@ -96,7 +96,7 @@ export async function walletFromSeedPhrase({ const keyPair = ECPair.fromPrivateKey(btcChild.privateKey!); // derive native segwit btc address - const nativeSegwitBtcChild = master.derivePath(getSegwitDerivationPath({ index, network})); + const nativeSegwitBtcChild = master.derivePath(getSegwitDerivationPath({ index, network })); const nativeSegwitBtcAddressKeypair = ECPair.fromPrivateKey(nativeSegwitBtcChild.privateKey!); const nativeSegwitBtcAddress = payments.p2wpkh({ @@ -140,25 +140,49 @@ export async function walletFromSeedPhrase({ }; } -export function getBitcoinDerivationPath({ account, index, network }: { account?: BigInt, index: BigInt; network: NetworkType }) { - const accountIndex = account ? account.toString() : '0' +export function getBitcoinDerivationPath({ + account, + index, + network, +}: { + account?: BigInt; + index: BigInt; + network: NetworkType; +}) { + const accountIndex = account ? account.toString() : '0'; return network === 'Mainnet' ? `${BTC_WRAPPED_SEGWIT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` - : `${BTC_WRAPPED_SEGWIT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}` + : `${BTC_WRAPPED_SEGWIT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; } -export function getSegwitDerivationPath({ account, index, network }: { account?: BigInt, index: BigInt; network: NetworkType }) { - const accountIndex = account ? account.toString() : '0' +export function getSegwitDerivationPath({ + account, + index, + network, +}: { + account?: BigInt; + index: BigInt; + network: NetworkType; +}) { + const accountIndex = account ? account.toString() : '0'; return network === 'Mainnet' ? `${BTC_SEGWIT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` - : `${BTC_SEGWIT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}` + : `${BTC_SEGWIT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; } -export function getTaprootDerivationPath({ account, index, network }: { account?: BigInt, index: BigInt; network: NetworkType }) { - const accountIndex = account ? account.toString() : '0' +export function getTaprootDerivationPath({ + account, + index, + network, +}: { + account?: BigInt; + index: BigInt; + network: NetworkType; +}) { + const accountIndex = account ? account.toString() : '0'; return network === 'Mainnet' ? `${BTC_TAPROOT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` - : `${BTC_TAPROOT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}` + : `${BTC_TAPROOT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; } export async function getBtcPrivateKey({ @@ -193,6 +217,22 @@ export async function getBtcTaprootPrivateKey({ return btcChild.privateKey!.toString('hex'); } +export async function getBtcNativeSegwitPrivateKey({ + seedPhrase, + index, + network, +}: { + seedPhrase: string; + index: BigInt; + network: NetworkType; +}): Promise { + const seed = await bip39.mnemonicToSeed(seedPhrase); + const master = bip32.fromSeed(seed); + + const nativeSegwitBtcChild = master.derivePath(getSegwitDerivationPath({ index, network })); + return nativeSegwitBtcChild.privateKey!.toString('hex'); +} + export function validateStxAddress({ stxAddress, network, @@ -237,8 +277,7 @@ export function validateBtcAddress({ btcAddress: string; network: NetworkType; }): boolean { - const btcNetwork = - network === 'Mainnet' ? btcAddressNetwork.mainnet : btcAddressNetwork.testnet; + const btcNetwork = network === 'Mainnet' ? btcAddressNetwork.mainnet : btcAddressNetwork.testnet; try { return validate(btcAddress, btcNetwork); } catch (error) { From 4d4244ef4a3bc08c58aaee1f461f71343519d2c8 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 19 Apr 2023 11:49:23 +0200 Subject: [PATCH 7/9] Added getRawTransaction function to esplora --- api/esplora/esploraAPiProvider.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/esplora/esploraAPiProvider.ts b/api/esplora/esploraAPiProvider.ts index 03f4f742..0e03b956 100644 --- a/api/esplora/esploraAPiProvider.ts +++ b/api/esplora/esploraAPiProvider.ts @@ -103,6 +103,11 @@ export default class BitcoinEsploraApiProvider extends ApiInstance implements Bi }; } + async getRawTransaction(txHash: string): Promise { + const data: string = await this.httpGet(`/tx/${txHash}/hex`); + return data; + } + async getLatestBlockHeight(): Promise { const data: number = await this.httpGet('/blocks/tip/height'); return data; From 87b4d3bff99d9cdd3c21a725d0a5c808a0d60321 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 24 Apr 2023 18:02:20 +0200 Subject: [PATCH 8/9] Added regtest support --- api/esplora/esploraAPiProvider.ts | 11 +++++++---- constant.ts | 2 ++ transactions/btcNetwork.ts | 9 ++++++++- types/network.ts | 2 +- wallet/index.ts | 21 ++++++++++++++++++--- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/api/esplora/esploraAPiProvider.ts b/api/esplora/esploraAPiProvider.ts index 0e03b956..55531a6c 100644 --- a/api/esplora/esploraAPiProvider.ts +++ b/api/esplora/esploraAPiProvider.ts @@ -3,7 +3,7 @@ import { BtcAddressBalanceResponse, BtcTransactionBroadcastResponse, } from '../../types/api/blockcypher/wallet'; -import { BTC_BASE_URI_MAINNET, BTC_BASE_URI_TESTNET } from '../../constant'; +import { BTC_BASE_URI_MAINNET, BTC_BASE_URI_TESTNET, BTC_BASE_URI_REGTEST } from '../../constant'; import { NetworkType } from '../../types/network'; import * as esplora from '../../types/api/esplora'; import { BitcoinApiProvider } from './types'; @@ -45,7 +45,11 @@ export default class BitcoinEsploraApiProvider extends ApiInstance implements Bi constructor(options: EsploraApiProviderOptions) { const { url, network } = options; super({ - baseURL: url || network == 'Mainnet' ? BTC_BASE_URI_MAINNET : BTC_BASE_URI_TESTNET, + baseURL: url || network === 'Mainnet' + ? BTC_BASE_URI_MAINNET + : network === 'Testnet' + ? BTC_BASE_URI_TESTNET + : BTC_BASE_URI_REGTEST }); this._network = network; } @@ -77,8 +81,7 @@ export default class BitcoinEsploraApiProvider extends ApiInstance implements Bi } async getUnspentUtxos(address: string): Promise { - const utxoSets = await this._getUnspentTransactions(address); - return utxoSets; + return await this._getUnspentTransactions(address); } async _getAddressTransactionCount(address: string) { diff --git a/constant.ts b/constant.ts index 49bfcce8..eef39a70 100644 --- a/constant.ts +++ b/constant.ts @@ -26,6 +26,8 @@ export const BTC_BASE_URI_MAINNET = 'https://mempool.space/api'; export const BTC_BASE_URI_TESTNET = 'https://mempool.space/testnet/api'; +export const BTC_BASE_URI_REGTEST = 'https://dev-oracle.dlc.link/electrs'; + export const NFT_BASE_URI = 'https://gamma.io/api/v1/collections'; export const XVERSE_API_BASE_URL = 'https://api.xverse.app'; diff --git a/transactions/btcNetwork.ts b/transactions/btcNetwork.ts index 82fb7898..18793e99 100644 --- a/transactions/btcNetwork.ts +++ b/transactions/btcNetwork.ts @@ -21,12 +21,19 @@ const bitcoinTestnet: BitcoinNetwork = { wif: 0xef, }; +const bitcoinRegtest: BitcoinNetwork = { + bech32: 'bcrt', + pubKeyHash: 0x6f, + scriptHash: 0xc4, + wif: 0xef, +}; + export const bitcoinNetworks: Record = { Mainnet: bitcoinMainnet, Testnet: bitcoinTestnet, + Regtest: bitcoinRegtest, }; export const getBtcNetwork = (networkType: NetworkType) => { return bitcoinNetworks[networkType]; }; - diff --git a/types/network.ts b/types/network.ts index a7545c30..48903bb1 100644 --- a/types/network.ts +++ b/types/network.ts @@ -1,6 +1,6 @@ export { StacksNetwork, StacksMainnet, StacksTestnet } from '@stacks/network'; -export type NetworkType = 'Mainnet' | 'Testnet' ; +export type NetworkType = 'Mainnet' | 'Testnet' | 'Regtest'; export type SettingsNetwork = { type: NetworkType; diff --git a/wallet/index.ts b/wallet/index.ts index 9b33c588..2b3e1847 100644 --- a/wallet/index.ts +++ b/wallet/index.ts @@ -73,6 +73,21 @@ export async function walletFromSeedPhrase({ }): Promise { const seed = await bip39.mnemonicToSeed(mnemonic); const rootNode = bip32.fromSeed(Buffer.from(seed)); + let bitcoinNetwork: networks.Network; + + switch (network) { + case 'Mainnet': + bitcoinNetwork = networks.bitcoin; + break; + case 'Testnet': + bitcoinNetwork = networks.testnet; + break; + case 'Regtest': + bitcoinNetwork = networks.regtest; + break; + default: + throw new Error('Invalid network provided.'); + } const deriveStxAddressKeychain = deriveStxAddressChain( network === 'Mainnet' ? ChainID.Mainnet : ChainID.Testnet, @@ -96,7 +111,7 @@ export async function walletFromSeedPhrase({ const nativeSegwitBtcAddress = payments.p2wpkh({ pubkey: nativeSegwitBtcAddressKeypair.publicKey, - network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, + network: bitcoinNetwork, }); const dlcBtcAddress = nativeSegwitBtcAddress.address!; @@ -111,10 +126,10 @@ export async function walletFromSeedPhrase({ const segwitBtcAddress = payments.p2sh({ redeem: payments.p2wpkh({ pubkey: keyPair.publicKey, - network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, + network: bitcoinNetwork, }), pubkey: keyPair.publicKey, - network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, + network: bitcoinNetwork, }); const btcAddress = segwitBtcAddress.address!; From 10d087622caa21f6646661626e32a0992831c55c Mon Sep 17 00:00:00 2001 From: Mahmoud Date: Fri, 26 May 2023 13:36:39 +0300 Subject: [PATCH 9/9] refactored native segwit address derivation --- account/index.ts | 12 +- connect/index.ts | 8 +- constant.ts | 2 +- tests/btc/psbt.test.ts | 10 +- tests/btc/transactions.test.ts | 2 +- tests/mocks/restore.mock.ts | 36 ++++- transactions/btc.ts | 2 +- transactions/psbt.ts | 4 +- transactions/stx.ts | 2 +- types/account.ts | 4 +- types/wallet.ts | 4 +- wallet/helper.ts | 44 +++++- wallet/index.ts | 272 +++------------------------------ wallet/utils/btc.ts | 115 ++++++++++++++ wallet/utils/stx.ts | 88 +++++++++++ 15 files changed, 329 insertions(+), 276 deletions(-) create mode 100644 wallet/utils/btc.ts create mode 100644 wallet/utils/stx.ts diff --git a/account/index.ts b/account/index.ts index bb7f39af..c659bef9 100644 --- a/account/index.ts +++ b/account/index.ts @@ -90,8 +90,8 @@ export async function restoreWalletWithAccounts( id: index, stxAddress: response.stxAddress, btcAddress: response.btcAddress, - dlcBtcAddress: response.dlcBtcAddress, - dlcBtcPublicKey: response.dlcBtcPublicKey, + mainBtcAddress: response.mainBtcAddress, + mainBtcPublicKey: response.mainBtcPublicKey, ordinalsAddress: response.ordinalsAddress, masterPubKey: response.masterPubKey, stxPublicKey: response.stxPublicKey, @@ -125,12 +125,12 @@ export async function createWalletAccount( stxAddress, btcAddress, ordinalsAddress, - dlcBtcAddress, + mainBtcAddress, masterPubKey, stxPublicKey, btcPublicKey, ordinalsPublicKey, - dlcBtcPublicKey, + mainBtcPublicKey, } = await walletFromSeedPhrase({ mnemonic: seedPhrase, index: BigInt(accountIndex), @@ -143,8 +143,8 @@ export async function createWalletAccount( id: accountIndex, stxAddress, btcAddress, - dlcBtcAddress, - dlcBtcPublicKey, + mainBtcAddress, + mainBtcPublicKey, ordinalsAddress, masterPubKey, stxPublicKey, diff --git a/connect/index.ts b/connect/index.ts index 36187a7a..bb7eef9c 100644 --- a/connect/index.ts +++ b/connect/index.ts @@ -1,10 +1,12 @@ import { createSha2Hash } from '@stacks/encryption'; import { ChainID } from '@stacks/transactions'; import { makeAuthResponse } from '@stacks/wallet-sdk'; -import { GAIA_HUB_URL } from '../constant'; -import { deriveStxAddressChain } from '../wallet/index'; import * as bip39 from 'bip39'; import { bip32 } from 'bitcoinjs-lib'; +import { hashMessage } from '@stacks/encryption'; +import { deriveStxAddressChain } from '../wallet/utils/stx'; +import { GAIA_HUB_URL } from '../constant'; + export async function createAuthResponse( seedPhrase: string, @@ -50,3 +52,5 @@ export async function createAuthResponse( return; } + +export { hashMessage }; \ No newline at end of file diff --git a/constant.ts b/constant.ts index 7db3c007..a932acc0 100644 --- a/constant.ts +++ b/constant.ts @@ -6,7 +6,7 @@ export const BTC_PATH = `m/49'/0'/0'/0/0`; export const BTC_WRAPPED_SEGWIT_PATH_PURPOSE = `m/49'/`; -export const BTC_SEGWIT_PATH_PURPOSE = `m/84'/`; +export const BTC_SEGWIT_PATH_PURPOSE = `m/84'/0'`; export const BTC_TAPROOT_PATH_PURPOSE = `m/86'/`; diff --git a/tests/btc/psbt.test.ts b/tests/btc/psbt.test.ts index 46b50ada..a77ddd45 100644 --- a/tests/btc/psbt.test.ts +++ b/tests/btc/psbt.test.ts @@ -18,9 +18,11 @@ describe('Bitcoin PSBT tests', () => { const nativeSegwitAddress2 = 'bc1q8agphg8kkn8ndvd5am8f44n3uzedcuaz437qdu'; const taprootAddress2 = 'bc1pzsm9pu47e7npkvxh9dcd0dc2qwqshxt2a9tt7aq3xe9krpl8e82sx6phdj'; + const wrappedSegwitAddress3 = '3Gve89xYfW9RZRgRdN7hzCjXAHMDc7QRDf'; const taprootAddress3 = 'bc1pyzfhlkq29sylwlv72ve52w8mn7hclefzhyay3dxh32r0322yx6uqajvr3y'; + const accounts = [ { @@ -28,6 +30,8 @@ describe('Bitcoin PSBT tests', () => { stxAddress: 'STXADDRESS1', btcAddress: wrappedSegwitAddress1, ordinalsAddress: taprootAddress1, + mainBtcAddress: '', + mainBtcPublicKey: '', masterPubKey: '12345', stxPublicKey: '123', btcPublicKey: '123', @@ -38,6 +42,8 @@ describe('Bitcoin PSBT tests', () => { stxAddress: 'STXADDRESS2', btcAddress: nativeSegwitAddress2, ordinalsAddress: taprootAddress2, + mainBtcAddress: '', + mainBtcPublicKey: '', masterPubKey: '12345', stxPublicKey: '123', btcPublicKey: '123', @@ -48,6 +54,8 @@ describe('Bitcoin PSBT tests', () => { stxAddress: 'STXADDRESS3', btcAddress: wrappedSegwitAddress3, ordinalsAddress: taprootAddress3, + mainBtcAddress: '', + mainBtcPublicKey: '', masterPubKey: '12345', stxPublicKey: '123', btcPublicKey: '123', @@ -77,7 +85,7 @@ describe('Bitcoin PSBT tests', () => { 'Mainnet' ); - expect(nativeSegwitPath).eq(`m/84'/0'/0'/0/1`); + expect(nativeSegwitPath).eq(`m/84'/0'/1'/0/0`); const taprootPath = getSigningDerivationPath( accounts, diff --git a/tests/btc/transactions.test.ts b/tests/btc/transactions.test.ts index 3ca72690..7cfd489c 100644 --- a/tests/btc/transactions.test.ts +++ b/tests/btc/transactions.test.ts @@ -8,7 +8,7 @@ import { getBtcFees, getBtcFeesForOrdinalSend, } from '../../transactions/btc'; -import { getBtcPrivateKey } from '../../wallet'; +import { getBtcPrivateKey } from '../../wallet/utils/btc'; import { testSeed } from '../mocks/restore.mock'; import { UTXO } from '../../types'; import BigNumber from 'bignumber.js'; diff --git a/tests/mocks/restore.mock.ts b/tests/mocks/restore.mock.ts index af42214b..bc28be5c 100644 --- a/tests/mocks/restore.mock.ts +++ b/tests/mocks/restore.mock.ts @@ -1,7 +1,9 @@ -import { Account } from "../../types"; +import { Account } from '../../types'; -export const testSeed = 'force kite borrow once shine pluck couch swift crystal swamp crumble essay'; +export const testSeed = + 'force kite borrow once shine pluck couch swift crystal swamp crumble essay'; + export const walletAccounts: Account[] = [ { id: 0, @@ -10,6 +12,8 @@ export const walletAccounts: Account[] = [ btcPublicKey: '032215d812282c0792c8535c3702cca994f5e3da9cd8502c3e190d422f0066fdff', ordinalsAddress: 'bc1pr09enf3yc43cz8qh7xwaasuv3xzlgfttdr3wn0q2dy9frkhrpdtsk05jqq', ordinalsPublicKey: '5b21869d6643175e0530aeec51d265290d036384990ee60bf089b23ff6b9a367', + mainBtcAddress: 'bc1qf8njhm2nj48x9kltxvmc7vyl9cq7raukwg6mjk', + mainBtcPublicKey: '023537a32d5ab338a6ba52f13708ea45c1e3cb33c26aff3fa182d9c66fd4b636ff', stxAddress: 'SP147ST7ESA3RES888QQMV6AK7GZK93ZR74A0GM7V', stxPublicKey: '025df9b0ea2c81e4f8360bf9a16638ed3678bc84dbdc04124f5db86996999aa9a8', }, @@ -18,11 +22,35 @@ export const walletAccounts: Account[] = [ masterPubKey: '024d30279814a0e609534af1d1969b7c24a6918029e1f9cb2134a427ebfb1f17c3', btcAddress: '3EMRvkWMLaUfzHPA7Un5qfLZDvbXHn385u', btcPublicKey: '022e633aba8838c039b2d2214f51ed284d3da7f585744f8975606376c23483d2c1', + mainBtcAddress: 'bc1qgxk5z97gempeav0xdrdcezhn9att7dev6gavyc', + mainBtcPublicKey: '03af366875dc848b7d8306b13c872f448791b7f7fbc8948e4452a07a74bff5f035', ordinalsAddress: 'bc1pnc669rz0hyncjzxdgfeqm0dfhfr84ues4dwgq4lr47zpltzvekss4ptlxw', ordinalsPublicKey: '380447c41546e736f3d4bf9dc075d2301f5252f33156e3564fd393eeffdaa347', stxAddress: 'SP1BKESAFFV8ACW007HACXB93VHRFHP83BT24Z3NF', stxPublicKey: '0302ec9c40f8d5daf319bf4b1556c7f51f1eb449dd96d05e7ed42a1056451dd656', }, + { + id: 2, + masterPubKey: '024d30279814a0e609534af1d1969b7c24a6918029e1f9cb2134a427ebfb1f17c3', + btcAddress: '3DUdrysnoh9CvMdtWhavTqRXhFvzg3aKNQ', + btcPublicKey: '036b7ca83c456a5b577d5631cd38c76c2d0540c25e6787bb4394001aebaea42d20', + mainBtcAddress: 'bc1q3xkz9q3fsxt6pxjxaq5w7yf6mhh4zxcu5d2fev', + mainBtcPublicKey: '03eb31409ecdcf37688b7d7ce1e915d8660208704695298b701a5652b05856d879', + ordinalsAddress: 'bc1pgu2hx6vkp0yja2sdeugkzs3xlxdwnj0wrasqletv8v98cn0vntesa2h8zv', + ordinalsPublicKey: 'ee3ad253e6696962b43ca3dd83a40dc3184e11ff0679f972a84704be36bc9225', + stxAddress: 'SP2BCTPVVHWN2VA8N85GRXJAPS10NR5YNFC6SY5SR', + stxPublicKey: '0268c588454446d214cbf7c7b0e970ba8cb3425f8976a045eed7f98112db7e1942', + }, + { + btcAddress: '35xwHGuXcn4ufM2KDttL2w5PPsT9DRSoR6', + btcPublicKey: '03289a3a4d3b28565f5b5459d1570d8954bc08d09fb8d276154ff9cfeb0447bd10', + id: 3, + mainBtcAddress: 'bc1q85l49tvgpg09ztcn6dnp0ph4pxqy82xzhq48w2', + mainBtcPublicKey: '037465ac1d27643f071b88203497ba0d07bdb05e060923cd93a32d765b4f5f7158', + masterPubKey: '024d30279814a0e609534af1d1969b7c24a6918029e1f9cb2134a427ebfb1f17c3', + ordinalsAddress: 'bc1p6cz06dkrngdeuapgfuvahpnkvelmrcryk422kmd0y7zmuu2nechsyan7nk', + ordinalsPublicKey: '2cf69ea437dd236422e4490244c94e97664f4ad065dd280d83df5d5a22bef6b9', + stxAddress: 'SP1X4G7MZNTHGFF6DA58PN7EWQ72DGMF7834P6X8', + stxPublicKey: '0289e90678355f7b73e53a798fdb0a656d8faa2a186900f64cb8c06b7155f0e419', + }, ]; - - diff --git a/transactions/btc.ts b/transactions/btc.ts index 27b62a60..da6777fb 100644 --- a/transactions/btc.ts +++ b/transactions/btc.ts @@ -3,7 +3,7 @@ import BigNumber from 'bignumber.js'; import { ErrorCodes, NetworkType, ResponseError, BtcFeeResponse, UTXO } from '../types'; import { fetchBtcFeeRate } from '../api/xverse'; -import { getBtcPrivateKey, getBtcTaprootPrivateKey } from '../wallet'; +import { getBtcPrivateKey, getBtcTaprootPrivateKey } from '../wallet/utils/btc'; import * as btc from '@scure/btc-signer'; import { hex } from '@scure/base'; import * as secp256k1 from '@noble/secp256k1'; diff --git a/transactions/psbt.ts b/transactions/psbt.ts index 8874678f..da17ac54 100644 --- a/transactions/psbt.ts +++ b/transactions/psbt.ts @@ -1,14 +1,12 @@ import { NetworkType, Account } from '../types'; - import { getBitcoinDerivationPath, getTaprootDerivationPath, getSegwitDerivationPath, -} from '../wallet'; +} from '../wallet/utils/btc'; import * as btc from '@scure/btc-signer'; import { hex, base64 } from '@scure/base'; import { getAddressInfo } from 'bitcoin-address-validation'; - import * as bip39 from 'bip39'; import { bip32 } from 'bitcoinjs-lib'; import * as secp256k1 from '@noble/secp256k1'; diff --git a/transactions/stx.ts b/transactions/stx.ts index b6aedf68..649c7616 100644 --- a/transactions/stx.ts +++ b/transactions/stx.ts @@ -44,7 +44,7 @@ import { getNonce as fetchNewNonce, } from '@stacks/transactions'; import { PostConditionsOptions, StxMempoolTransactionData } from 'types'; -import { getStxAddressKeyChain } from '../wallet/index'; +import { getStxAddressKeyChain } from '../wallet/utils/stx'; import { getNewNonce, makeFungiblePostCondition, makeNonFungiblePostCondition } from './helper'; import { UnsignedContractCallTransaction, diff --git a/types/account.ts b/types/account.ts index d19d3220..39b424c7 100644 --- a/types/account.ts +++ b/types/account.ts @@ -2,8 +2,8 @@ export interface Account { id: number; stxAddress: string; btcAddress: string; - dlcBtcAddress: string; - dlcBtcPublicKey: string; + mainBtcAddress: string; + mainBtcPublicKey: string; ordinalsAddress: string; masterPubKey: string; stxPublicKey: string; diff --git a/types/wallet.ts b/types/wallet.ts index 2500d0a1..c68d6f10 100644 --- a/types/wallet.ts +++ b/types/wallet.ts @@ -1,8 +1,8 @@ export interface BaseWallet { stxAddress: string; btcAddress: string; - dlcBtcAddress: string; - dlcBtcPublicKey: string; + mainBtcAddress: string; + mainBtcPublicKey: string; ordinalsAddress: string; masterPubKey: string; stxPublicKey: string; diff --git a/wallet/helper.ts b/wallet/helper.ts index ba9d5f16..97ece418 100644 --- a/wallet/helper.ts +++ b/wallet/helper.ts @@ -1,8 +1,50 @@ export function ecPairToHexString(secretKey: any) { - var ecPointHex = secretKey.privateKey.toString('hex'); + const ecPointHex = secretKey.privateKey.toString('hex'); if (secretKey.compressed) { return ecPointHex + '01'; } else { return ecPointHex; } } + +interface EncryptMnemonicArgs { + password: string; + seed: string; + passwordHashGenerator: (password: string) => Promise<{ + salt: string; + hash: string; + }>; + mnemonicEncryptionHandler: (seed: string, key: string) => Promise; +} + +interface DecryptMnemonicArgs { + password: string; + encryptedSeed: string; + passwordHashGenerator: (password: string) => Promise<{ + salt: string; + hash: string; + }>; + mnemonicDecryptionHandler: (seed: Buffer | string, key: string) => Promise; +} + +export async function encryptMnemonicWithCallback(cb: EncryptMnemonicArgs) { + const { mnemonicEncryptionHandler, passwordHashGenerator, password, seed } = cb; + try { + const { hash } = await passwordHashGenerator(password); + const encryptedSeedBuffer = await mnemonicEncryptionHandler(seed, hash); + return encryptedSeedBuffer.toString('hex'); + } catch (err) { + return Promise.reject(err); + } +} + +export async function decryptMnemonicWithCallback(cb: DecryptMnemonicArgs) { + const { mnemonicDecryptionHandler, passwordHashGenerator, password, encryptedSeed } = cb; + try { + const { hash } = await passwordHashGenerator(password); + const seedPhrase = await mnemonicDecryptionHandler(encryptedSeed, hash); + return seedPhrase; + } catch (err) { + return Promise.reject(err); + } +} \ No newline at end of file diff --git a/wallet/index.ts b/wallet/index.ts index 9b33c588..01cb307c 100644 --- a/wallet/index.ts +++ b/wallet/index.ts @@ -1,66 +1,24 @@ import crypto from 'crypto'; import * as bip39 from 'bip39'; -import { hashMessage } from '@stacks/encryption'; -import { - ENTROPY_BYTES, - STX_PATH_WITHOUT_INDEX, - BTC_WRAPPED_SEGWIT_PATH_PURPOSE, - BTC_SEGWIT_PATH_PURPOSE, - BTC_TAPROOT_PATH_PURPOSE, -} from '../constant'; +import * as btc from '@scure/btc-signer'; +import { hex } from '@scure/base'; +import * as secp256k1 from '@noble/secp256k1'; +import { payments, networks, ECPair, bip32 } from 'bitcoinjs-lib'; import { ChainID, publicKeyToString, getPublicKey, createStacksPrivateKey, - getAddressFromPrivateKey, - TransactionVersion, - AddressVersion, } from '@stacks/transactions'; -import { payments, networks, ECPair, bip32, BIP32Interface } from 'bitcoinjs-lib'; +import { + ENTROPY_BYTES, +} from '../constant'; import { NetworkType } from 'types/network'; -import { c32addressDecode } from 'c32check'; -import { ecPairToHexString } from './helper'; -import { Keychain } from 'types/api/xverse/wallet'; import { BaseWallet } from 'types/wallet'; -import { validate, Network as btcAddressNetwork } from 'bitcoin-address-validation'; -import * as btc from '@scure/btc-signer'; -import { hex } from '@scure/base'; -import * as secp256k1 from '@noble/secp256k1'; import { getBtcNetwork } from '../transactions/btcNetwork'; +import { deriveStxAddressChain } from './utils/stx'; +import { getBitcoinDerivationPath, getSegwitDerivationPath, getTaprootDerivationPath } from './utils/btc'; -export const derivationPaths = { - [ChainID.Mainnet]: STX_PATH_WITHOUT_INDEX, - [ChainID.Testnet]: STX_PATH_WITHOUT_INDEX, -}; - -function getDerivationPath(chain: ChainID, index: BigInt) { - return `${derivationPaths[chain]}${index.toString()}`; -} - -export function deriveStxAddressChain(chain: ChainID, index: BigInt = BigInt(0)) { - return (rootNode: BIP32Interface) => { - const childKey = rootNode.derivePath(getDerivationPath(chain, index)); - if (!childKey.privateKey) { - throw new Error('Unable to derive private key from `rootNode`, bip32 master keychain'); - } - const ecPair = ECPair.fromPrivateKey(childKey.privateKey); - const privateKey = ecPairToHexString(ecPair); - const txVersion = - chain === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet; - return { - childKey, - address: getAddressFromPrivateKey(privateKey, txVersion), - privateKey, - }; - }; -} - -export async function newWallet(): Promise { - const entropy = crypto.randomBytes(ENTROPY_BYTES); - const mnemonic = bip39.entropyToMnemonic(entropy); - return walletFromSeedPhrase({ mnemonic, index: 0n, network: 'Mainnet' }); -} export async function walletFromSeedPhrase({ mnemonic, @@ -68,7 +26,7 @@ export async function walletFromSeedPhrase({ network, }: { mnemonic: string; - index: BigInt; + index: bigint; network: NetworkType; }): Promise { const seed = await bip39.mnemonicToSeed(mnemonic); @@ -99,8 +57,8 @@ export async function walletFromSeedPhrase({ network: network === 'Mainnet' ? networks.bitcoin : networks.testnet, }); - const dlcBtcAddress = nativeSegwitBtcAddress.address!; - const dlcBtcPublicKey = nativeSegwitBtcAddressKeypair.publicKey.toString('hex'); + const mainBtcAddress = nativeSegwitBtcAddress.address!; + const mainBtcPublicKey = nativeSegwitBtcAddressKeypair.publicKey.toString('hex'); // derive taproot btc address const taprootBtcChild = master.derivePath(getTaprootDerivationPath({ index, network })); @@ -124,8 +82,8 @@ export async function walletFromSeedPhrase({ return { stxAddress, btcAddress, - dlcBtcAddress, - dlcBtcPublicKey, + mainBtcAddress, + mainBtcPublicKey, ordinalsAddress, masterPubKey, stxPublicKey, @@ -135,201 +93,13 @@ export async function walletFromSeedPhrase({ }; } -export function getBitcoinDerivationPath({ - account, - index, - network, -}: { - account?: BigInt; - index: BigInt; - network: NetworkType; -}) { - const accountIndex = account ? account.toString() : '0'; - return network === 'Mainnet' - ? `${BTC_WRAPPED_SEGWIT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` - : `${BTC_WRAPPED_SEGWIT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; -} - -export function getSegwitDerivationPath({ - account, - index, - network, -}: { - account?: BigInt; - index: BigInt; - network: NetworkType; -}) { - const accountIndex = account ? account.toString() : '0'; - return network === 'Mainnet' - ? `${BTC_SEGWIT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` - : `${BTC_SEGWIT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; -} - -export function getTaprootDerivationPath({ - account, - index, - network, -}: { - account?: BigInt; - index: BigInt; - network: NetworkType; -}) { - const accountIndex = account ? account.toString() : '0'; - return network === 'Mainnet' - ? `${BTC_TAPROOT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` - : `${BTC_TAPROOT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; -} - -export async function getBtcPrivateKey({ - seedPhrase, - index, - network, -}: { - seedPhrase: string; - index: BigInt; - network: NetworkType; -}): Promise { - const seed = await bip39.mnemonicToSeed(seedPhrase); - const master = bip32.fromSeed(seed); - - const btcChild = master.derivePath(getBitcoinDerivationPath({ index, network })); - return btcChild.privateKey!.toString('hex'); -} - -export async function getBtcTaprootPrivateKey({ - seedPhrase, - index, - network, -}: { - seedPhrase: string; - index: BigInt; - network: NetworkType; -}): Promise { - const seed = await bip39.mnemonicToSeed(seedPhrase); - const master = bip32.fromSeed(seed); - - const btcChild = master.derivePath(getTaprootDerivationPath({ index, network })); - return btcChild.privateKey!.toString('hex'); -} -export async function getBtcNativeSegwitPrivateKey({ - seedPhrase, - index, - network, -}: { - seedPhrase: string; - index: BigInt; - network: NetworkType; -}): Promise { - const seed = await bip39.mnemonicToSeed(seedPhrase); - const master = bip32.fromSeed(seed); - - const nativeSegwitBtcChild = master.derivePath(getSegwitDerivationPath({ index, network })); - return nativeSegwitBtcChild.privateKey!.toString('hex'); -} - -export function validateStxAddress({ - stxAddress, - network, -}: { - stxAddress: string; - network: NetworkType; -}) { - try { - const result = c32addressDecode(stxAddress); - if (result[0] && result[1]) { - const addressVersion = result[0]; - if (network === 'Mainnet') { - if ( - !( - addressVersion === AddressVersion.MainnetSingleSig || - addressVersion === AddressVersion.MainnetMultiSig - ) - ) { - return false; - } - } else { - if ( - result[0] !== AddressVersion.TestnetSingleSig && - result[0] !== AddressVersion.TestnetMultiSig - ) { - return false; - } - } - - return true; - } - return false; - } catch (error) { - return false; - } -} - -export function validateBtcAddress({ - btcAddress, - network, -}: { - btcAddress: string; - network: NetworkType; -}): boolean { - const btcNetwork = network === 'Mainnet' ? btcAddressNetwork.mainnet : btcAddressNetwork.testnet; - try { - return validate(btcAddress, btcNetwork); - } catch (error) { - return false; - } -} -interface EncryptMnemonicArgs { - password: string; - seed: string; - passwordHashGenerator: (password: string) => Promise<{ - salt: string; - hash: string; - }>; - mnemonicEncryptionHandler: (seed: string, key: string) => Promise; -} - -interface DecryptMnemonicArgs { - password: string; - encryptedSeed: string; - passwordHashGenerator: (password: string) => Promise<{ - salt: string; - hash: string; - }>; - mnemonicDecryptionHandler: (seed: Buffer | string, key: string) => Promise; -} - -export async function encryptMnemonicWithCallback(cb: EncryptMnemonicArgs) { - const { mnemonicEncryptionHandler, passwordHashGenerator, password, seed } = cb; - try { - const { hash } = await passwordHashGenerator(password); - const encryptedSeedBuffer = await mnemonicEncryptionHandler(seed, hash); - return encryptedSeedBuffer.toString('hex'); - } catch (err) { - return Promise.reject(err); - } -} - -export async function decryptMnemonicWithCallback(cb: DecryptMnemonicArgs) { - const { mnemonicDecryptionHandler, passwordHashGenerator, password, encryptedSeed } = cb; - try { - const { hash } = await passwordHashGenerator(password); - const seedPhrase = await mnemonicDecryptionHandler(encryptedSeed, hash); - return seedPhrase; - } catch (err) { - return Promise.reject(err); - } -} - -export async function getStxAddressKeyChain( - mnemonic: string, - chainID: ChainID, - accountIndex: number -): Promise { - const seed = await bip39.mnemonicToSeed(mnemonic); - const rootNode = bip32.fromSeed(Buffer.from(seed)); - const deriveStxAddressKeychain = deriveStxAddressChain(chainID, BigInt(accountIndex)); - return deriveStxAddressKeychain(rootNode); +export async function newWallet(): Promise { + const entropy = crypto.randomBytes(ENTROPY_BYTES); + const mnemonic = bip39.entropyToMnemonic(entropy); + return walletFromSeedPhrase({ mnemonic, index: 0n, network: 'Mainnet' }); } -export { hashMessage }; +export * from './helper'; +export * from './utils/btc'; +export * from './utils/stx'; diff --git a/wallet/utils/btc.ts b/wallet/utils/btc.ts new file mode 100644 index 00000000..fbd3c235 --- /dev/null +++ b/wallet/utils/btc.ts @@ -0,0 +1,115 @@ +import * as bip39 from 'bip39'; +import { validate, Network as btcAddressNetwork } from 'bitcoin-address-validation'; +import { + BTC_SEGWIT_PATH_PURPOSE, + BTC_TAPROOT_PATH_PURPOSE, + BTC_WRAPPED_SEGWIT_PATH_PURPOSE, +} from '../../constant'; +import { NetworkType } from '../../types/network'; +import { bip32 } from 'bitcoinjs-lib'; + +export function getBitcoinDerivationPath({ + account, + index, + network, +}: { + account?: bigint; + index: bigint; + network: NetworkType; +}) { + const accountIndex = account ? account.toString() : '0'; + return network === 'Mainnet' + ? `${BTC_WRAPPED_SEGWIT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` + : `${BTC_WRAPPED_SEGWIT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; +} + +export function getSegwitDerivationPath({ + index, + network, +}: { + index: bigint; + network: NetworkType; +}) { + // Final Derivation Path m/84'/0'/0'/0/0 + return network === 'Mainnet' + ? `${BTC_SEGWIT_PATH_PURPOSE}/${index}'/0/0` + : `${BTC_SEGWIT_PATH_PURPOSE}/${index}'/0/0`; +} + +export function getTaprootDerivationPath({ + account, + index, + network, +}: { + account?: bigint; + index: bigint; + network: NetworkType; +}) { + const accountIndex = account ? account.toString() : '0'; + return network === 'Mainnet' + ? `${BTC_TAPROOT_PATH_PURPOSE}0'/${accountIndex}'/0/${index.toString()}` + : `${BTC_TAPROOT_PATH_PURPOSE}1'/${accountIndex}'/0/${index.toString()}`; +} + +export async function getBtcPrivateKey({ + seedPhrase, + index, + network, +}: { + seedPhrase: string; + index: bigint; + network: NetworkType; +}): Promise { + const seed = await bip39.mnemonicToSeed(seedPhrase); + const master = bip32.fromSeed(seed); + + const btcChild = master.derivePath(getBitcoinDerivationPath({ index, network })); + return btcChild.privateKey!.toString('hex'); +} + +export async function getBtcTaprootPrivateKey({ + seedPhrase, + index, + network, +}: { + seedPhrase: string; + index: bigint; + network: NetworkType; +}): Promise { + const seed = await bip39.mnemonicToSeed(seedPhrase); + const master = bip32.fromSeed(seed); + + const btcChild = master.derivePath(getTaprootDerivationPath({ index, network })); + return btcChild.privateKey!.toString('hex'); +} + +export async function getBtcNativeSegwitPrivateKey({ + seedPhrase, + index, + network, +}: { + seedPhrase: string; + index: bigint; + network: NetworkType; +}): Promise { + const seed = await bip39.mnemonicToSeed(seedPhrase); + const master = bip32.fromSeed(seed); + + const nativeSegwitBtcChild = master.derivePath(getSegwitDerivationPath({ index, network })); + return nativeSegwitBtcChild.privateKey!.toString('hex'); +} + +export function validateBtcAddress({ + btcAddress, + network, +}: { + btcAddress: string; + network: NetworkType; +}): boolean { + const btcNetwork = network === 'Mainnet' ? btcAddressNetwork.mainnet : btcAddressNetwork.testnet; + try { + return validate(btcAddress, btcNetwork); + } catch (error) { + return false; + } +} diff --git a/wallet/utils/stx.ts b/wallet/utils/stx.ts new file mode 100644 index 00000000..efecd06d --- /dev/null +++ b/wallet/utils/stx.ts @@ -0,0 +1,88 @@ +import * as bip39 from 'bip39'; +import { + AddressVersion, + ChainID, + TransactionVersion, + getAddressFromPrivateKey, +} from '@stacks/transactions'; +import { BIP32Interface, ECPair, bip32 } from 'bitcoinjs-lib'; +import { ecPairToHexString } from '../helper'; +import { STX_PATH_WITHOUT_INDEX } from '../../constant'; +import { NetworkType } from '../../types/network'; +import { Keychain } from '../../types/api/xverse/wallet'; +import { c32addressDecode } from 'c32check'; + +export const stxDerivationPaths = { + [ChainID.Mainnet]: STX_PATH_WITHOUT_INDEX, + [ChainID.Testnet]: STX_PATH_WITHOUT_INDEX, +}; + +function getDerivationPath(chain: ChainID, index: bigint) { + return `${stxDerivationPaths[chain]}${index.toString()}`; +} + +export function deriveStxAddressChain(chain: ChainID, index = BigInt(0)) { + return (rootNode: BIP32Interface) => { + const childKey = rootNode.derivePath(getDerivationPath(chain, index)); + if (!childKey.privateKey) { + throw new Error('Unable to derive private key from `rootNode`, bip32 master keychain'); + } + const ecPair = ECPair.fromPrivateKey(childKey.privateKey); + const privateKey = ecPairToHexString(ecPair); + const txVersion = + chain === ChainID.Mainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet; + return { + childKey, + address: getAddressFromPrivateKey(privateKey, txVersion), + privateKey, + }; + }; +} + +export async function getStxAddressKeyChain( + mnemonic: string, + chainID: ChainID, + accountIndex: number +): Promise { + const seed = await bip39.mnemonicToSeed(mnemonic); + const rootNode = bip32.fromSeed(Buffer.from(seed)); + const deriveStxAddressKeychain = deriveStxAddressChain(chainID, BigInt(accountIndex)); + return deriveStxAddressKeychain(rootNode); +} + +export function validateStxAddress({ + stxAddress, + network, +}: { + stxAddress: string; + network: NetworkType; +}) { + try { + const result = c32addressDecode(stxAddress); + if (result[0] && result[1]) { + const addressVersion = result[0]; + if (network === 'Mainnet') { + if ( + !( + addressVersion === AddressVersion.MainnetSingleSig || + addressVersion === AddressVersion.MainnetMultiSig + ) + ) { + return false; + } + } else { + if ( + result[0] !== AddressVersion.TestnetSingleSig && + result[0] !== AddressVersion.TestnetMultiSig + ) { + return false; + } + } + + return true; + } + return false; + } catch (error) { + return false; + } +}