Custom UTXO transactions
For advanced use cases, ChainGate lets you construct UTXO transactions with explicit inputs and outputs. Instead of the standard transfer() flow (which automatically selects UTXOs), createTransaction() gives you full control over which UTXOs to spend and how to distribute the outputs.
Use createTransaction() when you need to:
- Consolidate UTXOs into a single output
- Spend specific UTXOs (coin selection)
- Create transactions with multiple recipients (batch payments)
- Implement custom change handling logic
Custom transactions require you to manage inputs, outputs, and fees manually. The fee is implicitly the difference between the sum of input amounts and the sum of output amounts. If outputs exceed inputs, the transaction is invalid.
Basic flow
import { ChainGate, importWallet } from 'chaingate'
const wallet = importWallet({ phrase: 'your phrase here...' })
const cg = new ChainGate({ apiKey: 'your-api-key' })
const bitcoin = cg.connect(cg.networks.bitcoin, wallet)
// 1. Create a custom transaction with explicit inputs and outputs
const tx = bitcoin.createTransaction({
inputs: [
{
txid: 'abc123...', // Transaction ID of the UTXO to spend
index: 0, // Output index within that transaction
amount: cg.networks.bitcoin.amount('0.001'), // Amount as Amount object
script: 'a914...' // Locking script (hex-encoded)
}
],
outputs: [
{
address: 'bc1qRecipient...',
amount: cg.networks.bitcoin.amount('0.0009')
}
]
})
// 2. The implied fee is inputs - outputs
console.log('Fee:', tx.fee().base(), tx.fee().symbol)
// 3. Sign and broadcast
const broadcasted = await tx.signAndBroadcast()
console.log('TX ID:', broadcasted.transactionId)
Input and output amounts are Amount objects, the same type used throughout ChainGate. Create them with network.amount() or pass them directly from other API responses (e.g., addressUtxos()).
Finding UTXOs to spend
Use addressUtxos() on the connector to list the UTXOs available for your wallet address:
const utxoResult = await bitcoin.addressUtxos()
for (const utxo of utxoResult.utxos) {
console.log(utxo.txid, utxo.n, utxo.amount.base(), utxo.script)
}
You can then use these UTXOs as inputs directly — utxo.amount is already an Amount:
const utxoResult = await bitcoin.addressUtxos()
const tx = bitcoin.createTransaction({
inputs: utxoResult.utxos.map(utxo => ({
txid: utxo.txid,
index: utxo.n,
amount: utxo.amount,
script: utxo.script
})),
outputs: [
{ address: 'bc1qRecipient...', amount: cg.networks.bitcoin.amount('0.0005') }
]
})
Modifying inputs and outputs
After creating a transaction, you can add or remove inputs and outputs before broadcasting:
const tx = bitcoin.createTransaction({ inputs: [...], outputs: [...] })
// Add more inputs
tx.addInput({
txid: 'def456...',
index: 1,
amount: cg.networks.bitcoin.amount('0.0005'),
script: '0014...'
})
// Remove an input by txid and index
tx.removeInput('abc123...', 0)
// Add another output
tx.addOutput({
address: 'bc1qAnother...',
amount: cg.networks.bitcoin.amount('0.0002')
})
// Remove an output by its position (0-based)
tx.removeOutput(1)
Inspecting the transaction
// Current inputs
console.log(tx.inputs())
// Current outputs
console.log(tx.outputs())
// Implied fee (sum of inputs - sum of outputs) — returns an Amount
console.log('Fee:', tx.fee().base(), tx.fee().symbol)
// Estimated transaction size in virtual bytes
console.log('Size:', tx.estimatedSizeBytes(), 'vbytes')
Automatic change output
Use setChangeAddress() to automatically compute and append a change output. The change amount is calculated by subtracting all existing outputs and the estimated fee from the total inputs. If the remaining amount is below the dust threshold, no change output is added and the remainder goes entirely to fees.
const tx = bitcoin.createTransaction({
inputs: [
{ txid: '...', index: 0, amount: cg.networks.bitcoin.amount('0.002'), script: '...' }
],
outputs: [
{ address: 'bc1qRecipient...', amount: cg.networks.bitcoin.amount('0.0005') }
]
})
// Fetch recommended fee rates
const fees = await tx.recommendedFees()
// Auto-compute and append the change output
tx.setChangeAddress('bc1qMyChange...', fees.normal)
console.log(tx.outputs())
// [
// { address: 'bc1qRecipient...', amount: Amount(0.0005 BTC) },
// { address: 'bc1qMyChange...', amount: Amount(0.001495 BTC) } // change (minus fee)
// ]
You can also pass a custom fee rate:
tx.setChangeAddress('bc1qMyChange...', { feePerKbSat: 20_000n })
Recommended fees
recommendedFees() fetches current network fee rates and estimates the fee for this transaction based on its current inputs and outputs. Returns null if the transaction has no inputs or no outputs.
const fees = await tx.recommendedFees()
if (fees) {
console.log('Low fee:', fees.low.estimatedFeeSat, 'sat')
console.log('Normal fee:', fees.normal.estimatedFeeSat, 'sat')
console.log('High fee:', fees.high.estimatedFeeSat, 'sat')
console.log('Maximum fee:', fees.maximum.estimatedFeeSat, 'sat')
}
Each tier includes:
| Property | Type | Description |
|---|---|---|
feePerKbSat | bigint | Fee rate in satoshis per kilobyte |
estimatedFeeSat | bigint | null | Estimated total fee for this transaction |
estimatedConfirmationSecs | number | Estimated seconds until confirmation |
enoughFunds | boolean | Whether the inputs cover all outputs plus this fee |
Bitcoin Cash
Custom transactions also work on Bitcoin Cash. Addresses can be provided in CashAddr or legacy format:
const bch = cg.connect(cg.networks.bitcoincash, wallet)
const tx = bch.createTransaction({
inputs: [
{ txid: '...', index: 0, amount: cg.networks.bitcoincash.amount('0.001'), script: '...' }
],
outputs: [
{ address: 'bitcoincash:qq...', amount: cg.networks.bitcoincash.amount('0.0009') }
]
})
const broadcasted = await tx.signAndBroadcast()
Full example: consolidating UTXOs
const bitcoin = cg.connect(cg.networks.bitcoin, wallet)
const myAddress = await bitcoin.address()
// Fetch all UTXOs
const utxoResult = await bitcoin.addressUtxos()
// Create a transaction spending all UTXOs to yourself
const tx = bitcoin.createTransaction({
inputs: utxoResult.utxos.map(utxo => ({
txid: utxo.txid,
index: utxo.n,
amount: utxo.amount,
script: utxo.script
})),
outputs: [] // We'll use setChangeAddress to create the output
})
// Auto-compute a single change output (consolidation)
const fees = await tx.recommendedFees()
tx.setChangeAddress(myAddress, fees.normal)
const broadcasted = await tx.signAndBroadcast()
console.log('Consolidated into TX:', broadcasted.transactionId)
Key points
- Full control -- You choose which UTXOs to spend and how to distribute outputs.
- Amount objects -- Input and output amounts use the
Amounttype. Create them withnetwork.amount()or pass them from API responses likeaddressUtxos(). - Implicit fee -- The fee is the difference between total inputs and total outputs.
fee()returns anAmount. - Mutation -- Use
addInput(),removeInput(),addOutput(),removeOutput()to modify the transaction after creation. - Change handling -- Use
setChangeAddress()to automatically append a change output with dust threshold handling. - Fee estimation -- Use
recommendedFees()to get network fee rates for the current transaction size. - Available on all UTXO networks -- Works on Bitcoin, Litecoin, Dogecoin, Bitcoin Cash, and Bitcoin Testnet.