Skip to content

Instantly share code, notes, and snippets.

@tunnckoCore
Created November 28, 2025 06:53
Show Gist options
  • Select an option

  • Save tunnckoCore/fbdc7c0e9e06ecdd7a9d45ac218efc8a to your computer and use it in GitHub Desktop.

Select an option

Save tunnckoCore/fbdc7c0e9e06ecdd7a9d45ac218efc8a to your computer and use it in GitHub Desktop.
Basic send BTC from WIF/privateKey
// 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);
});
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