Skip to content

Latest commit

 

History

History
169 lines (137 loc) · 11.2 KB

generating-transactions.md

File metadata and controls

169 lines (137 loc) · 11.2 KB

Generating Transactions

TransactionBuilder API

In order to simplify transaction creation, we provide a TransactionBuilder struct that manages witnesses, fee calculation, change addresses and such. Assume we have instantiated an instance under the variable builder for this explanation. The TransactionBuilder requires several protocol parameters governing Cardano to be created which is shown in the following section. These are specified initially in the genesis file for Cardano nodes.

The minimum required for a valid transaction is to add inputs, outputs, time-to-live and either set the fee explicitly with builder.set_fee(fee), or calculate it implicitly using builder.add_change_if_needed(address). Optionally a transaction can also have certificates, reward withdrawals, and metadata added to it. Any change made to the builder can impact the size and thus the fee so the fee should be the last thing set. If implicitly setting the fee any extra ADA (inputs + withdrawals - outputs + refund - deposit - min fee) is sent to the provided change address. Fees must be sufficient, i.e. inputs + withdrawals + refund >= outputs + deposit + fee which must be manually ensured if you explicitly set the fee. Any extra fee is not necessary and the extra ADA beyond that will be burned. Once the transaction is ready, const body = builder.build() can be called to return a ready TransactionBody.

Withdrawals are ADA withdrawn as part of the rewards generated by staking and deposits are refundable ADA locked while resources such as stake certificates or pool registrations exist on the blockchain. They are returned as refunds when these resources are deregistered/retired.

To get to a transaction ready to post on the blockchain, we must create a Transaction from that, which consists of the TransactionBody, a matching TransactionWitnessSet and optionally a TransactionMetadata. The witnesses and optional metadata must match those provided to the builder. The witnesses must sign the hash of the transaction body returned by hash_transaction(body). In addition to the witnesses for inputs, withdrawals and some certificates require witnesses as well. For example, staking address registration does not require a witness while stake address de-registration requires one. For any questions or doubts about the rules governing fees, deposits, rewards, certificates or which witness types are required refer to the shelley specs, specifically the Shelley design specification for general design. The formal specification could be useful for specific details as well. The design spec contains details about which certificates require which type of witnesses in the Certificates and Registrations section.

Example code

The example below builds a transaction with all 2 of the 3 input types: key and bootstrap. Multisig (script) inputs are essentially identical to key inputs, but using the scripthash instead of the keyhash, however they are not supported for implicit fee calculation yet. Fees are automatically calculated and sent to a change address in the example.

// instantiate the tx builder with the Cardano protocol parameters - these may change later on
const linearFee = CardanoWasm.LinearFee.new(
    CardanoWasm.BigNum.from_str('44'),
    CardanoWasm.BigNum.from_str('155381')
);
const txBuilderCfg = CardanoWasm.TransactionBuilderConfigBuilder.new()
    .fee_algo(linearFee)
    .pool_deposit(CardanoWasm.BigNum.from_str('500000000'))
    .key_deposit(CardanoWasm.BigNum.from_str('2000000'))
    .max_value_size(4000)
    .max_tx_size(8000)
    .coins_per_utxo_word(CardanoWasm.BigNum.from_str('34482'))
    .build();
const txBuilder = CardanoWasm.TransactionBuilder.new(txBuilderCfg);

// add a keyhash input - for ADA held in a Shelley-era normal address (Base, Enterprise, Pointer)
const prvKey = CardanoWasm.PrivateKey.from_bech32("ed25519e_sk16rl5fqqf4mg27syjzjrq8h3vq44jnnv52mvyzdttldszjj7a64xtmjwgjtfy25lu0xmv40306lj9pcqpa6slry9eh3mtlqvfjz93vuq0grl80");
txBuilder.add_key_input(
    prvKey.to_public().hash(),
    CardanoWasm.TransactionInput.new(
        CardanoWasm.TransactionHash.from_bytes(
            Buffer.from("8561258e210352fba2ac0488afed67b3427a27ccf1d41ec030c98a8199bc22ec", "hex")
        ), // tx hash
        0, // index
    ),
    CardanoWasm.Value.new(CardanoWasm.BigNum.from_str('3000000'))
);

