Skip to content

Commit

Permalink
Enabled automatic ATA creation in CW
Browse files Browse the repository at this point in the history
  • Loading branch information
silaslenihan committed Feb 11, 2025
1 parent 767c204 commit 1beeeb5
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 263 deletions.
2 changes: 1 addition & 1 deletion integration-tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
177 changes: 177 additions & 0 deletions integration-tests/relayinterface/lookups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
18 changes: 18 additions & 0 deletions integration-tests/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
}
88 changes: 85 additions & 3 deletions pkg/solana/chainwriter/chain_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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),
Expand Down
Loading

0 comments on commit 1beeeb5

Please sign in to comment.