From 12bceca2ae5b83f8ed4e0f23941106161d51c2f6 Mon Sep 17 00:00:00 2001 From: Nicolas Ayotte Date: Wed, 21 Sep 2022 16:40:22 -0400 Subject: [PATCH] Added optional token bundling in change Added tx_builder.add_bundled_change_if_needed than takes a max tokens bundle size parameter. This allows to split the change output into multiple outputs based on a fixed number of NFTs. Updated the (admittedly quite stale) example/index.ts and added a relevant test to example/index.spec.ts --- example/{.mocharc.js => .mocharc.cjs} | 0 example/consts.ts | 34 +++++ example/index.spec.ts | 140 +++++++++++++++------ example/index.ts | 48 ++++++- rust/json-gen/Cargo.lock | 16 ++- rust/pkg/cardano_serialization_lib.js.flow | 20 ++- rust/src/tx_builder.rs | 118 ++++++++++++++++- 7 files changed, 322 insertions(+), 54 deletions(-) rename example/{.mocharc.js => .mocharc.cjs} (100%) create mode 100644 example/consts.ts diff --git a/example/.mocharc.js b/example/.mocharc.cjs similarity index 100% rename from example/.mocharc.js rename to example/.mocharc.cjs diff --git a/example/consts.ts b/example/consts.ts new file mode 100644 index 00000000..93ab6c15 --- /dev/null +++ b/example/consts.ts @@ -0,0 +1,34 @@ +export const TEST_EPOCH_PARAMS = { + "epoch_no": 364, + "min_fee_a": 44, + "min_fee_b": 155381, + "max_block_size": 90112, + "max_tx_size": 16384, + "max_bh_size": 1100, + "key_deposit": 2000000, + "pool_deposit": 500000000, + "max_epoch": 18, + "optimal_pool_count": 500, + "influence": 0.3, + "monetary_expand_rate": 0.003, + "treasury_growth_rate": 0.2, + "decentralisation": 0, + "extra_entropy": null, + "protocol_major": 6, + "protocol_minor": 0, + "min_utxo_value": 34482, + "min_pool_cost": 340000000, + "nonce": "d932efa15a43b37c7d3389bed5711b1ff4d1038ce9a864cb66d8ed0fe9129852", + "block_hash": "bc1d3072c7266c26c75eb5c9c1126dd2e509192e86d3350356154aede791a8bd", + "cost_models": "{\"PlutusV1\": {\"bData-cpu-arguments\": 150000, \"iData-cpu-arguments\": 150000, \"trace-cpu-arguments\": 150000, \"mkCons-cpu-arguments\": 150000, \"fstPair-cpu-arguments\": 150000, \"mapData-cpu-arguments\": 150000, \"sndPair-cpu-arguments\": 150000, \"unBData-cpu-arguments\": 150000, \"unIData-cpu-arguments\": 150000, \"bData-memory-arguments\": 32, \"cekLamCost-exBudgetCPU\": 29773, \"cekVarCost-exBudgetCPU\": 29773, \"headList-cpu-arguments\": 150000, \"iData-memory-arguments\": 32, \"listData-cpu-arguments\": 150000, \"nullList-cpu-arguments\": 150000, \"tailList-cpu-arguments\": 150000, \"trace-memory-arguments\": 32, \"mkCons-memory-arguments\": 32, \"mkNilData-cpu-arguments\": 150000, \"unMapData-cpu-arguments\": 150000, \"cekApplyCost-exBudgetCPU\": 29773, \"cekConstCost-exBudgetCPU\": 29773, \"cekDelayCost-exBudgetCPU\": 29773, \"cekForceCost-exBudgetCPU\": 29773, \"chooseData-cpu-arguments\": 150000, \"chooseList-cpu-arguments\": 150000, \"chooseUnit-cpu-arguments\": 150000, \"constrData-cpu-arguments\": 150000, \"fstPair-memory-arguments\": 32, \"ifThenElse-cpu-arguments\": 1, \"mapData-memory-arguments\": 32, \"mkPairData-cpu-arguments\": 150000, \"sndPair-memory-arguments\": 32, \"unBData-memory-arguments\": 32, \"unIData-memory-arguments\": 32, \"unListData-cpu-arguments\": 150000, \"cekLamCost-exBudgetMemory\": 100, \"cekVarCost-exBudgetMemory\": 100, \"headList-memory-arguments\": 32, \"listData-memory-arguments\": 32, \"nullList-memory-arguments\": 32, \"sha2_256-memory-arguments\": 4, \"sha3_256-memory-arguments\": 4, \"tailList-memory-arguments\": 32, \"cekBuiltinCost-exBudgetCPU\": 29773, \"cekStartupCost-exBudgetCPU\": 100, \"mkNilData-memory-arguments\": 32, \"unConstrData-cpu-arguments\": 150000, \"unMapData-memory-arguments\": 32, \"cekApplyCost-exBudgetMemory\": 100, \"cekConstCost-exBudgetMemory\": 100, \"cekDelayCost-exBudgetMemory\": 100, \"cekForceCost-exBudgetMemory\": 100, \"chooseData-memory-arguments\": 32, \"chooseList-memory-arguments\": 32, \"chooseUnit-memory-arguments\": 32, \"constrData-memory-arguments\": 32, \"equalsData-memory-arguments\": 1, \"ifThenElse-memory-arguments\": 1, \"mkNilPairData-cpu-arguments\": 150000, \"mkPairData-memory-arguments\": 32, \"unListData-memory-arguments\": 32, \"blake2b_256-memory-arguments\": 4, \"sha2_256-cpu-arguments-slope\": 29175, \"sha3_256-cpu-arguments-slope\": 82363, \"cekBuiltinCost-exBudgetMemory\": 100, \"cekStartupCost-exBudgetMemory\": 100, \"equalsString-memory-arguments\": 1, \"indexByteString-cpu-arguments\": 150000, \"unConstrData-memory-arguments\": 32, \"addInteger-cpu-arguments-slope\": 0, \"decodeUtf8-cpu-arguments-slope\": 1000, \"encodeUtf8-cpu-arguments-slope\": 1000, \"equalsData-cpu-arguments-slope\": 10000, \"equalsInteger-memory-arguments\": 1, \"mkNilPairData-memory-arguments\": 32, \"blake2b_256-cpu-arguments-slope\": 29175, \"appendString-cpu-arguments-slope\": 1000, \"equalsString-cpu-arguments-slope\": 1000, \"indexByteString-memory-arguments\": 1, \"lengthOfByteString-cpu-arguments\": 150000, \"lessThanInteger-memory-arguments\": 1, \"sha2_256-cpu-arguments-intercept\": 2477736, \"sha3_256-cpu-arguments-intercept\": 0, \"addInteger-memory-arguments-slope\": 1, \"decodeUtf8-memory-arguments-slope\": 8, \"encodeUtf8-memory-arguments-slope\": 8, \"equalsByteString-memory-arguments\": 1, \"equalsInteger-cpu-arguments-slope\": 1326, \"modInteger-cpu-arguments-constant\": 148000, \"modInteger-memory-arguments-slope\": 1, \"addInteger-cpu-arguments-intercept\": 197209, \"consByteString-cpu-arguments-slope\": 1000, \"decodeUtf8-cpu-arguments-intercept\": 150000, \"encodeUtf8-cpu-arguments-intercept\": 150000, \"equalsData-cpu-arguments-intercept\": 150000, \"appendString-memory-arguments-slope\": 1, \"blake2b_256-cpu-arguments-intercept\": 2477736, \"equalsString-cpu-arguments-constant\": 1000, \"lengthOfByteString-memory-arguments\": 4, \"lessThanByteString-memory-arguments\": 1, \"lessThanInteger-cpu-arguments-slope\": 497, \"modInteger-memory-arguments-minimum\": 1, \"multiplyInteger-cpu-arguments-slope\": 11218, \"sliceByteString-cpu-arguments-slope\": 5000, \"subtractInteger-cpu-arguments-slope\": 0, \"appendByteString-cpu-arguments-slope\": 621, \"appendString-cpu-arguments-intercept\": 150000, \"divideInteger-cpu-arguments-constant\": 148000, \"divideInteger-memory-arguments-slope\": 1, \"equalsByteString-cpu-arguments-slope\": 247, \"equalsString-cpu-arguments-intercept\": 150000, \"addInteger-memory-arguments-intercept\": 1, \"consByteString-memory-arguments-slope\": 1, \"decodeUtf8-memory-arguments-intercept\": 0, \"encodeUtf8-memory-arguments-intercept\": 0, \"equalsInteger-cpu-arguments-intercept\": 136542, \"modInteger-memory-arguments-intercept\": 0, \"consByteString-cpu-arguments-intercept\": 150000, \"divideInteger-memory-arguments-minimum\": 1, \"lessThanByteString-cpu-arguments-slope\": 248, \"lessThanEqualsInteger-memory-arguments\": 1, \"multiplyInteger-memory-arguments-slope\": 1, \"quotientInteger-cpu-arguments-constant\": 148000, \"quotientInteger-memory-arguments-slope\": 1, \"sliceByteString-memory-arguments-slope\": 1, \"subtractInteger-memory-arguments-slope\": 1, \"appendByteString-memory-arguments-slope\": 1, \"appendString-memory-arguments-intercept\": 0, \"equalsByteString-cpu-arguments-constant\": 150000, \"lessThanInteger-cpu-arguments-intercept\": 179690, \"multiplyInteger-cpu-arguments-intercept\": 61516, \"remainderInteger-cpu-arguments-constant\": 148000, \"remainderInteger-memory-arguments-slope\": 1, \"sliceByteString-cpu-arguments-intercept\": 150000, \"subtractInteger-cpu-arguments-intercept\": 197209, \"verifyEd25519Signature-memory-arguments\": 1, \"appendByteString-cpu-arguments-intercept\": 396231, \"divideInteger-memory-arguments-intercept\": 0, \"equalsByteString-cpu-arguments-intercept\": 112536, \"quotientInteger-memory-arguments-minimum\": 1, \"consByteString-memory-arguments-intercept\": 0, \"lessThanEqualsByteString-memory-arguments\": 1, \"lessThanEqualsInteger-cpu-arguments-slope\": 1366, \"remainderInteger-memory-arguments-minimum\": 1, \"lessThanByteString-cpu-arguments-intercept\": 103599, \"multiplyInteger-memory-arguments-intercept\": 0, \"quotientInteger-memory-arguments-intercept\": 0, \"sliceByteString-memory-arguments-intercept\": 0, \"subtractInteger-memory-arguments-intercept\": 1, \"verifyEd25519Signature-cpu-arguments-slope\": 1, \"appendByteString-memory-arguments-intercept\": 0, \"remainderInteger-memory-arguments-intercept\": 0, \"lessThanEqualsByteString-cpu-arguments-slope\": 248, \"lessThanEqualsInteger-cpu-arguments-intercept\": 145276, \"modInteger-cpu-arguments-model-arguments-slope\": 118, \"verifyEd25519Signature-cpu-arguments-intercept\": 3345831, \"lessThanEqualsByteString-cpu-arguments-intercept\": 103599, \"divideInteger-cpu-arguments-model-arguments-slope\": 118, \"modInteger-cpu-arguments-model-arguments-intercept\": 425507, \"quotientInteger-cpu-arguments-model-arguments-slope\": 118, \"remainderInteger-cpu-arguments-model-arguments-slope\": 118, \"divideInteger-cpu-arguments-model-arguments-intercept\": 425507, \"quotientInteger-cpu-arguments-model-arguments-intercept\": 425507, \"remainderInteger-cpu-arguments-model-arguments-intercept\": 425507}}", + "price_mem": 0.0577, + "price_step": 0.0000721, + "max_tx_ex_mem": 14000000, + "max_tx_ex_steps": 10000000000, + "max_block_ex_mem": 62000000, + "max_block_ex_steps": 40000000000, + "max_val_size": 5000, + "collateral_percent": 150, + "max_collateral_inputs": 3, + "coins_per_utxo_size": 34482 +} \ No newline at end of file diff --git a/example/index.spec.ts b/example/index.spec.ts index e4a13f4c..57734fe3 100644 --- a/example/index.spec.ts +++ b/example/index.spec.ts @@ -1,33 +1,35 @@ -import CardanoWasm = require('rust-lib') +import wasm = require('rust-lib') import { expect } from 'chai' import 'mocha'; import { mnemonicToEntropy } from 'bip39'; +import { initTransactionBuilder, toHex } from './index'; +import { TEST_EPOCH_PARAMS } from './consts'; -function harden(num: number): number { +function harden (num: number): number { return 0x80000000 + num; } // Purpose derivation (See BIP43) enum Purpose { - CIP1852=1852, // see CIP 1852 + CIP1852 = 1852, // see CIP 1852 } // Cardano coin type (SLIP 44) enum CoinTypes { - CARDANO=1815, + CARDANO = 1815, } enum ChainDerivation { - EXTERNAL=0, // from BIP44 - INTERNAL=1, // from BIP44 - CHIMERIC=2, // from CIP1852 + EXTERNAL = 0, // from BIP44 + INTERNAL = 1, // from BIP44 + CHIMERIC = 2, // from CIP1852 } -function getCip1852Account(): CardanoWasm.Bip32PrivateKey { +function getCip1852Account (): wasm.Bip32PrivateKey { const entropy = mnemonicToEntropy( - [ "test", "walk", "nut", "penalty", "hip", "pave", "soap", "entry", "language", "right", "filter", "choice" ].join(' ') + ["test", "walk", "nut", "penalty", "hip", "pave", "soap", "entry", "language", "right", "filter", "choice"].join(' ') ) - const rootKey = CardanoWasm.Bip32PrivateKey.from_bip39_entropy( + const rootKey = wasm.Bip32PrivateKey.from_bip39_entropy( Buffer.from(entropy, 'hex'), Buffer.from(''), ); @@ -37,6 +39,7 @@ function getCip1852Account(): CardanoWasm.Bip32PrivateKey { .derive(harden(0)); // account #0 } + describe('Addresses', () => { it('derive base address', () => { // from address test vectors @@ -52,65 +55,130 @@ describe('Addresses', () => { .derive(0) .to_public(); - const baseAddr = CardanoWasm.BaseAddress.new( - 0, - CardanoWasm.StakeCredential.from_keyhash(utxoPubKey.to_raw_key().hash()), - CardanoWasm.StakeCredential.from_keyhash(stakeKey.to_raw_key().hash()), + const baseAddr = wasm.BaseAddress.new( + 1, + wasm.StakeCredential.from_keyhash(utxoPubKey.to_raw_key().hash()), + wasm.StakeCredential.from_keyhash(stakeKey.to_raw_key().hash()), ); - expect(baseAddr.to_address().to_bech32()).to.eq('addr1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqcyl47r'); + expect(baseAddr.to_address().to_bech32()) + .to.eq('addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqfjkjv7'); }) }); describe('Transactions', () => { - it('create transaction', () => { - const txBuilder = CardanoWasm.TransactionBuilder.new( - // all of these are taken from the mainnet genesis settings - CardanoWasm.LinearFee.new(CardanoWasm.BigNum.from_str('44'), CardanoWasm.BigNum.from_str('155381')), - CardanoWasm.BigNum.from_str('1000000'), - CardanoWasm.BigNum.from_str('500000000'), - CardanoWasm.BigNum.from_str('2000000') + it('creates a bundled nft transaction', () => { + const txBuilder = initTransactionBuilder(TEST_EPOCH_PARAMS) + + const address = wasm.ByronAddress.from_base58("Ae2tdPwUPEZLs4HtbuNey7tK4hTKrwNwYtGqp7bDfCy2WdR3P6735W5Yfpe"); + txBuilder.add_bootstrap_input( + address, + wasm.TransactionInput.new( + wasm.TransactionHash.from_bytes( + Buffer.from("488afed67b342d41ec08561258e210352fba2ac030c98a8199bc22ec7a27ccf1", "hex"), + ), + 0, // index + ), + wasm.Value.from_json(JSON.stringify({ + coin: '300000000', + multiasset: { + '7cfcafe81fc8f62e618cef2a18ec2fa68e1c6c6f8a1ecf6a19f89188': { + [toHex('nft 01')]: '1', + [toHex('nft 02')]: '1', + [toHex('nft 03')]: '1', + [toHex('nft 04')]: '1', + [toHex('nft 05')]: '1', + [toHex('nft 06')]: '1', + [toHex('nft 07')]: '1', + [toHex('nft 08')]: '1', + [toHex('nft 09')]: '1', + [toHex('nft 10')]: '1', + } + } + })) ); - const address = CardanoWasm.ByronAddress.from_base58("Ae2tdPwUPEZLs4HtbuNey7tK4hTKrwNwYtGqp7bDfCy2WdR3P6735W5Yfpe"); + txBuilder.add_output(wasm.TransactionOutput.new( + address.to_address(), + wasm.Value.from_json(JSON.stringify({ coin: '10000000' })) + )) + + txBuilder.set_ttl(410021); + + // calculate the min fee required and send any change to an address + const changeAddress = wasm.ByronAddress.from_base58("Ae2tdPwUPEYxiWbAt3hUCJsZ9knze88qNhuTQ1MGCKqsVFo5ddNyoTDBymr").to_address() + txBuilder.add_bundled_change_if_needed(changeAddress, 2) + + const txBody = txBuilder.build(); + const txHash = wasm.hash_transaction(txBody); + const witnesses = wasm.TransactionWitnessSet.new(); + const bootstrapWitnesses = wasm.BootstrapWitnesses.new(); + const bootstrapWitness = wasm.make_icarus_bootstrap_witness(txHash, address, getCip1852Account()); + bootstrapWitnesses.add(bootstrapWitness); + witnesses.set_bootstraps(bootstrapWitnesses); + const transaction = wasm.Transaction.new( + txBody, + witnesses, + undefined, // transaction metadata + ); + + console.log(`Change output was split into ${txBody.outputs().len() - 1}`) + for (let i = 0; i < txBody.outputs().len(); i++) { + console.log(i, txBody.outputs().get(i).amount().to_js_value()) + } + expect(txBody.outputs().len() - 1).equal(6) + + const txHex = toHex(transaction.to_bytes()); + console.log(txHex); + }) + + it('creates a transaction', () => { + const txBuilder = initTransactionBuilder(TEST_EPOCH_PARAMS) + + const address = wasm.ByronAddress.from_base58("Ae2tdPwUPEZLs4HtbuNey7tK4hTKrwNwYtGqp7bDfCy2WdR3P6735W5Yfpe"); txBuilder.add_bootstrap_input( address, - CardanoWasm.TransactionInput.new( - CardanoWasm.TransactionHash.from_bytes( + wasm.TransactionInput.new( + wasm.TransactionHash.from_bytes( Buffer.from("488afed67b342d41ec08561258e210352fba2ac030c98a8199bc22ec7a27ccf1", "hex"), ), 0, // index ), - CardanoWasm.BigNum.from_str('3000000') + wasm.Value.from_json(JSON.stringify({ + coin: '10000000' + })) ); txBuilder.add_output( - CardanoWasm.TransactionOutput.new( + wasm.TransactionOutput.new( address.to_address(), - // we can construct BigNum (Coin) from both a js BigInt (here) or from a string (below in fee) - CardanoWasm.BigNum.from_str("1000000"), + wasm.Value.from_json(JSON.stringify({ + coin: '2000000', + })) ), ); txBuilder.set_ttl(410021); // calculate the min fee required and send any change to an address - txBuilder.add_change_if_needed(CardanoWasm.ByronAddress.from_base58("Ae2tdPwUPEYxiWbAt3hUCJsZ9knze88qNhuTQ1MGCKqsVFo5ddNyoTDBymr").to_address()) + const changeAddress = wasm.ByronAddress.from_base58("Ae2tdPwUPEYxiWbAt3hUCJsZ9knze88qNhuTQ1MGCKqsVFo5ddNyoTDBymr").to_address() + txBuilder.add_bundled_change_if_needed(changeAddress, 2) const txBody = txBuilder.build(); - const txHash = CardanoWasm.hash_transaction(txBody); - const witnesses = CardanoWasm.TransactionWitnessSet.new(); - const bootstrapWitnesses = CardanoWasm.BootstrapWitnesses.new(); - const bootstrapWitness = CardanoWasm.make_icarus_bootstrap_witness(txHash,address,getCip1852Account()); + const txHash = wasm.hash_transaction(txBody); + const witnesses = wasm.TransactionWitnessSet.new(); + const bootstrapWitnesses = wasm.BootstrapWitnesses.new(); + const bootstrapWitness = wasm.make_icarus_bootstrap_witness(txHash, address, getCip1852Account()); bootstrapWitnesses.add(bootstrapWitness); witnesses.set_bootstraps(bootstrapWitnesses); - const transaction = CardanoWasm.Transaction.new( + const transaction = wasm.Transaction.new( txBody, witnesses, undefined, // transaction metadata ); - const txHex = Buffer.from(transaction.to_bytes()).toString("hex"); + const txHex = toHex(transaction.to_bytes()); console.log(txHex); }) + }); diff --git a/example/index.ts b/example/index.ts index 20266317..d654c132 100644 --- a/example/index.ts +++ b/example/index.ts @@ -2,6 +2,42 @@ import wasm = require('rust-lib') +export const toBigNum = (x: number | BigInt): wasm.BigNum => wasm.BigNum.from_str(x.toString()) +export const toHex = (bytes: any, format?: BufferEncoding) => Buffer.from(bytes, format).toString('hex') + +export type EpochParams = { + min_fee_a: number, + min_fee_b: number, + pool_deposit: number, + key_deposit: number, + max_val_size: number, + max_tx_size: number, + coins_per_utxo_size: number, +} + +export function initTransactionBuilder (epochParams: EpochParams): wasm.TransactionBuilder { + const { + min_fee_a, + min_fee_b, + pool_deposit, + key_deposit, + max_val_size, + max_tx_size, + coins_per_utxo_size + } = epochParams + + const config = wasm.TransactionBuilderConfigBuilder.new() + .fee_algo(wasm.LinearFee.new(toBigNum(min_fee_a), toBigNum(min_fee_b))) + .pool_deposit(toBigNum(pool_deposit)) + .key_deposit(toBigNum(key_deposit)) + .max_value_size(max_val_size) + .max_tx_size(max_tx_size) + .prefer_pure_change(true) + .coins_per_utxo_word(toBigNum(coins_per_utxo_size)) + + return wasm.TransactionBuilder.new(config.build()) +} + const metadata = wasm.TransactionMetadatum.from_bytes( Buffer.from("a200a16e4c615f52657073697374616e634568576173206865726501a56743686f6963657384a36b43616e6469646174654964782461616139353033612d366663352d343665612d396161302d62346339306633363161346368566f746552616e6b016a566f746557656967687401a36b43616e6469646174654964782438643634396331322d393336652d343662652d623635612d63313766333066353935373468566f746552616e6b026a566f746557656967687401a36b43616e6469646174654964782438316365376638652d393463332d343833352d393166632d32313436643531666131666368566f746552616e6b006a566f746557656967687400a36b43616e6469646174654964782434303735343061612d353862352d343063612d623438342d66343030343065623239393068566f746552616e6b036a566f746557656967687401694e6574776f726b49646f5468655265616c4164616d4465616e6a4f626a656374547970656a566f746542616c6c6f746a50726f706f73616c4964782438303036346332382d316230332d346631632d616266302d63613863356139386435623967566f7465724964782464393930613165382d636239302d346635392d623563662d613862353963396261386165", "hex") ); @@ -9,11 +45,11 @@ const map = metadata.as_map(); const keys = map.keys(); for (let i = 0; i < keys.len(); i++) { const val = keys.get(i); - console.log(Buffer.from(val.to_bytes()).toString('hex')); + console.log(toHex(val.to_bytes())); } const addr = wasm.Address.from_bech32('addr1qxy657awttf5avs2629f4hs6k5ulhw8f27akv30yws622dudj86zwkwhv3yjky5ntrmhcln5yxc05rcq0lhs8l78vd3qhc5eak'); -console.log(Buffer.from(addr.to_bytes()).toString('hex')); +console.log(toHex(addr.to_bytes())); // const addr = wasm.Address.from_bytes(Buffer.from('615c619e192407b2e972f04f0dda7c52aa8013d45ee7ba69d57041cad0', 'hex')); // console.log(addr.to_bech32()); @@ -22,10 +58,10 @@ console.log(Buffer.from(addr.to_bytes()).toString('hex')); // console.log(wasm.ByronAddress.is_valid('Ae2tdPwUPEZFAi4DxQaXeW9HAXYjdfvMWLgNXCJVjvweygZkAUiLjRwGfPr')); // console.log(wasm.ByronAddress.is_valid('Ae2tdPwUPEZ3MHKkpT5Bpj549vrRH7nBqYjNXnCV8G2Bc2YxNcGHEa8ykDp')); // const addr = wasm.Address.from_bech32( - // Buffer.from( - // 'stake1uy5mdzcepk905jj5vqzgyxly57ldhzegugzp9y7fruc6d5sqapp2u', - // 'hex' - // ), +// Buffer.from( +// 'stake1uy5mdzcepk905jj5vqzgyxly57ldhzegugzp9y7fruc6d5sqapp2u', +// 'hex' +// ), // ); // const enterpriseAddr = wasm.EnterpriseAddress.new( diff --git a/rust/json-gen/Cargo.lock b/rust/json-gen/Cargo.lock index 1031c39c..4a343fd2 100644 --- a/rust/json-gen/Cargo.lock +++ b/rust/json-gen/Cargo.lock @@ -37,7 +37,7 @@ checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "cardano-serialization-lib" -version = "11.0.5-alpha.1" +version = "11.0.5" dependencies = [ "bech32", "cbor_event", @@ -58,6 +58,7 @@ dependencies = [ "rand_os", "schemars", "serde", + "serde-wasm-bindgen", "serde_json", "sha2", "wasm-bindgen", @@ -423,6 +424,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfc62771e7b829b517cb213419236475f434fb480eddd76112ae182d274434a" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.133" @@ -511,8 +523,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if", - "serde", - "serde_json", "wasm-bindgen-macro", ] diff --git a/rust/pkg/cardano_serialization_lib.js.flow b/rust/pkg/cardano_serialization_lib.js.flow index d56c943b..0fc5840b 100644 --- a/rust/pkg/cardano_serialization_lib.js.flow +++ b/rust/pkg/cardano_serialization_lib.js.flow @@ -8159,6 +8159,20 @@ declare export class TransactionBuilder { */ add_change_if_needed(address: Address): boolean; + /** + * Warning: this function will mutate the /fee/ field + * Make sure to call this function last after setting all other tx-body properties + * Editing inputs, outputs, mint, etc. after change been calculated + * might cause a mismatch in calculated fee versus the required fee + * @param {Address} address + * @param {number | void} max_bundle_count + * @returns {boolean} + */ + add_bundled_change_if_needed( + address: Address, + max_bundle_count?: number + ): boolean; + /** * This method will calculate the script hash data * using the plutus datums and redeemers already present in the builder @@ -10274,12 +10288,6 @@ export interface PlutusMapJSON { } export type PlutusScriptJSON = string; export type PlutusScriptsJSON = string[]; -export interface PlutusWitnessJSON { - datum: PlutusDataJSON; - redeemer: RedeemerJSON; - script: string; -} -export type PlutusWitnessesJSON = PlutusWitnessJSON[]; export interface PoolMetadataJSON { pool_metadata_hash: string; url: URLJSON; diff --git a/rust/src/tx_builder.rs b/rust/src/tx_builder.rs index de54971f..0129f364 100644 --- a/rust/src/tx_builder.rs +++ b/rust/src/tx_builder.rs @@ -1284,6 +1284,18 @@ impl TransactionBuilder { /// Editing inputs, outputs, mint, etc. after change been calculated /// might cause a mismatch in calculated fee versus the required fee pub fn add_change_if_needed(&mut self, address: &Address) -> Result { + self.add_bundled_change_if_needed(address, None) + } + + /// Warning: this function will mutate the /fee/ field + /// Make sure to call this function last after setting all other tx-body properties + /// Editing inputs, outputs, mint, etc. after change been calculated + /// might cause a mismatch in calculated fee versus the required fee + pub fn add_bundled_change_if_needed( + &mut self, + address: &Address, + max_bundle_count: Option, + ) -> Result { let fee = match &self.fee { None => self.min_fee(), // generating the change output involves changing the fee @@ -1321,6 +1333,15 @@ impl TransactionBuilder { } let change_estimator = input_total.checked_sub(&output_total)?; if has_assets(change_estimator.multiasset()) { + fn will_adding_asset_go_over_bundle_count( + bundle_count: u32, + max_bundle_count: Option, + ) -> bool { + match max_bundle_count { + Some(max) => bundle_count + 1 > max, + None => false, + } + } fn will_adding_asset_make_output_overflow( output: &TransactionOutput, current_assets: &Assets, @@ -1349,6 +1370,7 @@ impl TransactionBuilder { } fn pack_nfts_for_change( max_value_size: u32, + max_bundle_count: Option, data_cost: &DataCost, change_address: &Address, change_estimator: &Value, @@ -1367,6 +1389,7 @@ impl TransactionBuilder { plutus_data: plutus_data.clone(), script_ref: script_ref.clone(), }; + let mut bundle_count: u32 = 0; // If this becomes slow on large TXs we can optimize it like the following // to avoid cloning + reserializing the entire output. // This would probably be more relevant if we use a smarter packing algorithm @@ -1401,7 +1424,10 @@ impl TransactionBuilder { let asset_name = asset_names.get(n); let value = assets.get(&asset_name).unwrap(); - if will_adding_asset_make_output_overflow( + if will_adding_asset_go_over_bundle_count( + bundle_count, + max_bundle_count, + ) || will_adding_asset_make_output_overflow( &output, &rebuilt_assets, (policy.clone(), asset_name.clone(), value), @@ -1433,9 +1459,11 @@ impl TransactionBuilder { next_nft = MultiAsset::new(); rebuilt_assets = Assets::new(); + bundle_count = 0 } rebuilt_assets.insert(&asset_name, &value); + bundle_count += 1; } next_nft.insert(policy, &rebuilt_assets); @@ -1480,6 +1508,7 @@ impl TransactionBuilder { { let nft_changes = pack_nfts_for_change( self.config.max_value_size, + max_bundle_count, &utxo_cost, address, &change_left, @@ -3020,7 +3049,7 @@ mod tests { &change_cred, &stake_cred, ) - .to_address(); + .to_address(); let added_change = tx_builder.add_change_if_needed(&change_addr); assert!(added_change.is_err()); @@ -3115,7 +3144,7 @@ mod tests { &change_cred, &stake_cred, ) - .to_address(); + .to_address(); let added_change = tx_builder.add_change_if_needed(&change_addr); assert!(added_change.is_err()); @@ -3852,6 +3881,89 @@ mod tests { assert!(output.amount.to_bytes().len() <= max_value_size as usize); } } + #[test] + fn build_tx_add_change_bundle_nfts() { + let max_bundle_count = 2; // super low max bundle count to test with fewer assets + let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 1)); + + let (multiasset, policy_ids, names) = create_multiasset(); + + let mut input_value = Value::new(&to_bignum(1000)); + input_value.set_multiasset(&multiasset); + + tx_builder.add_input( + &ByronAddress::from_base58( + "Ae2tdPwUPEZ5uzkzh1o2DHECiUi3iugvnnKHRisPgRRP3CTF4KCMvy54Xd3", + ) + .unwrap() + .to_address(), + &TransactionInput::new(&genesis_id(), 0), + &input_value, + ); + + let output_addr = ByronAddress::from_base58( + "Ae2tdPwUPEZD9QQf2ZrcYV34pYJwxK4vqXaF8EXkup1eYH73zUScHReM42b", + ) + .unwrap() + .to_address(); + let output_amount = Value::new(&to_bignum(232)); + + tx_builder + .add_output( + &TransactionOutputBuilder::new() + .with_address(&output_addr) + .next() + .unwrap() + .with_value(&output_amount) + .build() + .unwrap(), + ) + .unwrap(); + + let change_addr = ByronAddress::from_base58( + "Ae2tdPwUPEZGUEsuMAhvDcy94LKsZxDjCbgaiBBMgYpR8sKf96xJmit7Eho", + ) + .unwrap() + .to_address(); + + let added_change = tx_builder + .add_bundled_change_if_needed(&change_addr, Some(max_bundle_count)) + .unwrap(); + assert_eq!(added_change, true); + let final_tx = tx_builder.build().unwrap(); + assert_eq!(final_tx.outputs().len(), 3); + for (policy_id, asset_name) in policy_ids.iter().zip(names.iter()) { + assert!(final_tx + .outputs + .0 + .iter() + .find(|output| output.amount.multiasset.as_ref().map_or_else( + || false, + |ma| ma + .0 + .iter() + .find(|(pid, a)| *pid == policy_id + && a.0.iter().find(|(name, _)| *name == asset_name).is_some()) + .is_some() + )) + .is_some()); + } + for output in final_tx.outputs.0.iter() { + let count = match &output.amount.multiasset { + Some(ma) => { + let mut count: u32 = 0; + for (_, assets) in &ma.0 { + for (_, _) in &assets.0 { + count += 1; + } + } + count + } + None => 0, + }; + assert!(count <= max_bundle_count); + } + } #[test] fn build_tx_too_big_output() {