// add a bootstrap input - for ADA held in a Byron-era address
const byronAddress = CardanoWasm.ByronAddress.from_base58("Ae2tdPwUPEZLs4HtbuNey7tK4hTKrwNwYtGqp7bDfCy2WdR3P6735W5Yfpe");
txBuilder.add_bootstrap_input(
    byronAddress,
    CardanoWasm.TransactionInput.new(
    CardanoWasm.TransactionHash.from_bytes(
        Buffer.from("488afed67b342d41ec08561258e210352fba2ac030c98a8199bc22ec7a27ccf1", "hex"),
    ), // tx hash
    0, // index
    ),
    CardanoWasm.Value.new(CardanoWasm.BigNum.from_str('3000000'))
);

// base address
const shelleyOutputAddress = CardanoWasm.Address.from_bech32("addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w");
// pointer address
const shelleyChangeAddress = CardanoWasm.Address.from_bech32("addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspqgpsqe70et");

// add output to the tx
txBuilder.add_output(
    CardanoWasm.TransactionOutput.new(
    shelleyOutputAddress,
    CardanoWasm.Value.new(CardanoWasm.BigNum.from_str('1000000'))    
    ),
);

// set the time to live - the absolute slot value before the tx becomes invalid
txBuilder.set_ttl(410021);

// calculate the min fee required and send any change to an address
txBuilder.add_change_if_needed(shelleyChangeAddress);

// once the transaction is ready, we build it to get the tx body without witnesses
const txBody = txBuilder.build();
const txHash = CardanoWasm.hash_transaction(txBody);
const witnesses = CardanoWasm.TransactionWitnessSet.new();

// add keyhash witnesses
const vkeyWitnesses = CardanoWasm.Vkeywitnesses.new();
const vkeyWitness = CardanoWasm.make_vkey_witness(txHash, prvKey);
vkeyWitnesses.add(vkeyWitness);
witnesses.set_vkeys(vkeyWitnesses);

// add bootstrap (Byron-era) witnesses
const cip1852Account = CardanoWasm.Bip32PrivateKey.from_bech32('xprv1hretan5mml3tq2p0twkhq4tz4jvka7m2l94kfr6yghkyfar6m9wppc7h9unw6p65y23kakzct3695rs32z7vaw3r2lg9scmfj8ec5du3ufydu5yuquxcz24jlkjhsc9vsa4ufzge9s00fn398svhacse5su2awrw');
const bootstrapWitnesses = CardanoWasm.BootstrapWitnesses.new();
const bootstrapWitness = CardanoWasm.make_icarus_bootstrap_witness(
    txHash,
    byronAddress,
    cip1852Account,
);
bootstrapWitnesses.add(bootstrapWitness);
witnesses.set_bootstraps(bootstrapWitnesses);

// create the finalized transaction with witnesses
const transaction = CardanoWasm.Transaction.new(
    txBody,
    witnesses,
    undefined, // transaction metadata
);

A note on fees

Fees is Cardano Shelley are based directly on the size of the final encoded transaction. It is important to note that a transaction created by this library potentially can vary in size compared to one built with other tools. This is because transactions, as well as other Cardano Shelley structures, are encoded using CBOR a binary JSON-like encoding. Due to arrays and maps allowing both definite or indefinite length encoding in the encoded transaction created by the library, the size can vary. This is because definite encoding consists of a tag containing the size of the array/map which can be 1 or more bytes long depending on the number of elements the size of the encoded structure, while indefinite length encoding consists of a 1 byte starting tag and after all elements are listed, a 1 byte ending tag. These variances should should only be a couple bytes and cardano-serialization-lib uses definite encoding which is the same length or smaller for any reasonable sized transaction.

