diff --git a/cmd/soroban-rpc/internal/methods/simulate_transaction.go b/cmd/soroban-rpc/internal/methods/simulate_transaction.go index 4cbd523a..68394cc2 100644 --- a/cmd/soroban-rpc/internal/methods/simulate_transaction.go +++ b/cmd/soroban-rpc/internal/methods/simulate_transaction.go @@ -3,7 +3,10 @@ package methods import ( "context" "encoding/base64" + "encoding/json" + "errors" "fmt" + "strings" "github.com/creachadair/jrpc2" "github.com/creachadair/jrpc2/handler" @@ -34,12 +37,107 @@ type RestorePreamble struct { TransactionData string `json:"transactionData"` // SorobanTransactionData XDR in base64 MinResourceFee int64 `json:"minResourceFee,string"` } +type LedgerEntryChangeType int -// LedgerEntryDiff designates a change in a ledger entry. Before and After cannot be be omitted at the same time. +const ( + LedgerEntryChangeTypeCreated LedgerEntryChangeType = iota + LedgerEntryChangeTypeUpdated + LedgerEntryChangeTypeDeleted +) + +var ( + LedgerEntryChangeTypeName = map[LedgerEntryChangeType]string{ + LedgerEntryChangeTypeCreated: "created", + LedgerEntryChangeTypeUpdated: "updated", + LedgerEntryChangeTypeDeleted: "deleted", + } + LedgerEntryChangeTypeValue = map[string]LedgerEntryChangeType{ + "created": LedgerEntryChangeTypeCreated, + "updated": LedgerEntryChangeTypeUpdated, + "deleted": LedgerEntryChangeTypeDeleted, + } +) + +func (l LedgerEntryChangeType) String() string { + return LedgerEntryChangeTypeName[l] +} + +func (l LedgerEntryChangeType) MarshalJSON() ([]byte, error) { + return json.Marshal(l.String()) +} + +func (l *LedgerEntryChangeType) Parse(s string) error { + s = strings.TrimSpace(strings.ToLower(s)) + value, ok := LedgerEntryChangeTypeValue[s] + if !ok { + return fmt.Errorf("%q is not a valid ledger entry change type", s) + } + *l = value + return nil +} + +func (l *LedgerEntryChangeType) UnmarshalJSON(data []byte) (err error) { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + return l.Parse(s) +} + +func (l *LedgerEntryChange) FromXDRDiff(diff preflight.XDRDiff) error { + beforePresent := len(diff.Before) > 0 + afterPresent := len(diff.After) > 0 + var ( + entryXDR []byte + changeType LedgerEntryChangeType + ) + switch { + case beforePresent: + entryXDR = diff.Before + if afterPresent { + changeType = LedgerEntryChangeTypeUpdated + } else { + changeType = LedgerEntryChangeTypeDeleted + } + case afterPresent: + entryXDR = diff.After + changeType = LedgerEntryChangeTypeCreated + default: + return errors.New("missing before and after") + } + var entry xdr.LedgerEntry + + if err := xdr.SafeUnmarshal(entryXDR, &entry); err != nil { + return err + } + key, err := entry.LedgerKey() + if err != nil { + return err + } + keyB64, err := xdr.MarshalBase64(key) + if err != nil { + return err + } + l.Type = changeType + l.Key = keyB64 + if beforePresent { + before := base64.StdEncoding.EncodeToString(diff.Before) + l.Before = &before + } + if afterPresent { + after := base64.StdEncoding.EncodeToString(diff.After) + l.After = &after + } + return nil +} + +// LedgerEntryChange designates a change in a ledger entry. Before and After cannot be be omitted at the same time. // If Before is omitted, it constitutes a creation, if After is omitted, it constitutes a delation. -type LedgerEntryDiff struct { - Before string `json:"before,omitempty"` // LedgerEntry XDR in base64 - After string `json:"after,omitempty"` // LedgerEntry XDR in base64 +type LedgerEntryChange struct { + Type LedgerEntryChangeType + Key string // LedgerEntryKey in base64 + Before *string `json:"before"` // LedgerEntry XDR in base64 + After *string `json:"after"` // LedgerEntry XDR in base64 } type SimulateTransactionResponse struct { @@ -50,7 +148,7 @@ type SimulateTransactionResponse struct { Results []SimulateHostFunctionResult `json:"results,omitempty"` // an array of the individual host function call results Cost SimulateTransactionCost `json:"cost,omitempty"` // the effective cpu and memory cost of the invoked transaction execution. RestorePreamble *RestorePreamble `json:"restorePreamble,omitempty"` // If present, it indicates that a prior RestoreFootprint is required - StateDiff []LedgerEntryDiff `json:"stateDiff,omitempty"` // If present, it indicates how the state (ledger entries) will change as a result of the transaction execution. + StateChanges []LedgerEntryChange `json:"stateChanges,omitempty"` // If present, it indicates how the state (ledger entries) will change as a result of the transaction execution. LatestLedger uint32 `json:"latestLedger"` } @@ -157,10 +255,9 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge } } - stateDiff := make([]LedgerEntryDiff, len(result.LedgerEntryDiff)) - for i := 0; i < len(stateDiff); i++ { - stateDiff[i].Before = base64.StdEncoding.EncodeToString(result.LedgerEntryDiff[i].Before) - stateDiff[i].After = base64.StdEncoding.EncodeToString(result.LedgerEntryDiff[i].After) + stateChanges := make([]LedgerEntryChange, len(result.LedgerEntryDiff)) + for i := 0; i < len(stateChanges); i++ { + stateChanges[i].FromXDRDiff(result.LedgerEntryDiff[i]) } return SimulateTransactionResponse{ @@ -175,7 +272,7 @@ func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.Ledge }, LatestLedger: latestLedger, RestorePreamble: restorePreamble, - StateDiff: stateDiff, + StateChanges: stateChanges, } }) } diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index d4627d7e..d414f315 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -261,12 +261,18 @@ func TestSimulateTransactionSucceeds(t *testing.T) { assert.Equal(t, expectedXdr, resultXdr) // Check state diff - assert.Len(t, result.StateDiff, 1) - assert.Empty(t, result.StateDiff[0].Before) - assert.NotEmpty(t, result.StateDiff[0].After) + assert.Len(t, result.StateChanges, 1) + assert.Nil(t, result.StateChanges[0].Before) + assert.NotNil(t, result.StateChanges[0].After) + assert.Equal(t, methods.LedgerEntryChangeTypeCreated, result.StateChanges[0].Type) var after xdr.LedgerEntry - assert.NoError(t, xdr.SafeUnmarshalBase64(result.StateDiff[0].After, &after)) + assert.NoError(t, xdr.SafeUnmarshalBase64(*result.StateChanges[0].After, &after)) assert.Equal(t, xdr.LedgerEntryTypeContractCode, after.Data.Type) + entryKey, err := after.LedgerKey() + assert.NoError(t, err) + entryKeyB64, err := xdr.MarshalBase64(entryKey) + assert.NoError(t, err) + assert.Equal(t, entryKeyB64, result.StateChanges[0].Key) // test operation which does not have a source account withoutSourceAccountOp := createInstallContractCodeOperation("", contractBinary)