Emurgo / cardano-serialization-lib

This is a library, written in Rust, for serialization & deserialization of data structures used in Cardano's Haskell implementation of Alonzo along with useful utility functions.
Other
235 stars 125 forks source link

Using add_inputs_from #390

Closed bakon11 closed 2 years ago

bakon11 commented 2 years ago

Have a quick question on how to properly use add_inputs_from. Would greatly appreciate a example for UTXO with assets.

Main problem I am having, is if I have a UTXO with multiple assets in it and I am only transferring a single asset out of it.

Just using add_change_if_needed will not take the not transferred assets into consideration.

I started writing a function that will check which assets from the UTXO have been provided as an output and then generate the outputs for the unsent assets and send them to the change address.

But I was wondering if I can just use add_inputs_from and provide the UTXO that hold the said assets above and calculates the ones that aren't being transferred automatically to provide them to add_change_if_needed

Thank you.

radflipper commented 2 years ago

Not sure I follow the multi assets not included part but I just got add_inputs_from working a simple transaction with just an ADA value, heres some code if it helps:

async function getParameters() {
  const id = '<your blockfrost id>';

  const result = await axios.get(
    "https://cardano-testnet.blockfrost.io/api/v0/epochs/latest/parameters",
    {
      headers: {
        project_id: id,
      },
    }
  );

  return {
    linearFee: {
      minFeeA: result.data.min_fee_a.toString(),
      minFeeB: result.data.min_fee_b.toString(),
    },
    poolDeposit: result.data.pool_deposit,
    keyDeposit: result.data.key_deposit,
    coinsPerUtxoWord: result.data.coins_per_utxo_word,
    maxValSize: result.data.max_val_size,
    priceMem: result.data.price_mem,
    priceStep: result.data.price_step,
    maxTxSize: parseInt(result.data.max_tx_size),
  };
}

// account object from ccvault wallet connector
const account = await window.cardano.ccvault.enable();
// receiving address
const paymentAddress = "<address to send ADA to>";
// change address
const address = await account.getChangeAddress();
const changeAddress = Cardano.Address.from_bytes(
  Buffer.from(address, "hex")
).to_bech32();

// parameters for config
const protocolParameters = await this.getParameters();

// config
const txConfig = Cardano.TransactionBuilderConfigBuilder.new()
  .coins_per_utxo_word(
    Cardano.BigNum.from_str(protocolParameters.coinsPerUtxoWord)
  )
  .fee_algo(
    Cardano.LinearFee.new(
      Cardano.BigNum.from_str(protocolParameters.linearFee.minFeeA),
      Cardano.BigNum.from_str(protocolParameters.linearFee.minFeeB)
    )
  )
  .key_deposit(Cardano.BigNum.from_str(protocolParameters.keyDeposit))
  .pool_deposit(Cardano.BigNum.from_str(protocolParameters.poolDeposit))
  .max_tx_size(protocolParameters.maxTxSize)
  .max_value_size(protocolParameters.maxValSize)
  .prefer_pure_change(true)
  .build();

// builder
const txBuilder = Cardano.TransactionBuilder.new(txConfig);

// outputs
txBuilder.add_output(
  Cardano.TransactionOutputBuilder.new()
    .with_address(Cardano.Address.from_bech32(paymentAddress))
    .next()
    .with_value(Cardano.Value.new(Cardano.BigNum.from_str(sendAmount)))
    .build()
);

// convert utxos from wallet connector
const utxosFromWalletConnector = (await account.getUtxos()).map((utxo) =>
  Cardano.TransactionUnspentOutput.from_bytes(Buffer.from(utxo, "hex"))
);

// create TransactionUnspentOutputs for 'add_inputs_from' function
const utxoOutputs = Cardano.TransactionUnspentOutputs.new();
utxosFromWalletConnector.map((currentUtxo) => {
  utxoOutputs.add(currentUtxo);
});

// inputs with coin selection
// 0 for LargestFirst, 1 RandomImprove 2,3 Mutli asset
txBuilder.add_inputs_from(utxoOutputs, 0); 
txBuilder.add_change_if_needed(Cardano.Address.from_bech32(changeAddress));

