Skip to content

Commit

Permalink
Docs update
Browse files Browse the repository at this point in the history
Update docs to new crate structure.

Adds CBOR and JSON explanations

General updates as the docs were very outdated

Replaces #307 (same without plutus + messed up git rebase/merges)
  • Loading branch information
rooooooooob committed Jul 25, 2024
1 parent bf34fe4 commit 428437f
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 92 deletions.
14 changes: 12 additions & 2 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,26 @@ This is a library, written in Rust, that can be deployed to multiple platforms (

##### NPM packages

TODO: update these once we publish new versions:
- browser: [link](https://www.npmjs.com/package/@dcspark/cardano-multiplatform-lib-browser)
- nodejs: [link](https://www.npmjs.com/package/@dcspark/cardano-multiplatform-lib-nodejs)

There is also an outdated asm.js . It is strongly discouraged from using this as it is out of date and asm.js results in incredibly slow cryptographic operations.
- asm.js (strongly discouraged): [link](https://www.npmjs.com/package/@dcspark/cardano-multiplatform-lib-asmjs)

Note: If you are using WebPack, you must use version 5 or later for CML to work.

##### Rust crates

- crates: [link](https://crates.io/crates/cardano-multiplatform-lib)
The rust crates are split up by functionality.

- core: [link](https://crates.io/crates/cml-core)
- crypto: [link](https://crates.io/crates/cml-crypto)
- chain: [link](https://crates.io/crates/cml-chain)
- multi-era: [link](https://crates.io/crates/cml-multi-era)
- cip25: [link](https://crates.io/crates/cml-cip25)
- cip36: [link](https://crates.io/crates/cml-cip36)

Most users will likely be using primarily `cml-chain` for general uses, `cml-multi-era` if they need historical (pre-babbage eras) chain-parsing and `cip25` or `cip36` if they need those specific metadata standards.

##### Mobile bindings

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/modules/CIP25.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 3
---

# CIP25
Expand Down
116 changes: 28 additions & 88 deletions docs/docs/modules/builders/generating_transactions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,6 @@
sidebar_position: 4
---

# TransactionBuilder

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](https://github.com/input-output-hk/cardano-ledger-specs#cardano-ledger), 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

Expand All @@ -27,95 +12,50 @@ Fees are automatically calculated and sent to a change address in the example.

```javascript
// 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);

const txBuilder = makeTxBuilder();
const testnetId = 0;
// 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
const prvKey = CML.PrivateKey.from_bech32("ed25519e_sk16rl5fqqf4mg27syjzjrq8h3vq44jnnv52mvyzdttldszjj7a64xtmjwgjtfy25lu0xmv40306lj9pcqpa6slry9eh3mtlqvfjz93vuq0grl80");
const inputAddr = CML.EnterpriseAddress.new(testnetId, CML.StakeCredential.new_key(prvKey.to_public().hash())).to_address();
txBuilder.add_input(CML.SingleInputBuilder.new(
CML.TransactionInput.new(
CML.TransactionHash.from_hex("8561258e210352fba2ac0488afed67b3427a27ccf1d41ec030c98a8199bc22ec"), // 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'))
CML.TransactionOutput.new(
inputAddr,
CML.Value.from_coin(BigInt(6000000)),
)
);

// base address
const shelleyOutputAddress = CardanoWasm.Address.from_bech32("addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w");
const outputAddress = CML.Address.from_bech32("addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w");
// pointer address
const shelleyChangeAddress = CardanoWasm.Address.from_bech32("addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerspqgpsqe70et");
const changeAddress = CML.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'))
),
CML.TransactionOutputBuilder()
.with_address(outputAddress)
.next()
.with_value(CML.Value.from_coin(BigInt(1000000)))
.build()
);

// 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,
// this moves onto the next step of building the transaction: providing witnesses
const signedTxBuilder = tx_builder.build(
changeAddress,
CML.ChangeSelectionAlgo.Default
);
bootstrapWitnesses.add(bootstrapWitness);
witnesses.set_bootstraps(bootstrapWitnesses);

// create the finalized transaction with witnesses
const transaction = CardanoWasm.Transaction.new(
txBody,
witnesses,
undefined, // transaction metadata
);
// sign with the key that owns the input used
signedTxBuilder.add_vkey(CML.make_vkey_witness(txHash, prvKey));

const tx = signedTxBuilder.build_checked();
// ready to submit, can be converted to CBOR via tx.to_cbor_bytes() or to_cbor_hex() for hex
```
## 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](https://cbor.io/) 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-multiplatform-lib uses definite encoding which is the same length or smaller for any reasonable sized transaction.
Fees in Cardano 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 structures, are encoded using [CBOR](https://cbor.io/) 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-multiplatform-lib uses definite encoding by default which is the same length or smaller for any reasonable sized transaction.
79 changes: 79 additions & 0 deletions docs/docs/modules/builders/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# TransactionBuilder

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, 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, metadata, and minting 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 an `AuxiliaryData`.
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 [specs for the relevant era](https://github.com/input-output-hk/cardano-ledger-specs#cardano-ledger), specifically the Shelley design specification for general design for non-governance certificates. Refer to the Conway specs for those. 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.

# TransactionBuilderConfig

To correctly make transactions the builder must know some on-chain parameters such as the current fee costs, key deposits, etc. These can all potentially change, even if some have largely been static for large periods of time. We pass these into the builder via the `TransactionBuilderConfigBuilder`. For test things out hard-coding them might suffice, but these parameters should ideally be fetched from the current blockchain head or your transactions could fail to be accepted by the network or will end up paying higher fees. The cost models parameter is optional if you are not building a transaction that utilizes Plutus smart contracts.

Code examples for the builders will assume you have a `make_tx_builder()` function that creates a `TransactionBuilder` with the appropriate config.

# Blockfrost

One way of getting this information is via the `epochs/latest/parameters` endpoint of blockfrost. This can be automated from rust using the `cml-blockfrost` crate's `make_tx_builder_cfg()`. Blockfrost is by no means necessary but it can be convenient. It is possible to get this information by other means as well e.g. having a synced cardano node.

Using `cml-blockfrost` (rust):

```rust
let cfg = cml_blockfrost::make_tx_builder_cfg(&api).await.unwrap();
let mut tx_builder = TransactionBuilder::new(cfg);
```

This could also be done manually similar to below (or reference `cml-blockfrost`'s code)

Manually using WASM:

```javascript
let params = await blockfrost.epochsLatestParameters();

// cost order is based on lex ordering of keys
let costModels = CML.CostModels.new();
let v1Costs = params.cost_models['PlutusV1'];
if (v1Costs != null) {
let v1CMLCosts = CML.IntList.new();
for (key in Object.keys(v1Costs).toSorted()) {
v1CMLCosts.add(CML.Int.new(v1Costs[key]));
}
costModels.set_plutus_v1(v1CMLCosts);
}
// cost order is based on lex ordering of keys
let v2Costs = params.cost_models['PlutusV2'];
if (v2Costs != null) {
let v2CMLCosts = CML.IntList.new();
for (key in Object.keys(v2Costs).toSorted()) {
v2CMLCosts.add(CML.Int.new(v2Costs[key]));
}
costModels.set_plutus_v2(v2CMLCosts);
}
// note: as of writing this the sancho testnet format is different for v3
// compared to v1/v2. this may remain true once mainnet switches over so
// please inspect the object you are getting for cost models from blockfrost

let configBuilder = CML.TransactionBuilderConfigBuilder.new()
.fee_algo(CML.LinearFee.new(params.min_fee_a, params.min_fee_b))
.coins_per_utxo_byte(BigNum(params.coins_per_utxo_size))
.pool_deposit(BigNum(params.pool_deposit))
.key_deposit(BigNum(params.key_deposit))
.max_value_size(Number(params.max_val_size))
.max_tx_size(params.max_tx_size)
.ex_unit_prices(CML.ExUnitPrices.new(
CML.SubCoin.from_base10_f32(params.price_mem),
CML.SubCoin.from_base10_f32(params.price_step)
))
.cost_models(costModels)
.collateral_percentage(params.collateral_percent)
max_collateral_inputs(params.max_collateral_inputs);
let mut txBuilder = CML.TransactionBuilder.new(configBuilder.build());
```
Loading

0 comments on commit 428437f

Please sign in to comment.