From 1beeeb5a97f99a61393f9a719543e8fc788a95a2 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 29 Jan 2025 17:24:55 -0500 Subject: [PATCH] Enabled automatic ATA creation in CW --- integration-tests/go.mod | 2 +- .../relayinterface/lookups_test.go | 177 ++++++++++++++ integration-tests/utils/utils.go | 18 ++ pkg/solana/chainwriter/chain_writer.go | 88 ++++++- pkg/solana/chainwriter/chain_writer_test.go | 215 ------------------ pkg/solana/chainwriter/helpers.go | 49 ++-- pkg/solana/chainwriter/lookups.go | 10 + pkg/solana/utils/utils.go | 55 +++-- 8 files changed, 351 insertions(+), 263 deletions(-) diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 6635f35ba..fd902bd98 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -14,6 +14,7 @@ require ( github.com/lib/pq v1.10.9 github.com/pelletier/go-toml/v2 v2.2.3 github.com/rs/zerolog v1.33.0 + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250206215114-fb6c3c35e8e3 github.com/smartcontractkit/chainlink-common v0.4.2-0.20250205141137-8f50d72601bb github.com/smartcontractkit/chainlink-solana v1.1.2-0.20250204221232-93cfb3ea152b github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.21 @@ -343,7 +344,6 @@ require ( github.com/smartcontractkit/chain-selectors v1.0.37 // indirect github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203132120-f0d42463e405 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250206215114-fb6c3c35e8e3 // indirect github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc // indirect github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5 // indirect github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index c027af488..d35387eac 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -8,6 +8,7 @@ import ( "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" @@ -483,6 +484,7 @@ func TestLookupTables(t *testing.T) { txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) cw, err := chainwriter.NewSolanaChainWriterService(nil, solanaClient, txm, nil, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) t.Run("StaticLookup table resolves properly", func(t *testing.T) { pubKeys := chainwriter.CreateTestPubKeys(t, 8) @@ -637,3 +639,178 @@ func TestLookupTables(t *testing.T) { } }) } + +func TestCreateATAs(t *testing.T) { + ctx := tests.Context(t) + + sender, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + feePayer := sender.PublicKey() + + url, _ := utils.SetupTestValidatorWithAnchorPrograms(t, sender.PublicKey().String(), []string{"contract-reader-interface"}) + rpcClient := rpc.New(url) + + utils.FundAccounts(t, []solana.PrivateKey{sender}, rpcClient) + + cfg := config.NewDefault() + solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) + require.NoError(t, err) + + t.Run("returns no instructions when no ATA location is found", func(t *testing.T) { + lookups := []chainwriter.ATALookup{ + { + Location: "Invalid.Address", + WalletAddress: chainwriter.AccountConstant{ + Address: feePayer.String(), + }, + TokenProgram: chainwriter.AccountConstant{ + Address: solana.Token2022ProgramID.String(), + }, + MintAddress: chainwriter.AccountLookup{ + Location: "Invalid.Address", + }, + }, + } + + args := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{ + {Address: chainwriter.GetRandomPubKey(t).Bytes()}, + }, + } + + ataInstructions, err := chainwriter.CreateATAs(ctx, args, lookups, nil, solanaClient, testContractIDL, feePayer) + require.NoError(t, err) + require.Empty(t, ataInstructions) + }) + + t.Run("fails with multiple wallet addresses", func(t *testing.T) { + lookups := []chainwriter.ATALookup{ + { + Location: "", + WalletAddress: chainwriter.AccountLookup{ + Location: "Addresses", + }, + TokenProgram: chainwriter.AccountConstant{ + Address: solana.Token2022ProgramID.String(), + }, + MintAddress: chainwriter.AccountConstant{ + Address: chainwriter.GetRandomPubKey(t).String(), + }, + }, + } + + args := map[string][]solana.PublicKey{ + "Addresses": {chainwriter.GetRandomPubKey(t), chainwriter.GetRandomPubKey(t)}, + } + + _, err := chainwriter.CreateATAs(ctx, args, lookups, nil, solanaClient, testContractIDL, feePayer) + require.Contains(t, err.Error(), "expected exactly one wallet address, got 2") + }) + + t.Run("fails with mismatched mint and token programs", func(t *testing.T) { + lookups := []chainwriter.ATALookup{ + { + Location: "", + WalletAddress: chainwriter.AccountConstant{ + Address: feePayer.String(), + }, + TokenProgram: chainwriter.AccountConstant{ + Address: solana.Token2022ProgramID.String(), + }, + MintAddress: chainwriter.AccountLookup{ + Location: "Addresses", + }, + }, + } + + args := map[string][]solana.PublicKey{ + "Addresses": {chainwriter.GetRandomPubKey(t), chainwriter.GetRandomPubKey(t)}, + } + + _, err := chainwriter.CreateATAs(ctx, args, lookups, nil, solanaClient, testContractIDL, feePayer) + require.Contains(t, err.Error(), "expected equal number of token programs and mints, got 1 tokenPrograms and 2 mints") + }) + + t.Run("fails when mint is not a token address", func(t *testing.T) { + tokenProgram := solana.Token2022ProgramID + mint := chainwriter.GetRandomPubKey(t) + + ataAddress, _, err := tokens.FindAssociatedTokenAddress(tokenProgram, mint, feePayer) + require.NoError(t, err) + require.False(t, checkIfATAExists(t, rpcClient, ataAddress)) + lookups := []chainwriter.ATALookup{ + { + Location: "Inner.Address", + WalletAddress: chainwriter.AccountConstant{ + Address: feePayer.String(), + }, + TokenProgram: chainwriter.AccountConstant{ + Address: tokenProgram.String(), + }, + MintAddress: chainwriter.AccountLookup{ + Location: "Inner.Address", + }, + }, + } + + args := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{ + {Address: mint.Bytes()}, + }, + } + + ataInstructions, err := chainwriter.CreateATAs(ctx, args, lookups, nil, solanaClient, testContractIDL, feePayer) + require.NoError(t, err) + + tx := solanautils.CreateTx(ctx, t, rpcClient, ataInstructions, sender, rpc.CommitmentFinalized) + + _, err = rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{SkipPreflight: false, PreflightCommitment: rpc.CommitmentProcessed}) + require.Contains(t, err.Error(), "Program log: Error: Invalid Mint") + }) + + t.Run("successfully creates ATAs only when necessary", func(t *testing.T) { + tokenProgram := solana.Token2022ProgramID + mint := utils.CreateRandomToken(t, sender, solana.Token2022ProgramID, rpcClient) + + ataAddress, _, err := tokens.FindAssociatedTokenAddress(tokenProgram, mint, feePayer) + require.NoError(t, err) + require.False(t, checkIfATAExists(t, rpcClient, ataAddress)) + lookups := []chainwriter.ATALookup{ + { + Location: "Inner.Address", + WalletAddress: chainwriter.AccountConstant{ + Address: feePayer.String(), + }, + TokenProgram: chainwriter.AccountConstant{ + Address: tokenProgram.String(), + }, + MintAddress: chainwriter.AccountLookup{ + Location: "Inner.Address", + }, + }, + } + + args := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{ + {Address: mint.Bytes()}, + }, + } + + ataInstructions, err := chainwriter.CreateATAs(ctx, args, lookups, nil, solanaClient, testContractIDL, feePayer) + require.NoError(t, err) + + solanautils.SendAndConfirm(ctx, t, rpcClient, ataInstructions, sender, rpc.CommitmentFinalized) + require.True(t, checkIfATAExists(t, rpcClient, ataAddress)) + + // now, if we try to create the same ATA again, it should return no instructions + ataInstructions, err = chainwriter.CreateATAs(ctx, args, lookups, nil, solanaClient, testContractIDL, feePayer) + require.NoError(t, err) + require.Empty(t, ataInstructions) + }) +} + +func checkIfATAExists(t *testing.T, rpcClient *rpc.Client, ataAddress solana.PublicKey) bool { + _, err := rpcClient.GetAccountInfo(tests.Context(t), ataAddress) + return err == nil +} diff --git a/integration-tests/utils/utils.go b/integration-tests/utils/utils.go index c59458536..e54bb8398 100644 --- a/integration-tests/utils/utils.go +++ b/integration-tests/utils/utils.go @@ -15,6 +15,7 @@ import ( "github.com/pelletier/go-toml/v2" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" @@ -165,3 +166,20 @@ func CreateTestLookupTable(ctx context.Context, t *testing.T, c *rpc.Client, sen return table } + +func CreateRandomToken(t *testing.T, admin solana.PrivateKey, tokenProgram solana.PublicKey, client *rpc.Client) solana.PublicKey { + ctx := tests.Context(t) + mint, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + instructions, err := tokens.CreateToken(ctx, tokenProgram, mint.PublicKey(), admin.PublicKey(), uint8(0), client, rpc.CommitmentFinalized) + require.NoError(t, err) + + addMintModifier := func(tx *solana.Transaction, signers map[solana.PublicKey]solana.PrivateKey) error { + signers[mint.PublicKey()] = mint + return nil + } + + utils.SendAndConfirm(ctx, t, client, instructions, admin, rpc.CommitmentFinalized, addMintModifier) + return mint.PublicKey() +} diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 2d6c8d8de..6c0c236b6 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -3,13 +3,16 @@ package chainwriter import ( "context" "encoding/json" + "errors" "fmt" "math/big" + "strings" "github.com/gagliardetto/solana-go" addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" @@ -56,6 +59,7 @@ type MethodConfig struct { FromAddress string InputModifications commoncodec.ModifiersConfig ChainSpecificName string + ATAs []ATALookup LookupTables LookupTables Accounts []Lookup // Location in the args where the debug ID is stored @@ -220,6 +224,76 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( return filteredLookupTables } +// CreateATAs first checks if a specified location exists, then checks if the accounts derived from the +// ATALookups in the ChainWriter's configuration exist on-chain and creates them if they do not. +func CreateATAs(ctx context.Context, args any, lookups []ATALookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader, idl string, feePayer solana.PublicKey) ([]solana.Instruction, error) { + createATAInstructions := []solana.Instruction{} + for _, lookup := range lookups { + // Check if location exists + if lookup.Location != "" { + // TODO refactor GetValuesAtLocation to not return an error if the field doesn't exist + _, err := GetValuesAtLocation(args, lookup.Location) + if err != nil { + // field doesn't exist, so ignore ATA creation + if errors.Is(err, errFieldNotFound) { + continue + } + return nil, fmt.Errorf("error getting values at location: %w", err) + } + } + walletAddresses, err := GetAddresses(ctx, args, []Lookup{lookup.WalletAddress}, derivedTableMap, reader, idl) + if err != nil { + return nil, fmt.Errorf("error resolving wallet address: %w", err) + } + if len(walletAddresses) != 1 { + return nil, fmt.Errorf("expected exactly one wallet address, got %d", len(walletAddresses)) + } + wallet := walletAddresses[0].PublicKey + + tokenPrograms, err := GetAddresses(ctx, args, []Lookup{lookup.TokenProgram}, derivedTableMap, reader, idl) + if err != nil { + return nil, fmt.Errorf("error resolving token program address: %w", err) + } + + mints, err := GetAddresses(ctx, args, []Lookup{lookup.MintAddress}, derivedTableMap, reader, idl) + if err != nil { + return nil, fmt.Errorf("error resolving mint address: %w", err) + } + if len(tokenPrograms) != len(mints) { + return nil, fmt.Errorf("expected equal number of token programs and mints, got %d tokenPrograms and %d mints", len(tokenPrograms), len(mints)) + } + + for i := range tokenPrograms { + tokenProgram := tokenPrograms[i].PublicKey + mint := mints[i].PublicKey + + ataAddress, _, err := tokens.FindAssociatedTokenAddress(tokenProgram, mint, wallet) + if err != nil { + return nil, fmt.Errorf("error deriving ATA: %w", err) + } + + _, err = reader.GetAccountInfoWithOpts(ctx, ataAddress, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentFinalized, + }) + if err == nil { + continue + } + if !strings.Contains(err.Error(), "not found") { + return nil, fmt.Errorf("error reading account info for ATA: %w", err) + } + + ins, _, err := tokens.CreateAssociatedTokenAccount(tokenProgram, mint, wallet, feePayer) + if err != nil { + return nil, fmt.Errorf("error creating associated token account: %w", err) + } + createATAInstructions = append(createATAInstructions, ins) + } + } + + return createATAInstructions, nil +} + // SubmitTransaction builds, encodes, and enqueues a transaction using the provided program // configuration and method details. It relies on the configured IDL, account lookups, and // lookup tables to gather the necessary accounts and data. The function retrieves the latest @@ -280,6 +354,11 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) } + createATAinstructions, err := CreateATAs(ctx, args, methodConfig.ATAs, derivedTableMap, s.reader, programConfig.IDL, feePayer) + if err != nil { + return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) + } + // Filter the lookup table addresses based on which accounts are actually used filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) @@ -316,10 +395,13 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra discriminator := GetDiscriminator(methodConfig.ChainSpecificName) encodedPayload = append(discriminator[:], encodedPayload...) + // Combine the two sets of instructions into one slice + var instructions []solana.Instruction + instructions = append(instructions, createATAinstructions...) + instructions = append(instructions, solana.NewInstruction(programID, accounts, encodedPayload)) + tx, err := solana.NewTransaction( - []solana.Instruction{ - solana.NewInstruction(programID, accounts, encodedPayload), - }, + instructions, blockhash.Value.Blockhash, solana.TransactionPayer(feePayer), solana.TransactionAddressTables(filteredLookupTableMap), diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 7bafb1be9..fb3edc472 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -731,221 +731,6 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { }) } -func TestChainWriter_CCIPRouter(t *testing.T) { - t.Parallel() - - // setup admin key - adminPk, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - admin := adminPk.PublicKey() - - routerAddr := chainwriter.GetRandomPubKey(t) - destTokenAddr := chainwriter.GetRandomPubKey(t) - - poolKeys := []solana.PublicKey{destTokenAddr} - poolKeys = append(poolKeys, chainwriter.CreateTestPubKeys(t, 3)...) - - // simplified CCIP Config - does not contain full account list - ccipCWConfig := chainwriter.ChainWriterConfig{ - Programs: map[string]chainwriter.ProgramConfig{ - "ccip_router": { - Methods: map[string]chainwriter.MethodConfig{ - "execute": { - FromAddress: admin.String(), - InputModifications: []codec.ModifierConfig{ - &codec.RenameModifierConfig{ - Fields: map[string]string{"ReportContextByteWords": "ReportContext"}, - }, - &codec.RenameModifierConfig{ - Fields: map[string]string{"RawExecutionReport": "Report"}, - }, - }, - ChainSpecificName: "execute", - ArgsTransform: "CCIP", - LookupTables: chainwriter.LookupTables{}, - Accounts: []chainwriter.Lookup{ - chainwriter.AccountConstant{ - Name: "testAcc1", - Address: chainwriter.GetRandomPubKey(t).String(), - }, - chainwriter.AccountConstant{ - Name: "testAcc2", - Address: chainwriter.GetRandomPubKey(t).String(), - }, - chainwriter.AccountConstant{ - Name: "testAcc3", - Address: chainwriter.GetRandomPubKey(t).String(), - }, - chainwriter.AccountConstant{ - Name: "poolAddr1", - Address: poolKeys[0].String(), - }, - chainwriter.AccountConstant{ - Name: "poolAddr2", - Address: poolKeys[1].String(), - }, - chainwriter.AccountConstant{ - Name: "poolAddr3", - Address: poolKeys[2].String(), - }, - chainwriter.AccountConstant{ - Name: "poolAddr4", - Address: poolKeys[3].String(), - }, - }, - }, - "commit": { - FromAddress: admin.String(), - InputModifications: []codec.ModifierConfig{ - &codec.RenameModifierConfig{ - Fields: map[string]string{"ReportContextByteWords": "ReportContext"}, - }, - &codec.RenameModifierConfig{ - Fields: map[string]string{"RawReport": "Report"}, - }, - }, - ChainSpecificName: "commit", - ArgsTransform: "", - LookupTables: chainwriter.LookupTables{}, - Accounts: []chainwriter.Lookup{ - chainwriter.AccountConstant{ - Name: "testAcc1", - Address: chainwriter.GetRandomPubKey(t).String(), - }, - chainwriter.AccountConstant{ - Name: "testAcc2", - Address: chainwriter.GetRandomPubKey(t).String(), - }, - chainwriter.AccountConstant{ - Name: "testAcc3", - Address: chainwriter.GetRandomPubKey(t).String(), - }, - }, - }, - }, - IDL: ccipRouterIDL, - }, - }, - } - - ctx := tests.Context(t) - // mock client - rw := clientmocks.NewReaderWriter(t) - // mock estimator - ge := feemocks.NewEstimator(t) - - t.Run("CCIP execute is encoded successfully and ArgsTransform is applied correctly.", func(t *testing.T) { - // mock txm - txm := txmMocks.NewTxManager(t) - // initialize chain writer - cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, ccipCWConfig) - require.NoError(t, err) - - recentBlockHash := solana.Hash{} - rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() - - pda, _, err := solana.FindProgramAddress([][]byte{[]byte("token_admin_registry"), destTokenAddr.Bytes()}, routerAddr) - require.NoError(t, err) - - lookupTable := mockTokenAdminRegistryLookupTable(t, rw, pda) - - mockFetchLookupTableAddresses(t, rw, lookupTable, poolKeys) - - txID := uuid.NewString() - txm.On("Enqueue", mock.Anything, admin.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { - txData := tx.Message.Instructions[0].Data - payload := txData[8:] - var decoded ccip_router.Execute - dec := ag_binary.NewBorshDecoder(payload) - err = dec.Decode(&decoded) - require.NoError(t, err) - - tokenIndexes := *decoded.TokenIndexes - - require.Len(t, tokenIndexes, 1) - require.Equal(t, uint8(3), tokenIndexes[0]) - return true - }), &txID, mock.Anything).Return(nil).Once() - - // stripped back report just for purposes of example - abstractReport := ccipocr3.ExecutePluginReportSingleChain{ - Messages: []ccipocr3.Message{ - { - TokenAmounts: []ccipocr3.RampTokenAmount{ - { - DestTokenAddress: destTokenAddr.Bytes(), - }, - }, - }, - }, - } - - // Marshal the abstract report to json just for testing purposes. - encodedReport, err := json.Marshal(abstractReport) - require.NoError(t, err) - - args := chainwriter.ReportPreTransform{ - ReportContext: [2][32]byte{{0x01}, {0x02}}, - Report: encodedReport, - Info: ccipocr3.ExecuteReportInfo{ - MerkleRoots: []ccipocr3.MerkleRootChain{}, - AbstractReports: []ccipocr3.ExecutePluginReportSingleChain{abstractReport}, - }, - } - - submitErr := cw.SubmitTransaction(ctx, "ccip_router", "execute", args, txID, routerAddr.String(), nil, nil) - require.NoError(t, submitErr) - }) - - t.Run("CCIP commit is encoded successfully", func(t *testing.T) { - // mock txm - txm := txmMocks.NewTxManager(t) - // initialize chain writer - cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, ccipCWConfig) - require.NoError(t, err) - - recentBlockHash := solana.Hash{} - rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() - - type CommitArgs struct { - ReportContext [2][32]byte - Report []byte - Rs [][32]byte - Ss [][32]byte - RawVs [32]byte - Info ccipocr3.CommitReportInfo - } - - txID := uuid.NewString() - - // TODO: Replace with actual type from ccipocr3 - args := CommitArgs{ - ReportContext: [2][32]byte{{0x01}, {0x02}}, - Report: []byte{0x01, 0x02}, - Rs: [][32]byte{{0x01, 0x02}}, - Ss: [][32]byte{{0x01, 0x02}}, - RawVs: [32]byte{0x01, 0x02}, - Info: ccipocr3.CommitReportInfo{ - RemoteF: 1, - MerkleRoots: []ccipocr3.MerkleRootChain{}, - }, - } - - txm.On("Enqueue", mock.Anything, admin.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { - txData := tx.Message.Instructions[0].Data - payload := txData[8:] - var decoded ccip_router.Commit - dec := ag_binary.NewBorshDecoder(payload) - err := dec.Decode(&decoded) - require.NoError(t, err) - return true - }), &txID, mock.Anything).Return(nil).Once() - - submitErr := cw.SubmitTransaction(ctx, "ccip_router", "commit", args, txID, routerAddr.String(), nil, nil) - require.NoError(t, submitErr) - }) -} - func TestChainWriter_CCIPOfframp(t *testing.T) { t.Parallel() diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index c2fa5b832..f52970cdb 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -43,39 +43,48 @@ func FetchTestContractIDL() string { return testContractIDL } +var ( + errFieldNotFound = errors.New("key not found") +) + // GetValuesAtLocation parses through nested types and arrays to find all locations of values func GetValuesAtLocation(args any, location string) ([][]byte, error) { var vals [][]byte + // If the user specified no location, just return empty (no-op). + if location == "" { + return nil, nil + } + path := strings.Split(location, ".") - addressList, err := traversePath(args, path) + items, err := traversePath(args, path) if err != nil { return nil, err } - for _, value := range addressList { - // Dereference if it's a pointer - rv := reflect.ValueOf(value) + + for _, item := range items { + rv := reflect.ValueOf(item) if rv.Kind() == reflect.Ptr && !rv.IsNil() { - value = rv.Elem().Interface() + item = rv.Elem().Interface() } - if byteArray, ok := value.([]byte); ok { - vals = append(vals, byteArray) - } else if address, ok := value.(solana.PublicKey); ok { - vals = append(vals, address.Bytes()) - } else if num, ok := value.(uint64); ok { + switch value := item.(type) { + case []byte: + vals = append(vals, value) + case solana.PublicKey: + vals = append(vals, value.Bytes()) + case ccipocr3.UnknownAddress: + vals = append(vals, value) + case uint64: buf := make([]byte, 8) - binary.LittleEndian.PutUint64(buf, num) + binary.LittleEndian.PutUint64(buf, value) vals = append(vals, buf) - } else if addr, ok := value.(ccipocr3.UnknownAddress); ok { - vals = append(vals, addr) - } else if arr, ok := value.([32]uint8); ok { - vals = append(vals, arr[:]) - } else { + case [32]uint8: + vals = append(vals, value[:]) + default: return nil, fmt.Errorf("invalid value format at path: %s, type: %s", location, reflect.TypeOf(value).String()) } } - return vals, nil } @@ -135,7 +144,7 @@ func traversePath(data any, path []string) ([]any, error) { case reflect.Struct: field := val.FieldByName(path[0]) if !field.IsValid() { - return nil, errors.New("field not found: " + path[0]) + return []any{}, errFieldNotFound } return traversePath(field.Interface(), path[1:]) @@ -150,13 +159,13 @@ func traversePath(data any, path []string) ([]any, error) { if len(result) > 0 { return result, nil } - return nil, errors.New("no matching field found in array") + return []any{}, errFieldNotFound case reflect.Map: key := reflect.ValueOf(path[0]) value := val.MapIndex(key) if !value.IsValid() { - return nil, errors.New("key not found: " + path[0]) + return []any{}, errFieldNotFound } return traversePath(value.Interface(), path[1:]) default: diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index dc3abd359..635e7edd0 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -98,6 +98,16 @@ type AccountsFromLookupTable struct { LookupOpts } +type ATALookup struct { + // Field that determines whether the ATA lookup is necessary. Basically + // just need to check this field exists. Dot separated location. + Location string + // If the field exists, initialize a ATA account using the Wallet, Token Program, and Mint addresses below + WalletAddress Lookup + TokenProgram Lookup + MintAddress Lookup +} + 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 { diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index d7aa1c1a1..287994a06 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -15,6 +15,7 @@ import ( commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" ) @@ -42,6 +43,35 @@ func SendAndConfirm(ctx context.Context, t *testing.T, rpcClient *rpc.Client, in func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, instructions []solana.Instruction, signerAndPayer solana.PrivateKey, commitment rpc.CommitmentType, skipPreflight bool, opts ...TxModifier) *rpc.GetTransactionResult { + tx := CreateTx(ctx, t, rpcClient, instructions, signerAndPayer, commitment, opts...) + + txsig, err := rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{SkipPreflight: skipPreflight, PreflightCommitment: rpc.CommitmentProcessed}) + require.NoError(t, err) + + var txStatus rpc.ConfirmationStatusType + count := 0 + for txStatus != rpc.ConfirmationStatusType(commitment) && txStatus != rpc.ConfirmationStatusFinalized { + count++ + statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) + require.NoError(t, sigErr) + if statusRes != nil && len(statusRes.Value) > 0 && statusRes.Value[0] != nil { + txStatus = statusRes.Value[0].ConfirmationStatus + } + time.Sleep(100 * time.Millisecond) + if count > 500 { + require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) + } + } + + txres, err := rpcClient.GetTransaction(ctx, txsig, &rpc.GetTransactionOpts{ + Commitment: commitment, + }) + require.NoError(t, err) + return txres +} + +func CreateTx(ctx context.Context, t *testing.T, rpcClient *rpc.Client, instructions []solana.Instruction, + signerAndPayer solana.PrivateKey, commitment rpc.CommitmentType, opts ...TxModifier) *solana.Transaction { hashRes, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) require.NoError(t, err) @@ -67,30 +97,7 @@ func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, i return &priv }) require.NoError(t, err) - - txsig, err := rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{SkipPreflight: skipPreflight, PreflightCommitment: rpc.CommitmentProcessed}) - require.NoError(t, err) - - var txStatus rpc.ConfirmationStatusType - count := 0 - for txStatus != rpc.ConfirmationStatusType(commitment) && txStatus != rpc.ConfirmationStatusFinalized { - count++ - statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) - require.NoError(t, sigErr) - if statusRes != nil && len(statusRes.Value) > 0 && statusRes.Value[0] != nil { - txStatus = statusRes.Value[0].ConfirmationStatus - } - time.Sleep(100 * time.Millisecond) - if count > 500 { - require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) - } - } - - txres, err := rpcClient.GetTransaction(ctx, txsig, &rpc.GetTransactionOpts{ - Commitment: commitment, - }) - require.NoError(t, err) - return txres + return tx } // InjectAddressModifier injects AddressModifier into InputModifications and OutputModifications.