From 70ce21e9ae2278ac679fc86c5302ef6695d8f63b Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 10 Feb 2025 10:14:07 -0500 Subject: [PATCH] Added optional field for lookups --- .../relayinterface/lookups_test.go | 41 +- pkg/solana/chainwriter/ccip_example_config.go | 405 ------------------ pkg/solana/chainwriter/chain_writer.go | 23 +- pkg/solana/chainwriter/chain_writer_test.go | 169 +++++++- pkg/solana/chainwriter/lookups.go | 31 +- pkg/solana/chainwriter/transform_registry.go | 18 +- 6 files changed, 226 insertions(+), 461 deletions(-) delete mode 100644 pkg/solana/chainwriter/ccip_example_config.go diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 9290122f0..c027af488 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -48,7 +48,7 @@ func TestAccountContant(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := constantConfig.Resolve(tests.Context(t), nil, nil, nil, testContractIDL) + result, err := constantConfig.Resolve(tests.Context(t), nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -76,7 +76,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: chainwriter.MetaBool{Value: true}, IsWritable: chainwriter.MetaBool{Value: true}, } - result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil, testContractIDL) + result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -110,7 +110,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: chainwriter.MetaBool{Value: true}, IsWritable: chainwriter.MetaBool{Value: true}, } - result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil, testContractIDL) + result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.NoError(t, err) for i, meta := range result { require.Equal(t, expectedMeta[i], meta) @@ -131,7 +131,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: chainwriter.MetaBool{Value: true}, IsWritable: chainwriter.MetaBool{Value: true}, } - _, err := lookupConfig.Resolve(ctx, testArgs, nil, nil, testContractIDL) + _, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.Error(t, err) }) @@ -161,7 +161,7 @@ func TestAccountLookups(t *testing.T) { }, } - result, err := lookupConfig.Resolve(ctx, args, nil, nil, testContractIDL) + result, err := lookupConfig.Resolve(ctx, args, nil, nil) require.NoError(t, err) for i, meta := range result { @@ -199,7 +199,7 @@ func TestAccountLookups(t *testing.T) { Bitmaps: []uint64{5, 3}, } - _, err := lookupConfig.Resolve(ctx, args, nil, nil, testContractIDL) + _, err := lookupConfig.Resolve(ctx, args, nil, nil) require.Contains(t, err.Error(), "bitmap value is not a single value") }) @@ -226,7 +226,7 @@ func TestAccountLookups(t *testing.T) { }, } - _, err := lookupConfig.Resolve(ctx, args, nil, nil, testContractIDL) + _, err := lookupConfig.Resolve(ctx, args, nil, nil) require.Contains(t, err.Error(), "error reading bitmap from location") }) @@ -253,7 +253,7 @@ func TestAccountLookups(t *testing.T) { }, } - _, err := lookupConfig.Resolve(ctx, args, nil, nil, testContractIDL) + _, err := lookupConfig.Resolve(ctx, args, nil, nil) require.Contains(t, err.Error(), "invalid value format at path") }) } @@ -286,7 +286,7 @@ func TestPDALookups(t *testing.T) { IsWritable: true, } - result, err := pdaLookup.Resolve(ctx, nil, nil, nil, testContractIDL) + result, err := pdaLookup.Resolve(ctx, nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -321,7 +321,7 @@ func TestPDALookups(t *testing.T) { "another_seed": seed2, } - result, err := pdaLookup.Resolve(ctx, args, nil, nil, testContractIDL) + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -341,7 +341,7 @@ func TestPDALookups(t *testing.T) { "test_seed": []byte("data"), } - _, err := pdaLookup.Resolve(ctx, args, nil, nil, testContractIDL) + _, err := pdaLookup.Resolve(ctx, args, nil, nil) require.Error(t, err) require.Contains(t, err.Error(), "key not found") }) @@ -377,7 +377,7 @@ func TestPDALookups(t *testing.T) { "another_seed": seed2, } - result, err := pdaLookup.Resolve(ctx, args, nil, nil, testContractIDL) + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -415,7 +415,7 @@ func TestPDALookups(t *testing.T) { "array_seed": arraySeed, } - result, err := pdaLookup.Resolve(ctx, args, nil, nil, testContractIDL) + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -455,7 +455,7 @@ func TestPDALookups(t *testing.T) { "seed2": arraySeed2, } - result, err := pdaLookup.Resolve(ctx, args, nil, nil, testContractIDL) + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -491,7 +491,7 @@ func TestLookupTables(t *testing.T) { DerivedLookupTables: nil, StaticLookupTables: []solana.PublicKey{table}, } - _, staticTableMap, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig, testContractIDL) + _, staticTableMap, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, resolveErr) require.Equal(t, pubKeys, staticTableMap[table]) }) @@ -512,7 +512,7 @@ func TestLookupTables(t *testing.T) { }, StaticLookupTables: nil, } - derivedTableMap, _, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig, testContractIDL) + derivedTableMap, _, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, resolveErr) addresses, ok := derivedTableMap["DerivedTable"][table.String()] @@ -540,7 +540,7 @@ func TestLookupTables(t *testing.T) { StaticLookupTables: nil, } - _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig, testContractIDL) + _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) require.Error(t, err) require.Contains(t, err.Error(), "error fetching account info for table") // Example error message }) @@ -553,7 +553,7 @@ func TestLookupTables(t *testing.T) { StaticLookupTables: []solana.PublicKey{invalidTable}, } - _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig, testContractIDL) + _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) require.Error(t, err) require.Contains(t, err.Error(), "error fetching account info for table") // Example error message }) @@ -581,7 +581,7 @@ func TestLookupTables(t *testing.T) { }, } - derivedTableMap, _, err := cw.ResolveLookupTables(ctx, testArgs, lookupConfig, testContractIDL) + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, testArgs, lookupConfig) require.NoError(t, err) addresses, ok := derivedTableMap["DerivedTable"][table.String()] @@ -619,6 +619,7 @@ func TestLookupTables(t *testing.T) { InternalField: chainwriter.InternalField{ TypeName: "LookupTableDataAccount", Location: "LookupTable", + IDL: testContractIDL, }, }, }, @@ -626,7 +627,7 @@ func TestLookupTables(t *testing.T) { StaticLookupTables: nil, } - derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupConfig, testContractIDL) + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupConfig) require.NoError(t, err) addresses, ok := derivedTableMap["DerivedTable"][lookupTable.String()] diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go deleted file mode 100644 index cd7a844a6..000000000 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ /dev/null @@ -1,405 +0,0 @@ -package chainwriter - -import ( - "github.com/gagliardetto/solana-go" - "github.com/smartcontractkit/chainlink-common/pkg/codec" -) - -func TestConfig() { - // Fake constant addresses for the purpose of this example. - routerProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6B" - commonAddressesLookupTable := solana.MustPublicKeyFromBase58("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H") - - sysvarInstructionsAddress := solana.SysVarInstructionsPubkey.String() - - fromAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6J" - - // NOTE: This is not the real IDL, since the real one is some 3000+ lines long. In the plugin, the IDL will be imported. - executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` - - executeConfig := MethodConfig{ - FromAddress: fromAddress, - InputModifications: []codec.ModifierConfig{ - &codec.RenameModifierConfig{ - Fields: map[string]string{"ReportContextByteWords": "ReportContext"}, - }, - &codec.RenameModifierConfig{ - Fields: map[string]string{"RawExecutionReport": "Report"}, - }, - }, - ChainSpecificName: "execute", - ArgsTransform: "CCIP", - // LookupTables are on-chain stores of accounts. They can be used in two ways: - // 1. As a way to store a list of accounts that are all associated together (i.e. Token State registry) - // 2. To compress the transactions in a TX and reduce the size of the TX. (The traditional way) - LookupTables: LookupTables{ - // DerivedLookupTables are useful in both the ways described above. - // a. The user can configure any type of look up to get a list of lookupTables to read from. - // b. The ChainWriter reads from this lookup table and store the internal addresses in memory - // c. Later, in the []Accounts the user can specify which accounts to include in the TX with an AccountsFromLookupTable lookup. - // d. Lastly, the lookup table is used to compress the size of the transaction. - DerivedLookupTables: []DerivedLookupTable{ - { - Name: "PoolLookupTable", - // In this case, the user configured the lookup table accounts to use a PDALookup, which - // generates a list of one of more PDA accounts based on the input parameters. Specifically, - // there will be multiple PDA accounts if there are multiple addresses in the message, otherwise, - // there will only be one PDA account to read from. The internal field LookupTable - // of the PDA account corresponds to the pool lookup table(s). - Accounts: PDALookups{ - Name: "TokenAdminRegistry", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - // Seeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. - Seeds: []Seed{ - {Static: []byte("token_admin_registry")}, - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.TokenAmounts.DestTokenAddress"}}, - }, - IsSigner: false, - IsWritable: false, - InternalField: InternalField{ - TypeName: "TokenAdminRegistry", - Location: "LookupTable", - }, - }, - }, - }, - // Static lookup tables are the traditional use case (point 2 above) of Lookup tables. These are lookup - // tables which contain commonly used addresses in all CCIP execute transactions. The ChainWriter reads - // these lookup tables and appends them to the transaction to reduce the size of the transaction. - StaticLookupTables: []solana.PublicKey{ - commonAddressesLookupTable, - }, - }, - // The Accounts field is where the user specifies which accounts to include in the transaction. Each Lookup - // resolves to one or more on-chain addresses. - Accounts: []Lookup{ - // The accounts can be of any of the following types: - // 1. Account constant - // 2. Account Lookup - Based on data from input parameters - // 3. Lookup Table content - Get all the accounts from a lookup table - // 4. PDA Account Lookup - Based on another account and a seed/s - // Nested PDA Account with seeds from: - // -> input parameters - // -> constant - // PDALookups can resolve to multiple addresses if: - // A) The PublicKey lookup resolves to multiple addresses (i.e. multiple token addresses) - // B) The Seeds or ValueSeeds resolve to multiple values - // PDA lookup with constant seed - PDALookups{ - Name: "RouterAccountConfig", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("config")}, - }, - IsSigner: false, - IsWritable: false, - }, - PDALookups{ - Name: "SourceChainState", - // PublicKey is a constant account in this case, not a lookup. - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - // Similar to the TokenAdminRegistry above, the user is looking up PDA accounts based on the dest tokens. - Seeds: []Seed{ - {Static: []byte("source_chain_state")}, - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.Header.DestChainSelector"}}, - }, - IsSigner: false, - IsWritable: false, - }, - // PDA lookup to get the Router Report Accounts. - PDALookups{ - Name: "CommitReport", - // The public key is a constant Router address. - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("commit_report")}, - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.Header.DestChainSelector"}}, - {Dynamic: AccountLookup{ - // The seed is the merkle root of the report, as passed into the input params. - Location: "Info.MerkleRoots.MerkleRoot", - }}, - }, - IsSigner: false, - IsWritable: true, - }, - // Static PDA lookup - PDALookups{ - Name: "ExternalExecutionConfig", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("external_execution_config")}, - }, - IsSigner: false, - IsWritable: false, - }, - // feePayer/authority address - AccountConstant{ - Name: "Authority", - Address: fromAddress, - IsSigner: true, - IsWritable: true, - }, - // Account constant - AccountConstant{ - Name: "SystemProgram", - Address: solana.SystemProgramID.String(), - IsSigner: false, - IsWritable: false, - }, - // Account constant - AccountConstant{ - Name: "SysvarInstructions", - Address: sysvarInstructionsAddress, - IsSigner: false, - IsWritable: false, - }, - // Static PDA lookup - PDALookups{ - Name: "ExternalTokenPoolsSigner", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("external_token_pools_signer")}, - }, - IsSigner: false, - IsWritable: false, - }, - // User specified accounts - formatted as AccountMeta - AccountLookup{ - Name: "UserAccounts", - Location: "Info.AbstractReports.Message.ExtraArgsDecoded.Accounts", - IsWritable: MetaBool{BitmapLocation: "Info.AbstractReports.Message.ExtraArgsDecoded.IsWritableBitmap"}, - IsSigner: MetaBool{Value: false}, - }, - // PDA Account Lookup - Based on an account lookup and an address lookup - PDALookups{ - Name: "ReceiverAssociatedTokenAccount", - PublicKey: AccountConstant{ - Address: solana.SPLAssociatedTokenAccountProgramID.String(), - }, - Seeds: []Seed{ - // receiver address - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.Receiver"}}, - // token programs - {Dynamic: AccountsFromLookupTable{ - LookupTableName: "PoolLookupTable", - IncludeIndexes: []int{6}, - }}, - // mint - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.TokenAmounts.DestTokenAddress"}}, - }, - IsSigner: false, - IsWritable: false, - }, - // PDA Account Lookup - Based on an account lookup and an address lookup - PDALookups{ - Name: "SenderAssociatedTokenAccount", - PublicKey: AccountConstant{ - Address: solana.SPLAssociatedTokenAccountProgramID.String(), - }, - Seeds: []Seed{ - // sender address - {Static: []byte(fromAddress)}, - // token program - {Dynamic: AccountsFromLookupTable{ - LookupTableName: "PoolLookupTable", - IncludeIndexes: []int{6}, - }}, - // mint - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.TokenAmounts.DestTokenAddress"}}, - }, - IsSigner: false, - IsWritable: false, - }, - PDALookups{ - Name: "PerChainTokenConfig", - // PublicKey is a constant account in this case, not a lookup. - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - // Similar to the TokenAdminRegistry above, the user is looking up PDA accounts based on the dest tokens. - Seeds: []Seed{ - {Static: []byte("ccip_tokenpool_billing")}, - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.Header.DestChainSelector"}}, - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.TokenAmounts.DestTokenAddress"}}, - }, - IsSigner: false, - IsWritable: false, - }, - PDALookups{ - Name: "PoolChainConfig", - // constant public key - PublicKey: AccountsFromLookupTable{ - LookupTableName: "PoolLookupTable", - // PoolProgram - IncludeIndexes: []int{2}, - }, - Seeds: []Seed{ - {Static: []byte("ccip_tokenpool_chainconfig")}, - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.Header.DestChainSelector"}}, - {Dynamic: AccountLookup{Location: "Info.AbstractReports.Messages.TokenAmounts.DestTokenAddress"}}, - }, - IsSigner: false, - IsWritable: false, - }, - // Lookup Table content - Get the accounts from the derived lookup table(s) - AccountsFromLookupTable{ - LookupTableName: "PoolLookupTable", - IncludeIndexes: []int{}, // If left empty, all addresses will be included. Otherwise, only the specified indexes will be included. - }, - }, - // TBD where this will be in the report - // This will be appended to every error message - DebugIDLocation: "AbstractReport.Message.MessageID", - } - - commitConfig := MethodConfig{ - FromAddress: fromAddress, - InputModifications: []codec.ModifierConfig{ - &codec.RenameModifierConfig{ - Fields: map[string]string{"ReportContextByteWords": "ReportContext"}, - }, - &codec.RenameModifierConfig{ - Fields: map[string]string{"RawReport": "Report"}, - }, - }, - ChainSpecificName: "commit", - LookupTables: LookupTables{ - StaticLookupTables: []solana.PublicKey{ - commonAddressesLookupTable, - }, - }, - Accounts: []Lookup{ - // Static PDA lookup - PDALookups{ - Name: "RouterAccountConfig", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("config")}, - }, - IsSigner: false, - IsWritable: false, - }, - PDALookups{ - Name: "SourceChainState", - // PublicKey is a constant account in this case, not a lookup. - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - // Similar to the TokenAdminRegistry above, the user is looking up PDA accounts based on the dest tokens. - Seeds: []Seed{ - {Static: []byte("source_chain_state")}, - {Dynamic: AccountLookup{Location: "Info.MerkleRoots.ChainSel"}}, - }, - IsSigner: false, - IsWritable: true, - }, - // PDA lookup to get the Router Report Accounts. - PDALookups{ - Name: "RouterReportAccount", - // The public key is a constant Router address. - PublicKey: AccountConstant{ - Address: routerProgramAddress, - IsSigner: false, - IsWritable: false, - }, - Seeds: []Seed{ - {Static: []byte("commit_report")}, - {Dynamic: AccountLookup{Location: "Info.MerkleRoots.ChainSel"}}, - {Dynamic: AccountLookup{ - // The seed is the merkle root of the report, as passed into the input params. - Location: "Info.MerkleRoots.MerkleRoot", - }}, - }, - IsSigner: false, - IsWritable: false, - }, - // feePayer/authority address - AccountConstant{ - Name: "Authority", - Address: fromAddress, - IsSigner: true, - IsWritable: true, - }, - // Account constant - AccountConstant{ - Name: "SystemProgram", - Address: solana.SystemProgramID.String(), - IsSigner: false, - IsWritable: false, - }, - // Account constant - AccountConstant{ - Name: "SysvarInstructions", - Address: sysvarInstructionsAddress, - IsSigner: false, - IsWritable: false, - }, - // Static PDA lookup - PDALookups{ - Name: "GlobalState", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("state")}, - }, - IsSigner: false, - IsWritable: false, - }, - // PDA lookup - PDALookups{ - Name: "BillingTokenConfig", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("fee_billing_token_config")}, - {Dynamic: AccountLookup{Location: "Info.TokenPrices.TokenID"}}, - }, - IsSigner: false, - IsWritable: false, - }, - // PDA lookup - PDALookups{ - Name: "ChainConfigGasPrice", - PublicKey: AccountConstant{ - Address: routerProgramAddress, - }, - Seeds: []Seed{ - {Static: []byte("dest_chain_state")}, - {Dynamic: AccountLookup{Location: "Info.MerkleRoots.ChainSel"}}, - }, - IsSigner: false, - IsWritable: false, - }, - }, - DebugIDLocation: "", - } - - chainWriterConfig := ChainWriterConfig{ - Programs: map[string]ProgramConfig{ - "ccip-router": { - Methods: map[string]MethodConfig{ - "execute": executeConfig, - "commit": commitConfig, - }, - IDL: executionReportSingleChainIDL, - }, - }, - } - _ = chainWriterConfig -} diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 8109491a8..2d6c8d8de 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -146,10 +146,14 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program ### Error Handling: - Errors are wrapped with the `debugID` for easier tracing. */ -func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader, idl string) ([]*solana.AccountMeta, error) { +func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) { var addresses []*solana.AccountMeta for _, accountConfig := range accounts { - meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, reader, idl) + meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, reader) + if accountConfig.IsOptional() && err != nil { + // skip optional accounts if they are not found + continue + } if err != nil { return nil, err } @@ -260,13 +264,13 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Fetch derived and static table maps - derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables, programConfig.IDL) + derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables) if err != nil { return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } // Resolve account metas - accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, s.reader, programConfig.IDL) + accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, s.reader) if err != nil { return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) } @@ -350,7 +354,7 @@ func (s *SolanaChainWriterService) GetFeeComponents(ctx context.Context) (*types }, nil } -func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args any, lookupTables LookupTables, idl string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { +func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args any, lookupTables LookupTables) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) @@ -358,7 +362,10 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args for _, derivedLookup := range lookupTables.DerivedLookupTables { // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resolves to more // than one address - lookupTableMap, err := s.loadTable(ctx, args, derivedLookup, idl) + lookupTableMap, err := s.loadTable(ctx, args, derivedLookup) + if derivedLookup.Optional && err != nil { + continue + } if err != nil { return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) } @@ -386,9 +393,9 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args return derivedTableMap, staticTableMap, nil } -func (s *SolanaChainWriterService) loadTable(ctx context.Context, args any, rlt DerivedLookupTable, idl string) (map[string]map[string][]*solana.AccountMeta, error) { +func (s *SolanaChainWriterService) loadTable(ctx context.Context, args any, rlt DerivedLookupTable) (map[string]map[string][]*solana.AccountMeta, error) { // Resolve all addresses specified by the identifier - lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, s.reader, idl) + lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, s.reader) if err != nil { return nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 2141b3001..fb3edc472 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -109,6 +109,7 @@ func TestChainWriter_GetAddresses(t *testing.T) { InternalField: chainwriter.InternalField{ TypeName: "LookupTableDataAccount", Location: "LookupTable", + IDL: testContractIDL, }, }, }, @@ -160,11 +161,11 @@ func TestChainWriter_GetAddresses(t *testing.T) { } // Fetch derived table map - derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig, testContractIDL) + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) require.NoError(t, err) // Resolve account metas - accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw, testContractIDL) + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) require.NoError(t, err) // account metas should be returned in the same order as the provided account lookup configs @@ -204,11 +205,11 @@ func TestChainWriter_GetAddresses(t *testing.T) { } // Fetch derived table map - derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig, testContractIDL) + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) require.NoError(t, err) // Resolve account metas - accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw, testContractIDL) + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) require.NoError(t, err) require.Len(t, accounts, 2) @@ -228,11 +229,11 @@ func TestChainWriter_GetAddresses(t *testing.T) { } // Fetch derived table map - derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig, testContractIDL) + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) require.NoError(t, err) // Resolve account metas - accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw, testContractIDL) + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) require.NoError(t, err) require.Len(t, accounts, 3) @@ -240,6 +241,147 @@ func TestChainWriter_GetAddresses(t *testing.T) { require.Equal(t, storedPubkey, accounts[i].PublicKey) } }) + + t.Run("optional lookups", func(t *testing.T) { + const invalidLocation = "Invalid.Path" + + t.Run("AccountLookup error is skipped when Lookup is optional", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountLookup{ + Name: "OptionalAccountLookup", + Location: invalidLocation, + IsSigner: chainwriter.MetaBool{Value: false}, + IsWritable: chainwriter.MetaBool{Value: false}, + LookupOpts: chainwriter.LookupOpts{Optional: true}, + }, + } + + args := Arguments{} + + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, nil, rw) + require.NoError(t, err) + require.Empty(t, accounts) + }) + + t.Run("AccountLookup error is returned when Lookup is required", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountLookup{ + Name: "NonOptionalAccountLookup", + Location: invalidLocation, + IsSigner: chainwriter.MetaBool{Value: false}, + IsWritable: chainwriter.MetaBool{Value: false}, + LookupOpts: chainwriter.LookupOpts{Optional: false}, + }, + } + + args := Arguments{} + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, nil, rw) + require.Error(t, err) + require.Nil(t, accounts) + }) + + t.Run("PDALookups error is skipped when Lookup is optional", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.PDALookups{ + Name: "OptionalPDA", + PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: solana.SystemProgramID.String()}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Location: invalidLocation}}, + }, + LookupOpts: chainwriter.LookupOpts{Optional: true}, + }, + } + + args := Arguments{} + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, nil, rw) + require.NoError(t, err) + require.Empty(t, accounts) + }) + + t.Run("PDALookups error is returned when Lookup is required", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.PDALookups{ + Name: "NonOptionalPDA", + PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: solana.SystemProgramID.String()}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Location: invalidLocation}}, + }, + LookupOpts: chainwriter.LookupOpts{Optional: false}, + }, + } + + args := Arguments{} + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, nil, rw) + require.Error(t, err) + require.Nil(t, accounts) + }) + + t.Run("DerivedLookupTable error is skipped when Lookup is optional", func(t *testing.T) { + lookupTables := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "OptionalDerivedTable", + Optional: true, + Accounts: chainwriter.AccountLookup{ + Location: invalidLocation, + }, + }, + }, + } + + args := Arguments{} + derivedMap, staticMap, err := cw.ResolveLookupTables(ctx, args, lookupTables) + require.NoError(t, err) + require.Empty(t, derivedMap) + require.Empty(t, staticMap) + }) + + t.Run("DerivedLookupTable error is returned when Lookup is required", func(t *testing.T) { + lookupTables := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "NonOptionalDerivedTable", + Accounts: chainwriter.AccountLookup{ + Location: invalidLocation, + }, + Optional: false, + }, + }, + } + + args := Arguments{} + _, _, err := cw.ResolveLookupTables(ctx, args, lookupTables) + require.Error(t, err) + }) + + t.Run("AccountsFromLookupTable error is skipped when Lookup is optional", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "NonExistent", + LookupOpts: chainwriter.LookupOpts{Optional: true}, + }, + } + + args := Arguments{} + + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, nil, rw) + require.NoError(t, err) + require.Empty(t, accounts) + }) + + t.Run("AccountsFromLookupTable error is returned when Lookup is required", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "NonExistent", + LookupOpts: chainwriter.LookupOpts{Optional: false}, + }, + } + + args := Arguments{} + _, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, nil, rw) + require.Error(t, err) + }) + }) } func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { @@ -296,6 +438,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { InternalField: chainwriter.InternalField{ TypeName: "LookupTableDataAccount", Location: "LookupTable", + IDL: testContractIDL, }, }, }, @@ -313,6 +456,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { InternalField: chainwriter.InternalField{ TypeName: "LookupTableDataAccount", Location: "LookupTable", + IDL: testContractIDL, }, }, }, @@ -334,11 +478,11 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { } // Fetch derived table map - derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig, testContractIDL) + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) require.NoError(t, err) // Resolve account metas - accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw, testContractIDL) + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) require.NoError(t, err) // Filter the lookup table addresses based on which accounts are actually used @@ -356,11 +500,11 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { accountLookupConfig := []chainwriter.Lookup{} // Fetch derived table map - derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig, testContractIDL) + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) require.NoError(t, err) // Resolve account metas - accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw, testContractIDL) + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) require.NoError(t, err) // Filter the lookup table addresses based on which accounts are actually used @@ -379,11 +523,11 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { } // Fetch derived table map - derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig, testContractIDL) + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) require.NoError(t, err) // Resolve account metas - accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw, testContractIDL) + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) require.NoError(t, err) // Filter the lookup table addresses based on which accounts are actually used @@ -452,6 +596,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { InternalField: chainwriter.InternalField{ TypeName: "LookupTableDataAccount", Location: "LookupTable", + IDL: testContractIDL, }, }, }, diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 8d98845b6..dc3abd359 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -15,7 +15,16 @@ import ( // Lookup is an interface that defines a method to resolve an address (or multiple addresses) from a given definition. type Lookup interface { - Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader, idl string) ([]*solana.AccountMeta, error) + Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) + IsOptional() bool +} + +type LookupOpts struct { + Optional bool +} + +func (cl LookupOpts) IsOptional() bool { + return cl.Optional } // AccountConstant represents a fixed address, provided in Base58 format, converted into a `solana.PublicKey`. @@ -24,6 +33,7 @@ type AccountConstant struct { Address string IsSigner bool IsWritable bool + LookupOpts } // AccountLookup dynamically derives an account address from args using a specified location path. @@ -33,6 +43,7 @@ type AccountLookup struct { // IsSigner and IsWritable can either be a constant bool or a location to a bitmap which decides the bools IsSigner MetaBool IsWritable MetaBool + LookupOpts } type MetaBool struct { @@ -57,12 +68,14 @@ type PDALookups struct { IsWritable bool // OPTIONAL: On-chain location and type of desired data from PDA (e.g. a sub-account of the data account) InternalField InternalField + LookupOpts } type InternalField struct { // must map directly to IDL type TypeName string Location string + IDL string } // LookupTables represents a list of lookup tables that are used to derive addresses for a program. @@ -75,15 +88,17 @@ type LookupTables struct { type DerivedLookupTable struct { Name string Accounts Lookup + Optional bool } // AccountsFromLookupTable extracts accounts from a lookup table that was previously read and stored in memory. type AccountsFromLookupTable struct { LookupTableName string IncludeIndexes []int + LookupOpts } -func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader, _ string) ([]*solana.AccountMeta, error) { +func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { address, err := solana.PublicKeyFromBase58(ac.Address) if err != nil { return nil, fmt.Errorf("error getting account from constant: %w", err) @@ -97,7 +112,7 @@ func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[str }, nil } -func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader, _ string) ([]*solana.AccountMeta, error) { +func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { derivedValues, err := GetValuesAtLocation(args, al.Location) if err != nil { return nil, fmt.Errorf("error getting account from lookup: %w", err) @@ -156,7 +171,7 @@ func resolveBitMap(mb MetaBool, args any, length int) ([]bool, error) { return result, nil } -func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, _ client.Reader, _ string) ([]*solana.AccountMeta, error) { +func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { // Fetch the inner map for the specified lookup table name innerMap, ok := derivedTableMap[alt.LookupTableName] if !ok { @@ -186,8 +201,8 @@ func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTabl return result, nil } -func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader, idl string) ([]*solana.AccountMeta, error) { - publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, reader, idl) +func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) { + publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, reader) if err != nil { return nil, fmt.Errorf("error getting public key for PDALookups: %w", err) } @@ -219,7 +234,7 @@ func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map } var idlCodec codec.IDL - if err = json.Unmarshal([]byte(idl), &idlCodec); err != nil { + if err = json.Unmarshal([]byte(pda.InternalField.IDL), &idlCodec); err != nil { return nil, fmt.Errorf("failed to unmarshal IDL for PDA: %s, error: %w", pda.Name, err) } @@ -297,7 +312,7 @@ func getSeedBytesCombinations( } } else { // Get address seeds from the lookup - seedAddresses, err := GetAddresses(ctx, args, []Lookup{dynamicSeed}, derivedTableMap, reader, "") + seedAddresses, err := GetAddresses(ctx, args, []Lookup{dynamicSeed}, derivedTableMap, reader) if err != nil { return nil, fmt.Errorf("error getting address seed: %w", err) } diff --git a/pkg/solana/chainwriter/transform_registry.go b/pkg/solana/chainwriter/transform_registry.go index dd4e9b04c..1cd8008bd 100644 --- a/pkg/solana/chainwriter/transform_registry.go +++ b/pkg/solana/chainwriter/transform_registry.go @@ -55,9 +55,10 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a InternalField: InternalField{ TypeName: "ReferenceAddresses", Location: "Router", + IDL: offrampProgramConfig.IDL, }, } - accountMetas, err := routerAddrLookup.Resolve(ctx, nil, nil, cw.reader, offrampProgramConfig.IDL) + accountMetas, err := routerAddrLookup.Resolve(ctx, nil, nil, cw.reader) if err != nil { return nil, fmt.Errorf("failed to fetch the router program address from the reference addresses account: %w", err) } @@ -65,6 +66,12 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a return nil, fmt.Errorf("expect 1 address to be returned for router address, received %d: %w", len(accountMetas), err) } + // Fetch router config to use to fetch TokenAdminRegistry + routerProgramConfig, ok := cw.config.Programs["ccip-router"] + if !ok { + return nil, fmt.Errorf("ccip-router program not found in config") + } + routerAddress := accountMetas[0].PublicKey TokenPoolLookupTable := LookupTables{ DerivedLookupTables: []DerivedLookupTable{ @@ -84,19 +91,14 @@ func CCIPArgsTransform(ctx context.Context, cw *SolanaChainWriterService, args a InternalField: InternalField{ TypeName: "TokenAdminRegistry", Location: "LookupTable", + IDL: routerProgramConfig.IDL, }, }, }, }, } - // Fetch router config to use to fetch TokenAdminRegistry - routerProgramConfig, ok := cw.config.Programs["ccip-router"] - if !ok { - return nil, fmt.Errorf("ccip-router program not found in config") - } - - tableMap, _, err := cw.ResolveLookupTables(ctx, args, TokenPoolLookupTable, routerProgramConfig.IDL) + tableMap, _, err := cw.ResolveLookupTables(ctx, args, TokenPoolLookupTable) if err != nil { return nil, err }