gagliardetto / solana-go

Go SDK library and RPC client for the Solana Blockchain
Apache License 2.0
829 stars 247 forks source link

Transaction failed to sanitize accounts offsets correctly #199

Open lucasoares opened 5 months ago

lucasoares commented 5 months ago

Maybe it's related to https://github.com/gagliardetto/solana-go/issues/134, but idk if it is the same issue.

I'm trying to port raydium-sdk to golang using solana-go and everything is going well, but I can't get the Liquidity.fetchInfo working on golang.

Raydium's SDK implementation for the fetchInfo function is: creates an instruction, simulates it, and then parses the logs from the simulation to get liquidity pool information.

The function to create the instruction: https://github.com/raydium-io/raydium-sdk/blob/c44123e6df72c589a55eca6a595fd1153a176546/src/liquidity/liquidity.ts#L1318

Then this transaction instruction is included in a transaction and sent to RPC using the simulateMultipleInstruction function: https://github.com/raydium-io/raydium-sdk/blob/c44123e6df72c589a55eca6a595fd1153a176546/src/common/web3.ts#L235.

I made a dummy code with the raw implementation for both ts and golang using a RANDOM (no idea who owns it) liquidity pool as an example:

ts implementation:

import { AccountMetaReadonly, struct, u8 } from "@raydium-io/raydium-sdk";
import { Connection, PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js";

export const testFetchInfo = async () => {
    const connection = new Connection("https://api.mainnet-beta.solana.com", {
        commitment: 'confirmed',
    });

    const LAYOUT = struct([u8('instruction'), u8('simulateType')]);
    const data = Buffer.alloc(LAYOUT.span)
    LAYOUT.encode(
        {
            instruction: 12,
            simulateType: 0,
        },
        data,
    );

    const keys = [
        // amm
        AccountMetaReadonly(new PublicKey('Gatxv6KgbmvWHsAScveUPyjRPZ72eyFzGy8s9YFTiXg7'), false),
        AccountMetaReadonly(new PublicKey('5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1'), false),
        AccountMetaReadonly(new PublicKey('2CjSHRgusrrh7Ke4PFwnJnBq83pRMNAVgffZsDspoRmi'), false),
        AccountMetaReadonly(new PublicKey('G2ZwmtSEZMD9E9oJfXG7rnVLZ7gadAJuBpabYLDCS5Gi'), false),
        AccountMetaReadonly(new PublicKey('2vkJSGSirnLVeUCn81FCNgwndPuCSYE79v4fUei1hB6X'), false),
        AccountMetaReadonly(new PublicKey('CFH6nuBREVstWc3MYgf2SEZw7wh9F7bLGNK3cKesSBM6'), false),
        // serum
        AccountMetaReadonly(new PublicKey('F534RAiGy6EikSHBNes2uxCeHdJD8YALY7c1PpHhKtL6'), false),
        AccountMetaReadonly(new PublicKey('54aRTv111QAv2KwbAFYK6QsMNmyw6sftN4PgSVP7c2cW'), false),
    ];

    var instruction = new TransactionInstruction({
        programId: new PublicKey('675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'),
        keys,
        data,
    });

    const feePayer = new PublicKey('RaydiumSimuLateTransaction11111111111111111');
    let transaction = new Transaction();
    transaction.feePayer = feePayer;
    transaction.add(instruction);

    const hash = await connection.getLatestBlockhash();

    transaction.lastValidBlockHeight = hash.lastValidBlockHeight;
    transaction.recentBlockhash = hash.blockhash;

    var result = await connection.simulateTransaction(transaction);
    console.log("LOGS", result.value.logs);
};

Output:

LOGS [
  'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [1]',
  'Program log: calc_exact len:0',
  'Program consumption: 190159 units remaining',
  'Program consumption: 190053 units remaining',
  'Program log: GetPoolData: {"status":6,"coin_decimals":6,"pc_decimals":9,"lp_decimals":6,"pool_pc_amount":7151137742,"pool_coin_amount":751087799653977,"pnl_pc_amount":0,"pnl_coin_amount":0,"pool_lp_supply":2202271554554,"pool_open_time":0,"amm_id":"Gatxv6KgbmvWHsAScveUPyjRPZ72eyFzGy8s9YFTiXg7"}',
  'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 29401 of 200000 compute units',
  'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success'
]

golang implementation:

import (
    "bytes"
    "context"
    "fmt"
    "testing"

    bin "github.com/gagliardetto/binary"
    solgo "github.com/gagliardetto/solana-go"
    solrpc "github.com/gagliardetto/solana-go/rpc"
    "github.com/gagliardetto/solana-go/rpc/jsonrpc"
    "github.com/stretchr/testify/require"
)

func TestFetchInfoImplementationIntegration(t *testing.T) {
    if testing.Short() {
        return
    }

    url := "https://api.mainnet-beta.solana.com"

    jsonRpcClient := jsonrpc.NewClient(url)
    client := solrpc.NewWithCustomRPCClient(jsonRpcClient)

    out, err := client.GetHealth(context.Background())
    require.NoError(t, err)
    require.Equal(t, "ok", out)

    keys := solgo.AccountMetaSlice{
        // Amm
        // Pool ID
        solgo.Meta(solgo.MPK("Gatxv6KgbmvWHsAScveUPyjRPZ72eyFzGy8s9YFTiXg7")),
        // Program Authority
        solgo.Meta(solgo.MPK("5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1")),
        // Open Orders
        solgo.Meta(solgo.MPK("2CjSHRgusrrh7Ke4PFwnJnBq83pRMNAVgffZsDspoRmi")),
        // Base Vault
        solgo.Meta(solgo.MPK("G2ZwmtSEZMD9E9oJfXG7rnVLZ7gadAJuBpabYLDCS5Gi")),
        // Quote Vault
        solgo.Meta(solgo.MPK("2vkJSGSirnLVeUCn81FCNgwndPuCSYE79v4fUei1hB6X")),
        // Lp Mint
        solgo.Meta(solgo.MPK("CFH6nuBREVstWc3MYgf2SEZw7wh9F7bLGNK3cKesSBM6")),
        // Serum
        // Market ID
        solgo.Meta(solgo.MPK("F534RAiGy6EikSHBNes2uxCeHdJD8YALY7c1PpHhKtL6")),
        // Event Queue
        solgo.Meta(solgo.MPK("54aRTv111QAv2KwbAFYK6QsMNmyw6sftN4PgSVP7c2cW")),
    }

    programId := solgo.MPK("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8")

    type simulateData struct {
        Instruction  uint8
        SimulateType uint8
    }
    data := simulateData{
        Instruction:  12,
        SimulateType: 0,
    }

    buf := new(bytes.Buffer)
    if err := bin.NewBinEncoder(buf).Encode(data); err != nil {
        require.Fail(t, "unable to encode instruction: %v", err)
    }

    instruction := solgo.NewInstruction(programId, keys, buf.Bytes())

        // `fetchInfo` implementation uses RaydiumSimuLateTransaction11111111111111111 as signer
    transactionBuilder := solgo.NewTransactionBuilder().
        SetFeePayer(solgo.MPK("RaydiumSimuLateTransaction11111111111111111")).
        AddInstruction(instruction)

    hash, err := client.GetLatestBlockhash(context.Background(), solrpc.CommitmentConfirmed)
    require.NoError(t, err)

    tx, err := transactionBuilder.SetRecentBlockHash(hash.Value.Blockhash).Build()
    require.NoError(t, err)

    simulation, err := client.SimulateTransactionWithOpts(context.Background(), tx, &solrpc.SimulateTransactionOpts{
        SigVerify:              false,
        Commitment:             solrpc.CommitmentConfirmed,
        ReplaceRecentBlockhash: false,
    })
    require.NoError(t, err)

    fmt.Println(simulation.Value.Logs)
}

Output:

(*jsonrpc.RPCError)(0xc000161200)({
 Code: (int) -32602,
 Message: (string) (len=78) "invalid transaction: Transaction failed to sanitize accounts offsets correctly",
 Data: (interface {}) <nil>
})

I'm trying to debug why this is happening, but because of my lack of understanding about solana-go internals, it is going pretty slowly...

Thanks.

lucasoares commented 5 months ago

I think it may be related to the transaction version or transaction encoding... The resulting bytes of the transaction serialization are very different from the ts code... Needs more debugging...

Edit: doing a little more debugging, if I sign the transaction, the resulting bytes will be parsed correctly and the simulation will run "correctly". I think the current marshal algorithm can't work for transactions without signatures.

The problem is: signing the transaction will not work for me, since I need the signer to be RaydiumSimuLateTransaction11111111111111111 to the raydium simulation to return the result I need...

Any idea about what needs to be done to be able to simulate transactions without signing?

Edit2: I made it work by adding a random signature in the tx.Signatures, but that should not be necessary, it is a bug..

jamalphasquad commented 2 months ago

I am getting the same issue when implementing raydium sdk in go, did you find anything that could help? @lucasoares

bilalakhter commented 2 months ago

@jamalphasquad , We need to add a random secret for signing Tx I can see logs now return from RPC,Pushed the changes

longtruongg commented 2 months ago

same issue, i fixed it with create new sign transaction,

       recent, err := cluster.GetRecentBlockhash(context.Background(), "")
       if err != nil {
        return "", fmt.Errorf("cannot get recent blockHash  -> %v", err)
    }
    payer, err := solana.PrivateKeyFromBase58("privatekey")
    if err != nil {
        return "", fmt.Errorf("private key from   -> %v", err)
    }
       tx, err := solana.NewTransaction(
        []solana.Instruction{
            system.NewTransferInstruction(
                solana.LAMPORTS_PER_SOL/1000, payer.PublicKey(), toAddr).Build(),
        },
        recent.Value.Blockhash,
        solana.TransactionPayer(payer.PublicKey()),
    )
    if err != nil {
        return "", fmt.Errorf("new transaction  -> %v", err)
    }
    _, err = tx.Sign(
        func(key solana.PublicKey) *solana.PrivateKey {
            if payer.PublicKey().Equals(key) {
                return &payer
            }
            return nil
        })

    fmt.Println("waiting 10sec")
    result, err := cluster.SendTransaction(context.Background(), tx) 

maybe this is useful

lbtsm commented 1 month ago

Have you solved it? I have the same problem and I am looking for a solution. @lucasoares

lucasoares commented 1 month ago

Have you solved it? I have the same problem and I am looking for a solution. @lucasoares

Yea, I made it work by adding a random signature in the tx.Signatures haha sad but the only workaround I found.