UNPKG

@atomiqlabs/sdk

Version:

atomiq labs SDK for cross-chain swaps between smart chains and bitcoin

226 lines (205 loc) 9 kB
import {CoinselectAddressTypes} from "../coinselect2"; import {BTC_NETWORK, NETWORK, pubECDSA, randomPrivateKeyBytes, TEST_NETWORK} from "@scure/btc-signer/utils" import {getAddress, Transaction, WIF} from "@scure/btc-signer"; import {Buffer} from "buffer"; import {identifyAddressType, BitcoinWallet} from "./BitcoinWallet"; import {BitcoinNetwork, BitcoinRpcWithAddressIndex, getLogger} from "@atomiqlabs/base"; import {HDKey} from "@scure/bip32"; import {entropyToMnemonic, generateMnemonic, mnemonicToSeed} from "@scure/bip39"; import {wordlist} from "@scure/bip39/wordlists/english.js"; import {sha256} from "@noble/hashes/sha2"; import {BitcoinWalletUtxo, BitcoinWalletUtxoBase} from "./IBitcoinWallet"; const logger = getLogger("SingleAddressBitcoinWallet: "); /** * Bitcoin wallet implementation deriving a single address from a WIF encoded private key * * @category Bitcoin */ export class SingleAddressBitcoinWallet extends BitcoinWallet { protected readonly privKey?: Uint8Array; protected readonly pubkey: Uint8Array; protected readonly address: string; protected readonly addressType: CoinselectAddressTypes; constructor( mempoolApi: BitcoinRpcWithAddressIndex<any>, _network: BitcoinNetwork | BTC_NETWORK, addressDataOrWIF: string | {address: string, publicKey: string}, feeMultiplier: number = 1.25, feeOverride?: number ) { const network = typeof(_network)==="object" ? _network : BitcoinWallet.bitcoinNetworkToObject(_network); super(mempoolApi, network, feeMultiplier, feeOverride); if(typeof(addressDataOrWIF)==="string") { try { this.privKey = WIF(network).decode(addressDataOrWIF); } catch(e) { this.privKey = WIF().decode(addressDataOrWIF); } this.pubkey = pubECDSA(this.privKey); const address = getAddress("wpkh", this.privKey, network); if(address==null) throw new Error("Failed to generate p2wpkh address from the provided private key!"); this.address = address; this.addressType = identifyAddressType(this.address, network); } else { this.address = addressDataOrWIF.address; this.addressType = identifyAddressType(this.address, network); this.pubkey = Buffer.from(addressDataOrWIF.publicKey, "hex"); // Some wallets seem to be returning a full 33-byte compressed pubkey instead of a taproot // 32-byte long X-only key. Handle these cases here if(this.addressType==="p2tr") { if(this.pubkey.length!==33) return; const leadingByte = this.pubkey[0]; if(leadingByte!==0x03 && leadingByte!==0x02) throw new Error("Invalid public key passed for taproot bitcoin wallet, expected an X-only 32-byte public key, or a compressed 33-byte public key"); logger.debug(`constructor(): Converting compressed public key ${addressDataOrWIF.publicKey} to taproot X-only 32-byte public key`); this.pubkey = this.pubkey.slice(1); } } } /** * Returns all the wallet addresses controlled by the wallet * * @protected */ protected toBitcoinWalletAccounts(): [{pubkey: string, address: string, addressType: CoinselectAddressTypes}] { return [{ pubkey: Buffer.from(this.pubkey).toString("hex"), address: this.address, addressType: this.addressType }]; } /** * @inheritDoc */ async sendTransaction(address: string, amount: bigint, feeRate?: number): Promise<string> { if(!this.privKey) throw new Error("Not supported."); const {psbt, fee} = await super._getPsbt(this.toBitcoinWalletAccounts(), address, Number(amount), feeRate); if(psbt==null) throw new Error(`Not enough funds, required for fee: ${fee} sats!`); psbt.sign(this.privKey); psbt.finalize(); const txHex = Buffer.from(psbt.extract()).toString("hex"); return await super._sendTransaction(txHex); } /** * @inheritDoc */ async fundPsbt(inputPsbt: Transaction, feeRate?: number, utxos?: BitcoinWalletUtxo[], spendFully?: boolean): Promise<Transaction> { const {psbt} = await super._fundPsbt(this.toBitcoinWalletAccounts(), inputPsbt, feeRate, utxos, spendFully); if(psbt==null) { throw new Error("Not enough balance!"); } return psbt; } /** * @inheritDoc */ async signPsbt(psbt: Transaction, signInputs: number[]): Promise<Transaction> { if(!this.privKey) throw new Error("Not supported."); for(let signInput of signInputs) { psbt.signIdx(this.privKey, signInput); } return psbt; } /** * @inheritDoc */ async getTransactionFee(address: string, amount: bigint, feeRate?: number): Promise<number> { const {fee} = await super._getPsbt(this.toBitcoinWalletAccounts(), address, Number(amount), feeRate); return fee; } /** * @inheritDoc */ async getFundedPsbtFee(basePsbt: Transaction, feeRate?: number): Promise<number> { const {fee} = await super._fundPsbt(this.toBitcoinWalletAccounts(), basePsbt, feeRate); return fee; } /** * @inheritDoc */ getReceiveAddress(): string { return this.address; } /** * Returns the public key of the wallet */ getPublicKey(): string { return Buffer.from(this.pubkey).toString("hex"); } /** * @inheritDoc */ getBalance(): Promise<{ confirmedBalance: bigint, unconfirmedBalance: bigint }> { return this._getBalance(this.address); } /** * @inheritDoc */ getSpendableBalance(psbt?: Transaction, feeRate?: number, outputAddressType?: CoinselectAddressTypes, utxos?: BitcoinWalletUtxoBase[]): Promise<{ balance: bigint, feeRate: number, totalFee: number }> { return this._getSpendableBalance([{address: this.address, addressType: this.addressType}], psbt, feeRate, outputAddressType, utxos); } /** * @inheritDoc */ async getUtxoPool(): Promise<BitcoinWalletUtxo[]> { return this._getUtxoPool(this.address, this.addressType); } /** * Generates a new random private key WIF that can be used to instantiate the bitcoin wallet instance * * @returns A WIF encoded bitcoin private key */ static generateRandomPrivateKey(network?: BitcoinNetwork | BTC_NETWORK): string { const networkObject = network==null || typeof(network)==="object" ? network : BitcoinWallet.bitcoinNetworkToObject(network); return WIF(networkObject).encode(randomPrivateKeyBytes()); } /** * Generates a 12-word long mnemonic from any entropy source with 128-bits or more, the entropy is first hashed * using sha256, and the first 16 bytes of the hash are used to generate the mnemonic * * @param entropy Entropy to use for generating the mnemonic */ static mnemonicFromEntropy(entropy: Buffer): string { if(entropy.length<16) throw new Error("Requires at least 128-bit entropy (16 bytes)"); const entropyHash = Buffer.from(sha256(entropy)).subarray(0, 16); return entropyToMnemonic(entropyHash, wordlist); } /** * Generates a random 12-word long mnemonic */ static generateRandomMnemonic(): string { return generateMnemonic(wordlist, 128) } /** * Generates a WIF private key from mnemonic phrase * * @param mnemonic Mnemonic to generate the WIF key from * @param network Optional bitcoin network to generate the WIF for * @param derivationPath Optional custom derivation path to use for deriving the wallet */ static async mnemonicToPrivateKey( mnemonic: string, network?: BitcoinNetwork | BTC_NETWORK, derivationPath?: string ): Promise<string> { const networkObject = network==null || typeof(network)==="object" ? network : BitcoinWallet.bitcoinNetworkToObject(network); derivationPath = networkObject==null || networkObject.bech32===NETWORK.bech32 ? "m/84'/0'/0'/0/0" //Mainnet : "m/84'/1'/0'/0/0"; //Testnet const seed = await mnemonicToSeed(mnemonic); const hdKey = HDKey.fromMasterSeed(seed); const privateKey = hdKey.derive(derivationPath).privateKey; if(privateKey==null) throw new Error("Cannot derive private key from the mnemonic!"); return WIF(networkObject).encode(privateKey); } }