js-algorand-sdk icon indicating copy to clipboard operation
js-algorand-sdk copied to clipboard

Add base64 serialization/deserialization functions

Open fabrice102 opened this issue 3 years ago • 3 comments

This commit adds:

  • base64 serialization/deserialization functions for unsigned transactions
  • algodclient.sendBase64RawTransaction that allows sending directly base64-encoded signed transactions

This is very useful for wallets such as AlgoSigner that takes as inputs and outputs base64 encoding of canonical msgpack encodings.

See https://github.com/PureStake/algosigner/blob/369785fa305b2f08850e64e1b56a1e8cb7254860/docs/dApp-integration.md#working-with-transactions

Currently, unsigned transactions need to be converted in two steps:

let binaryTxs = [tx1.toByte(), tx2.toByte()];
let base64Txs = binaryTxs.map((binary) => AlgoSigner.encoding.msgpackToBase64(binary));

This commit simplifies this into:

let base64Txs = [tx1.toBase64(), tx2.toBase64()];

Similarly, returned signed transactions need to be converted to array of byte arrays before being sent:

let binarySignedTxs = signedTxs.map((tx) => AlgoSigner.encoding.base64ToMsgpack(tx.blob));
await client.sendRawTransaction(binarySignedTxs).do();

This commit simplifies this into:

await client.sendBase64RawTransaction(signedTxs);

Unit testing hacwsve been added for Transaction.base64, algosdk.encodeBase64UnsignedTransaction, algosdk.decodeBase64UnsignedTransaction.

I was not sure how to run cucumber testings. Instead I manually tested algodclient.sendBase64RawTrabsaction with the following code:

const algosdk = require("algosdk");

(async () => {
    const passphrase = "my passphrase";

    const myAccount = algosdk.mnemonicToSecretKey(passphrase)
    console.log("My address: %s", myAccount.addr);

    const algodToken = "";
    const algodServer = "https://testnet.algoexplorerapi.io";
    const algodPort = "";

    const algodClient = new algosdk.Algodv2(algodToken, algodServer, algodPort);

    const params = await algodClient.getTransactionParams().do();
    const note = algosdk.encodeObj("hello");

    // test with single tx
    const txn = algosdk.makePaymentTxnWithSuggestedParams(myAccount.addr, myAccount.addr, 1100000, undefined, note, params);

    const stx = txn.signTxn(myAccount.sk);
    const stxBase64 = Buffer.from(stx).toString("base64");

    await algodClient.sendBase64RawTransaction(stxBase64).do();

    // test with group of two txs
    const txn1 = algosdk.makePaymentTxnWithSuggestedParams(myAccount.addr, myAccount.addr, 100000, undefined, note, params);
    const txn2 = algosdk.makePaymentTxnWithSuggestedParams(myAccount.addr, myAccount.addr, 200000, undefined, note, params);

    algosdk.assignGroupID([txn1, txn2]);

    const stx1 = txn1.signTxn(myAccount.sk);
    const stx2 = txn2.signTxn(myAccount.sk);
    const stx1Base64 = Buffer.from(stx1).toString("base64");
    const stx2Base64 = Buffer.from(stx2).toString("base64");

    await algodClient.sendBase64RawTransaction([stx1Base64, stx2Base64]).do();
})();

fabrice102 avatar Jun 10 '21 02:06 fabrice102

If the primary purpose of this PR is to make it easier for users to convert between Transaction objects and an encoded form for use with 3rd party libraries, I question whether this is the best way to address the issue.

It seems to me like it would be in everyone's interest if AlgoSigner & similar libraries were to accept Transaction objects directly, encode them as needed behind the scenes, and return signed transactions as a decoded Uint8Array array for immediate use with Algodv2.sendRawTransaction.

jasonpaulos avatar Jun 10 '22 21:06 jasonpaulos

The reason why we prefer AlgoSigner and wallets in general not to take Transaction objects is that Transaction objects are not standardized: a new version of the SDK may use a different format.

Using the Transaction object means that the AlgoSigner/wallet object needs to use the exact same SDK version as the dApp (which is not an option in many cases, especially since multiple wallets may use multiple different versions) or that the wallet is able to automatically convert between different formats (which is a potentially very complex task). We actually have had such issues in the past with using directly Transaction objects and that is why AlgoSigner transitioned away from it. (Issues arised with the use of the group ID that was reset in some cases and with the fees, where flat/non-flat fee may cause issues.)

On the other hand, the base64 of the msgpack is fully standardized by the specs of the blockchain: there is a single way of writing/reading it. This allows to decorrelate the SDK from the wallet from the SDK from the dApp in a completely secure way.

fabrice102 avatar Jun 10 '22 21:06 fabrice102

That makes a lot of sense, thanks for explaining why using Transaction objects is not desirable.

It seems like it would be acceptable to use a Uint8Array containing the msgpack encoding instead of a base64-encoded string of the msgpack encoding though. I acknowledge that at this point we are splitting hairs about formats, but since the SDK already uses this representation natively, I think it would be preferable if 3rd party libraries accepted this format as well.

If it's helpful, this SDK probably offer base64 encoding and decoding utility functions. That would be slightly preferable over this PR, since users can pair decoding functions however they want with the rest of the SDK.

jasonpaulos avatar Jun 14 '22 21:06 jasonpaulos

Closing now that base64 conversion convenience functions have been added to the 3.0.0 branch

jasonpaulos avatar Feb 16 '24 20:02 jasonpaulos