5

Disclaimer: I asked this on SO and also in Discord, but was directed here, so here I am...

I'm doing a simple transaction with a single transfer instruction for 0,1 SOL from one account to another. Then in my backend service I want to get the transaction and verify the data it carries - specifically that a transfer has been made for 0,1 SOL.

I use getTransaction with the tx signature and get a response like this:

{
  message: Message {
    header: {
      numReadonlySignedAccounts: 0,
      numReadonlyUnsignedAccounts: 1,
      numRequiredSignatures: 1
    },
    accountKeys: [ [PublicKey], [PublicKey], [PublicKey] ],
    recentBlockhash: '9S44wiNxXZSdP5VTG6nvaumUJBiUW1DGUXmhVfuhwTMh',
    instructions: [ [Object] ],
    indexToProgramIds: Map(1) { 2 => [PublicKey] }
  },
  signatures: [
    '8ykRq1XtgrtymXVkVhsWjaDrid5FkKzRPJrarzJX9a6EArbEUYMrst6vVC6TydDRG4sagSciK6pP5Lw9ZDnt3RD'
  ]
}

So, I dig into message.instructions and find the following object:

{ accounts: [ 0, 1 ], data: '3Bxs411Dtc7pkFQj', programIdIndex: 2 }

Ok, so data is the base58-encoded string '3Bxs411Dtc7pkFQj'. I decode that from base58 (using bs58), but that only gives me a Uint8Array, which I am not really sure how to convert into a JS object.

I can see in the tx in Solscan that the data information is decoded into hex:

enter image description here

And I can also get this same info in my script:

---> Instructions:
{ accounts: [ 0, 1 ], data: '3Bxs411Dtc7pkFQj', programIdIndex: 2 }

---> Instructions Data:
base58: 3Bxs411Dtc7pkFQj
hex: 0200000000e1f50500000000

But not sure what to do next. How do I get the actual info inside the data?

So, I guess the question is: How to decode the instruction data into a JS object?

2 Answers 2

2

There are two different questions here.

Then in my backend service I want to get the transaction and verify the data it carries - specifically that a transfer has been made for 0,1 SOL.

When you perform a getTransaction(), it actually returns all involved account balances before and after the transaction in meta field:

{
  "meta": {
    "err": null,
    "fee": 5000,
    "innerInstructions": [],
    "postBalances": [499998932500, 26858640, 1, 1, 1],
    "postTokenBalances": [],
    "preBalances": [499998937500, 26858640, 1, 1, 1],
    "preTokenBalances": [],
    "status": {
      "Ok": null
    }
  },
  "slot": 430,
  "transaction": {
    "message": {
      "accountKeys": [
        "3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe",
        "AjozzgE83A3x1sHNUR64hfH7zaEBWeMaFuAN9kQgujrc",
        "SysvarS1otHashes111111111111111111111111111",
        "SysvarC1ock11111111111111111111111111111111",
        "Vote111111111111111111111111111111111111111"
      ],

For example, in the above sample response, index 0 of preBalances and postBalances indicates a -5000 lamports change. This corresponds to index 0 account in accountKeys so now we know 3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe had 5000 SOLs deducted (presumably the transaction fee).

You can use this technique to find all the account balance changes as part of your transaction.

PS. Btw the "Account Balances" tables in most explorers are just parsing these values into a table.

PPS. There is also a getParsedTranscation() that will make that structure a little bit more friendly.


So, I guess the question is: How to decode the instruction data into a JS object?

This is a different kettle of fish. And I should note that instruction data is no guarantee that the transfer happened. Think of it as the request that was sent.

But lemme answer that general question anyway:

To decode instruction data, you need to know its layout. This layout is defined in the on-chain program so if you have access to that, then you can redefine it in JavaScript, using @solana/buffer-layout.

In your case, you are using a well-known program and Solana Labs already created a JavaScript decoder for it in @solana/web3.js. See https://solana-labs.github.io/solana-web3.js/classes/SystemInstruction.html#decodeTransfer

2
3

We implemented similar logic as part of the Solana Pay SDK

The thought process is as follows:

  1. get the transaction
  2. get the pre and post balances
  3. validate the change we expected happened
const response = await connection.getTransaction(signature);
const message = response.transaction.message;
const meta = response.meta;
const accountIndex = message.accountKeys.findIndex((pubkey) => pubkey.equals(recipient));
const preBalance = new BigNumber(meta.preBalances[accountIndex] || 0).div(LAMPORTS_PER_SOL);
const postBalance = new BigNumber(meta.postBalances[accountIndex] || 0).div(LAMPORTS_PER_SOL)

if (postAmount.minus(preAmount).lt(amount)) throw new Error('amount not transferred');
2
  • So, the data prop inside the instructions is useless in this use-case? What's the point of that object then? Commented Jul 22, 2022 at 13:22
  • I wouldn't say it's useless, but for your use-case, there's a more sure way of verifying the transfer. More details on the data: docs.solana.com/developing/programming-model/…. You could also look into use cases for signing a message, "can" be more robust than manually comparing the data. solana.stackexchange.com/a/537/50
    – C.OG
    Commented Jul 22, 2022 at 13:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.