diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index e805e4b612..38d3a32a3b 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -1,6 +1,7 @@ package test import ( + "context" "crypto/sha256" "fmt" "os" @@ -14,6 +15,8 @@ import ( "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" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gotest.tools/v3/icmd" ) @@ -63,6 +66,152 @@ func TestCLIContractDeployAndInvoke(t *testing.T) { require.Contains(t, output, `["Hello","world"]`) } +func TestCLISimulateTransactionBumpAndRestoreFootprint(t *testing.T) { + test := NewCLITest(t) + ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) + client := jrpc2.NewClient(ch, nil) + + sourceAccount := keypair.Root(StandaloneNetworkPassphrase) + address := sourceAccount.Address() + account := txnbuild.NewSimpleAccount(address, 0) + + helloWorldContract := getHelloWorldContract(t) + + params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ + SourceAccount: &account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + createInstallContractCodeOperation(account.AccountID, helloWorldContract), + }, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + }) + tx, err := txnbuild.NewTransaction(params) + assert.NoError(t, err) + sendSuccessfulTransaction(t, client, sourceAccount, tx) + + params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ + SourceAccount: &account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + createCreateContractOperation(t, address, helloWorldContract, StandaloneNetworkPassphrase), + }, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + }) + tx, err = txnbuild.NewTransaction(params) + assert.NoError(t, err) + sendSuccessfulTransaction(t, client, sourceAccount, tx) + + contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) + runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", contractID)) + + // get the counter ledger entry expiration + contractIDHash := xdr.Hash(contractID) + counterSym := xdr.ScSymbol("COUNTER") + key := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contractIDHash, + }, + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvSymbol, + Sym: &counterSym, + }, + Durability: xdr.ContractDataDurabilityPersistent, + }, + } + + binKey, err := key.MarshalBinary() + assert.NoError(t, err) + + expirationKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.LedgerKeyExpiration{ + KeyHash: sha256.Sum256(binKey), + }, + } + + keyB64, err := xdr.MarshalBase64(expirationKey) + require.NoError(t, err) + getLedgerEntryrequest := methods.GetLedgerEntryRequest{ + Key: keyB64, + } + var getLedgerEntryResult methods.GetLedgerEntryResponse + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + assert.NoError(t, err) + var entry xdr.LedgerEntryData + assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) + + assert.Equal(t, xdr.LedgerEntryTypeExpiration, entry.Type) + initialExpirationSeq := entry.Expiration.ExpirationLedgerSeq + + // bump the initial expiration + params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ + SourceAccount: &account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.BumpFootprintExpiration{ + LedgersToExpire: 20, + Ext: xdr.TransactionExt{ + V: 1, + SorobanData: &xdr.SorobanTransactionData{ + Resources: xdr.SorobanResources{ + Footprint: xdr.LedgerFootprint{ + ReadOnly: []xdr.LedgerKey{key}, + }, + }, + }, + }, + }, + }, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + }) + tx, err = txnbuild.NewTransaction(params) + assert.NoError(t, err) + sendSuccessfulTransaction(t, client, sourceAccount, tx) + + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + assert.NoError(t, err) + assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) + assert.Equal(t, xdr.LedgerEntryTypeExpiration, entry.Type) + newExpirationSeq := entry.Expiration.ExpirationLedgerSeq + assert.Greater(t, newExpirationSeq, initialExpirationSeq) + + // Wait until it expires + waitForExpiration := func() { + expired := false + for i := 0; i < 50; i++ { + err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) + assert.NoError(t, err) + assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) + assert.Equal(t, xdr.LedgerEntryTypeExpiration, entry.Type) + // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger + currentLedger := getLedgerEntryResult.LatestLedger + 1 + if xdr.Uint32(currentLedger) > entry.Expiration.ExpirationLedgerSeq { + expired = true + t.Logf("ledger entry expired") + break + } + t.Log("waiting for ledger entry to expire at ledger", entry.Expiration.ExpirationLedgerSeq) + time.Sleep(time.Second) + } + require.True(t, expired) + } + waitForExpiration() + output := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", contractID)) + require.Equal(t, "2", output) +} + func runSuccessfulCLICmd(t *testing.T, cmd string) string { res := runCLICommand(t, cmd) require.NoError(t, res.Error, fmt.Sprintf("stderr:\n%s\nstdout:\n%s\n", res.Stderr(), res.Stdout()))