const txBody = txBuilder.build();
const transaction = Cardano.Transaction.new(
  txBuilder.build(),
  Cardano.TransactionWitnessSet.new()
);
const witness = await account.signTx(
  Buffer.from(transaction.to_bytes(), "hex").toString("hex")
);

const signedTx = Cardano.Transaction.new(
  txBody,
  Cardano.TransactionWitnessSet.from_bytes(Buffer.from(witness, "hex")),
  undefined // transaction metadata
);

const txHash = await account.submitTx(
  Buffer.from(signedTx.to_bytes()).toString("hex")
);
bakon11 commented 2 years ago

The utxo being passed here from the wallet collector: is it just the txix or is it the the whole txix#index?

// convert utxos from wallet connector
const utxosFromWalletConnector = (await account.getUtxos()).map((utxo) =>
  Cardano.TransactionUnspentOutput.from_bytes(Buffer.from(utxo, "hex"))
);
radflipper commented 2 years ago

The wallet connector returns a hex encoded bytes string of a TransactionUnspentOutput theres probably better way to go from that to the array of TransactionUnspentOutputs required by add_imputs_from but it got the job done

bakon11 commented 2 years ago

So I pass my utxos as an array of objects like so: [{ txix: string, txIndex: number, inputValue: string }]

When I get to Convert provided UTXOs which in your example it is convert utxos from wallet connector

I get the following error: Deserialization failed in TransactionUnspentOutput because: No variant matched

As you can see I am only passing the txix without the index, I'm wondering if that's what my issue is. Maybe I should just create a normal array out of it txix&index?

// Set all provided UTXOs as inputs and their values and indexes
    const utxoOutputs = CardanoWasm.TransactionUnspentOutputs.new();
    if ( JSON.parse(utxos).length > 0 ){
      console.log("Convert provided UTXOs");
      const utxosFromWallet = JSON.parse(utxos).map(( utxo: any ) => {
        console.log( "adding input: " + utxo.txix )
        CardanoWasm.TransactionUnspentOutput.from_bytes(Buffer.from(utxo.txix, "hex"))
      });

      // create TransactionUnspentOutputs for 'add_inputs_from' function
      utxosFromWallet.map(( currentUtxo: any ) => {
        utxoOutputs.add(currentUtxo);
      });
    };
radflipper commented 2 years ago

in this issue

https://github.com/Emurgo/cardano-serialization-lib/issues/285

@vsubhuman has an example with the txix and index to create an input which I think you can then use to construct a TransactionUnspentOutput something like

CardanoWasm.TransactionUnspentOutput.new(
    CardanoWasm.TransactionInput.new(
        CardanoWasm.TransactionHash.from_bytes(
            Buffer.from(txix, "hex")
        ), // tx hash
        txixIndex, // index
     ),
     CardanoWasm.TransactionOutputBuilder.new()            
       .with_address(CardanoWasm.Address.from_bech32("addr1vyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmnqs6l44z").unwrap())
       .next().unwrap()
       .with_value(&value)
       .build().unwrap()
)

thats allot of copy pasta not something I actually ran so buyer beware ;)

If you search the library there are some tests that have examples of how they go together that I used

bakon11 commented 2 years ago

So I have it working when providing multiple UTXOs with just lovelace in them where it'll calculate everything.

The issue I'm running into, is when I try to spend a UTXO with multiple assets in it and I'm only sending one of those assets to another address and keeping the other two.

I get a unbalanced UTXO error, if I just have a single output for the asset I'm trying to send and was wanting to use add_inputs_from to auto detect the left over assets to create the change outputs automatically.

radflipper commented 2 years ago

Not entirely sure I follow, haven't worked with tx sending other assets but off the top of my head you need to build the tx for multi asset as well as setting a MutliAsset output for whatever asset you're sending, I've seen a couple issues here with multi asset examples but didn't really note them since that wasn't what I was doing at the time. If you go towards the end of tx_builders.rs in the rust/src file of the library you'll find this test that might help, but again not something I've done

