Struct TransactionBuilder

struct TransactionBuilder ;

Constructors

NameDescription
this (unlocker, refundMe) Construct a new transaction builder with the provided refund address
this (unlocker, utxo, hash) Convenience constructor that calls this.attach(Output, Hash)

Methods

NameDescription
attach (tx) Attaches all or one output(s) of a transaction to this builder
attach (utxo, hash, freeze_fee) Attaches to an Output according to a hash
attach (rng) Attaches to a range of tuples.
deduct (amount) Deduct a certain amount
draw (amount, toward) Splits the attached input into multiple outputs of the given amounts.
feeRate (fee_rate) Set the feeRate property of the resulting transaction
lock (height) Set the lock_height property of the resulting transaction
payload (data) Set the payload used by the Transaction
refund (toward) Resets the state and changes the address of the refund transaction
sign (outputs_type, unlock_age, freeze_fee) Finalize the transaction, signing the input, and reset the builder
signWithSpecificKey (tx, ) Sign with a given key and append ubytes if given
split (toward) Similar to draw(Amount, PublicKey[]), but uses all available funds
unlockSigner (unlocker) Sets the unlocker function to sign the inputs

Aliases

NameDescription
Unlocker Define Unlocker function to sign the inputs

Example

Test for a split with the same amount of outputs as inputs Essentially doing an equality transformation

immutable Number = GenesisBlock.payments.front.outputs.length;
assert(Number == 8);

const tx = TxBuilder(GenesisBlock.payments.front)
    .split(WK.Keys.byRange.map!(k => k.address).take(Number))
    .sign();

// This transaction splits to 8 outputs
assert(tx.inputs.length == Number);
assert(tx.outputs.length == Number);
// Since the amount is evenly distributed in Genesis,
// they all have the same value
auto implied_fees = tx.outputs.map!(o => o.value).fold!((a,b) => a - b)(sumOfGenesisFirstTxOutputs());
assert(implied_fees >= fee_rate * tx.sizeInBytes);
assert(tx.outputs == [
    Output(Amount(59_499_999_9871_462L), WK.Keys.A.address),
    Output(Amount(59_499_999_9871_462L), WK.Keys.C.address),
    Output(Amount(59_499_999_9871_462L), WK.Keys.D.address),
    Output(Amount(59_499_999_9871_462L), WK.Keys.E.address),
    Output(Amount(59_499_999_9871_462L), WK.Keys.F.address),
    Output(Amount(59_499_999_9871_462L), WK.Keys.G.address),
    Output(Amount(59_499_999_9871_462L), WK.Keys.H.address),
    Output(Amount(59_499_999_9871_462L), WK.Keys.J.address),
].sort.array);
// check we have not lost any coin
assert((Amount(59_499_999_9871_462L) * 8) + implied_fees == Amount(59_500_000_0000_000L) * 8);

Example

Test with twice as many outputs as inputs

immutable Number = GenesisBlock.payments.front.outputs.length * 2;
assert(Number == 16);

const resTx1 = TxBuilder(GenesisBlock.payments.front)
    .split(WK.Keys.byRange.map!(k => k.address).take(Number))
    .sign();

// This transaction has 8 inputs
assert(resTx1.inputs.length == GenesisBlock.payments.front.outputs.length);
// The transaction splits to 16 outputs
assert(resTx1.outputs.length == Number);

// 488M / 16
const totalInputs = sumOfGenesisFirstTxOutputs();
auto outputs_1 = resTx1.outputs.map!(o => o.value).reduce!((a,b) => a + b);
auto implied_fees = totalInputs - outputs_1;
assert(implied_fees >= fee_rate * resTx1.sizeInBytes);
auto outputs = outputs_1;
auto refund = outputs.div(Number);
assert(refund == Amount(0));
assert(resTx1.outputs.map!(o => o.value).reduce!(max) == outputs);
assert(resTx1.outputs.count!(o => o.value == outputs) == Number);

// Test with multi input keys
// Split into 32 outputs
const resTx2 = TxBuilder(resTx1)
    .split(iota(Number * 2).map!(_ => KeyPair.random().address))
    .sign();

// This transaction has 32 txs
assert(resTx2.inputs.length == Number);
assert(resTx2.outputs.length == Number * 2);
auto outputs_2 = resTx2.outputs.map!(o => o.value).reduce!((a,b) => a + b);
auto implied_fees_2 = outputs_1 - outputs_2;
assert(implied_fees_2 >= fee_rate * resTx2.sizeInBytes);
auto refund_2 = outputs_2.div(Number * 2);
assert(refund_2 == Amount(0));
assert(resTx2.outputs.map!(o => o.value).reduce!(max) == outputs_2);
assert(resTx2.outputs.count!(o => o.value == outputs_2) == Number * 2);

Example

Test with small remainder

immutable Number = 3;
auto fee_rate = Amount(700);

const result = TxBuilder(GenesisBlock.payments.front)
    .split(WK.Keys.byRange.map!(k => k.address).take(Number))
    .sign();

