From ed183d4831cebfc8468f0b2819f2b9c761b1927d 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 --- .../relayinterface/lookups_test.go | 177 ++++++++++++++ pkg/solana/chainwriter/chain_writer.go | 90 +++++++- 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 | 75 ++++-- 6 files changed, 353 insertions(+), 263 deletions(-) diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 221b08652..18010bcd6 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" commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" @@ -482,6 +483,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) @@ -635,3 +637,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 := utils.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, 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) + + utils.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/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 1bf8a3a8b..94c4d0043 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" @@ -55,6 +58,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 @@ -214,6 +218,78 @@ 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) + } + + // Not sure if + 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 @@ -274,6 +350,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) @@ -310,10 +391,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 9c984b69a..81a330c74 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -799,221 +799,6 @@ func TestChainWriter_CCIPRouter(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_GetTransactionStatus(t *testing.T) { t.Parallel() diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 6e2a3e5be..1c135fb69 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 36719538a..982201b23 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -83,6 +83,16 @@ type AccountsFromLookupTable struct { IncludeIndexes []int } +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, _ string) ([]*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 764c236de..ca172e9e3 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -18,6 +18,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" ) @@ -47,7 +49,36 @@ 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 { - hashRes, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + 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, commitment) require.NoError(t, err) tx, err := solana.NewTransaction( @@ -72,30 +103,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 } var ( @@ -217,3 +225,20 @@ func SetupTestValidatorWithAnchorPrograms(t *testing.T, upgradeAuthority string, rpcURL, wsURL := client.SetupLocalSolNodeWithFlags(t, flags...) return rpcURL, wsURL } + +func CreateRandomToken(t *testing.T, admin solana.PrivateKey, client *rpc.Client) solana.PublicKey { + ctx := tests.Context(t) + mint, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + instructions, err := tokens.CreateToken(ctx, solana.Token2022ProgramID, 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 + } + + SendAndConfirm(ctx, t, client, instructions, admin, rpc.CommitmentFinalized, addMintModifier) + return mint.PublicKey() +}