#[test]
    fn tx_builder_cip2_largest_first_multiasset() {
        // we have a = 0 so we know adding inputs/outputs doesn't change the fee so we can analyze more
        let linear_fee = LinearFee::new(&to_bignum(0), &to_bignum(0));
        let mut tx_builder = create_tx_builder_with_fee(&create_linear_fee(0, 0));
        let pid1 = PolicyID::from([1u8; 28]);
        let pid2 = PolicyID::from([2u8; 28]);
        let asset_name1 = AssetName::new(vec![1u8; 8]).unwrap();
        let asset_name2 = AssetName::new(vec![2u8; 11]).unwrap();
        let asset_name3 = AssetName::new(vec![3u8; 9]).unwrap();

        let mut output_value = Value::new(&to_bignum(415));
        let mut output_ma = MultiAsset::new();
        output_ma.set_asset(&pid1, &asset_name1, to_bignum(5));
        output_ma.set_asset(&pid1, &asset_name2, to_bignum(1));
        output_ma.set_asset(&pid2, &asset_name2, to_bignum(2));
        output_ma.set_asset(&pid2, &asset_name3, to_bignum(4));
        output_value.set_multiasset(&output_ma);
        tx_builder.add_output(&TransactionOutput::new(
            &Address::from_bech32("addr1vyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmnqs6l44z").unwrap(),
            &output_value
        )).unwrap();

        let mut available_inputs = TransactionUnspentOutputs::new();
        // should not be taken
        available_inputs.add(&make_input(0u8, Value::new(&to_bignum(150))));

        // should not be taken
        let mut input1 = make_input(1u8, Value::new(&to_bignum(200)));
        let mut ma1 = MultiAsset::new();
        ma1.set_asset(&pid1, &asset_name1, to_bignum(10));
        ma1.set_asset(&pid1, &asset_name2, to_bignum(1));
        ma1.set_asset(&pid2, &asset_name2, to_bignum(2));
        input1.output.amount.set_multiasset(&ma1);
        available_inputs.add(&input1);

        // taken first to satisfy pid1:asset_name1 (but also satisfies pid2:asset_name3)
        let mut input2 = make_input(2u8, Value::new(&to_bignum(10)));
        let mut ma2 = MultiAsset::new();
        ma2.set_asset(&pid1, &asset_name1, to_bignum(20));
        ma2.set_asset(&pid2, &asset_name3, to_bignum(4));
        input2.output.amount.set_multiasset(&ma2);
        available_inputs.add(&input2);

        // taken second to satisfy pid1:asset_name2 (but also satisfies pid2:asset_name1)
        let mut input3 = make_input(3u8, Value::new(&to_bignum(50)));
        let mut ma3 = MultiAsset::new();
        ma3.set_asset(&pid2, &asset_name1, to_bignum(5));
        ma3.set_asset(&pid1, &asset_name2, to_bignum(15));
        input3.output.amount.multiasset = Some(ma3);
        available_inputs.add(&input3);

        // should not be taken either
        let mut input4 = make_input(4u8, Value::new(&to_bignum(10)));
        let mut ma4 = MultiAsset::new();
        ma4.set_asset(&pid1, &asset_name1, to_bignum(10));
        ma4.set_asset(&pid1, &asset_name2, to_bignum(10));
        input4.output.amount.multiasset = Some(ma4);
        available_inputs.add(&input4);

        // taken third to satisfy pid2:asset_name_2
        let mut input5 = make_input(5u8, Value::new(&to_bignum(10)));
        let mut ma5 = MultiAsset::new();
        ma5.set_asset(&pid1, &asset_name2, to_bignum(10));
        ma5.set_asset(&pid2, &asset_name2, to_bignum(3));
        input5.output.amount.multiasset = Some(ma5);
        available_inputs.add(&input5);

        // should be taken to get enough ADA
        let input6 = make_input(6u8, Value::new(&to_bignum(400)));
        available_inputs.add(&input6);

        // should not be taken
        available_inputs.add(&make_input(7u8, Value::new(&to_bignum(100))));
        tx_builder.add_inputs_from(&available_inputs, CoinSelectionStrategyCIP2::LargestFirstMultiAsset).unwrap();
        let change_addr = ByronAddress::from_base58("Ae2tdPwUPEZGUEsuMAhvDcy94LKsZxDjCbgaiBBMgYpR8sKf96xJmit7Eho").unwrap().to_address();
        let change_added = tx_builder.add_change_if_needed(&change_addr).unwrap();
        assert!(change_added);
        let tx = tx_builder.build().unwrap();

        assert_eq!(2, tx.outputs().len());
        assert_eq!(4, tx.inputs().len());
        // check order expected per-asset
        assert_eq!(2u8, tx.inputs().get(0).transaction_id().0[0]);
        assert_eq!(3u8, tx.inputs().get(1).transaction_id().0[0]);
        assert_eq!(5u8, tx.inputs().get(2).transaction_id().0[0]);
        assert_eq!(6u8, tx.inputs().get(3).transaction_id().0[0]);

        let change = tx.outputs().get(1).amount;
        assert_eq!(from_bignum(&change.coin), 55);
        let change_ma = change.multiasset().unwrap();
        assert_eq!(15, from_bignum(&change_ma.get_asset(&pid1, &asset_name1)));
        assert_eq!(24, from_bignum(&change_ma.get_asset(&pid1, &asset_name2)));
        assert_eq!(1, from_bignum(&change_ma.get_asset(&pid2, &asset_name2)));
        assert_eq!(0, from_bignum(&change_ma.get_asset(&pid2, &asset_name3)));
        let expected_input = input2.output.amount
            .checked_add(&input3.output.amount)
            .unwrap()
            .checked_add(&input5.output.amount)
            .unwrap()
            .checked_add(&input6.output.amount)
            .unwrap();
        let expected_change = expected_input.checked_sub(&output_value).unwrap();
        assert_eq!(expected_change, change);
    }
