bloxbean / cardano-client-lib

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

Cannot Submit CCL Transactions from Frontend Wallets #401

Closed fabianbormann closed 2 months ago

fabianbormann commented 2 months ago

I use the CCL to prepare a transaction in my backend, which I send back as a hex string to the frontend for the user to sign and submit using their favorite browser wallet. While there is no problem with the generation, and the signing looks fine, there is an error after the signature for the wallets that I tested (Vesper, Nami, and Eternl).

The error message is:

"{\"contents\":{\"contents\":[\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 \\\"expected list len or indef\\\")\",\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 \\\"expected list len or indef\\\")\",\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 \\\"expected list len or indef\\\")\",\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 \\\"expected list len or indef\\\")\",\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 \\\"expected list len or indef\\\")\",\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 \\\"expected list len or indef\\\")\"],\"tag\":\"TxCmdTxReadError\"},\"tag\":\"TxSubmitFail\"}"

I simplified the transaction as much as possible, but even with this basic transaction, it shows the same behavior:

String sender = "addr_test1qzfw9tj3lvpae32ugu2sdl34hhk6m8pxdvxsns4ch37tg3ghw90ykk35yf6dp56stjk6qjja9342yf4j4d7cgf3453gsz77xa5";
String receiver = "addr_test1qp7dhqxetsqn6d6k6k0kkzek7dgaprvpecu6d0w464f54xmxp8zfncgu2y5ug5ud9t6sgu6fsmqlym2yhyq9l54405sqwynupf";

Tx tx = new Tx()
        .payToAddress(receiver, Amount.ada(5))
        .from(sender);

QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService);
Transaction transaction = quickTxBuilder
        .compose(tx)
        .feePayer(sender)
        .withRequiredSigners(paymentCredentialHash.get()) // senders payment credential
        .build();

return transaction.serializeToHex();

My frontend code:

const api = await (window as any).cardano.nami.enable();
// ...
if (response.status === 200) {
  try {
    const preparedTransaction = response.data.preparedTransaction;
    const signedTransaction = await api.signTx(preparedTransaction, true);
    const txId = await api.submitTx(signedTransaction);
  } catch (error) {
    console.error(error);
  }
}

In this case, Nami opens as expected and allows the transaction to be signed, which looks as expected. The issue occurs during submission. I also tried sending the signed transaction back to my backend and submitting it using the CCL, but it results in the same issue.

fabianbormann commented 2 months ago

This is the signed transaction (using different sender and receiver addresses than above)

a10081825820373bc5f0bc56c5cc5c7f96c3251e5c11bfb8a6fcc6abefa4e5c3be6a29d316db5840c4ff15b7af57707f8ef65ede374cfe707705ad180f4db0336ea632356a43c48f77a5916d470545ac323596574becf978083ae57e3f68caacbb104b38c4549905

This is the unsigned transaction

