bloxbean / cardano-client-lib

Cardano client library in Java
https://cardano-client.dev
MIT License
118 stars 51 forks source link

coinselection when wallet amount is close to output #285

Open khcd opened 1 year ago

khcd commented 1 year ago

scenario:

wallet has 3ADA, lovelace 3000000

building a transaction with two outputs, one that mints one NFT and then sends one ada, lovelace 1000000 to another address. Getting an error com.bloxbean.cardano.client.api.exception.InsufficientBalanceException: Not enough funds for [{lovelace=159310}]

coin selection algo LargestFirstUtxoSelectionStrategy, although I have tried randomImprov, it results in a different error

now the strange thing is if the wallet starts with 5000000 lovelace, same outputs the transaction goes through.

it could be straight cardano issue and not library related, although I thought to try here first

satran004 commented 1 year ago

@kang-chen Can you please provide an example of transaction building code to reproduce the error?

khcd commented 1 year ago

yeap this is the gist of it, replaced destination address and mnenomic strings:

https://gist.github.com/kang-chen/c9219417cc56e13698549182318fd0cc

satran004 commented 1 year ago

Thanks @kang-chen . I will check

satran004 commented 1 year ago

@kang-chen I have found the root cause of this issue. This is an edge case scenario that can be fixed by optimizing the generated outputs. In your example, you will need around 3.2 ADA at the sender's address.

Let me explain the root cause.

