Struct TransactionBuilder
struct TransactionBuilder
;
Constructors
Name | Description |
---|---|
this
(unlocker, refundMe)
|
Construct a new transaction builder with the provided refund address |
this
(unlocker, utxo, hash)
|
Convenience constructor that calls this
|
Methods
Name | Description |
---|---|
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
Name | Description |
---|---|
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);