84a900818258204952356ec45df548251b0aea3e6c75c649062f029cb5f9d95cded3c7a7308bb7000182825839001564641507c7fcb96eee86f7fce9b5db19d4fafb2d728465e324524298fbdf5f43d7d000d00353fd7816f5ffc76c7e5dce2d6794fab103251a001e8480825839009a3eea4ce6b74af9e77213f4e94af602588a09fd552afb5113520ff6b83abf370a14870fdfd6ccb35f8b3e62a68e465ed1e096c5a6f5b9d61a002a7358021a0003536805a1581df007d005982b3acac32aee6f6e9d3eb8c897f54bb396255845ca69d5c7000b5820d941a1c0f102b907f0739f58bd77c77aa5877bd907e842724d2cdac392a6c3910d818258204952356ec45df548251b0aea3e6c75c649062f029cb5f9d95cded3c7a7308bb7000e81581c9a3eea4ce6b74af9e77213f4e94af602588a09fd552afb5113520ff610825839009a3eea4ce6b74af9e77213f4e94af602588a09fd552afb5113520ff6b83abf370a14870fdfd6ccb35f8b3e62a68e465ed1e096c5a6f5b9d61a00474e24111a0004fd1ca20581840300d8799f58406135393161366434306266343230343034613031313733336366623762313930643632633635626630626364613332623537623237376439616439663134366540581cb83abf370a14870fdfd6ccb35f8b3e62a68e465ed1e096c5a6f5b9d6427b7dff8219b6831a013300cf06815902c65902c301000032323232323232232322533300532323253330083370e900218049baa00113253330093007300a37540022a66601266e1d2002300a3754601c60166ea80044c8c94ccc02cc8c8cc004004dd6180218079baa00722533301100114a0264a66601e64a66602066ebcc054c058c048dd50009ba65333010300e482024bd00452f5bded8c0264646600200297adef6c60225333016001133017337609801014000374c00697adef6c6013232323253330173375e66018911000024c103d879800013301b33760981014000374c00e00a2a66602e66e3d2210000213301b337609801014000374c00e00626603666ec0dd48011ba600133006006003375660300066eb8c058008c068008c060004c8cc0040052f5bded8c044a66602a00226602c66ec130010140004c01051a001e8480004bd6f7b630099191919299980b19baf3300b4881000024c103d879800013301a337609810140004c01051a001e848000005153330163371e91010000213301a3376098010140004c01051a001e84800000313301a337606ea4008dd4000998030030019bad3017003375c602a0046032004602e002266ebcc018c048dd5180318091baa0014c0122d8799f581c1564641507c7fcb96eee86f7fce9b5db19d4fafb2d728465e3245242ff0014a0602800429444cc00c00c004c050004894ccc034cdc80010008a60103d87980001533300d3371e0040022980103d87a800014c103d87b8000132533300c3001375c6004601c6ea802c4c004dd7180198071baa00b14a0466e212000371a00229408c0400048c03cc040c0400045858c034c028dd50008b18061806801180580098039baa00114984d9594ccc00cc004c010dd500109919191919191919299980718088010a4c2c6e64dd7180780098078011bae300d001300d00237326eb8c02c004c02c008dd7180480098029baa00216370e90002b9a5573aaae7955cfaba05742ae881f5f6
satran004 commented 2 months ago

@fabianbormann

It seems that the first hex string is for the witness set, not the signed transaction. The witness set contains both the vkey and the signature.

You can try this code to deserialize the hex string:

String hex = "a10081825820373bc5f0bc56c5cc5c7f96c3251e5c11bfb8a6fcc6abefa4e5c3be6a29d316db5840c4ff15b7af57707f8ef65ede374cfe707705ad180f4db0336ea632356a43c48f77a5916d470545ac323596574becf978083ae57e3f68caacbb104b38c4549905";

var witnessSet = TransactionWitnessSet.deserialize((Map)CborSerializationUtil.deserialize(HexUtil.decodeHexString(hex)));
System.out.println(witnessSet);

In the backend, once you get the witness set from the frontend, you need to assemble the final signed transaction by adding the witness set to the unsigned transaction.

Similar to this example https://github.com/bloxbean/bloxbean-playground/blob/607dad24dda45d22b54e7ae8377a992d05d255d9/src/main/java/com/bloxbean/playground/minter/MinterService.java#L127

satran004 commented 2 months ago

Alternatively, if you can send the full signed tx to the backend, the step to assemble is not required. The above example is specific to multi-sig use case, where the second signer is maintained in the backend.

fabianbormann commented 2 months ago

Thank you @satran004 the process that you explained works like charm

    public Result<String> submit(String transactionHex, String witnessSetHex) throws CborDeserializationException, CborSerializationException, ApiException {
        BackendService backendService = new KoiosBackendService(Constants.KOIOS_PREPROD_URL);

        TransactionWitnessSet witnessSet = TransactionWitnessSet.deserialize((Map) CborSerializationUtil.deserialize(HexUtil.decodeHexString(witnessSetHex)));
        Transaction transaction = Transaction.deserialize(HexUtil.decodeHexString(transactionHex));
        transaction.getWitnessSet().setVkeyWitnesses(witnessSet.getVkeyWitnesses());

        return backendService.getTransactionService().submitTransaction(transaction.serialize());
    }

I would close this issue now. 😊