diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index b14d15b87..a14f07518 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -704,6 +704,62 @@ func TestLookupTables(t *testing.T) { require.Equal(t, lookupKeys[i], address.PublicKey) } }) + + t.Run("Resolving optional derived lookup table does not return error", func(t *testing.T) { + // Deployed contract_reader_interface contract + programID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") + + args := map[string]interface{}{ + "seed1": []byte("lookup"), + } + + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.Lookup{PDALookups: &chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.Lookup{AccountConstant: &chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{Name: "missing_seed", Location: "missing_seed"}}}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + TypeName: "LookupTableDataAccount", + Location: "LookupTable", + IDL: testContractIDL, + }}, + }, + Optional: true, + }, + }, + } + + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupConfig) + require.NoError(t, err) + + pdaWithAccountLookupSeed := chainwriter.Lookup{ + PDALookups: &chainwriter.PDALookups{ + PublicKey: chainwriter.Lookup{AccountConstant: &chainwriter.AccountConstant{Address: chainwriter.GetRandomPubKey(t).String()}}, + Seeds: []chainwriter.Seed{ + { + Dynamic: chainwriter.Lookup{ + AccountsFromLookupTable: &chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{}, + }, + }, + }, + }, + }, + Optional: true, + } + + accounts, err := chainwriter.GetAddresses(ctx, nil, []chainwriter.Lookup{pdaWithAccountLookupSeed}, derivedTableMap, multiClient) + require.NoError(t, err) + require.Empty(t, accounts) + }) } func TestCreateATAs(t *testing.T) { @@ -936,6 +992,31 @@ func TestCreateATAs(t *testing.T) { require.NoError(t, err) require.Empty(t, ataInstructions, "No new instructions should be returned if ATAs already exist") }) + + t.Run("optional ATA creation does not return error if lookups fail", func(t *testing.T) { + lookups := []chainwriter.ATALookup{ + { + Location: "Inner.Address", + WalletAddress: chainwriter.Lookup{AccountConstant: &chainwriter.AccountConstant{ + Address: feePayer.String(), + }}, + TokenProgram: chainwriter.Lookup{AccountConstant: &chainwriter.AccountConstant{ + Address: chainwriter.GetRandomPubKey(t).String(), + }}, + MintAddress: chainwriter.Lookup{AccountLookup: &chainwriter.AccountLookup{ + Location: "Inner.BadLocation", + }}, + Optional: true, + }, + } + args := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{{Address: chainwriter.GetRandomPubKey(t).Bytes()}}, + } + + ataInstructions, err := chainwriter.CreateATAs(ctx, args, lookups, nil, multiClient, testContractIDL, feePayer, logger.Test(t)) + require.NoError(t, err) + require.Len(t, ataInstructions, 0) + }) } func checkIfATAExists(t *testing.T, rpcClient *rpc.Client, ataAddress solana.PublicKey) bool { diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index d1f3b60fc..a17c5d923 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -253,7 +253,9 @@ func CreateATAs(ctx context.Context, args any, lookups []ATALookup, derivedTable } } walletAddresses, err := GetAddresses(ctx, args, []Lookup{lookup.WalletAddress}, derivedTableMap, client) - if err != nil { + if lookup.Optional && isIgnorableError(err) { + continue + } else if err != nil { return nil, fmt.Errorf("error resolving wallet address: %w", err) } if len(walletAddresses) != 1 { @@ -262,12 +264,16 @@ func CreateATAs(ctx context.Context, args any, lookups []ATALookup, derivedTable wallet := walletAddresses[0].PublicKey tokenPrograms, err := GetAddresses(ctx, args, []Lookup{lookup.TokenProgram}, derivedTableMap, client) - if err != nil { + if lookup.Optional && isIgnorableError(err) { + continue + } else if err != nil { return nil, fmt.Errorf("error resolving token program address: %w", err) } mints, err := GetAddresses(ctx, args, []Lookup{lookup.MintAddress}, derivedTableMap, client) - if err != nil { + if lookup.Optional && isIgnorableError(err) { + continue + } else if err != nil { return nil, fmt.Errorf("error resolving mint address: %w", err) } if len(tokenPrograms) != len(mints) { @@ -384,7 +390,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error finding transform function: %w", tfErr), debugID) } s.lggr.Debugw("Applying args transformation", "contract", contractName, "method", method) - args, accounts, err = transformFunc(ctx, s, args, accounts, toAddress) + args, accounts, err = transformFunc(ctx, args, accounts, derivedTableMap) if err != nil { return errorWithDebugID(fmt.Errorf("error transforming args: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 4eaec79ff..9b8635279 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -779,7 +779,48 @@ func TestChainWriter_CCIPOfframp(t *testing.T) { }, ChainSpecificName: "execute", ArgsTransform: "CCIPExecute", - LookupTables: chainwriter.LookupTables{}, + LookupTables: chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "PoolLookupTable", + Accounts: chainwriter.Lookup{ + PDALookups: &chainwriter.PDALookups{ + Name: "TokenAdminRegistry", + PublicKey: chainwriter.Lookup{ + PDALookups: &chainwriter.PDALookups{ + Name: ccipconsts.ContractNameRouter, + PublicKey: chainwriter.Lookup{ + AccountConstant: &chainwriter.AccountConstant{Address: offrampAddr.String()}, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("reference_addresses")}, + }, + // Reads the address from the reference addresses account + InternalField: chainwriter.InternalField{ + TypeName: "ReferenceAddresses", + Location: "Router", + IDL: ccipOfframpIDL, + }, + }, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("token_admin_registry")}, + {Dynamic: chainwriter.Lookup{AccountConstant: &chainwriter.AccountConstant{Address: destTokenAddr.String()}}}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + TypeName: "TokenAdminRegistry", + Location: "LookupTable", + // TokenAdminRegistry is in the router program so need to provide the router's IDL + IDL: ccipRouterIDL, + }, + }, + }, + Optional: true, // Lookup table is optional if DestTokenAddress is not present in report + }, + }, + }, Accounts: []chainwriter.Lookup{ {AccountConstant: &chainwriter.AccountConstant{ Name: "testAcc1", @@ -842,10 +883,6 @@ func TestChainWriter_CCIPOfframp(t *testing.T) { }, IDL: ccipOfframpIDL, }, - // Requires only the IDL for the CCIPArgsTransform to fetch the TokenAdminRegistry - ccipconsts.ContractNameRouter: { - IDL: ccipRouterIDL, - }, }, } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index e3782b56d..0c0551684 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -102,6 +102,7 @@ type ATALookup struct { WalletAddress Lookup TokenProgram Lookup MintAddress Lookup + Optional bool } func (l Lookup) validate() error { diff --git a/pkg/solana/chainwriter/transform_registry.go b/pkg/solana/chainwriter/transform_registry.go index 4bdb480a1..00ba7185f 100644 --- a/pkg/solana/chainwriter/transform_registry.go +++ b/pkg/solana/chainwriter/transform_registry.go @@ -7,7 +7,6 @@ import ( "github.com/gagliardetto/solana-go" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/ccip_offramp" - ccipconsts "github.com/smartcontractkit/chainlink-ccip/pkg/consts" "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" ) @@ -27,7 +26,7 @@ type ReportPostTransform struct { TokenIndexes []byte } -func FindTransform(id string) (func(context.Context, *SolanaChainWriterService, any, solana.AccountMetaSlice, string) (any, solana.AccountMetaSlice, error), error) { +func FindTransform(id string) (func(context.Context, any, solana.AccountMetaSlice, map[string]map[string][]*solana.AccountMeta) (any, solana.AccountMetaSlice, error), error) { switch id { case "CCIPExecute": return CCIPExecuteArgsTransform, nil @@ -40,73 +39,26 @@ func FindTransform(id string) (func(context.Context, *SolanaChainWriterService, // This Transform function looks up the token pool addresses in the accounts slice and augments the args // with the indexes of the token pool addresses in the accounts slice. -func CCIPExecuteArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, solana.AccountMetaSlice, error) { - // Fetch offramp config to use to fetch the router address - offrampProgramConfig, ok := cw.config.Programs[ccipconsts.ContractNameOffRamp] +func CCIPExecuteArgsTransform(ctx context.Context, args any, accounts solana.AccountMetaSlice, tableMap map[string]map[string][]*solana.AccountMeta) (any, solana.AccountMetaSlice, error) { + argsTyped, ok := args.(ReportPreTransform) if !ok { - return nil, nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameOffRamp) - } - // PDA lookup to fetch router address - routerAddrLookup := PDALookups{ - Name: "ReferenceAddresses", - PublicKey: Lookup{AccountConstant: &AccountConstant{ - Address: toAddress, - }}, - Seeds: []Seed{ - {Static: []byte("reference_addresses")}, - }, - // Reads the router address from the reference addresses PDA - InternalField: InternalField{ - TypeName: "ReferenceAddresses", - Location: "Router", - IDL: offrampProgramConfig.IDL, - }, - } - accountMetas, err := routerAddrLookup.Resolve(ctx, nil, nil, cw.client) - if err != nil { - return nil, nil, fmt.Errorf("failed to fetch the router program address from the reference addresses account: %w", err) - } - if len(accountMetas) != 1 { - return nil, nil, fmt.Errorf("expect 1 address to be returned for router address, received %d: %w", len(accountMetas), err) + return nil, nil, fmt.Errorf("args is not of type ReportPreTransform") } - - // Fetch router config to use to fetch TokenAdminRegistry - routerProgramConfig, ok := cw.config.Programs[ccipconsts.ContractNameRouter] - if !ok { - return nil, nil, fmt.Errorf("%s program not found in config", ccipconsts.ContractNameRouter) + argsTransformed := ReportPostTransform{ + ReportContext: argsTyped.ReportContext, + Report: argsTyped.Report, + AbstractReport: argsTyped.AbstractReport, + Info: argsTyped.Info, } - routerAddress := accountMetas[0].PublicKey - TokenPoolLookupTable := LookupTables{ - DerivedLookupTables: []DerivedLookupTable{ - { - Name: "PoolLookupTable", - Accounts: Lookup{PDALookups: &PDALookups{ - Name: "TokenAdminRegistry", - PublicKey: Lookup{AccountConstant: &AccountConstant{ - Address: routerAddress.String(), - }}, - Seeds: []Seed{ - {Static: []byte("token_admin_registry")}, - {Dynamic: Lookup{AccountLookup: &AccountLookup{Location: "Info.AbstractReports.Messages.TokenAmounts.DestTokenAddress"}}}, - }, - IsSigner: false, - IsWritable: false, - InternalField: InternalField{ - TypeName: "TokenAdminRegistry", - Location: "LookupTable", - IDL: routerProgramConfig.IDL, - }, - }}, - }, - }, + registryTables, exists := tableMap["PoolLookupTable"] + // If PoolLookupTable does not exist in the table map, token indexes are not needed + // Return with empty TokenIndexes + if !exists { + argsTransformed.TokenIndexes = []byte{} + return argsTransformed, accounts, nil } - tableMap, _, err := cw.ResolveLookupTables(ctx, args, TokenPoolLookupTable) - if err != nil { - return nil, nil, err - } - registryTables := tableMap["PoolLookupTable"] tokenPoolAddresses := []solana.PublicKey{} for _, table := range registryTables { tokenPoolAddresses = append(tokenPoolAddresses, table[0].PublicKey) @@ -128,24 +80,12 @@ func CCIPExecuteArgsTransform(ctx context.Context, cw *SolanaChainWriterService, return nil, nil, fmt.Errorf("missing token pools in accounts") } - argsTyped, ok := args.(ReportPreTransform) - if !ok { - return nil, nil, fmt.Errorf("args is not of type ReportPreTransform") - } - - argsTransformed := ReportPostTransform{ - ReportContext: argsTyped.ReportContext, - Report: argsTyped.Report, - AbstractReport: argsTyped.AbstractReport, - Info: argsTyped.Info, - TokenIndexes: tokenIndexes, - } - + argsTransformed.TokenIndexes = tokenIndexes return argsTransformed, accounts, nil } // This Transform function trims off the GlobalState account from commit transactions if there are no token or gas price updates -func CCIPCommitAccountTransform(ctx context.Context, cw *SolanaChainWriterService, args any, accounts solana.AccountMetaSlice, toAddress string) (any, solana.AccountMetaSlice, error) { +func CCIPCommitAccountTransform(ctx context.Context, args any, accounts solana.AccountMetaSlice, _ map[string]map[string][]*solana.AccountMeta) (any, solana.AccountMetaSlice, error) { var tokenPriceVals, gasPriceVals [][]byte var err error tokenPriceVals, err = GetValuesAtLocation(args, "Info.TokenPrices.TokenID") diff --git a/pkg/solana/chainwriter/transform_registry_test.go b/pkg/solana/chainwriter/transform_registry_test.go index bbeba440b..5244f3a91 100644 --- a/pkg/solana/chainwriter/transform_registry_test.go +++ b/pkg/solana/chainwriter/transform_registry_test.go @@ -1,46 +1,18 @@ package chainwriter_test import ( - "context" "testing" "github.com/gagliardetto/solana-go" - ccipconsts "github.com/smartcontractkit/chainlink-ccip/pkg/consts" "github.com/smartcontractkit/chainlink-ccip/pkg/types/ccipocr3" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/testutils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" - clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" ) func Test_CCIPExecuteArgsTransform(t *testing.T) { ctx := tests.Context(t) - offrampAddress := chainwriter.GetRandomPubKey(t) - routerAddress := chainwriter.GetRandomPubKey(t) - - // simplified CCIP Config - only IDLs are required for CCIPExecute ArgsTransform - ccipCWConfig := chainwriter.ChainWriterConfig{ - Programs: map[string]chainwriter.ProgramConfig{ - ccipconsts.ContractNameOffRamp: { - IDL: ccipOfframpIDL, - }, - // Requires only the IDL for the CCIPArgsTransform to fetch the TokenAdminRegistry - ccipconsts.ContractNameRouter: { - IDL: ccipRouterIDL, - }, - }, - } - // mock client - rw := clientmocks.NewReaderWriter(t) - mc := *client.NewMultiClient(func(context.Context) (client.ReaderWriter, error) { - return rw, nil - }) - // initialize chain writer - cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), mc, nil, nil, ccipCWConfig) - require.NoError(t, err) destTokenAddr := chainwriter.GetRandomPubKey(t) poolKeys := []solana.PublicKey{destTokenAddr} @@ -61,13 +33,17 @@ func Test_CCIPExecuteArgsTransform(t *testing.T) { accounts := []*solana.AccountMeta{{PublicKey: poolKeys[0]}, {PublicKey: poolKeys[1]}} t.Run("CCIPExecute ArgsTransform includes token indexes", func(t *testing.T) { - pda, _, err := solana.FindProgramAddress([][]byte{[]byte("token_admin_registry"), destTokenAddr.Bytes()}, routerAddress) - require.NoError(t, err) + tableMap := make(map[string]map[string][]*solana.AccountMeta) + tableMap["PoolLookupTable"] = make(map[string][]*solana.AccountMeta) + lookupTablePubkey := chainwriter.GetRandomPubKey(t) + + poolKeysMeta := make([]*solana.AccountMeta, 0, 2) + for _, poolKey := range poolKeys { + poolKeysMeta = append(poolKeysMeta, &solana.AccountMeta{PublicKey: poolKey}) + } + tableMap["PoolLookupTable"][lookupTablePubkey.String()] = poolKeysMeta - lookupTable := mockTokenAdminRegistryLookupTable(t, rw, pda) - mockFetchRouterAddress(t, rw, routerAddress, offrampAddress) - mockFetchLookupTableAddresses(t, rw, lookupTable, poolKeys) - transformedArgs, newAccounts, err := chainwriter.CCIPExecuteArgsTransform(ctx, cw, args, accounts, offrampAddress.String()) + transformedArgs, newAccounts, err := chainwriter.CCIPExecuteArgsTransform(ctx, args, accounts, tableMap) require.NoError(t, err) // Accounts should be unchanged require.Len(t, newAccounts, 2) @@ -76,11 +52,21 @@ func Test_CCIPExecuteArgsTransform(t *testing.T) { require.NotNil(t, typedArgs.TokenIndexes) require.Len(t, typedArgs.TokenIndexes, 1) }) + + t.Run("CCIPExecute ArgsTransform includes empty token indexes if lookup table not found", func(t *testing.T) { + transformedArgs, newAccounts, err := chainwriter.CCIPExecuteArgsTransform(ctx, args, accounts, nil) + require.NoError(t, err) + // Accounts should be unchanged + require.Len(t, newAccounts, 2) + typedArgs, ok := transformedArgs.(chainwriter.ReportPostTransform) + require.True(t, ok) + require.NotNil(t, typedArgs.TokenIndexes) + require.Len(t, typedArgs.TokenIndexes, 0) + }) } func Test_CCIPCommitAccountTransform(t *testing.T) { ctx := tests.Context(t) - offrampAddress := chainwriter.GetRandomPubKey(t) key1 := chainwriter.GetRandomPubKey(t) key2 := chainwriter.GetRandomPubKey(t) t.Run("CCIPCommit ArgsTransform does not affect accounts if token prices exist", func(t *testing.T) { @@ -92,7 +78,7 @@ func Test_CCIPCommitAccountTransform(t *testing.T) { }, } accounts := []*solana.AccountMeta{{PublicKey: key1}, {PublicKey: key2}} - _, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, nil, args, accounts, offrampAddress.String()) + _, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, args, accounts, nil) require.NoError(t, err) require.Len(t, newAccounts, 2) }) @@ -103,7 +89,7 @@ func Test_CCIPCommitAccountTransform(t *testing.T) { Info: ccipocr3.CommitReportInfo{}, } accounts := []*solana.AccountMeta{{PublicKey: key1}, {PublicKey: key2}} - _, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, nil, args, accounts, offrampAddress.String()) + _, newAccounts, err := chainwriter.CCIPCommitAccountTransform(ctx, args, accounts, nil) require.NoError(t, err) require.Len(t, newAccounts, 1) }) diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index 19fba84a0..3bf9b9f4c 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -448,7 +448,7 @@ func (txm *Txm) processConfirmations(ctx context.Context, client client.ReaderWr // if signature has an error, end polling unless blockhash not found and expiration rebroadcast is enabled if status.Err != nil { - txm.handleErrorSignatureStatus(sig, status) + txm.handleErrorSignatureStatus(ctx, sig, status) continue } @@ -490,7 +490,7 @@ func (txm *Txm) handleNotFoundSignatureStatus(sig solanaGo.Signature) { // handleErrorSignatureStatus handles the case where a transaction signature has an error on-chain. // If the error is BlockhashNotFound and expiration rebroadcast is enabled, it skips error handling to allow rebroadcasting. // Otherwise, it marks the transaction as errored. -func (txm *Txm) handleErrorSignatureStatus(sig solanaGo.Signature, status *rpc.SignatureStatusesResult) { +func (txm *Txm) handleErrorSignatureStatus(ctx context.Context, sig solanaGo.Signature, status *rpc.SignatureStatusesResult) { // We want to rebroadcast rather than drop tx if expiration rebroadcast is enabled when blockhash was not found. // converting error to string so we are able to check if it contains the error message. if status.Err != nil && strings.Contains(fmt.Sprintf("%v", status.Err), "BlockhashNotFound") && txm.cfg.TxExpirationRebroadcast() { @@ -499,7 +499,7 @@ func (txm *Txm) handleErrorSignatureStatus(sig solanaGo.Signature, status *rpc.S // Process error to determine the corresponding state and type. // Skip marking as errored if error considered to not be a failure. - if txState, errType := txm.ProcessError(sig, status.Err, false); errType != NoFailure { + if txState, errType := txm.ProcessError(ctx, sig, status.Err, false); errType != NoFailure { id, err := txm.txs.OnError(sig, txm.cfg.TxRetentionTimeout(), txState, errType) if err != nil { txm.lggr.Infow(fmt.Sprintf("failed to mark transaction as %s", txState.String()), "id", id, "signature", sig, "error", err) @@ -668,7 +668,10 @@ func (txm *Txm) simulate() { } // Process error to determine the corresponding state and type. // Certain errors can be considered not to be failures during simulation to allow the process to continue - if txState, errType := txm.ProcessError(msg.signatures[0], res.Err, true); errType != NoFailure { + if txState, errType := txm.ProcessError(ctx, msg.signatures[0], res.Err, true); errType != NoFailure { + if len(res.Logs) > 0 { + txm.lggr.Debugw("simulated transaction error logs", "logs", res.Logs) + } id, err := txm.txs.OnError(msg.signatures[0], txm.cfg.TxRetentionTimeout(), txState, errType) if err != nil { txm.lggr.Errorw(fmt.Sprintf("failed to mark transaction as %s", txState.String()), "id", id, "err", err) @@ -831,7 +834,10 @@ func (txm *Txm) EstimateComputeUnitLimit(ctx context.Context, tx *solanaGo.Trans } // Process error to determine the corresponding state and type. // Certain errors can be considered not to be failures during simulation to allow the process to continue - if txState, errType := txm.ProcessError(sig, res.Err, true); errType != NoFailure { + if txState, errType := txm.ProcessError(ctx, sig, res.Err, true); errType != NoFailure { + if len(res.Logs) > 0 { + txm.lggr.Debugw("simulated transaction error logs", "logs", res.Logs) + } err := txm.txs.OnPrebroadcastError(id, txm.cfg.TxRetentionTimeout(), txState, errType) if err != nil { return 0, fmt.Errorf("failed to process error %v for tx ID %s: %w", res.Err, id, err) @@ -875,7 +881,7 @@ func (txm *Txm) simulateTx(ctx context.Context, tx *solanaGo.Transaction) (res * } // ProcessError parses and handles relevant errors found in simulation results -func (txm *Txm) ProcessError(sig solanaGo.Signature, resErr interface{}, simulation bool) (txState txmutils.TxState, errType TxErrType) { +func (txm *Txm) ProcessError(ctx context.Context, sig solanaGo.Signature, resErr interface{}, simulation bool) (txState txmutils.TxState, errType TxErrType) { if resErr != nil { // handle various errors // https://github.com/solana-labs/solana/blob/master/sdk/src/transaction/error.rs @@ -914,6 +920,9 @@ func (txm *Txm) ProcessError(sig solanaGo.Signature, resErr interface{}, simulat // transaction will encounter execution error/revert case strings.Contains(errStr, "InstructionError"): txm.lggr.Debugw("InstructionError", logValues...) + if !simulation { + txm.fetchTransactionLogs(ctx, sig) + } return txmutils.FatallyErrored, errType // transaction contains an invalid account reference case strings.Contains(errStr, "InvalidAccountIndex"): @@ -989,6 +998,24 @@ func (txm *Txm) rebroadcastWithGivenBlockhash(ctx context.Context, pTx pendingTx return newSig, nil } +// fetchTransactionLogs will fetch the logs for a transaction for better debugging +// Do not fail or return error to avoid affecting normal processes just for better debug logs +func (txm *Txm) fetchTransactionLogs(ctx context.Context, sig solana.Signature) { + client, err := txm.client.Get(ctx) + if err != nil { + txm.lggr.Errorw("failed to get client", "error", err) + return + } + tx, err := client.GetTransaction(ctx, sig) + if err != nil { + txm.lggr.Debugw("failed to fetch transaction for its logs", "sig", sig) + return + } + if tx.Meta != nil && len(tx.Meta.LogMessages) > 0 { + txm.lggr.Debugw("failed transaction logs", "logs", tx.Meta.LogMessages) + } +} + // Close close service func (txm *Txm) Close() error { return txm.StopOnce("Txm", func() error { diff --git a/pkg/solana/txm/txm_internal_test.go b/pkg/solana/txm/txm_internal_test.go index d0229a63e..15fc7b1fd 100644 --- a/pkg/solana/txm/txm_internal_test.go +++ b/pkg/solana/txm/txm_internal_test.go @@ -939,6 +939,11 @@ func TestTxm_disabled_confirm_timeout_with_retention(t *testing.T) { mc.On("SimulateTx", mock.Anything, signed(0, true, computeUnitLimitDefault), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{}, nil).Once() + mc.On("GetTransaction", mock.Anything, mock.Anything).Return(&rpc.GetTransactionResult{ + Meta: &rpc.TransactionMeta{ + LogMessages: []string{"tx error log"}, + }, + }, nil).Once() statuses[sig] = func() (out *rpc.SignatureStatusesResult) { defer wg.Done() return &rpc.SignatureStatusesResult{Err: errors.New("InstructionError")} diff --git a/pkg/solana/txm/txm_unit_test.go b/pkg/solana/txm/txm_unit_test.go index 6ed62233a..e4766caa5 100644 --- a/pkg/solana/txm/txm_unit_test.go +++ b/pkg/solana/txm/txm_unit_test.go @@ -155,6 +155,7 @@ func TestTxm_EstimateComputeUnitLimit(t *testing.T) { func TestTxm_ProcessError(t *testing.T) { t.Parallel() + ctx := tests.Context(t) // setup mock keystore mkey := keyMocks.NewSimpleKeystore(t) @@ -173,12 +174,12 @@ func TestTxm_ProcessError(t *testing.T) { }, } // returns no failure if BlockhashNotFound encountered during simulation - txState, errType := txm.ProcessError(solana.Signature{}, err, true) + txState, errType := txm.ProcessError(ctx, solana.Signature{}, err, true) require.Equal(t, solanatxm.NoFailure, errType) require.Equal(t, txmutils.NotFound, txState) // default enum value // returns error if BlockhashNotFound encountered during normal processing - txState, errType = txm.ProcessError(solana.Signature{}, err, false) + txState, errType = txm.ProcessError(ctx, solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) require.Equal(t, txmutils.Errored, txState) // default enum value }) @@ -190,18 +191,23 @@ func TestTxm_ProcessError(t *testing.T) { }, } // returns no failure if AlreadyProcessed encountered during simulation - txState, errType := txm.ProcessError(solana.Signature{}, err, true) + txState, errType := txm.ProcessError(ctx, solana.Signature{}, err, true) require.Equal(t, solanatxm.NoFailure, errType) require.Equal(t, txmutils.NotFound, txState) // default enum value // returns error if AlreadyProcessed encountered during normal processing - txState, errType = txm.ProcessError(solana.Signature{}, err, false) + txState, errType = txm.ProcessError(ctx, solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) require.Equal(t, txmutils.Errored, txState) // default enum value }) t.Run("process fatal error cases", func(t *testing.T) { t.Parallel() fatalErrorCases := []string{"InstructionError", "InvalidAccountIndex", "InvalidWritableAccount", "AddressLookupTableNotFound", "InvalidAddressLookupTableData", "InvalidAddressLookupTableIndex", "AccountNotFound", "ProgramAccountNotFound"} + client.On("GetTransaction", mock.Anything, mock.Anything).Return(&rpc.GetTransactionResult{ + Meta: &rpc.TransactionMeta{ + LogMessages: []string{"tx error log"}, + }, + }, nil).Once() for _, errCase := range fatalErrorCases { t.Run(fmt.Sprintf("process %s error", errCase), func(t *testing.T) { t.Parallel() @@ -211,12 +217,12 @@ func TestTxm_ProcessError(t *testing.T) { }, } // returns fatal error if InstructionError encountered during simulation - txState, errType := txm.ProcessError(solana.Signature{}, err, true) + txState, errType := txm.ProcessError(ctx, solana.Signature{}, err, true) require.Equal(t, solanatxm.TxFailSimRevert, errType) require.Equal(t, txmutils.FatallyErrored, txState) // default enum value // returns fatal error if InstructionError encountered during normal processing - txState, errType = txm.ProcessError(solana.Signature{}, err, false) + txState, errType = txm.ProcessError(ctx, solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) require.Equal(t, txmutils.FatallyErrored, txState) // default enum value }) @@ -230,12 +236,12 @@ func TestTxm_ProcessError(t *testing.T) { }, } // returns fatal error if InstructionError encountered during simulation - txState, errType := txm.ProcessError(solana.Signature{}, err, true) + txState, errType := txm.ProcessError(ctx, solana.Signature{}, err, true) require.Equal(t, solanatxm.TxFailSimOther, errType) require.Equal(t, txmutils.Errored, txState) // default enum value // returns fatal error if InstructionError encountered during normal processing - txState, errType = txm.ProcessError(solana.Signature{}, err, false) + txState, errType = txm.ProcessError(ctx, solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) require.Equal(t, txmutils.Errored, txState) // default enum value })