michaelbarbas commented 2 years ago

So I have it working when providing multiple UTXOs with just lovelace in them where it'll calculate everything.

The issue I'm running into, is when I try to spend a UTXO with multiple assets in it and I'm only sending one of those assets to another address and keeping the other two.

I get a unbalanced UTXO error, if I just have a single output for the asset I'm trying to send and was wanting to use add_inputs_from to auto detect the left over assets to create the change outputs automatically.

Hey, can you give me some insight on how you did this? I cant quite figure it out.

bakon11 commented 2 years ago

So I removed the section of the code where I send assets since its not 100% complete yet. But in the example below if you provide the amount of ADA you want to send for the outputValue param and enough UTXOs. So if you want to send 50ada and have two UTXOs with 30ada each, using add_change_if_needed it will calculate the change UTXO automatically.


/*
utxoKey, // is the utxo key derived from the account prv key 
accountKeyPrv
  .derive(0) // 0 external || 1 change || 2 stake key
  .derive(index) // index
utxos, // [{ txix: string, txIndex: number, inputValue: string }]
assets, // [{ policyID: string, assetName: string, assetAmount: string }]
metadata, // [{ label: string, metadata: { sendfrom: "cardano box!!!!", msg: "Testing auto change calculation" } }]
outputAddress, // address you're sending assets or lovelace to
outputValue, // how much lovelace are you sending
changeAddress, // where unused assets or lovelace from UTXO should go to
*/

import CardanoWasm = require('@emurgo/cardano-serialization-lib-nodejs')

