diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..9498e50b Binary files /dev/null and b/.DS_Store differ diff --git a/account/index.ts b/account/index.ts index 69e9ca06..c659bef9 100644 --- a/account/index.ts +++ b/account/index.ts @@ -90,6 +90,8 @@ export async function restoreWalletWithAccounts( id: index, stxAddress: response.stxAddress, btcAddress: response.btcAddress, + mainBtcAddress: response.mainBtcAddress, + mainBtcPublicKey: response.mainBtcPublicKey, ordinalsAddress: response.ordinalsAddress, masterPubKey: response.masterPubKey, stxPublicKey: response.stxPublicKey, @@ -123,10 +125,12 @@ export async function createWalletAccount( stxAddress, btcAddress, ordinalsAddress, + mainBtcAddress, masterPubKey, stxPublicKey, btcPublicKey, ordinalsPublicKey, + mainBtcPublicKey, } = await walletFromSeedPhrase({ mnemonic: seedPhrase, index: BigInt(accountIndex), @@ -139,6 +143,8 @@ export async function createWalletAccount( id: accountIndex, stxAddress, btcAddress, + mainBtcAddress, + mainBtcPublicKey, ordinalsAddress, masterPubKey, stxPublicKey, diff --git a/api/esplora/esploraAPiProvider.ts b/api/esplora/esploraAPiProvider.ts index d5ca8479..1667d6c1 100644 --- a/api/esplora/esploraAPiProvider.ts +++ b/api/esplora/esploraAPiProvider.ts @@ -3,6 +3,7 @@ import { BLOCKCYPHER_BASE_URI_MAINNET, BLOCKCYPHER_BASE_URI_TESTNET, BTC_BASE_URI_MAINNET, + BTC_BASE_URI_REGTEST, BTC_BASE_URI_TESTNET, } from '../../constant'; import { @@ -29,7 +30,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; @@ -159,6 +164,12 @@ 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 getRecommendedFees(): Promise { const data: esplora.RecommendedFeeResponse = await this.httpGet('/v1/fees/recommended'); return data; 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..387a02c8 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'/`; @@ -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 BLOCKCYPHER_BASE_URI_MAINNET = 'https://api.blockcypher.com/v1/btc/main'; export const BLOCKCYPHER_BASE_URI_TESTNET = 'https://api.blockcypher.com/v1/btc/test3'; 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/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/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 2d8c60b0..39b424c7 100644 --- a/types/account.ts +++ b/types/account.ts @@ -2,6 +2,8 @@ export interface Account { id: number; stxAddress: string; btcAddress: string; + mainBtcAddress: string; + mainBtcPublicKey: string; ordinalsAddress: string; masterPubKey: string; stxPublicKey: string; diff --git a/types/api/blockcypher/wallet.ts b/types/api/blockcypher/wallet.ts index 5f6dd5a4..e07b72bb 100644 --- a/types/api/blockcypher/wallet.ts +++ b/types/api/blockcypher/wallet.ts @@ -80,6 +80,10 @@ export interface BtcTransactionData extends TransactionData { isOrdinal: boolean; } +export interface BtcTransactionDataHexIncluded extends BtcTransactionData { + hex: string; +} + export interface Input { addresses: string[]; output_index: number; diff --git a/types/network.ts b/types/network.ts index 57f266fe..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/types/wallet.ts b/types/wallet.ts index ce2face7..c68d6f10 100644 --- a/types/wallet.ts +++ b/types/wallet.ts @@ -1,6 +1,8 @@ export interface BaseWallet { stxAddress: string; btcAddress: 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 3f4931d4..a57f6e30 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,11 +26,26 @@ export async function walletFromSeedPhrase({ network, }: { mnemonic: string; - index: BigInt; + index: bigint; network: NetworkType; }): 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, @@ -90,6 +63,18 @@ export async function walletFromSeedPhrase({ const btcChild = master.derivePath(getBitcoinDerivationPath({ index, network })); const keyPair = ECPair.fromPrivateKey(btcChild.privateKey!); + // derive native segwit btc address + const nativeSegwitBtcChild = master.derivePath(getSegwitDerivationPath({ index, network })); + const nativeSegwitBtcAddressKeypair = ECPair.fromPrivateKey(nativeSegwitBtcChild.privateKey!); + + const nativeSegwitBtcAddress = payments.p2wpkh({ + pubkey: nativeSegwitBtcAddressKeypair.publicKey, + network: bitcoinNetwork, + }); + + const mainBtcAddress = nativeSegwitBtcAddress.address!; + const mainBtcPublicKey = nativeSegwitBtcAddressKeypair.publicKey.toString('hex'); + // derive taproot btc address const taprootBtcChild = master.derivePath(getTaprootDerivationPath({ index, network })); const privKey = hex.decode(taprootBtcChild.privateKey!.toString('hex')); @@ -99,11 +84,12 @@ 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!; const btcPublicKey = keyPair.publicKey.toString('hex'); const taprootInternalPubKey = secp256k1.schnorr.getPublicKey(privKey); @@ -111,6 +97,8 @@ export async function walletFromSeedPhrase({ return { stxAddress, btcAddress, + mainBtcAddress, + mainBtcPublicKey, ordinalsAddress, masterPubKey, stxPublicKey, @@ -120,185 +108,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 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; + } +}