The current implementation of TxBuilder creates transaction outputs for all application-defined outputs (1 ADA to the fee address, 1 token to the receiver's address). Additionally, it creates a change output for the sender's address.

When the fee address is not equal to the change address, there will be 3 outputs, and each output needs a minimum of 1 ADA and a transaction fee. Therefore, the total ADA required at the sender's address will be more than 3 ADA.

However, when the fee address is the same as the change address, ideally, the fee output and the change output should be merged so that it doesn't require a minimum ADA for one additional output. Currently, change output merging is only supported before balanceTx, but it is not done during the input selection phase. A simple optimization in createFromSender to merge the change output with any output that has the same address should fix this issue. I will try to address this in the next release.

khcd commented 1 year ago

Thanks @satran004 for the detailed explanation, going to test it tomorrow. Want me to close the ticket or leave it open for reference in the next release?

satran004 commented 1 year ago

Let’s keep it open

khcd commented 1 year ago

Tested, with 3200000 lovelace it doesn't work but 3500000 lovelace works. Can test further but it's trial and error.

Still using LargestFirstUtxoSelectionStrategy for coinSelection.

satran004 commented 1 year ago

@kang-chen This issue has been fixed. If you want to test, you can use the current snapshot version. 0.5.0-alpha.5-SNAPSHOT Please check the README page to find out how to configure for snapshot release. For fix/enhancements detail, you can check PR #290

khcd commented 1 year ago

Hey @satran004 so I had a chance to test today with:

    implementation 'com.bloxbean.cardano:cardano-client-lib:0.5.0-alpha.5-SNAPSHOT'
    implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.5.0-alpha.5-SNAPSHOT'

neither 2.4 or 2.5 ADA worked, which should be enough to cover the minting Ada attachment and the 1.0 Ada output to external address. Still using LargestFirstUtxoSelectionStrategy.

error (with 2.5 ADA) com.bloxbean.cardano.client.api.exception.InsufficientBalanceException: Not enough funds for [{lovelace=607590}]

I'll take a look at your PR but not sure if I can help.

satran004 commented 1 year ago

Thanks @kang-chen for trying it out.

I've quickly verified it with both the Composable Function Api and Quick Tx using alpha.5. It appears to work for me with 2.5 Ada.

https://preprod.cardanoscan.io/transaction/4e7e502e71c96a1a4f1d7a15002cf739032c8777f102fdd5f000b597443ad414?tab=utxo

https://preprod.cardanoscan.io/transaction/2aa33e3daff4600fc5627e526ddc6193ad8d130b90c599bc562d77e872b765aa

Here are my examples for Composable Function and Quick Tx

Composable Functions


    public void transfer() throws Exception {
        String senderMnemonic = "xxxxxxx";
        Account sender = new Account(Networks.testnet(), senderMnemonic);
        String senderAddress = sender.baseAddress();

        String receiver1 = "addr_test1qqwpl7h3g84mhr36wpetk904p7fchx2vst0z696lxk8ujsjyruqwmlsm344gfux3nsj6njyzj3ppvrqtt36cp9xyydzqzumz82";
        Policy policy = PolicyUtil.createMultiSigScriptAllPolicy("policy-1", 1);

        //NFT-1
        MultiAsset multiAsset1 = new MultiAsset();
        multiAsset1.setPolicyId(policy.getPolicyId());
        Asset asset = new Asset("TestNFT", BigInteger.valueOf(1));
        multiAsset1.getAssets().add(asset);

        //Define outputs
        //Output using Output class
        Output mintOutput = Output.builder()
                .address(senderAddress)
                .policyId(policy.getPolicyId())
                .assetName("TestNFT")
                .qty(BigInteger.valueOf(1))
                .build();

        Output feeOutput = Output.builder()
                .address(receiver1)
                .assetName(LOVELACE)
                .qty(adaToLovelace(1))
                .build();

        //Create TxBuilder function
        TxBuilder txBuilder =
                mintOutput.mintOutputBuilder()
                        .and(feeOutput.mintOutputBuilder())
                        .buildInputs(createFromSender(senderAddress, senderAddress))
                        .andThen(mintCreator(policy.getPolicyScript(), multiAsset1))
                        .andThen(balanceTx(senderAddress, 2));

        //Build and sign transaction
        Transaction signedTransaction = TxBuilderContext.init(utxoSupplier, protocolParamsSupplier)
                .buildAndSign(txBuilder, signerFrom(sender).andThen(signerFrom(policy)));

        Result<String> result = backendService.getTransactionService().submitTransaction(signedTransaction.serialize());
        System.out.println(result);
    }

QuickTx Api

 void transfer() {
        Policy policy = PolicyUtil.createMultiSigScriptAtLeastPolicy("test_policy", 1, 1);

        String mn = "xxxxxx";
        Account fromAcc = new Account(Networks.testnet(), mn);

        QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService);
        Tx tx = new Tx()
                .payToAddress(fromAcc.baseAddress(), Amount.ada(1))
                .mintAssets(policy.getPolicyScript(), new Asset("MyAsset", BigInteger.valueOf(1)), receiver1)
                .from(fromAcc.baseAddress());

        Result<String> result = quickTxBuilder.compose(tx)
                .withSigner(SignerProviders.signerFrom(fromAcc))
                .withSigner(SignerProviders.signerFrom(policy))
                .completeAndWait(System.out::println);

        assertTrue(result.isSuccessful());
        checkIfUtxoAvailable(result.getValue(), sender1Addr);
    }
khcd commented 1 year ago

update: I will revisit this and check out the difference between our transaction code, haven't had the time just yet.

Thank you!

AdelaDella commented 1 year ago

谢谢@kang-chen尝试一下。

我已经使用 alpha.5 通过 Composable Function Api 和 Quick Tx 快速验证了它。它似乎适合我使用 2.5 Ada。

https://preprod.cardanoscan.io/transaction/4e7e502e71c96a1a4f1d7a15002cf739032c8777f102fdd5f000b597443ad414?tab=utxo

https://preprod.cardanoscan.io/transaction/2aa33e3daff4600fc5627e526ddc6193ad8d130b90c599bc562d77e872b765aa

以下是我的可组合函数和 Quick Tx 示例