const genTx = async ( utxoKey: any, utxos: string, assets:string, metadata: string, outputAddress: string, outputValue: string, changeAddress: string, txTTL: number ) => {
  let includeMeta = 0;
  try{
    // instantiate the tx builder with the Cardano protocol parameters - these may change later on
    const txBuilder = await CardanoWasm.TransactionBuilder.new(
        CardanoWasm.TransactionBuilderConfigBuilder.new()
        .fee_algo( CardanoWasm.LinearFee.new(CardanoWasm.BigNum.from_str('44'),CardanoWasm.BigNum.from_str('155381')))
        .pool_deposit(CardanoWasm.BigNum.from_str('500000000'),)
        .key_deposit( CardanoWasm.BigNum.from_str('2000000'),)
        .coins_per_utxo_word(CardanoWasm.BigNum.from_str('34482'))
        .max_value_size(5000)
        .max_tx_size(16384)
        .build()
    );
    // Output for ADA being send, Assets will use min required coin change will be calculated after all inputs are passed
    console.log( "adding Ada spent output");
    txBuilder.add_output(
      CardanoWasm.TransactionOutputBuilder.new()
      .with_address( CardanoWasm.Address.from_bech32(outputAddress) )
      .next()
      .with_value( CardanoWasm.Value.new( CardanoWasm.BigNum.from_str(outputValue)))
      .build()
    );

    // METADATA
    if( JSON.parse(metadata).length > 0 ){
      // MetaData stuff
      console.log('adding meta');
      const generalTxMeta = CardanoWasm.GeneralTransactionMetadata.new()
      const auxData = CardanoWasm.AuxiliaryData.new();

      JSON.parse(metadata).map(( meta: any) => 
        generalTxMeta.insert(
          CardanoWasm.BigNum.from_str( meta.label ),
          CardanoWasm.encode_json_str_to_metadatum(
            JSON.stringify(meta.metadata),
            0
          )
        )
      );
      await auxData.set_metadata(generalTxMeta);
      await txBuilder.set_auxiliary_data(auxData);
      includeMeta = 1;
    };

    // set all provided UTXOs as inputs and their values and indexes
    if ( JSON.parse(utxos).length > 0 ){
      console.log("adding inputs");
      JSON.parse(utxos).map(( utxo: any ) => {
        console.log( "adding input: " + utxo.txix )
        // set utxo input map the array 
        txBuilder.add_input(
          CardanoWasm.Address.from_bech32(changeAddress),
          CardanoWasm.TransactionInput.new(
            CardanoWasm.TransactionHash.from_bytes(
                Buffer.from(utxo.txix, "hex")
            ), // tx hash
            utxo.txIndex, // index
          ),
          CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(utxo.inputValue))
        );
      });
    };

    // set the time to live - the absolute slot value before the tx becomes invalid
    console.log("setting ttl");
    await txBuilder.set_ttl(txTTL);

    // calculate the min fee required and send any change to an address
    console.log("setting change");
    await txBuilder.add_change_if_needed( CardanoWasm.Address.from_bech32(changeAddress) );

    // once the transaction is ready, we build it to get the tx body without witnesses
    console.log("Building and singing TX");
    const newTX = await txBuilder.build_tx();
    const txHash = await CardanoWasm.hash_transaction(newTX.body());

    // add keyhash witnesses
    const witnesses = await CardanoWasm.TransactionWitnessSet.new();
    const vkeyWitnesses = await CardanoWasm.Vkeywitnesses.new();
    const vkeyWitness = await await CardanoWasm.make_vkey_witness(txHash, utxoKey.to_raw_key());
    await vkeyWitnesses.add(vkeyWitness);
    await witnesses.set_vkeys(vkeyWitnesses);

    // create the finalized transaction with witnesses
    const transaction = await CardanoWasm.Transaction.new(
      newTX.body(),
      witnesses,
      includeMeta == 1 ? newTX.auxiliary_data() : undefined, //metadata
    );

    const txHex = await Buffer.from(transaction.to_bytes()).toString("hex");
    console.log(txHex);
    return(txHex);
  }catch(error){
    console.log( error );
    return( error );
  };
};
michaelbarbas commented 2 years ago

Awesome thanks! Just what I needed, got it working now. I appreciate your help.

michaelbarbas commented 2 years ago

Have you attempted to submit the tx made from this code? Not able to generate a "valid" transaction to submit on the cli.

michaelbarbas commented 2 years ago

getting an OutsideValidityIntervalUTxO error