Programming Bitcoin
Learn how to use Lit to trigger transactions on Bitcoin.
Overview
Introduction to Bitcoin's Limitations
Bitcoin, the world's first and largest blockchain, is renowned for its security and decentralization but lacks native programmability. This limitation hinders the development of complex decentralized applications directly on the Bitcoin network.
The absence of programmability restricts Bitcoin's ecosystem to simple value transfers. Enabling programmability would allow for the creation of smart contracts, decentralized applications, and innovative real-world solutions. Lit Protocol's Programmable Key Pairs (PKPs) offer a solution by bringing programmability to Bitcoin.
Triggering a Bitcoin (P2PKH) Transaction with Lit Protocol
With Lit PKPs, you can create programmable, policy-controlled key pairs that interact with the Bitcoin network, enabling functionalities that were previously unattainable. This guide demonstrates how a P2PKH transaction can be prepared and signed using a Lit Protocol PKP on Bitcoin. Note that you can use Lit to sign different Bitcoin transaction types as well (i.e. P2SH, P2WPKH), but the required setup will differ from the following example.
Background
Programmable Key Pairs (PKPs)
PKPs are threshold public/private key pairs created by the Lit network using Distributed Key Generation (DKG). Each Lit node generates a share of the private key, and more than two-thirds of these shares must be collected to execute a given action (i.e. signing a transaction).
PKPs can be programmed with custom logic to automate on-chain actions and communicate across different blockchains to enable chain abstraction. They can be used to create non-custodial wallets, automate transactions, and build complex decentralized applications.
Your Multi-Chain Account
Since generating signatures with Lit is handled off-chain, one of the significant uses of PKPs is serving as a wallet across multiple blockchains, including Bitcoin, Ethereum, Cosmos, and others. This means you aren't restricted to deploying on a single chain and instead can use Lit to build apps and experiences that span across the entire web3 ecosystem.
Lit Actions: Making Keys Programmable
Lit Actions are custom JavaScript programs that can be used to dictate the signing logic for PKPs to follow. For example, a simple Lit Action may take in an input via API (like a blockchain event) and produce a signature (using a PKP) when a given condition is met.
Use Cases
Integrating PKPs with Bitcoin transactions via Lit Protocol unlocks a wealth of possibilities for developers and users alike. One of the most compelling use cases is the automation of transactions based on specific conditions or events. For example, you can program a PKP to authorize and sign a Bitcoin transaction only when certain state is true on a smart contract platform—such as a 'liquidate' function being called. Block height is the example demonstrated in this guide. This conditional execution can be extended to more real-world scenarios like releasing payments upon contract fulfillment, automating payroll when work milestones are achieved, or triggering transactions based on market conditions, like dollar cost averaging.
Another significant use case is the creation of multi-chain decentralized applications (dApps) and wallets. Since PKPs can interact with multiple blockchains—including Bitcoin, Ethereum, and Cosmos—you can build applications and chain abstraction protocols that operate seamlessly across different networks. This enables users to manage assets on multiple chains using a single PKP as well as use programmable keys for orchestration. The result is simplifying the user experience and fostering interoperability in the blockchain ecosystem.
Additionally, the programmable nature of PKPs through Lit Actions allows for enhanced security features, such as multi-signature approvals and compliance checks, making them ideal for corporate treasury management, decentralized finance (DeFi) applications, and other scenarios requiring robust control over transaction authorization
Example: Conditional Bitcoin Transaction Signing
To illustrate the capabilities of PKPs and Lit Actions, we will demonstrate the following example:
- Objective: Use a PKP to sign a Bitcoin transaction.
- Condition: The transaction will only be signed when the current Bitcoin block height number is odd.
Bitcoin Transaction Types
In Bitcoin's long lifetime, different transaction algorithms have emerged with improvements being made at every step. As mentioned previously, this example will use the legacy P2PKH (Pay-to-PubKey-Hash) transaction format.
P2PKH - Legacy Transactions
- P2PKH (Pay-to-PubKey-Hash) is the most common type of Bitcoin transaction. The Bitcoin is sent to the recipient's Bitcoin address, which is derived from their public key. The recipient must provide a signature that matches the public key hash in order to spend the Bitcoin.
- In this transaction type, the sender provides a script with the recipient's hashed public key, and the recipient must sign the transaction to unlock the funds.
- Addresses of this type start with a
1
.
P2WPKH - SegWit Transactions
- P2WPKH (Pay-to-Witness-Pubkey-Hash) is an upgrade from the legacy P2PKH transactions. This type introduces a Segregated Witness (SegWit), separating the signature data (witness data) from the main transaction data, functionally reducing the transaction size and therefore lowering the transaction fee.
- This transaction type benefits from reduced transaction fees, enhanced privacy with Bench32, and enables advanced protocol developments (i.e. Lightning Network, Taproot). This is now the recommended transaction type for Bitcoin.
- Addresses of this type start with a
bc1
.
P2SH
- P2SH (Pay-to-Script-Hash) transactions send the Bitcoin to a script hash instead of a public key hash. The recipient must provide a script that matches the hash along with the necessary data to satisfy the conditions specified in the script.
- This transaction type is commonly used for multi-signature wallets, requiring multiple keys to sign before the script hashes match.
- Addresses of this type start with a
3
.
Bitcoin has many transaction types, so you may also be interested in others, like Nested SegWit or P2TR (Pay-to-Taproot). More information on these can be found here.
Guide: Using a PKP to Sign a Transaction on Bitcoin
Prerequisites
Before compiling and trying this example out, there are a few things you'll need to prepare.
- You will need to mint a PKP. The fastest way to do this is through the Lit Explorer. Please make sure you mint the PKP on the same network you run the example on.
- An Ethereum wallet. Please make sure that this wallet was used to mint the PKP, and therefore has ownership of it.
- The PKP must have a UTXO. Without a UTXO, the PKP will be unable to send any Bitcoin and this example will fail. To find the Base58 address of the PKP public key, you can visit the code in the
Deriving a BTC Base58 Address from the Public Key
part of this example. This code will compute the P2PKH address of your public key. You can then send Bitcoin to the output address.
Complete Code Example
After making sure you have met the prerequisites, you can choose to visit the complete code example here. That being said, we strongly recommend you read through the following explanation.
Explaining the Implementation
ENVs
PKP_PUBLIC_KEY=
ETHEREUM_PRIVATE_KEY=
BTC_DESTINATION_ADDRESS=
BROADCAST_URL=https://mempool.space/api/tx
LIT_NETWORK=
LIT_CAPACITY_CREDIT_TOKEN_ID=
- REQUIRED
PKP_PUBLIC_KEY
: Public key of the PKP. It must be owned by the account associated with the given Ethereum private key. Do not include the0x
. - REQUIRED
ETHEREUM_PRIVATE_KEY
: The Ethereum account that will be used to pay for usage of the Lit network and owns the PKP. - REQUIRED
BTC_DESTINATION_ADDRESS
: The Bitcoin address to receive the Bitcoin sent from the PKP. - GIVEN
BROADCAST_URL
: The endpoint URL that the transaction hex will be submitted to. This is used to broadcast the transaction. - OPTIONAL
LIT_NETWORK
: The Lit network to execute this example on. If one is not provided, the example will default to the Datil network. If you wish to use a different network, say datil-test, then set the ENV todatil-test
. - OPTIONAL
LIT_CAPACITY_CREDIT_TOKEN_ID
: The capacity credit that will be used to create acapacityDelegationAuthSig
and pay for usage of the Lit network. If not provided, a new one will need to be minted. This requires the Ethereum account to have LittstLPX
tokens.
Packages
Lit Packages
import { LitNodeClient } from "@lit-protocol/lit-node-client";
import { LitNetwork, LIT_RPC } from "@lit-protocol/constants";
import {
createSiweMessageWithRecaps,
generateAuthSig,
LitAbility,
LitActionResource,
LitPKPResource,
} from "@lit-protocol/auth-helpers";
import { LitContracts } from "@lit-protocol/contracts-sdk";
import { LIT_NETWORKS_KEYS } from "@lit-protocol/types";
Other Packages
import BN from "bn.js";
import mempoolJS from "@mempool/mempool.js";
import fetch from 'node-fetch';
import elliptic from 'elliptic';
import * as bitcoin from "bitcoinjs-lib";
import * as ethers from "ethers";
import * as ecc from "tiny-secp256k1";
import * as bip66 from "bip66";
import * as crypto from 'crypto';
Constants
Here we inject the ENVs into our file, create a new ethers.Wallet
, initialize instances of the elliptic curve cryptography (ECC), and set the bitcoinjs-lib
to use the cryptography of secp256k1
.
const PKP_PUBLIC_KEY = process.env["PKP_PUBLIC_KEY"]!;
const ETHEREUM_PRIVATE_KEY = process.env["ETHEREUM_PRIVATE_KEY"]!;
const BTC_DESTINATION_ADDRESS = process.env["BTC_DESTINATION_ADDRESS"]!;
const BROADCAST_URL = process.env["BROADCAST_URL"];
const LIT_NETWORK = process.env["LIT_NETWORK"] as LIT_NETWORKS_KEYS || LitNetwork.Datil;
const LIT_CAPACITY_CREDIT_TOKEN_ID = process.env["LIT_CAPACITY_CREDIT_TOKEN_ID"];
const ethersWallet = new ethers.Wallet(ETHEREUM_PRIVATE_KEY,
new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));
const address = ethersWallet.address;
const EC = elliptic.ec;
bitcoin.initEccLib(ecc);
Lit Implementation
Lit Connections
Using Lit requires a connection to the Lit network, which can be established using LitNodeClient
.
In case a LIT_CAPACITY_CREDIT_TOKEN_ID
is not provided, we also connect to LitContracts
. This can be used to mint a new capacity credit.
let litNodeClient: LitNodeClient | undefined;
litNodeClient = new LitNodeClient({
litNetwork: LIT_NETWORK,
debug: false,
});
await litNodeClient.connect();
const litContracts = new LitContracts({
signer: ethersWallet,
network: LIT_NETWORK,
debug: false,
});
await litContracts.connect();
Interacting with the Lit Network
The Lit network requires session signatures to authenticate your current session and define the session capabilities.
let capacityTokenId = LIT_CAPACITY_CREDIT_TOKEN_ID;
if (!capacityTokenId) {
console.log("🔄 No Capacity Credit provided, minting a new one...");
const mintResult = await litContracts.mintCapacityCreditsNFT({
requestsPerKilosecond: 10,
daysUntilUTCMidnightExpiration: 1,
});
capacityTokenId = mintResult.capacityTokenIdStr;
console.log(`✅ Minted new Capacity Credit with ID: ${capacityTokenId}`);
} else {
console.log(
`ℹ️ Using provided Capacity Credit with ID: ${LIT_CAPACITY_CREDIT_TOKEN_ID}`
);
}
console.log("🔄 Creating capacityDelegationAuthSig...");
const { capacityDelegationAuthSig } =
await litNodeClient.createCapacityDelegationAuthSig({
dAppOwnerWallet: ethersWallet,
capacityTokenId,
delegateeAddresses: [address],
uses: "1",
});
console.log("✅ Capacity Delegation Auth Sig created");
console.log("🔄 Getting Session Signatures...");
const sessionSigs = await litNodeClient.getSessionSigs({
chain: "ethereum",
expiration: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(), // 24 hours
capabilityAuthSigs: [capacityDelegationAuthSig],
resourceAbilityRequests: [
{
resource: new LitPKPResource("*"),
ability: LitAbility.PKPSigning,
},
{
resource: new LitActionResource("*"),
ability: LitAbility.LitActionExecution,
},
],
authNeededCallback: async ({
resourceAbilityRequests,
expiration,
uri,
}) => {
const toSign = await createSiweMessageWithRecaps({
uri: uri!,
expiration: expiration!,
resources: resourceAbilityRequests!,
walletAddress: address,
nonce: await litNodeClient!.getLatestBlockhash(),
litNodeClient,
});
return await generateAuthSig({
signer: ethersWallet,
toSign,
});
},
});
console.log("✅ Got Session Signatures");
- If a
LIT_CAPACITY_CREDIT_TOKEN_ID
is not provided, we mint a new one and use it to create acapacityDelegationAuthSig
. This AuthSig is used to pay for usage of the test and production Lit networks. - We then generate session signatures, specifying that our session has the ability to use PKPs for signing and to execute Lit Actions.
Implementing Bitcoin P2PKH
Deriving a BTC Base58 Address from the Public Key
const pubKeyBuffer = Buffer.from(PKP_PUBLIC_KEY, "hex");
const sha256Hash = crypto.createHash('sha256').update(pubKeyBuffer).digest();
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
const btcAddress = bitcoin.address.toBase58Check(ripemd160Hash, 0x00);
A Bitcoin address is derived through a hashing algorithm which combines SHA-256 and RIPEMD160. The algorithm first computes the SHA-256 hash of the public key and then the RIPEMD160 hash of the result. We can then derive the Base58Check encoding, adding the mainnet network prefix 0x00
.
- Take the uncompressed public key, convert it to a Buffer. This is required for the following hashing algorithms. (65 bytes, starting with byte
04
)- Expected format:
04 ea ec 6d 85 f9 68 ea e2 4c 0f e0 34 ... 52 more bytes
- Expected format:
- Compute the SHA-256 hash of the public key Buffer. (32 bytes)
- Expected format:
bd 09 96 cf f7 5c 36 cf d3 d1 32 6a a9 ... 19 more bytes
- Compute the RIPEMD160 hash of the SHA-256 hash. This is the PKH used in P2PKH (Pay-to-PubKey-Hash) addresses. (20 bytes)
- Expected format:
8e 12 20 fa 50 f5 2a ef a2 ee 9b 5e ea ca ae 51 ea ae 5a d7
- Expected format:
- Derive the Base58Check encoding. This adds the defined network prefix
0x00
(Bitcoin mainnet), computes the checksum by performing a double SHA-256 hash on the network-prefixed public key, and returns the network-prefixed public key hash with the checksum appended.- Expected format:
1DxCfrSR4LkTxAamyEoZMHZbT9JpfJqaVj
- Expected format:
Fetching the UTXO Information
Here we create an instance of mempoolJS to interact with the Bitcoin blockchain.
const { bitcoin: { addresses, transactions } } = mempoolJS({
hostname: "mempool.space",
network: "mainnet",
});
const addressTxsUtxos = await addresses.getAddressTxsUtxo({
address: btcAddress,
});
const utxo = addressTxsUtxos[0];
const amountToSend = BigInt(utxo.value - 500);
- Initialize the mempoolJs library with the hostname and the Bitcoin mainnet as the network.
- Fetch the UTXO (Unspent Transaction Output) information for our Bitcoin address.
- Specify the first UTXO of the address.
- Define the amount to send our destination address as the entire value of the UTXO and subtract 500 Satoshis as the network fee. This ensures our transaction doesn't encounter a dust error and spends the entire UTXO amount.
const utxoTxDetails = await transactions.getTx({ txid: utxo.txid });
const scriptPubKey = utxoTxDetails.vout[utxo.vout].scriptpubkey;
We then derive the scriptpubkey
using the transaction details of our UTXO. The scriptPubKey
is a vital part of Bitcoin transactions, as it defines how a Bitcoin can be spent. Scripts typically ensure that in order to spend the Bitcoin, the spender must be able to produce a signature belonging to the defined public key.
Building the Transaction
Now that we have collected the necessary information, the next step is building the Bitcoin transaction for our PKP to sign.
const tx = new bitcoin.Transaction();
tx.version = 2;
tx.addInput(Buffer.from(utxo.txid, "hex").reverse(), utxo.vout);
const network = bitcoin.networks.bitcoin;
tx.addOutput(bitcoin.address.toOutputScript(BTC_DESTINATION_ADDRESS, network),
amountToSend);
const scriptPubKeyBuffer = Buffer.from(scriptPubKey, "hex");
const sighash = tx.hashForSignature(
0,
bitcoin.script.compile(scriptPubKeyBuffer),
bitcoin.Transaction.SIGHASH_ALL
);
- Initialize a
Transaction
from thebitcoinjs-lib
library. - Set the Transaction version as
2
. - Define our network as the Bitcoin mainnet.
- Convert the
scriptPubKey
from a hexadecimal string to a Buffer. - Generate a hash of the transaction to be signed with the
bitcoin.Transaction.hashForSignature()
method. We provide the index of the input being signed (0), the previous output script of our UTXO, and we useSIGHASH_ALL
to specify the signing of all of the inputs and outputs in the transaction.- Expected format:
[ 105, 95, 131, 73, 35, 152, 246, 141, 140, 71, 143, 33, 101, 234, 126, 30, 87, 96, 102, 107, 158, 57, 183, 233, 159, 35, 212, 14, 9, 83, 182, 95]
- Expected format:
Add an output to our transaction. This involves using the toOutputScript
, which converts the destination address to its corresponding output script. We also specify the amount to send the destination address.
"outs": [
{
"n": 0,
"script": {
"addresses": [
"34tpDpkBjDZD8tSSfijJjbGS7MzLQKwBxc"
],
"asm": "OP_HASH160 232399ba7086d1f345f69de5c1ca476c031a16f5 OP_EQUAL",
"hex": "a914232399ba7086d1f345f69de5c1ca476c031a16f587"
},
"value": 3419
}
]
Add an input to the transaction. This involves first converting the hex string to a Buffer. The Buffer is then reversed. This is because it is currently in big-endian format, but Bitcoin internally uses little-endian format. We then specify the utxo.vout
as the output from the previous transaction being spent by the input in the current transaction.
"ins": [
{
"n": 0,
"script": {
"asm": "3045022100b97d65eb48e780cae23d4b84ec739f0a7e2de8788cb3df6c00d84fdad4f8c93f022069d212af48b88c4441e3cce00c3b37afb3213a00f76dd7251d4b485db809444a01 eaec6d85f968eae24c0fe034ae1626cca3554a1c57ccaf7572978a2e17e3b9fdcc52eb135616efd50dbebbdeb2c7373f6e571b9ce7b61d80b20144de3b92602c",
"hex": "483045022100b97d65eb48e780cae23d4b84ec739f0a7e2de8788cb3df6c00d84fdad4f8c93f022069d212af48b88c4441e3cce00c3b37afb3213a00f76dd7251d4b485db809444a0140eaec6d85f968eae24c0fe034ae1626cca3554a1c57ccaf7572978a2e17e3b9fdcc52eb135616efd50dbebbdeb2c7373f6e571b9ce7b61d80b20144de3b92602c"
},
"sequence": 4294967295,
"txid": "6b727883f87ee12a5d0009d61d7b64db096fbd725f9e7e973080816b96edd4bd",
"witness": []
}
]
Signing the Transaction with the PKP
To sign the transaction with the PKP, we will execute a Lit Action. In this example, the transaction will only be signed if the current block height on the Bitcoin mainnet is odd.
// Filename: litAction.ts
// @ts-nocheck
const _litActionCode = async () => {
try {
const url = "https://mempool.space/api/blocks/tip/height";
const resp = await fetch(url).then((response) => response.json());
if (Number(resp) % 2 === 0 ){
Lit.Actions.setResponse({ response: "Block height is even! Don't sign!"});
console.log("Current block height:", resp);
return;
}
const sigShare = await LitActions.signEcdsa({ toSign, publicKey, sigName: 'btcSignature' });
Lit.Actions.setResponse({ response: 'true' });
} catch (error) {
Lit.Actions.setResponse({ response: error.message });
}
};
export const litActionCode = `(${_litActionCode.toString()})();`;
- Define the endpoint URL for the HTTP request to fetch the current Bitcoin block height.
- In this example, if the block height is even, we will refuse to sign and terminate the Lit Action execution.
- If the block height is odd, we will sign the data in the
toSign
variable using our PKP and set the response as true.
Executing the Lit Action
This Lit Action is executed in our main file with the following method, where we designate the sighash as the content our PKP will sign.
import { litActionCode } from "./litAction";
const litActionResponse = await litNodeClient.executeJs({
code: litActionCode,
sessionSigs,
jsParams: {
toSign: sighash,
publicKey: PKP_PUBLIC_KEY,
},
});
if (litActionResponse.response === "Block height is even! Don't sign!") {
return "Block height was even; transaction not signed.";
}
Upon successful signing, the litActionResponse
variable will appear similar to:
litActionResponse: {
claims: {},
signatures: {
btcSignature: {
r: 'd50b9c39e72bf0167d8ca769f4d3dcebf985d4330a108cdcbe407d9b88acb5e2',
s: '62d25cb024bf2eaa52bbf5fd2fbd8e58e964d9724be824c56f1c3204e7fd862c',
recid: 1,
signature: '0xd50b9c39e72bf0167d8ca769f4d3dcebf985d4330a108cdcbe407d9b88acb5e262d25cb024bf2eaa52bbf5fd2fbd8e58e964d9724be824c56f1c3204e7fd862c1c',
publicKey: '04EAEC6D85F968EAE24C0FE034AE1626CCA3554A1C57CCAF7572978A2E17E3B9FDCC52EB135616EFD50DBEBBDEB2C7373F6E571B9CE7B61D80B20144DE3B92602C',
dataSigned: '695F83492398F68D8C478F2165EA7E1E5760666B9E39B7E99F23D40E0953B65F'
}
},
response: true,
logs: ''
}
If the block height was even, the transaction was not signed. We are informed of this through the Lit Action response.
Converting from an Ethereum-like Signature to Bitcoin-like
Now that our PKP has successfully signed the transaction, we need to broadcast it to the Bitcoin mainnet. The issue with our current signature is that the formatting is that of an ECDSA signature, which contains the 32-byte components r
, s
, and signature
. We need to use these components to construct a signature that is acceptable for broadcasting on the Bitcoin blockchain.
let r = Buffer.from(litActionResponse.signatures.btcSignature.r, "hex");
let s = Buffer.from(litActionResponse.signatures.btcSignature.s, "hex");
let rBN = new BN(r);
let sBN = new BN(s);
const secp256k1 = new EC('secp256k1');
const n = secp256k1.curve.n;
if (sBN.cmp(n.divn(2)) === 1) {
sBN = n.sub(sBN);
}
r = rBN.toArrayLike(Buffer, 'be', 32);
s = sBN.toArrayLike(Buffer, 'be', 32);
function ensurePositive(buffer: Buffer) {
if (buffer[0] & 0x80) {
const newBuffer = Buffer.alloc(buffer.length + 1);
newBuffer[0] = 0x00;
buffer.copy(newBuffer, 1);
return newBuffer;
}
return buffer;
}
r = ensurePositive(r);
s = ensurePositive(s);
let derSignature;
try {
derSignature = bip66.encode(r, s);
} catch (error) {
console.error('Error during DER encoding:', error);
throw error;
}
- Extract the
r
ands
values from thebtcSignature
, covnvert them from a hexadecimal string to a Buffer. - Create an instance of the secp256k1 elliptic curve, which is the elliptic curve used in Bitcoin's public key cryptography..
- Extract the number of points, or the order (
n
), on the elliptic curve. - Implement low-S normalization, which ensures that
s
is less than half of the order. Bitcoin requires this operation to prevent transaction malleability. - Convert the
r
ands
values from a BigNumber into a 32-byte Buffer in big-endian order. This is done so next we can ensure the positivity of ther
ands
values. - We ensure positivity of the
r
ands
values using theensurePositive
helper function. This function:- Checks if the most significant bit (MSB) of the first byte is set (i.e. the number is negative).
- If so, we construct a new buffer one byte longer than the original.
- We then prepend
0x00
to ensure the Buffer is positive. - The original buffer is copied into the new buffer starting at index 1. This ensures that only the MSB has changed.
- If the MSB was not set from the beginning, we can return the original buffer.
- After ensuring positivity of the
r
ands
values, we can format the signature for the transaction. This involves encoding using the BIP66 (Bitcoin Improvement Proposal 66), which is a standard for encoding ECDSA signatures in Bitcoin. It defines a strict DER (Distinguished Encoding Rules) encoding.
Defining the Input Script
const signatureWithHashType = Buffer.concat([
derSignature,
Buffer.from([bitcoin.Transaction.SIGHASH_ALL]),
]);
const scriptSig = bitcoin.script.compile([
signatureWithHashType,
Buffer.from(PKP_PUBLIC_KEY, "hex"),
]);
tx.setInputScript(0, scriptSig);
const txHex = tx.toHex();
- Append the Bitcoin-formatted signature with the hash type
SIGHASH_ALL
. Bitcoin requires that the hash type used during signing be appended to the signature. This informs the network how the transaction was hashed and what parts of it are covered by the signature. - We must then compile the
scriptSig
. ThesigScript
provides the necessary data to unlock and spend a Bitcoin UTXO. For a P2PKH transaction, thesigScript
requires the signature with the SIGHASH type as well as the public key. - We then attach the
sigScript
to the transaction input, also defining the index as 0. - Finally, we convert the convert the transaction to a hex value. This is what is broadcasted to the Bitcoin blockchain.
Broadcasting the Transaction
Our transaction is finally ready to be broadcasted, and our Bitcoin sent to the destination address. We can do this by invoking the mempoolJS REST API and sending the https://mempool.space/api/tx
endpoint a HTTP request containing our transaction hex value.
const broadcastTransaction = async (txHex: string) => {
try {
const response = await fetch(BROADCAST_URL, {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: txHex,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error broadcasting transaction: ${errorText}`);
}
const txid = await response.text();
console.log(`Transaction broadcasted successfully. TXID: ${txid}`);
return txid;
} catch (error: any) {
console.error(error.message);
}
};
broadcastTransaction(txHex);
- Send the HTTP request to the endpoint.
- If the response is invalid, throw an error.
- If the response is valid, console.log the response (transaction id).
- Expected format:
Transaction broadcasted successfully. TXID: 57d0430318a389c5ee447ae99b8858179863dd771f64e8aa580672216755f2f5
- Expected format:
Next Steps
By the end of this guide you should be capable of sending a Bitcoin P2PKH transaction from a PKP.
Now that you've realized Lit PKPs have the potential to sign Bitcoin transactions, the sky is the limit from here. Adding multiple UTXOs to the inputs, implementing functionality for P2SH or P2WPKH, or writing a more complex trigger for the Lit Action are all possibilities.
If you'd like to learn more about Lit, check out the Lit Protocol Documentation, more specific functionality for PKPs, or advanced usage of Lit Actions.
If you'd like to request an example of another Bitcoin transaction type, please go this form to request it.