Created
November 28, 2025 06:53
-
-
Save tunnckoCore/fbdc7c0e9e06ecdd7a9d45ac218efc8a to your computer and use it in GitHub Desktop.
Basic send BTC from WIF/privateKey
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Example usage | |
| const privateKeyWIF = "WIF_HERE"; | |
| const recipientAddress = `bc1pk42g9szvyl7xxdpdszn5ql6ce2mzc0wmy05y97qfmu7hs8uftnns5tc37k`; | |
| const amount = 32000; | |
| const feeRate = 1; | |
| sendBTC(privateKeyWIF, recipientAddress, amount, feeRate) | |
| .then((result) => { | |
| console.log("Send successful:", result); | |
| }) | |
| .catch((err) => { | |
| console.error("Send failed:", err); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import * as btc from "@scure/btc-signer"; | |
| import { secp256k1 } from "@noble/curves/secp256k1.js"; | |
| const BLOCKSTREAM_URL = "https://blockstream.info/api"; | |
| interface UTXO { | |
| txid: string; | |
| vout: number; | |
| value: number; | |
| status: { | |
| confirmed: boolean; | |
| block_height?: number; | |
| }; | |
| } | |
| async function getUTXOs(address: string): Promise<UTXO[]> { | |
| const response = await fetch(`${BLOCKSTREAM_URL}/address/${address}/utxo`); | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch UTXOs: ${response.statusText}`); | |
| } | |
| return response.json(); | |
| } | |
| async function broadcastTx(txHex: string): Promise<string> { | |
| const response = await fetch(`https://blockchain.info/pushtx`, { | |
| method: "POST", | |
| body: `tx=${txHex}`, | |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`Failed to broadcast tx: ${response.statusText}`); | |
| } | |
| return response.text(); | |
| } | |
| function estimateTxSize(numInputs: number, numOutputs: number): number { | |
| return 10 + 50 * numInputs + 43 * numOutputs; | |
| } | |
| async function sendBTC( | |
| privateKeyWIF: string, | |
| recipientAddress: string, | |
| targetAmount: number, | |
| feeRate: number = 1 | |
| ) { | |
| // Decode WIF using SDK | |
| const privateKeyBuf = btc.WIF().decode(privateKeyWIF); | |
| console.log("wif.decode::::", { privateKeyBuf }); | |
| const privateKey = privateKeyBuf; | |
| // Derive internal pubkey | |
| const internalPubkey = secp256k1.getPublicKey(privateKey, true).slice(1); | |
| // Derive address using SDK | |
| const senderAddress = `bc1pqh4m8gkmch9knq4hlfqk3fgz7da5axk77ngev2cu97ac7ag0lxwqw8ddgv`; | |
| // Get UTXOs | |
| console.log("Fetching UTXOs for address:", senderAddress); | |
| const utxos = await getUTXOs(senderAddress); | |
| console.log("UTXOs received:", utxos); | |
| if (utxos.length === 0) { | |
| throw new Error("No UTXOs available"); | |
| } | |
| // Select all UTXOs, sorted by txid | |
| const selectedUTXOs = utxos.sort((a, b) => a.txid.localeCompare(b.txid)); | |
| const totalInput = selectedUTXOs.reduce((sum, u) => sum + u.value, 0); | |
| if (totalInput < targetAmount) { | |
| throw new Error("Insufficient funds"); | |
| } | |
| // Estimate fee | |
| const numInputs = selectedUTXOs.length; | |
| const numOutputs = 1; // No change output | |
| const txSize = estimateTxSize(numInputs, numOutputs); | |
| const estimatedFee = Math.ceil(txSize * feeRate); | |
| const fee = Math.max(estimatedFee, 744); // Min relay fee | |
| const amount = totalInput - fee; | |
| if (amount <= 0) { | |
| throw new Error("Insufficient funds after fee"); | |
| } | |
| // Create transaction | |
| const tx = new btc.Transaction(); | |
| // Add inputs | |
| for (const utxo of selectedUTXOs) { | |
| tx.addInput({ | |
| txid: utxo.txid, | |
| index: utxo.vout, | |
| tapInternalKey: internalPubkey, | |
| witnessUtxo: { | |
| script: btc.p2tr(internalPubkey).script, | |
| amount: BigInt(utxo.value), | |
| }, | |
| }); | |
| } | |
| // Add output (no change) | |
| tx.addOutputAddress(recipientAddress, BigInt(amount)); | |
| // Don't add change to avoid dust error | |
| // const change = totalInput - amount - fee; | |
| // if (change > 546) { | |
| // tx.addOutputAddress(senderAddress, BigInt(change)); | |
| // } | |
| // Sign | |
| tx.sign(privateKey); | |
| // Finalize | |
| tx.finalize(); | |
| // Get hex | |
| const txHex = tx.hex; | |
| console.log("txHex::::", txHex); | |
| // Broadcast | |
| const txId = await broadcastTx(txHex); | |
| console.log("txId:::", txId); | |
| return { txId, fee }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment