From 7f27d798422a8f0d5f76a9f098d2ca6ad85d9208 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Wed, 30 Nov 2022 16:49:23 +0000 Subject: [PATCH] Add rpc getLedgerEntry method --- cmd/soroban-cli/src/rpc/mod.rs | 10 +- cmd/soroban-cli/src/serve.rs | 20 +++ cmd/soroban-rpc/internal/jsonrpc.go | 1 + .../internal/methods/get_ledger_entry.go | 83 +++++++++++ .../internal/test/get_ledger_entry_test.go | 139 ++++++++++++++++++ 5 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 cmd/soroban-rpc/internal/methods/get_ledger_entry.go create mode 100644 cmd/soroban-rpc/internal/test/get_ledger_entry_test.go diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index 4fd920061..a602f6c0a 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -1,6 +1,6 @@ use jsonrpsee_core::{client::ClientT, rpc_params}; use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder}; -use soroban_env_host::xdr::{Error as XdrError, ScVal, TransactionEnvelope, WriteXdr}; +use soroban_env_host::xdr::{Error as XdrError, LedgerKey, ScVal, TransactionEnvelope, WriteXdr}; use std::time::{Duration, Instant}; use tokio::time::sleep; @@ -185,4 +185,12 @@ impl Client { .request("getContractData", rpc_params![contract_id, base64_key]) .await?) } + + pub async fn get_ledger_entry(&self, key: LedgerKey) -> Result { + let base64_key = key.to_xdr_base64()?; + Ok(self + .client()? + .request("getLedgerEntry", rpc_params![base64_key]) + .await?) + } } diff --git a/cmd/soroban-cli/src/serve.rs b/cmd/soroban-cli/src/serve.rs index c44cb70a2..5cb9a14dd 100644 --- a/cmd/soroban-cli/src/serve.rs +++ b/cmd/soroban-cli/src/serve.rs @@ -146,6 +146,7 @@ async fn handler( ("getContractData", Some(Requests::GetContractData((contract_id, key)))) => { get_contract_data(&contract_id, key, &ledger_file) } + ("getLedgerEntry", Some(Requests::StringArg(key))) => get_ledger_entry(key, &ledger_file), ("getTransactionStatus", Some(Requests::StringArg(b))) => { get_transaction_status(&transaction_status_map, b).await } @@ -233,6 +234,25 @@ fn get_contract_data( })) } +fn get_ledger_entry(key_xdr: String, ledger_file: &PathBuf) -> Result { + // Initialize storage and host + let state = snapshot::read(ledger_file)?; + let key = LedgerKey::from_xdr_base64(key_xdr)?; + + let snap = Rc::new(snapshot::Snap { + ledger_entries: state.1, + }); + let mut storage = Storage::with_recording_footprint(snap); + let ledger_entry = storage.get(&key)?; + + Ok(json!({ + "xdr": ledger_entry.data.to_xdr_base64()?, + "lastModifiedLedgerSeq": ledger_entry.last_modified_ledger_seq, + // TODO: Find "real" ledger seq number here + "latestLedger": 1, + })) +} + fn parse_transaction( txn_xdr: &str, passphrase: &str, diff --git a/cmd/soroban-rpc/internal/jsonrpc.go b/cmd/soroban-rpc/internal/jsonrpc.go index 6dc513999..284ffba1e 100644 --- a/cmd/soroban-rpc/internal/jsonrpc.go +++ b/cmd/soroban-rpc/internal/jsonrpc.go @@ -47,6 +47,7 @@ func NewJSONRPCHandler(params HandlerParams) (Handler, error) { bridge := jhttp.NewBridge(handler.Map{ "getHealth": methods.NewHealthCheck(), "getAccount": methods.NewAccountHandler(params.AccountStore), + "getLedgerEntry": methods.NewGetLedgerEntryHandler(params.Logger, params.CoreClient), "getTransactionStatus": methods.NewGetTransactionStatusHandler(params.TransactionProxy), "sendTransaction": methods.NewSendTransactionHandler(params.TransactionProxy), "simulateTransaction": methods.NewSimulateTransactionHandler(params.Logger, params.CoreClient), diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go new file mode 100644 index 000000000..f8abbfb33 --- /dev/null +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go @@ -0,0 +1,83 @@ +package methods + +import ( + "context" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/code" + "github.com/creachadair/jrpc2/handler" + + "github.com/stellar/go/clients/stellarcore" + proto "github.com/stellar/go/protocols/stellarcore" + "github.com/stellar/go/support/log" + "github.com/stellar/go/xdr" +) + +type GetLedgerEntryRequest struct { + Key string `json:"key"` +} + +type GetLedgerEntryResponse struct { + XDR string `json:"xdr"` + LastModifiedLedger int64 `json:"lastModifiedLedgerSeq,string"` + LatestLedger int64 `json:"latestLedger,string"` +} + +// NewGetLedgerEntryHandler returns a json rpc handler to retrieve a contract data ledger entry from stellar cre +func NewGetLedgerEntryHandler(logger *log.Entry, coreClient *stellarcore.Client) jrpc2.Handler { + return handler.New(func(ctx context.Context, request GetLedgerEntryRequest) (GetLedgerEntryResponse, error) { + var key xdr.LedgerKey + if err := xdr.SafeUnmarshalBase64(request.Key, &key); err != nil { + logger.WithError(err).WithField("request", request). + Info("could not unmarshal ledgerKey from getLedgerEntry request") + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: code.InvalidParams, + Message: "cannot unmarshal key value", + } + } + + coreResponse, err := coreClient.GetLedgerEntry(ctx, key) + if err != nil { + logger.WithError(err).WithField("request", request). + Info("could not submit getLedgerEntry request to core") + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: code.InternalError, + Message: "could not submit request to core", + } + } + + if coreResponse.State == proto.DeadState { + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: code.InvalidRequest, + Message: "not found", + } + } + + var ledgerEntry xdr.LedgerEntry + if err = xdr.SafeUnmarshalBase64(coreResponse.Entry, &ledgerEntry); err != nil { + logger.WithError(err).WithField("request", request). + WithField("response", coreResponse). + Info("could not parse ledger entry") + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: code.InternalError, + Message: "could not parse core response", + } + } + + response := GetLedgerEntryResponse{ + LastModifiedLedger: int64(ledgerEntry.LastModifiedLedgerSeq), + LatestLedger: coreResponse.Ledger, + } + if response.XDR, err = xdr.MarshalBase64(ledgerEntry.Data); err != nil { + logger.WithError(err).WithField("request", request). + WithField("response", coreResponse). + Info("could not serialize ledger entry data") + return GetLedgerEntryResponse{}, &jrpc2.Error{ + Code: code.InternalError, + Message: "could not serialize ledger entry data", + } + } + + return response, nil + }) +} diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go b/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go new file mode 100644 index 000000000..4b4a46adf --- /dev/null +++ b/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go @@ -0,0 +1,139 @@ +package test + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/code" + "github.com/creachadair/jrpc2/jhttp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" +) + +func TestGetLedgerEntryNotFound(t *testing.T) { + test := NewTest(t) + + ch := jhttp.NewChannel(test.server.URL, nil) + client := jrpc2.NewClient(ch, nil) + + sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() + contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) + keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + ContractId: contractID, + Key: getContractCodeLedgerKey(), + }, + }) + require.NoError(t, err) + request := methods.GetLedgerEntryRequest{ + Key: keyB64, + } + + var result methods.GetLedgerEntryResponse + jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) + assert.Equal(t, "not found", jsonRPCErr.Message) + assert.Equal(t, code.InvalidRequest, jsonRPCErr.Code) +} + +func TestGetLedgerEntryInvalidParams(t *testing.T) { + test := NewTest(t) + + ch := jhttp.NewChannel(test.server.URL, nil) + client := jrpc2.NewClient(ch, nil) + + request := methods.GetLedgerEntryRequest{ + Key: "<>@@#$", + } + + var result methods.GetLedgerEntryResponse + jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) + assert.Equal(t, "cannot unmarshal key value", jsonRPCErr.Message) + assert.Equal(t, code.InvalidParams, jsonRPCErr.Code) +} + +func TestGetLedgerEntryDeadlineError(t *testing.T) { + test := NewTest(t) + test.coreClient.HTTP = &http.Client{ + Timeout: time.Microsecond, + } + + ch := jhttp.NewChannel(test.server.URL, nil) + client := jrpc2.NewClient(ch, nil) + + sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() + contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) + keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + ContractId: contractID, + Key: getContractCodeLedgerKey(), + }, + }) + require.NoError(t, err) + request := methods.GetLedgerEntryRequest{ + Key: keyB64, + } + + var result methods.GetLedgerEntryResponse + jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) + assert.Equal(t, "could not submit request to core", jsonRPCErr.Message) + assert.Equal(t, code.InternalError, jsonRPCErr.Code) +} + +func TestGetLedgerEntrySucceeds(t *testing.T) { + test := NewTest(t) + + ch := jhttp.NewChannel(test.server.URL, nil) + client := jrpc2.NewClient(ch, nil) + + kp := keypair.Root(StandaloneNetworkPassphrase) + account := txnbuild.NewSimpleAccount(kp.Address(), 0) + + // Install and create the contract first + for _, op := range []txnbuild.Operation{ + createInstallContractCodeOperation(t, account.AccountID, testContract, true), + createCreateContractOperation(t, account.AccountID, testContract, StandaloneNetworkPassphrase, true), + } { + assertSendTransaction(t, client, kp, txnbuild.TransactionParams{ + SourceAccount: &account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{op}, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + }) + } + + sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() + contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) + keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + ContractId: contractID, + Key: getContractCodeLedgerKey(), + }, + }) + require.NoError(t, err) + request := methods.GetLedgerEntryRequest{ + Key: keyB64, + } + + var result methods.GetLedgerEntryResponse + err = client.CallResult(context.Background(), "getLedgerEntry", request, &result) + assert.NoError(t, err) + assert.Greater(t, result.LatestLedger, int64(0)) + assert.GreaterOrEqual(t, result.LatestLedger, result.LastModifiedLedger) + var scVal xdr.ScVal + assert.NoError(t, xdr.SafeUnmarshalBase64(result.XDR, &scVal)) + assert.Equal(t, testContract, scVal.MustObj().MustContractCode().MustWasmId()) +}