可组合函数


    public void transfer() throws Exception {
        String senderMnemonic = "xxxxxxx";
        Account sender = new Account(Networks.testnet(), senderMnemonic);
        String senderAddress = sender.baseAddress();

        String receiver1 = "addr_test1qqwpl7h3g84mhr36wpetk904p7fchx2vst0z696lxk8ujsjyruqwmlsm344gfux3nsj6njyzj3ppvrqtt36cp9xyydzqzumz82";
        Policy policy = PolicyUtil.createMultiSigScriptAllPolicy("policy-1", 1);

        //NFT-1
        MultiAsset multiAsset1 = new MultiAsset();
        multiAsset1.setPolicyId(policy.getPolicyId());
        Asset asset = new Asset("TestNFT", BigInteger.valueOf(1));
        multiAsset1.getAssets().add(asset);

        //Define outputs
        //Output using Output class
        Output mintOutput = Output.builder()
                .address(senderAddress)
                .policyId(policy.getPolicyId())
                .assetName("TestNFT")
                .qty(BigInteger.valueOf(1))
                .build();

        Output feeOutput = Output.builder()
                .address(receiver1)
                .assetName(LOVELACE)
                .qty(adaToLovelace(1))
                .build();

        //Create TxBuilder function
        TxBuilder txBuilder =
                mintOutput.mintOutputBuilder()
                        .and(feeOutput.mintOutputBuilder())
                        .buildInputs(createFromSender(senderAddress, senderAddress))
                        .andThen(mintCreator(policy.getPolicyScript(), multiAsset1))
                        .andThen(balanceTx(senderAddress, 2));

        //Build and sign transaction
        Transaction signedTransaction = TxBuilderContext.init(utxoSupplier, protocolParamsSupplier)
                .buildAndSign(txBuilder, signerFrom(sender).andThen(signerFrom(policy)));

        Result<String> result = backendService.getTransactionService().submitTransaction(signedTransaction.serialize());
        System.out.println(result);
    }

QuickTx API

 void transfer() {
        Policy policy = PolicyUtil.createMultiSigScriptAtLeastPolicy("test_policy", 1, 1);

        String mn = "xxxxxx";
        Account fromAcc = new Account(Networks.testnet(), mn);

        QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService);
        Tx tx = new Tx()
                .payToAddress(fromAcc.baseAddress(), Amount.ada(1))
                .mintAssets(policy.getPolicyScript(), new Asset("MyAsset", BigInteger.valueOf(1)), receiver1)
                .from(fromAcc.baseAddress());

        Result<String> result = quickTxBuilder.compose(tx)
                .withSigner(SignerProviders.signerFrom(fromAcc))
                .withSigner(SignerProviders.signerFrom(policy))
                .completeAndWait(System.out::println);

        assertTrue(result.isSuccessful());
        checkIfUtxoAvailable(result.getValue(), sender1Addr);
    }

Output output=Output. builder() is the handling fee set internally in buildAndSign. So how to set the handling fee before. The remaining balance after deducting the handling fee is fully transferred out. The current demo cannot achieve this

khcd commented 1 year ago

@satran004 @BlockByteMath I revisited this finally. Still can't process the transaction with 2.5 ADA unfortunately, the min amount that works seems to be 3.5ADA. I tried pretty much mimicking your transaction, making sure the version was correct snapshot. The only difference between our two txn mine had a couple of metadata data fields attached.

TxBuilder txBuilder = createFromMintOutput(mintOutput).and(feeOutput.outputBuilder()) .buildInputs(createFromSender(senderAddress, senderAddress)) .andThen(mintCreator(policy.getPolicyScript(), multiAsset)) .andThen(metadataProvider(nftMetadata)) .andThen(metadataProvider(transactionMetadata)) .andThen(balanceTx(senderAddress, 2));

Which I removed for the test.

I haven't tried preprod, I'm testing on preview.

What do you want to do, leave the issue until alpha.5 is released and then close?

satran004 commented 1 year ago

Thanks @kang-chen . The latest release is 0.5.0-beta2 :) . It should be fixed in that. I will verify it again tomorrow.

khcd commented 1 year ago

Thanks @kang-chen . The latest release is 0.5.0-beta2 :) . It should be fixed in that. I will verify it again tomorrow.

Did a quick test with: implementation 'com.bloxbean.cardano:cardano-client-lib:0.5.0-beta3-SNAPSHOT' implementation 'com.bloxbean.cardano:cardano-client-backend-blockfrost:0.5.0-beta3-SNAPSHOT'

I tried with LargestFirstUtxoSelectionStrategy and DefaultUtxoSelectionStrategyImpl on preview network

Still fails to process with 2.5 ADA

Just giving you a quick update as I was already setup to test. Thanks