diff --git a/chain/rust/src/utils.rs b/chain/rust/src/utils.rs index 579ab599..cb16e6cd 100644 --- a/chain/rust/src/utils.rs +++ b/chain/rust/src/utils.rs @@ -12,7 +12,7 @@ use std::iter::IntoIterator; use crate::{ crypto::hash::{hash_script, ScriptHashNamespace}, plutus::{Language, PlutusScript, PlutusV1Script, PlutusV2Script, PlutusV3Script}, - NativeScript, Script, + NativeScript, Script, SubCoin, }; impl Script { @@ -627,6 +627,20 @@ impl Deserialize for NetworkId { } } +impl SubCoin { + /// Converts base 10 floats to SubCoin. + /// This is the format used by blockfrost for ex units + /// Warning: If the passed in float was not meant to be base 10 + /// this might result in a slightly inaccurate fraction. + pub fn from_base10_f32(f: f32) -> Self { + let mut denom = 1u64; + while (f * (denom as f32)).fract().abs() > f32::EPSILON { + denom *= 10; + } + Self::new((f * (denom as f32)).ceil() as u64, denom) + } +} + // Represents the cddl: #6.258([+ T]) / [* T] // it DOES NOT and CAN NOT have any encoding detials per element! // so you can NOT use it on any primitives so must be serializable directly diff --git a/chain/wasm/src/utils.rs b/chain/wasm/src/utils.rs index 8f9fee05..ab5dce36 100644 --- a/chain/wasm/src/utils.rs +++ b/chain/wasm/src/utils.rs @@ -1,3 +1,5 @@ +use crate::SubCoin; + use super::{Int, Script, ScriptHash}; use cml_chain::plutus::Language; use wasm_bindgen::prelude::{wasm_bindgen, JsError}; @@ -86,6 +88,17 @@ impl NetworkId { } } +#[wasm_bindgen] +impl SubCoin { + /// Converts base 10 floats to SubCoin. + /// This is the format used by blockfrost for ex units + /// Warning: If the passed in float was not meant to be base 10 + /// this might result in a slightly inaccurate fraction. + pub fn from_base10_f32(f: f32) -> Self { + cml_chain::SubCoin::from_base10_f32(f).into() + } +} + // we provide direct From/Into conversions between NonemptySet and TList // to allow the auto-generated code to work directly without changes macro_rules! impl_wasm_conversions_into { diff --git a/core/wasm/src/wasm_wrappers.rs b/core/wasm/src/wasm_wrappers.rs index 95288975..4f002476 100644 --- a/core/wasm/src/wasm_wrappers.rs +++ b/core/wasm/src/wasm_wrappers.rs @@ -417,6 +417,13 @@ macro_rules! impl_wasm_cbor_api { cml_core::serialization::Serialize::to_cbor_bytes(&self.0) } + /** + * Serialize this type to CBOR bytes using canonical CBOR encodings + */ + pub fn to_canonical_cbor_bytes(&self) -> Vec { + cml_core::serialization::Serialize::to_canonical_cbor_bytes(&self.0) + } + /** * Create this type from CBOR bytes */ @@ -440,6 +447,13 @@ macro_rules! impl_wasm_cbor_api { hex::encode(self.to_cbor_bytes()) } + /** + * Serialize this type to CBOR bytes using canonical CBOR encodings as hex bytes + */ + pub fn to_canonical_cbor_hex(&self) -> String { + hex::encode(self.to_canonical_cbor_bytes()) + } + /** * Create this type from the CBOR bytes encoded as a hex string. * This is useful for interfacing with CIP30 diff --git a/docs/docs/index.md b/docs/docs/index.md index abc65997..51d3d29b 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -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 diff --git a/docs/docs/modules/CIP25.mdx b/docs/docs/modules/CIP25.mdx index 09e54de9..725236ac 100644 --- a/docs/docs/modules/CIP25.mdx +++ b/docs/docs/modules/CIP25.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 3 --- # CIP25 diff --git a/docs/docs/modules/builders/generating_transactions.mdx b/docs/docs/modules/builders/generating_transactions.mdx index f0555970..c9b88f58 100644 --- a/docs/docs/modules/builders/generating_transactions.mdx +++ b/docs/docs/modules/builders/generating_transactions.mdx @@ -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 @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/docs/modules/builders/index.mdx b/docs/docs/modules/builders/index.mdx new file mode 100644 index 00000000..851258bf --- /dev/null +++ b/docs/docs/modules/builders/index.mdx @@ -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()); +``` \ No newline at end of file diff --git a/docs/docs/modules/cbor.mdx b/docs/docs/modules/cbor.mdx new file mode 100644 index 00000000..51536c15 --- /dev/null +++ b/docs/docs/modules/cbor.mdx @@ -0,0 +1,88 @@ +--- +sidebar_position: 1 +--- + +# CBOR + +Cardano on-chain types are stored using [CBOR](https://www.rfc-editor.org/rfc/rfc7049), a data format similar to JSON but with many more features and in binary. + +## Tool Interoperability + +Due to CBOR's flexibility it is possible that one piece of CBOR can be represented in multiple ways in the binary encoding. This causes problems when using CBOR taken on-chain or from another tool and using it with another tool. Notably, one small difference in the binary encoding of CBOR could result in hashes being totally different. e.g. metadatum hashes or transaction hashes calculated in a dApp might be different than in the wallet causing the entire transaction to be rejected by the network. + +CML solves this by supporting automatically every single possible CBOR encoding variation. On-chain types created by deserializing from CBOR bytes will remember these details and re-serializing will use them and result in the same CBOR bytes, unlike some other tools. + +## Rust + +On-chan types in rust can (de)serialize to/from CBOR Via the `Serialize`/`Deserialize` and `ToBytes`/`FromBytes` traits located within the `cml_core::serialize` module. + +Most on-chain types implement the `Serialize` and `Deserialize` traits. These traits guarantee that all CBOR encoding details are preserved upon deserialization and upon serialization it is possible to choose between canonical CBOR encoding and arbitrary encodings (the original it was decoded from). + +Byron-era types do not implement `Serialize`/`Deserialize` and instead implement `ToBytes`/`FromBytes`. Byron on-chain types are always in canonical CBOR so this was not necessary. + +The types in the `cip25` module also do not support `Serialize`/`Deserialize` in favor of `ToBytes`/`FromBytes`. The underlying metadata on-chain does and you should use the types in`cml_core::metadata` + +```rust +use cml_core::serialization::{Serialize, Deserialize}; +let canonical_cbor_hex = "825820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01"; +// these all represent the following CBOR: +// [ ; array of 2 elements (transaction input struct) +// 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, ; bytes (tx hash) +// 1 ; unsigned integer (tx index) +// ] +let non_canonical_cbor = [ + canonical_cbor_hex, + "825820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1a00000001", + "9f5f48aaaaaaaaaaaaaaaa48aaaaaaaaaaaaaaaa48aaaaaaaaaaaaaaaa48aaaaaaaaaaaaaaaaff01ff", + "9900025820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa190001", + "9b00000000000000025f41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aaff1b0000000000000001", +]; +for orig_cbor_hex in non_canonical_cbor { + let tx_in = TransactionInput::from_cbor_bytes(&hex::decode(orig_cbor_hex).unwrap()).unwrap(); + // serialize back to cbor bytes using the same cbor encoding details so it will match + // the format where it came from + assert_eq!(hex::encode(tx_in.to_cbor_bytes()), orig_cbor_hex); + // no matter how it was created it will represent the same data and can be encoded to + // canonical cbor bytes which will be the same as all of these are the same transaction input + assert_eq!(hex::encode(tx_in.to_canonical_cbor_bytes()), canonical_cbor_hex); +} +``` + +## WASM + +All on-chain types have the traits directly exposed on each struct as the methods: +* `.to_cbor_bytes()` +* `.to_canonical_cbor_bytes()` +* `.from_cbor_bytes()` +* `.to_cbor_hex()` +* `.to_canonical_cbor_hex()` +* `.from_cbor_hex()` + +The hex ones are useful for working with CIP-30 (dApp connector). + +On post-Byron on-chain types this delegates to `Serialize`/`Deserialize` (see rust section) and preserve round-trip always. CIP25 and Byron types will always serialize to canonical CBOR. All on-chain data during the Byron era has to be canonical CBOR so this is not a big issue but is worth noting. + +```javascript +let canonicalCborHex = "825820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01"; +// these all represent the following CBOR: +// [ ; array of 2 elements (transaction input struct) +// 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, ; bytes (tx hash) +// 1 ; unsigned integer (tx index) +// ] +let nonCanonicalCbor = [ + canonicalCborHex, + "825820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1a00000001", + "9f5f48aaaaaaaaaaaaaaaa48aaaaaaaaaaaaaaaa48aaaaaaaaaaaaaaaa48aaaaaaaaaaaaaaaaff01ff", + "9900025820aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa190001", + "9b00000000000000025f41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aa41aaff1b0000000000000001", +]; +for (let origCborHex of nonCanonicalCbor) { + let txIn = CML.TransactionInput.from_cbor_hex(orig_cbor_hex); + // serialize back to cbor bytes using the same cbor encoding details so it will match + // the format where it came from + console.assert(txIn.to_cbor_hex() == origCborHex); + // no matter how it was created it will represent the same data and can be encoded to + // canonical cbor bytes which will be the same as all of these are the same transaction input + console.assert(txIn.to_canonical_cbor_hex() == canonicalCborHex); +} +``` \ No newline at end of file diff --git a/docs/docs/modules/cip36.mdx b/docs/docs/modules/cip36.mdx index 4e7a6cf1..bd146e02 100644 --- a/docs/docs/modules/cip36.mdx +++ b/docs/docs/modules/cip36.mdx @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 4 --- # CIP36 diff --git a/docs/docs/modules/json.mdx b/docs/docs/modules/json.mdx new file mode 100644 index 00000000..5cc74e66 --- /dev/null +++ b/docs/docs/modules/json.mdx @@ -0,0 +1,63 @@ +--- +sidebar_position: 2 +--- + +# JSON + +## General structs + +All on-chain types have to/from JSON support. The vast majority is auto-generated but some have custom logic e.g. `Url`, `Ipv4`, `BigInteger`, etc. + +### WASM + +In WASM JSON conversions are exposed by `.to_json()` and `.from_json()` methods on all supported wrappers. There is also a `to_js_value()`. + +```javascript +let txInJson = "{\"transaction_id\":\"0fba1404ed9b82b41938ba2e8bda7bec8cce813fb7e7cd7692b43caa76fe891c\",\"index\":3}"; + +let txIn = CML.TransactionInput.from_json(txInJson); + +console.log(`txIn JSON: ${txIn.to_json()}`); +``` + +### Rust + +JSON conversions are exposed in rust via the [`serde::Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) and [`serde::Deserialize`](https://docs.rs/serde/latest/serde/trait.Deserialize.html) traits together with `serde_json`. + +example: +```rust +let tx_in_json = "{\"transaction_id\":\"0fba1404ed9b82b41938ba2e8bda7bec8cce813fb7e7cd7692b43caa76fe891c\",\"index\":3}"; + +// from JSON using serde_json::from_str() - note the type annotations +let tx_in: TransactionInput = serde_json::from_str(tx_in_json).unwrap(); + +// to JSON using serde_json::to_string() - use to_string_pretty() if you want more human-readable formatting +println!("tx_in JSON: {}", serde_json::to_string(&tx_in).unwrap()); +``` + +## Metadata + +Metadata, on top of the generic API mentioned above, has specific JSON functionality for compatability with cardano-node. + +There are three formats on `MetadataJsonSchema`. `NoConversions` is the stricted, stricter than cardano-node and only converts when there are no implicit conversions at all. `BasicConversions` is the node's `TxMetadataJsonNoSchema` and `DetailedSchema` its `TxMetadataJsonDetailedSchema`. See `MetadataJsonSchema` for more info on the schema. + +```javascript +let basic_json = "{\"0x8badf00d\": \"0xdeadbeef\",\"9\": 5,\"obj\": {\"a\":[{\"5\": 2},{}]}}"; +let metadatum = CML.encode_json_str_to_metadatum(basic_json, CML.MetadataJsonSchema.BasicConversions); +console.log(`detailed json: ${CML.decode_metadatum_to_json_str(metadatum, CML.MetadataJsonSchema.DetailedSchema)}`); +// OUTPUT: +// detailed json: {"map":[{"k":{"bytes":"8badf00d"},"v":{"bytes":"deadbeef"}},{"k":{"int":9},"v":{"int":5}},{"k":{"string":"obj"},"v":{"map":[{"k":{"string":"a"},"v":{"list":[{"map":[{"k":{"int":5},"v":{"int":2}}]},{"map":[]}]}}]}}]} +``` + +## Plutus Datums + +Plutus datums also have additional cardano-node JSON support. Remember that Plutus has no String datum so the strings there will be converted to utf8 bytes. See `CardanoNodePlutusDatumSchema` for more info on the schema. + +```javascript +let basic_json = "{ \"100\": [ { \"x\": \"0\", \"y\": 1 } ], \"foo\": \"0x0000baadf00d0000cafed00d0000deadbeef0000\" }"; +let datum = CML.encode_json_str_to_plutus_datum(basic_json, CML.CardanoNodePlutusDatumSchema.BasicConversions); +console.log(`detailed json: ${CML.decode_plutus_datum_to_json_str(datum, CML.CardanoNodePlutusDatumSchema.DetailedSchema, +)}`); +// OUTPUT: +// detailed json: {"map":[{"k":{"int":100},"v":{"list":[{"map":[{"k":{"bytes":"78"},"v":{"bytes":"30"}},{"k":{"bytes":"79"},"v":{"int":1}}]}]}},{"k":{"bytes":"666f6f"},"v":{"bytes":"0000baadf00d0000cafed00d0000deadbeef0000"}}]} +``` \ No newline at end of file diff --git a/docs/docs/modules/multi-era/_category_.json b/docs/docs/modules/multi-era/_category_.json new file mode 100644 index 00000000..35a0d5b7 --- /dev/null +++ b/docs/docs/modules/multi-era/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Multi-Era", + "position": 2, + "link": { + "type": "generated-index" + } +} diff --git a/docs/docs/modules/multi-era/index.md b/docs/docs/modules/multi-era/index.md new file mode 100644 index 00000000..b037f89c --- /dev/null +++ b/docs/docs/modules/multi-era/index.md @@ -0,0 +1,7 @@ +# Multi-Era + +This crate contains all the on-chain types for previous eras (Byron, Shelley, Alonzo, Babbage, etc). There are also wrappers around this era if you need era-agnostic types e.g. parsing all blocks from genesis. The wrappers support the current era as well. + +## Parsing blocks across eras + +`MultiEraBlock` can be used for this. Take care about the format you are giving it. Some tools (e.g. Pallas/Oura) won't give you the block format from the binary spec directly, but will instead have it wrapped in some network wrapper array containing the explicit era tag. If your CBOR looks like `[uint, ]` (likely starting with `82` in hex e.g. `8201`, `8204`, `8207`, etc) then you should use `MultiEraBlock.from_explicit_network_cbor_bytes()` instead of `MultiEraBlock.from_cbor_bytes()`. \ No newline at end of file diff --git a/docs/docs/modules/wasm.mdx b/docs/docs/modules/wasm.mdx new file mode 100644 index 00000000..680a2337 --- /dev/null +++ b/docs/docs/modules/wasm.mdx @@ -0,0 +1,15 @@ +--- +sidebar_position: 6 +--- + +# WASM Usage + +## Memory Management + +If you are using CML from the browser this section is likely irrelevant for you. +Using CML from a javascript environment with weakrefs enabled should have automatic memory cleanup. +If this is not the case (e.g. non-javascript/typescript WASM environment), or you are using CML inside of a very tight loop that is executed hundreds of thousands of times in a short period it might be advisable to explicitly call `.free()` on any CML types after they are used. +This is because while from an environment with weakrefs the types will eventually be freed automatically, +it is still possible to use excessive memory or run out if, for example, large CML types are created in a constant loop that runs many times (e.g. hundreds of thousands of times without a break), as the automatic cleanup will not be run in time. Do not worry about this for normal CML usage. +Do not call `.free()` on a type or use it after `.free()` has been called on it already. +WASM types passed into other CML APIs will be done so by reference and will not have their `.free()` method called just by doing so, but will still eventually be cleaned up if weakrefs are available. \ No newline at end of file