diff --git a/rpc/client_test.go b/rpc/client_test.go index a4d069d1f..b81dcf9af 100644 --- a/rpc/client_test.go +++ b/rpc/client_test.go @@ -2648,7 +2648,163 @@ func TestClient_IsBlockhashValid(t *testing.T) { } func TestClient_SimulateTransaction(t *testing.T) { - // TODO + responseBody := `{"context":{"slot":218},"value":{"accounts":null,"logs":["Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri invoke [1]","Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri consumed 2366 of 1400000 compute units","Program return: 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri KgAAAAAAAAA=","Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success"],"unitsConsumed":2366}}` + server, closer := mockJSONRPC(t, stdjson.RawMessage(wrapIntoRPC(responseBody))) + defer closer() + client := New(server.URL) + + txData := []byte{1, 2, 3, 4} // dummy transaction data + out, err := client.SimulateRawTransactionWithOpts( + context.Background(), + txData, + nil, + ) + require.NoError(t, err) + + assert.Nil(t, out.Value.Err) + assert.Len(t, out.Value.Logs, 4) + assert.Equal(t, uint64(2366), *out.Value.UnitsConsumed) + + reqBody := server.RequestBody(t) + assert.NotNil(t, reqBody["id"]) + reqBody["id"] = any(nil) + + assert.Equal(t, + map[string]any{ + "id": any(nil), + "jsonrpc": "2.0", + "method": "simulateTransaction", + "params": []any{ + base64.StdEncoding.EncodeToString(txData), + map[string]any{ + "encoding": "base64", + }, + }, + }, + reqBody, + ) + + expected := mustJSONToInterface([]byte(responseBody)) + got := mustJSONToInterface(mustAnyToJSON(out)) + assert.Equal(t, expected, got, "both deserialized values must be equal") +} + +func TestClient_SimulateTransactionWithOpts_AllOptions(t *testing.T) { + responseBody := `{"context":{"slot":218},"value":{"err":null,"logs":["Program log: hello"],"accounts":null,"unitsConsumed":1000}}` + server, closer := mockJSONRPC(t, stdjson.RawMessage(wrapIntoRPC(responseBody))) + defer closer() + client := New(server.URL) + + txData := []byte{1, 2, 3, 4} + minContextSlot := uint64(100) + out, err := client.SimulateRawTransactionWithOpts( + context.Background(), + txData, + &SimulateTransactionOpts{ + SigVerify: true, + Commitment: CommitmentProcessed, + ReplaceRecentBlockhash: true, + InnerInstructions: true, + MinContextSlot: &minContextSlot, + Accounts: &SimulateTransactionAccountsOpts{ + Encoding: solana.EncodingBase64, + Addresses: []solana.PublicKey{solana.MustPublicKeyFromBase58("7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932")}, + }, + }, + ) + require.NoError(t, err) + assert.NotNil(t, out) + + reqBody := server.RequestBody(t) + reqBody["id"] = any(nil) + + assert.Equal(t, + map[string]any{ + "id": any(nil), + "jsonrpc": "2.0", + "method": "simulateTransaction", + "params": []any{ + base64.StdEncoding.EncodeToString(txData), + map[string]any{ + "encoding": "base64", + "sigVerify": true, + "commitment": string(CommitmentProcessed), + "replaceRecentBlockhash": true, + "innerInstructions": true, + "minContextSlot": float64(100), + "accounts": map[string]any{ + "encoding": string(solana.EncodingBase64), + "addresses": []any{"7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932"}, + }, + }, + }, + }, + reqBody, + ) +} + +func TestClient_SimulateTransaction_InnerInstructions(t *testing.T) { + responseBody := `{"context":{"slot":300},"value":{"logs":["Program log: invoke"],"accounts":null,"unitsConsumed":5000,"innerInstructions":[{"index":0,"instructions":[{"programIdIndex":2,"accounts":[0,1],"data":"3Bxs4ThwQbE4vyj5","stackHeight":2}]}],"returnData":{"programId":"83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri","data":["KgAAAAAAAAA=","base64"]}}}` + server, closer := mockJSONRPC(t, stdjson.RawMessage(wrapIntoRPC(responseBody))) + defer closer() + client := New(server.URL) + + txData := []byte{1, 2, 3, 4} + out, err := client.SimulateRawTransactionWithOpts( + context.Background(), + txData, + &SimulateTransactionOpts{ + InnerInstructions: true, + }, + ) + require.NoError(t, err) + + require.Len(t, out.Value.InnerInstructions, 1) + assert.Equal(t, uint16(0), out.Value.InnerInstructions[0].Index) + require.Len(t, out.Value.InnerInstructions[0].Instructions, 1) + assert.Equal(t, uint16(2), out.Value.InnerInstructions[0].Instructions[0].ProgramIDIndex) + assert.Equal(t, []uint16{0, 1}, out.Value.InnerInstructions[0].Instructions[0].Accounts) + assert.Equal(t, uint16(2), out.Value.InnerInstructions[0].Instructions[0].StackHeight) + + require.NotNil(t, out.Value.ReturnData) + assert.Equal(t, solana.MustPublicKeyFromBase58("83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri"), out.Value.ReturnData.ProgramId) + + expected := mustJSONToInterface([]byte(responseBody)) + got := mustJSONToInterface(mustAnyToJSON(out)) + assert.Equal(t, expected, got, "both deserialized values must be equal") +} + +func TestClient_SimulateTransaction_FullResult(t *testing.T) { + responseBody := `{"context":{"slot":400},"value":{"logs":["Program log: ok"],"accounts":null,"unitsConsumed":3000,"loadedAccountsDataSize":1024,"fee":5000,"preBalances":[10000000,0],"postBalances":[9995000,0],"loadedAddresses":{"readonly":["11111111111111111111111111111111"],"writable":[]},"replacementBlockhash":{"blockhash":"EETubP5AKHgjPAhzPkToc6S4eibc4FFqQGnHR1Sh9rAr","lastValidBlockHeight":500}}}` + server, closer := mockJSONRPC(t, stdjson.RawMessage(wrapIntoRPC(responseBody))) + defer closer() + client := New(server.URL) + + txData := []byte{1, 2, 3, 4} + out, err := client.SimulateRawTransactionWithOpts( + context.Background(), + txData, + &SimulateTransactionOpts{ + ReplaceRecentBlockhash: true, + InnerInstructions: true, + }, + ) + require.NoError(t, err) + + assert.Nil(t, out.Value.Err) + assert.Equal(t, uint64(3000), *out.Value.UnitsConsumed) + assert.Equal(t, uint32(1024), *out.Value.LoadedAccountsDataSize) + assert.Equal(t, uint64(5000), *out.Value.Fee) + assert.Equal(t, []uint64{10000000, 0}, out.Value.PreBalances) + assert.Equal(t, []uint64{9995000, 0}, out.Value.PostBalances) + require.NotNil(t, out.Value.ReplacementBlockhash) + assert.Equal(t, uint64(500), out.Value.ReplacementBlockhash.LastValidBlockHeight) + require.NotNil(t, out.Value.LoadedAddresses) + assert.Len(t, out.Value.LoadedAddresses.ReadOnly, 1) + + expected := mustJSONToInterface([]byte(responseBody)) + got := mustJSONToInterface(mustAnyToJSON(out)) + assert.Equal(t, expected, got, "both deserialized values must be equal") } func TestClient_GetFeeForMessage(t *testing.T) { diff --git a/rpc/simulateTransaction.go b/rpc/simulateTransaction.go index 37dfddd98..d42cd8d59 100644 --- a/rpc/simulateTransaction.go +++ b/rpc/simulateTransaction.go @@ -43,6 +43,41 @@ type SimulateTransactionResult struct { // The number of compute budget units consumed during the processing of this transaction. UnitsConsumed *uint64 `json:"unitsConsumed,omitempty"` + + // The size of loaded accounts data in bytes. + LoadedAccountsDataSize *uint32 `json:"loadedAccountsDataSize,omitempty"` + + // The most-recent return data generated by an instruction in the transaction. + ReturnData *ReturnData `json:"returnData,omitempty"` + + // If innerInstructions were requested, a list of inner instructions. + InnerInstructions []InnerInstruction `json:"innerInstructions,omitempty"` + + // The replacement blockhash used when replaceRecentBlockhash is true. + ReplacementBlockhash *ReplacementBlockhash `json:"replacementBlockhash,omitempty"` + + // Fee this transaction was charged. + Fee *uint64 `json:"fee,omitempty"` + + // Array of u64 account balances from before the transaction was processed. + PreBalances []uint64 `json:"preBalances,omitempty"` + + // Array of u64 account balances after the transaction was processed. + PostBalances []uint64 `json:"postBalances,omitempty"` + + // List of token balances from before the transaction was processed. + PreTokenBalances []TokenBalance `json:"preTokenBalances,omitempty"` + + // List of token balances from after the transaction was processed. + PostTokenBalances []TokenBalance `json:"postTokenBalances,omitempty"` + + // Addresses loaded from address lookup tables. + LoadedAddresses *LoadedAddresses `json:"loadedAddresses,omitempty"` +} + +type ReplacementBlockhash struct { + Blockhash solana.Hash `json:"blockhash"` + LastValidBlockHeight uint64 `json:"lastValidBlockHeight"` } // SimulateTransaction simulates sending a transaction. @@ -70,6 +105,14 @@ type SimulateTransactionOpts struct { // (default: false, conflicts with SigVerify) ReplaceRecentBlockhash bool + // If true the response will include inner instructions. + // These inner instructions will be jsonParsed where possible, + // otherwise json. (default: false) + InnerInstructions bool + + // The minimum slot that the request can be evaluated at. + MinContextSlot *uint64 + Accounts *SimulateTransactionAccountsOpts } @@ -118,6 +161,12 @@ func (cl *Client) SimulateRawTransactionWithOpts( if opts.ReplaceRecentBlockhash { obj["replaceRecentBlockhash"] = opts.ReplaceRecentBlockhash } + if opts.InnerInstructions { + obj["innerInstructions"] = opts.InnerInstructions + } + if opts.MinContextSlot != nil { + obj["minContextSlot"] = *opts.MinContextSlot + } if opts.Accounts != nil { obj["accounts"] = M{ "encoding": opts.Accounts.Encoding,