UTxO Selection

The TransactionBuilder struct allows you to manually enter inputs and outputs to create a valid transaction, of course, this means that you'll have to calculate things such as fees, change outputs, and perform UTxO selection on the inputs yourself.

The TransactionBuilder struct has some exposed APIs that may be helpful in performing these actions. Namely the builder.add_inputs_from_and_change function. The function first looks at the outputs that already exists in the builder, then attempts to balance the transaction using inputs that are given in the arguments of the function. The function will set inputs, outputs and fees in the builder.

The reason why all 3 have to be set within a single function, is because unfortunately, they all affect each other. Performing UTxO selection on some given outputs may result in some extra change output, which maybe increase the fees needed, which in turn, may change the inputs required, thereby changing what's required in the change output, and so on.

Further complications arise due to the minimum UTxO value requirements on the Cardano network, which is tied to the size of the output. Tokens significantly increase the size of each output, and so any inputs with tokens complicates UTxO selection somewhat.

builder.add_inputs_from_and_change should correctly perform UTxO selection, add these into the builder.inputs, add one extra output for change, and set the builder.fee.

Example Code

Here is a quick example of how it might be used

const txBuilder = wasm.TransactionBuilder.new(
    wasm.TransactionBuilderConfigBuilder.new()
      .fee_algo(wasm.LinearFee.new(wasm.BigNum.from_str('44'), wasm.BigNum.from_str('155381')))
      .coins_per_utxo_word(wasm.BigNum.from_str('34482'))
      .pool_deposit(wasm.BigNum.from_str('500000000'))
      .key_deposit(wasm.BigNum.from_str('2000000'))
      .ex_unit_prices(
        wasm.ExUnitPrices.new(
          wasm.UnitInterval.new(wasm.BigNum.from_str('577'), wasm.BigNum.from_str('10000')),
          wasm.UnitInterval.new(wasm.BigNum.from_str('721'), wasm.BigNum.from_str('10000000')),
        ),
      )
      .max_value_size(5000)
      .max_tx_size(16384)
      .build(),
  )
const utxos = [
    "82825820731224c9d2bc3528578009fec9f9e34a67110aca2bd4dde0f050845a2daf660d0082583900436075347d6a452eba4289ae345a8eb15e73eb80979a7e817d988fc56c8e2cfd5a9478355fa1d60759f93751237af3299d7faa947023e493821a001deabfa1581c9a5e0d55cdf4ce4e19c8acbff7b4dafc890af67a594a4c46d7dd1c0fa14001",
    "82825820a04996d5ef87fdece0c74625f02ee5c1497a06e0e476c5095a6b0626b295074a00825839001772f234940519e71318bb9c5c8ad6eacfe8fd91a509050624e3855e6c8e2cfd5a9478355fa1d60759f93751237af3299d7faa947023e4931a0016e360"
]
const output = wasm.TransactionOutput.new(wasm.Address.from_bech32("addr_test1qppkqaf5044y2t46g2y6udz636c4uultszte5l5p0kvgl3tv3ck06k550q64lgwkqavljd63yda0x2va074fguprujfsjre4xh"), wasm.Value.new(wasm.BigNum.from_str("969750")))
txBuilder.add_output(output)

const wasmUtxos = wasm.TransactionUnspentOutputs.new();
for (let i = 0; i < utxos.length; i++) {
    wasmUtxos.add(wasm.TransactionUnspentOutput.from_hex(utxos[i]));
    }
const wasmChangeConfig = wasm.ChangeConfig.new(wasm.Address.from_bech32("addr_test1qqzf7fhgm0gf370ngxgpskg5c3kgp2g0u4ltxlrmsvumaztv3ck06k550q64lgwkqavljd63yda0x2va074fguprujfs43mc83"))

txBuilder.add_inputs_from_and_change(wasmUtxos, wasm.CoinSelectionStrategyCIP2.LargestFirstMultiAsset, wasmChangeConfig)

const transaction = txBuilder.build_tx()