assert(result.outputs.length == Number);
const totalInputs = sumOfGenesisFirstTxOutputs();
auto outputs = result.outputs.map!(o => o.value).reduce!((a,b) => a + b);
auto implied_fees = totalInputs - outputs;
assert(implied_fees >= fee_rate * result.sizeInBytes);
auto refund = outputs.div(Number);
assert(refund == Amount(0));
assert(result.outputs.map!(o => o.value).reduce!(max) == outputs);
assert(result.outputs.count!(o => o.value == outputs) == Number);

// This transaction has 3 outputs
assert(result.inputs.length == 8);
assert(result.inputs.isSorted);
assert(result.outputs.length == 3);
assert(result.outputs.isSorted);

Example

Test with one output key

const result = TxBuilder(GenesisBlock.payments.front)
    .split([WK.Keys.A.address])
    .sign();

// This transaction has 1 txs
assert(result.inputs.length == 8);
assert(result.outputs.length == 1);

const totalInputs = sumOfGenesisFirstTxOutputs();
auto implied_fees = totalInputs - result.outputs[0].value;
assert(implied_fees >= fee_rate * result.sizeInBytes);

Example

Test changing the refund address (and merging outputs by extension)

immutable Number = 3;
const result = TxBuilder(GenesisBlock.payments.front)
    // Refund needs to be called first as it resets the outputs
    .refund(WK.Keys.Z.address)
    .draw(Amount(100_000_000_0000_000L), WK.Keys.byRange.map!(k => k.address).take(Number))
    .sign();

// This transaction has 4 outputs (3 draw and 1 refund)
assert(result.inputs.length == 8);
assert(result.outputs.length == Number + 1);

const totalInputs = sumOfGenesisFirstTxOutputs();
auto outputs = result.outputs.map!(o => o.value).reduce!((a,b) => a + b);
auto implied_fees = totalInputs - outputs;
assert(implied_fees >= fee_rate * result.sizeInBytes);
assert(result.outputs == [
    Output(totalInputs - implied_fees - Amount(100_000_000_0000_000L) * 3, WK.Keys.Z.address),
    Output(Amount(100_000_000_0000_000L), WK.Keys.A.address),
    Output(Amount(100_000_000_0000_000L), WK.Keys.C.address),
    Output(Amount(100_000_000_0000_000L), WK.Keys.D.address),
].sort.array);

Example

Test with a range of tuples

Output[4] outs = [
    Output(Amount(1_000_000), WK.Keys.A.address),
    Output(Amount(2_000_000), WK.Keys.C.address),
    Output(Amount(3_000_000), WK.Keys.D.address),
    Output(Amount(4_000_000), WK.Keys.E.address),
];

// The hash is incorrect (it's not a proper UTXO hash)
// but TxBuilder only care about strictly monotonic hashes
auto tup_rng = outs[].zip(outs[].map!(o => o.hashFull()));
auto result = TxBuilder(WK.Keys.F.address).attach(tup_rng).sign();

auto fees = fee_rate * result.sizeInBytes;
Amount total;
outs.each!(o => total += o.value);
auto expectedAmount = total - fees;

assert(result.inputs.length == 4);
assert(result.outputs.length == 1);
assert(result.outputs[0] == Output(expectedAmount, WK.Keys.F.address));

Example

auto spendable = genesisSpendable();
assert(!genesisSpendable.empty);
Amount total;
genesisSpendable.each!(txb => total += txb.leftover.value);
// Arbitrarily low value
assert(total > Amount.MinFreezeAmount);

Example

Test with unfrozen remainder

const result = TxBuilder(GenesisBlock.payments.front)
    .draw(Amount.UnitPerCoin * 50_000, WK.Keys.byRange.map!(k => k.address).take(3))
    .sign(OutputType.Freeze, 0, 10_000.coins);

// This transaction has 4 outputs (3 freeze + 1 refund)
assert(result.inputs.length == 8);
assert(result.outputs.length == 4);

// 488M / 3
assert(result.outputs == [
    Output(Amount(50_000_0000_000L), WK.Keys.A.address, OutputType.Freeze),
    Output(Amount(50_000_0000_000L), WK.Keys.C.address, OutputType.Freeze),
    Output(Amount(50_000_0000_000L), WK.Keys.D.address, OutputType.Freeze),
    Output(Amount(475_819_999_9129_200L), WK.Keys.Genesis.address),
].sort.array);

Example

Test with unfrozen remainder with different fee rate

auto fee_rate = Amount(900);    // Using higher than min fee rate
const freezeAmount = 50_000.coins;
const result = TxBuilder(GenesisBlock.payments.front)
    .feeRate(fee_rate)
    .draw(freezeAmount, WK.Keys.byRange.map!(k => k.address).takeExactly(1))
    .sign(OutputType.Freeze, 0, 10_000.coins);

// This transaction has 2 outputs (1 freeze + 1 refund)
assert(result.inputs.length == 8);
assert(result.outputs.length == 2);

auto fees = (fee_rate * result.sizeInBytes) + 10_000.coins;
assert(result.outputs.count!(o => o.value == freezeAmount && o.type == OutputType.Freeze) == 1);
auto refund = sumOfGenesisFirstTxOutputs() - freezeAmount - fees;
assert(result.outputs.count!(o => o.value == refund && o.type == OutputType.Payment) == 1);