Transaction Output
Overview
The TransactionOutput class in the BSV TypeScript SDK represents an output in a Bitcoin transaction. Each output specifies an amount of satoshis and a locking script that defines the conditions under which those coins can be spent. Outputs create new UTXOs (Unspent Transaction Outputs) that can be referenced as inputs in future transactions.
Purpose
Define where coins are sent in a transaction with locking scripts
Specify the amount of satoshis for each output
Create spendable UTXOs for future transactions
Support standard payment types (P2PKH, P2PK, OP_RETURN)
Enable change outputs with automatic value calculation
Handle custom locking conditions for smart contracts
Serialize and deserialize outputs for transaction broadcasting
Basic Usage
import { Transaction, P2PKH, Script, OP } from '@bsv/sdk';
const tx = new Transaction();
// Add a standard P2PKH payment output
tx.addOutput({
lockingScript: new P2PKH().lock('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'),
satoshis: 10000
});
// Add a change output (amount calculated automatically)
tx.addOutput({
lockingScript: new P2PKH().lock(changeAddress),
change: true
});
// Add an OP_RETURN data output
tx.addOutput({
lockingScript: new Script()
.writeOpCode(OP.OP_FALSE)
.writeOpCode(OP.OP_RETURN)
.writeBin(Buffer.from('Hello BSV')),
satoshis: 0
});
console.log('Outputs:', tx.outputs.length);
console.log('Total output value:', tx.outputs.reduce((sum, o) => sum + o.satoshis, 0));Key Features
1. Standard Payment Outputs
Create outputs for sending payments to addresses:
import { Transaction, P2PKH, PrivateKey } from '@bsv/sdk';
const tx = new Transaction();
// P2PKH output (most common - pay to public key hash)
const recipientAddress = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa';
tx.addOutput({
lockingScript: new P2PKH().lock(recipientAddress),
satoshis: 50000 // 0.0005 BSV
});
// P2PK output (pay to public key directly)
const recipientPubKey = PrivateKey.fromRandom().toPublicKey();
tx.addOutput({
lockingScript: new Script()
.writeBin(recipientPubKey.encode(true))
.writeOpCode(OP.OP_CHECKSIG),
satoshis: 25000
});
// Multiple outputs to different recipients
const recipients = [
{ address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', satoshis: 10000 },
{ address: '1B2Y3Z...', satoshis: 20000 },
{ address: '1C3X4W...', satoshis: 30000 }
];
recipients.forEach(recipient => {
tx.addOutput({
lockingScript: new P2PKH().lock(recipient.address),
satoshis: recipient.satoshis
});
});
console.log('Created', tx.outputs.length, 'payment outputs');2. Change Outputs
Automatically calculate change from transaction inputs:
import { Transaction, PrivateKey, P2PKH } from '@bsv/sdk';
const privKey = PrivateKey.fromWif('L5EY1SbTvvPNSdCYQe1EJHfXCBBT4PmnF6CDbzCm9iifZptUvDGB');
const changeAddress = privKey.toPublicKey().toAddress();
const tx = new Transaction();
// Add input (100,000 satoshis)
tx.addInput({
sourceTXID: '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b',
sourceOutputIndex: 0,
sourceSatoshis: 100000,
unlockingScriptTemplate: new P2PKH().unlock(privKey, 'all', false, 100000)
});
// Add payment output (60,000 satoshis)
tx.addOutput({
lockingScript: new P2PKH().lock('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'),
satoshis: 60000
});
// Add change output - NEVER reuse the input address!
tx.addOutput({
lockingScript: new P2PKH().lock(changeAddress),
change: true // Amount calculated automatically
});
// Calculate fee (deducts from change output)
await tx.fee();
console.log('Change output value:', tx.outputs[1].satoshis);
// Example: 39800 satoshis (100000 - 60000 - 200 fee)
await tx.sign();3. OP_RETURN Data Outputs
Store arbitrary data on the blockchain:
import { Transaction, Script, OP } from '@bsv/sdk';
const tx = new Transaction();
// Simple OP_RETURN with text
tx.addOutput({
lockingScript: new Script()
.writeOpCode(OP.OP_FALSE)
.writeOpCode(OP.OP_RETURN)
.writeBin(Buffer.from('Hello BSV')),
satoshis: 0 // OP_RETURN outputs typically have 0 value
});
// Multi-field OP_RETURN (protocol data)
tx.addOutput({
lockingScript: new Script()
.writeOpCode(OP.OP_FALSE)
.writeOpCode(OP.OP_RETURN)
.writeBin(Buffer.from('19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut')) // B:// protocol
.writeBin(Buffer.from('image/png', 'utf8'))
.writeBin(Buffer.from('filename.png', 'utf8'))
.writeBin(Buffer.from([0x89, 0x50, 0x4e, 0x47])), // PNG header
satoshis: 0
});
// Large data output (BSV supports large OP_RETURN)
const largeData = Buffer.alloc(100000); // 100 KB
largeData.fill('BSV');
tx.addOutput({
lockingScript: new Script()
.writeOpCode(OP.OP_FALSE)
.writeOpCode(OP.OP_RETURN)
.writeBin(largeData),
satoshis: 0
});
console.log('OP_RETURN data size:', largeData.length, 'bytes');4. Custom Locking Scripts
Create outputs with custom spending conditions:
import { Transaction, Script, OP, Hash } from '@bsv/sdk';
const tx = new Transaction();
// Hash puzzle output (anyone with secret can spend)
const secret = Buffer.from('my_secret');
const secretHash = Hash.sha256(secret);
tx.addOutput({
lockingScript: new Script()
.writeOpCode(OP.OP_HASH256)
.writeBin(Array.from(secretHash))
.writeOpCode(OP.OP_EQUAL),
satoshis: 10000
});
// Time-locked output (can only spend after block height/time)
const lockTime = 1700000000; // Unix timestamp
tx.addOutput({
lockingScript: new Script()
.writeBin(Buffer.from(lockTime.toString(16), 'hex'))
.writeOpCode(OP.OP_CHECKLOCKTIMEVERIFY)
.writeOpCode(OP.OP_DROP)
.writeOpCode(OP.OP_DUP)
.writeOpCode(OP.OP_HASH160)
.writeBin(Buffer.from('pubkeyhash', 'hex'))
.writeOpCode(OP.OP_EQUALVERIFY)
.writeOpCode(OP.OP_CHECKSIG),
satoshis: 50000
});
// Multi-signature output (2-of-3)
const pubKey1 = Buffer.from('02abc...', 'hex');
const pubKey2 = Buffer.from('03def...', 'hex');
const pubKey3 = Buffer.from('02123...', 'hex');
tx.addOutput({
lockingScript: new Script()
.writeOpCode(OP.OP_2) // Require 2 signatures
.writeBin(pubKey1)
.writeBin(pubKey2)
.writeBin(pubKey3)
.writeOpCode(OP.OP_3) // Out of 3 keys
.writeOpCode(OP.OP_CHECKMULTISIG),
satoshis: 100000
});
console.log('Created custom locking outputs');API Reference
TransactionOutput Interface
interface TransactionOutput {
// Amount in satoshis
satoshis: number;
// Locking script (defines spending conditions)
lockingScript: Script;
// Whether this is a change output (amount calculated automatically)
change?: boolean;
}Creating Outputs
// Add output to transaction
tx.addOutput(output: TransactionOutput): Transaction
// Access outputs
tx.outputs: TransactionOutput[]
// Get specific output
const output = tx.outputs[0];Output Properties
output.satoshis // number - Amount in satoshis
output.lockingScript // Script - Locking script
output.change // boolean | undefined - Is change outputSerialization
// Transaction outputs are serialized as part of transaction
const txHex = tx.toHex();
const txBinary = tx.toBinary();
// Parse transaction with outputs
const parsedTx = Transaction.fromHex(txHex);
console.log('Outputs:', parsedTx.outputs.length);
console.log('First output value:', parsedTx.outputs[0].satoshis);Common Patterns
Pattern 1: Payment Distribution
Split payment among multiple recipients:
import { Transaction, PrivateKey, P2PKH } from '@bsv/sdk';
async function distributePayment(
sourceUTXO: { txid: string; vout: number; satoshis: number },
recipients: Array<{ address: string; amount: number }>,
privKey: PrivateKey,
changeAddress: string
): Promise<Transaction> {
const tx = new Transaction();
// Add input
tx.addInput({
sourceTXID: sourceUTXO.txid,
sourceOutputIndex: sourceUTXO.vout,
sourceSatoshis: sourceUTXO.satoshis,
unlockingScriptTemplate: new P2PKH().unlock(
privKey,
'all',
false,
sourceUTXO.satoshis
)
});
// Add output for each recipient
recipients.forEach(recipient => {
tx.addOutput({
lockingScript: new P2PKH().lock(recipient.address),
satoshis: recipient.amount
});
});
// Add change output
tx.addOutput({
lockingScript: new P2PKH().lock(changeAddress),
change: true
});
// Calculate fees and sign
await tx.fee();
await tx.sign();
console.log('Distributed to', recipients.length, 'recipients');
return tx;
}
// Usage
const utxo = { txid: '4a5e...', vout: 0, satoshis: 100000 };
const recipients = [
{ address: '1A1z...', amount: 10000 },
{ address: '1B2y...', amount: 20000 },
{ address: '1C3x...', amount: 30000 }
];
const privKey = PrivateKey.fromWif('L5EY...');
const tx = await distributePayment(utxo, recipients, privKey, '1D4w...');Pattern 2: Data and Payment Combined
Store data while making payments:
import { Transaction, P2PKH, Script, OP } from '@bsv/sdk';
class DataPaymentTransaction {
static create(
paymentAddress: string,
paymentSatoshis: number,
data: Buffer[],
changeAddress: string
): Transaction {
const tx = new Transaction();
// Output 0: Payment to recipient
tx.addOutput({
lockingScript: new P2PKH().lock(paymentAddress),
satoshis: paymentSatoshis
});
// Output 1: OP_RETURN data
const dataScript = new Script()
.writeOpCode(OP.OP_FALSE)
.writeOpCode(OP.OP_RETURN);
data.forEach(chunk => dataScript.writeBin(chunk));
tx.addOutput({
lockingScript: dataScript,
satoshis: 0
});
// Output 2: Change
tx.addOutput({
lockingScript: new P2PKH().lock(changeAddress),
change: true
});
return tx;
}
}
// Usage: Invoice with payment and data
const tx = DataPaymentTransaction.create(
'1A1z...', // Payment recipient
50000, // Payment amount
[
Buffer.from('INVOICE'),
Buffer.from('INV-2024-001'),
Buffer.from(JSON.stringify({ items: ['item1', 'item2'], total: 50000 }))
],
'1D4w...' // Change address
);
// Add input and finalize
// ... add inputs, calculate fee, sign ...Pattern 3: Atomic Swap Output Structure
Create outputs for atomic swap transactions:
import { Transaction, Script, OP, Hash } from '@bsv/sdk';
class AtomicSwapOutput {
/**
* Create Hash Time Locked Contract (HTLC) output for atomic swap
*/
static createHTLC(
payeeHash: Buffer,
payerHash: Buffer,
secret: Buffer,
lockTime: number,
amount: number
): TransactionOutput {
const secretHash = Hash.sha256(secret);
const lockingScript = new Script()
// Path 1: Payee provides secret
.writeOpCode(OP.OP_IF)
.writeOpCode(OP.OP_HASH256)
.writeBin(Array.from(secretHash))
.writeOpCode(OP.OP_EQUALVERIFY)
.writeOpCode(OP.OP_DUP)
.writeOpCode(OP.OP_HASH160)
.writeBin(payeeHash)
.writeOpCode(OP.OP_ELSE)
// Path 2: Payer reclaims after timeout
.writeBin(Buffer.from(lockTime.toString(16), 'hex'))
.writeOpCode(OP.OP_CHECKLOCKTIMEVERIFY)
.writeOpCode(OP.OP_DROP)
.writeOpCode(OP.OP_DUP)
.writeOpCode(OP.OP_HASH160)
.writeBin(payerHash)
.writeOpCode(OP.OP_ENDIF)
.writeOpCode(OP.OP_EQUALVERIFY)
.writeOpCode(OP.OP_CHECKSIG);
return {
lockingScript,
satoshis: amount
};
}
/**
* Create both sides of atomic swap
*/
static createSwapTransactions(
party1: { hash: Buffer; amount: number },
party2: { hash: Buffer; amount: number },
secret: Buffer,
lockTime1: number,
lockTime2: number
): { tx1: Transaction; tx2: Transaction } {
// Transaction 1: Party 1 locks funds
const tx1 = new Transaction();
tx1.addOutput(
this.createHTLC(party2.hash, party1.hash, secret, lockTime1, party1.amount)
);
// Transaction 2: Party 2 locks funds (same secret hash)
const tx2 = new Transaction();
tx2.addOutput(
this.createHTLC(party1.hash, party2.hash, secret, lockTime2, party2.amount)
);
return { tx1, tx2 };
}
}
// Usage: Create atomic swap between two parties
const secret = Buffer.from('shared_secret_12345');
const party1Hash = Buffer.from('party1_pubkey_hash', 'hex');
const party2Hash = Buffer.from('party2_pubkey_hash', 'hex');
const { tx1, tx2 } = AtomicSwapOutput.createSwapTransactions(
{ hash: party1Hash, amount: 100000 },
{ hash: party2Hash, amount: 200000 },
secret,
Math.floor(Date.now() / 1000) + 86400, // 24 hours
Math.floor(Date.now() / 1000) + 86400
);
console.log('Atomic swap transactions created');Security Considerations
Output Value Validation: Ensure output values don't exceed input values (accounting for fees).
Change Address Security: Never reuse addresses. Generate new addresses for change outputs.
Dust Outputs: Very small outputs (below dust threshold) may not be relayed by nodes. Minimum is typically 546 satoshis for P2PKH.
OP_RETURN Data: While BSV supports large OP_RETURN, verify miner policies for data size acceptance.
Locking Script Validation: Test custom locking scripts thoroughly before using with real funds.
Zero-Value Outputs: Only OP_RETURN outputs should have zero satoshis. Payment outputs must have positive values.
Performance Considerations
Output Count: More outputs increase transaction size and fees. Consolidate when possible.
Script Complexity: Complex locking scripts increase size. Keep scripts simple when possible.
OP_RETURN Size: Large OP_RETURN data increases transaction size proportionally. Consider overlay networks for very large data.
Change Output Management: Always include change output to avoid losing funds to fees.
Output Ordering: Output order doesn't affect validity but may affect privacy. Consider randomizing output order.
Related Components
Transaction - Build complete transactions with outputs
TransactionInput - Inputs that reference outputs
Script - Create locking scripts
ScriptTemplate - Templates for common locking patterns
P2PKH - Standard payment template
Code Examples
See complete working examples in:
Best Practices
Always include change output to avoid losing funds to fees
Never reuse addresses - generate new address for change
Validate total output value doesn't exceed input value
Use appropriate output types - P2PKH for payments, OP_RETURN for data
Set OP_RETURN outputs to 0 satoshis unless protocol requires otherwise
Check dust limits - ensure outputs meet minimum value requirements
Test custom scripts thoroughly before production use
Document OP_RETURN data format for protocol interoperability
Consider miner policies for large data outputs
Implement proper error handling for output creation failures
Troubleshooting
Issue: Output value exceeds input value
Solution: Ensure total outputs (including fees) don't exceed inputs.
const totalInput = tx.inputs.reduce((sum, i) => sum + (i.sourceSatoshis || 0), 0);
const totalOutput = tx.outputs.reduce((sum, o) => sum + o.satoshis, 0);
if (totalOutput > totalInput) {
throw new Error(`Output (${totalOutput}) exceeds input (${totalInput})`);
}Issue: Dust output rejected
Solution: Ensure outputs meet minimum dust threshold (546 satoshis for P2PKH).
const DUST_LIMIT = 546;
tx.outputs.forEach((output, index) => {
if (output.satoshis > 0 && output.satoshis < DUST_LIMIT) {
console.warn(`Output ${index} is dust: ${output.satoshis} satoshis`);
}
});Issue: Change output has negative value
Solution: Ensure sufficient input value to cover outputs and fees.
await tx.fee(); // Calculate fees
// Check if change output is negative
const changeOutput = tx.outputs.find(o => o.change);
if (changeOutput && changeOutput.satoshis < 0) {
throw new Error('Insufficient funds for transaction');
}Issue: OP_RETURN too large
Solution: Check miner policies and consider splitting data or using overlay networks.
const MAX_OP_RETURN = 100000; // Example limit
const opReturnOutput = tx.outputs.find(o =>
o.lockingScript.chunks.some(c => c.op === OP.OP_RETURN)
);
if (opReturnOutput) {
const size = opReturnOutput.lockingScript.toBinary().length;
if (size > MAX_OP_RETURN) {
console.warn(`OP_RETURN size (${size}) exceeds recommended limit`);
}
}Further Reading
Bitcoin Outputs - Transaction output structure
OP_RETURN - Data storage on blockchain
Dust - Understanding dust limits
Locking Scripts - Bitcoin Script for outputs
BRC-8 - Transaction envelopes
BSV SDK Documentation - Official SDK docs
Status
✅ Complete
Last updated
