diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index bd5087af8..3c17b5887 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -102,8 +102,8 @@ func TestConfig() { }, // Lookup Table content - Get the accounts from the derived lookup table above AccountsFromLookupTable{ - LookupTablesName: "RegistryTokenState", - IncludeIndexes: []int{}, // If left empty, all addresses will be included. Otherwise, only the specified indexes will be included. + LookupTableName: "RegistryTokenState", + IncludeIndexes: []int{}, // If left empty, all addresses will be included. Otherwise, only the specified indexes will be included. }, // Account Lookup - Based on data from input parameters // In this case, the user wants to add the destination token addresses to the transaction. diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index cfc82c2cf..e1a86bc27 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -3,7 +3,10 @@ package chainwriter_test import ( "context" "crypto/rand" + "fmt" "math/big" + "os" + "reflect" "sync" "testing" "time" @@ -15,6 +18,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" relayconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -35,7 +39,126 @@ func TestChainWriter_GetAddresses(t *testing.T) {} func TestChainWriter_FilterLookupTableAddresses(t *testing.T) {} -func TestChainWriter_SubmitTransaction(t *testing.T) {} +func TestChainWriter_SubmitTransaction(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + lggr := logger.Test(t) + cfg := config.NewDefault() + // Retain transactions after finality or error to maintain their status in memory + cfg.Chain.TxRetentionTimeout = relayconfig.MustNewDuration(5 * time.Second) + // Disable bumping to avoid issues with send tx mocking + cfg.Chain.FeeBumpPeriod = relayconfig.MustNewDuration(0 * time.Second) + rw := clientmocks.NewReaderWriter(t) + rw.On("GetLatestBlock", mock.Anything).Return(&rpc.GetBlockResult{}, nil).Maybe() + rw.On("SlotHeight", mock.Anything).Return(uint64(0), nil).Maybe() + loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return rw, nil }) + ge := feemocks.NewEstimator(t) + // mock solana keystore + keystore := keyMocks.NewSimpleKeystore(t) + keystore.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil).Maybe() + + // initialize and start TXM + txm := txm.NewTxm(uuid.NewString(), loader, nil, cfg, keystore, lggr) + require.NoError(t, txm.Start(ctx)) + t.Cleanup(func() { require.NoError(t, txm.Close()) }) + + idlJSON, err := os.ReadFile("../../../contracts/target/idl/write_test.json") + require.NoError(t, err) + // TODO: Get IDL and address + programID := "" + programIDL := string(idlJSON) + + args := map[string]interface{}{ + "seed1": []byte("data"), + "lookup_table": chainwriter.GetRandomPubKey(t), + } + fmt.Println(args) + + adminPk, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + admin := adminPk.PublicKey() + + // TODO: Replace all random and create mocks + cwConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + "write_test": { + Methods: map[string]chainwriter.MethodConfig{ + "initialize": { + FromAddress: admin.String(), + InputModifications: commoncodec.ModifiersConfig{ + &commoncodec.DropModifierConfig{ + // Drop seed1 since it shouldn't be in the instruction data + Fields: []string{"seed1"}, + }, + }, + ChainSpecificName: "initialize", + LookupTables: chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID}, + Seeds: []chainwriter.Lookup{ + // extract seed1 for PDA lookup + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: []string{chainwriter.GetRandomPubKey(t).String()}, + }, + Accounts: []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "Constant", + Address: chainwriter.GetRandomPubKey(t).String(), + IsSigner: false, + IsWritable: false, + }, + chainwriter.AccountLookup{ + Name: "LookupTable", + Location: "lookup_table", + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID}, + Seeds: []chainwriter.Lookup{ + // extract seed1 for PDA lookup + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: false, + IsWritable: false, + // Just get the address of the account, nothing internal. + InternalField: chainwriter.InternalField{}, + }, + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0}, + }, + }, + }, + }, + IDL: programIDL, + }, + }, + } + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, cwConfig) + require.NoError(t, err) + + fmt.Println(cw) +} func TestChainWriter_GetTransactionStatus(t *testing.T) { t.Parallel() diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 4d5d00600..b3ae6b712 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -1,12 +1,18 @@ package chainwriter import ( + "context" + "crypto/sha256" "errors" "fmt" "reflect" "strings" + "testing" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" + "github.com/test-go/testify/require" ) // GetValuesAtLocation parses through nested types and arrays to find all locations of values @@ -120,3 +126,76 @@ func traversePath(data any, path []string) ([]any, error) { return nil, errors.New("unexpected type encountered at path: " + path[0]) } } + +func InitializeDataAccount( + ctx context.Context, + t *testing.T, + client *rpc.Client, + programID solana.PublicKey, + admin solana.PrivateKey, + lookupTable solana.PublicKey, +) { + pda, _, err := solana.FindProgramAddress([][]byte{[]byte("data")}, programID) + require.NoError(t, err) + + discriminator := GetDiscriminator("initialize") + + instructionData := append(discriminator[:], lookupTable.Bytes()...) + + instruction := solana.NewInstruction( + programID, + solana.AccountMetaSlice{ + solana.Meta(pda).WRITE(), + solana.Meta(admin.PublicKey()).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + instructionData, + ) + + // Send and confirm the transaction + utils.SendAndConfirm(ctx, t, client, []solana.Instruction{instruction}, admin, rpc.CommitmentFinalized) +} + +func GetDiscriminator(instruction string) [8]byte { + fullHash := sha256.Sum256([]byte("global:" + instruction)) + var discriminator [8]byte + copy(discriminator[:], fullHash[:8]) + return discriminator +} + +func GetRandomPubKey(t *testing.T) solana.PublicKey { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + return privKey.PublicKey() +} + +func CreateTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { + addresses := make([]solana.PublicKey, num) + for i := 0; i < num; i++ { + addresses[i] = GetRandomPubKey(t) + } + return addresses +} + +func CreateTestLookupTable(ctx context.Context, t *testing.T, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { + // Create lookup tables + slot, serr := c.GetSlot(ctx, rpc.CommitmentFinalized) + require.NoError(t, serr) + table, instruction, ierr := utils.NewCreateLookupTableInstruction( + sender.PublicKey(), + sender.PublicKey(), + slot, + ) + require.NoError(t, ierr) + utils.SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) + + // add entries to lookup table + utils.SendAndConfirm(ctx, t, c, []solana.Instruction{ + utils.NewExtendLookupTableInstruction( + table, sender.PublicKey(), sender.PublicKey(), + addresses, + ), + }, sender, rpc.CommitmentConfirmed) + + return table +} diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 1aa9ae92d..edeb10f27 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -69,8 +69,8 @@ type DerivedLookupTable struct { // AccountsFromLookupTable extracts accounts from a lookup table that was previously read and stored in memory. type AccountsFromLookupTable struct { - LookupTablesName string - IncludeIndexes []int + LookupTableName string + IncludeIndexes []int } func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { @@ -106,9 +106,9 @@ func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[st 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.LookupTablesName] + innerMap, ok := derivedTableMap[alt.LookupTableName] if !ok { - return nil, fmt.Errorf("lookup table not found: %s", alt.LookupTablesName) + return nil, fmt.Errorf("lookup table not found: %s", alt.LookupTableName) } var result []*solana.AccountMeta @@ -125,7 +125,7 @@ func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTabl for publicKey, metas := range innerMap { for _, index := range alt.IncludeIndexes { if index < 0 || index >= len(metas) { - return nil, fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName) + return nil, fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTableName) } result = append(result, metas[index]) } diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go index 46395dfb8..2d40d8e11 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/pkg/solana/chainwriter/lookups_test.go @@ -2,7 +2,6 @@ package chainwriter_test import ( "context" - "crypto/sha256" "reflect" "testing" "time" @@ -41,7 +40,7 @@ type DataAccount struct { func TestAccountContant(t *testing.T) { t.Run("AccountConstant resolves valid address", func(t *testing.T) { - expectedAddr := getRandomPubKey(t) + expectedAddr := chainwriter.GetRandomPubKey(t) expectedMeta := []*solana.AccountMeta{ { PublicKey: expectedAddr, @@ -62,7 +61,7 @@ func TestAccountContant(t *testing.T) { } func TestAccountLookups(t *testing.T) { t.Run("AccountLookup resolves valid address with just one address", func(t *testing.T) { - expectedAddr := getRandomPubKey(t) + expectedAddr := chainwriter.GetRandomPubKey(t) testArgs := TestArgs{ Inner: []InnerArgs{ {Address: expectedAddr.Bytes()}, @@ -88,8 +87,8 @@ func TestAccountLookups(t *testing.T) { }) t.Run("AccountLookup resolves valid address with just multiple addresses", func(t *testing.T) { - expectedAddr1 := getRandomPubKey(t) - expectedAddr2 := getRandomPubKey(t) + expectedAddr1 := chainwriter.GetRandomPubKey(t) + expectedAddr2 := chainwriter.GetRandomPubKey(t) testArgs := TestArgs{ Inner: []InnerArgs{ @@ -124,7 +123,7 @@ func TestAccountLookups(t *testing.T) { }) t.Run("AccountLookup fails when address isn't in args", func(t *testing.T) { - expectedAddr := getRandomPubKey(t) + expectedAddr := chainwriter.GetRandomPubKey(t) testArgs := TestArgs{ Inner: []InnerArgs{ @@ -146,7 +145,7 @@ func TestPDALookups(t *testing.T) { programID := solana.SystemProgramID t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { - seed := getRandomPubKey(t) + seed := chainwriter.GetRandomPubKey(t) pda, _, err := solana.FindProgramAddress([][]byte{seed.Bytes()}, programID) require.NoError(t, err) @@ -235,8 +234,8 @@ func TestPDALookups(t *testing.T) { }) t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { - seed1 := getRandomPubKey(t) - seed2 := getRandomPubKey(t) + seed1 := chainwriter.GetRandomPubKey(t) + seed2 := chainwriter.GetRandomPubKey(t) pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes()}, programID) require.NoError(t, err) @@ -296,8 +295,8 @@ func TestLookupTables(t *testing.T) { cw, err := chainwriter.NewSolanaChainWriterService(solanaClient, txm, nil, chainwriter.ChainWriterConfig{}) t.Run("StaticLookup table resolves properly", func(t *testing.T) { - pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) + pubKeys := chainwriter.CreateTestPubKeys(t, 8) + table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, StaticLookupTables: []string{table.String()}, @@ -307,8 +306,8 @@ func TestLookupTables(t *testing.T) { require.Equal(t, pubKeys, staticTableMap[table]) }) t.Run("Derived lookup table resolves properly with constant address", func(t *testing.T) { - pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) + pubKeys := chainwriter.CreateTestPubKeys(t, 8) + table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -334,7 +333,7 @@ func TestLookupTables(t *testing.T) { }) t.Run("Derived lookup table fails with invalid address", func(t *testing.T) { - invalidTable := getRandomPubKey(t) + invalidTable := chainwriter.GetRandomPubKey(t) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ @@ -357,7 +356,7 @@ func TestLookupTables(t *testing.T) { }) t.Run("Static lookup table fails with invalid address", func(t *testing.T) { - invalidTable := getRandomPubKey(t) + invalidTable := chainwriter.GetRandomPubKey(t) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, @@ -370,8 +369,8 @@ func TestLookupTables(t *testing.T) { }) t.Run("Derived lookup table resolves properly with account lookup address", func(t *testing.T) { - pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) + pubKeys := chainwriter.CreateTestPubKeys(t, 8) + table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -406,10 +405,10 @@ func TestLookupTables(t *testing.T) { // Deployed write_test contract programID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") - lookupKeys := createTestPubKeys(t, 5) - lookupTable := CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) + lookupKeys := chainwriter.CreateTestPubKeys(t, 5) + lookupTable := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) - InitializeDataAccount(ctx, t, rpcClient, programID, sender, lookupTable) + chainwriter.InitializeDataAccount(ctx, t, rpcClient, programID, sender, lookupTable) args := map[string]interface{}{ "seed1": []byte("data"), @@ -447,76 +446,3 @@ func TestLookupTables(t *testing.T) { } }) } - -func InitializeDataAccount( - ctx context.Context, - t *testing.T, - client *rpc.Client, - programID solana.PublicKey, - admin solana.PrivateKey, - lookupTable solana.PublicKey, -) { - pda, _, err := solana.FindProgramAddress([][]byte{[]byte("data")}, programID) - require.NoError(t, err) - - discriminator := getDiscriminator("initialize") - - instructionData := append(discriminator[:], lookupTable.Bytes()...) - - instruction := solana.NewInstruction( - programID, - solana.AccountMetaSlice{ - solana.Meta(pda).WRITE(), - solana.Meta(admin.PublicKey()).SIGNER().WRITE(), - solana.Meta(solana.SystemProgramID), - }, - instructionData, - ) - - // Send and confirm the transaction - utils.SendAndConfirm(ctx, t, client, []solana.Instruction{instruction}, admin, rpc.CommitmentFinalized) -} - -func getDiscriminator(instruction string) [8]byte { - fullHash := sha256.Sum256([]byte("global:" + instruction)) - var discriminator [8]byte - copy(discriminator[:], fullHash[:8]) - return discriminator -} - -func getRandomPubKey(t *testing.T) solana.PublicKey { - privKey, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - return privKey.PublicKey() -} - -func createTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { - addresses := make([]solana.PublicKey, num) - for i := 0; i < num; i++ { - addresses[i] = getRandomPubKey(t) - } - return addresses -} - -func CreateTestLookupTable(ctx context.Context, t *testing.T, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { - // Create lookup tables - slot, serr := c.GetSlot(ctx, rpc.CommitmentFinalized) - require.NoError(t, serr) - table, instruction, ierr := utils.NewCreateLookupTableInstruction( - sender.PublicKey(), - sender.PublicKey(), - slot, - ) - require.NoError(t, ierr) - utils.SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) - - // add entries to lookup table - utils.SendAndConfirm(ctx, t, c, []solana.Instruction{ - utils.NewExtendLookupTableInstruction( - table, sender.PublicKey(), sender.PublicKey(), - addresses, - ), - }, sender, rpc.CommitmentConfirmed) - - return table -}