diff --git a/contracts/artifacts/localnet/write_test-keypair.json b/contracts/artifacts/localnet/write_test-keypair.json index c4e6e125c..dfb18e9c4 100644 --- a/contracts/artifacts/localnet/write_test-keypair.json +++ b/contracts/artifacts/localnet/write_test-keypair.json @@ -1 +1,6 @@ -[26,39,164,161,246,97,149,0,58,187,146,162,53,35,107,2,117,242,83,171,48,7,63,240,69,221,239,45,97,55,112,106,192,228,214,205,123,71,58,23,62,229,166,213,149,122,96,145,35,150,16,156,247,199,242,108,173,80,62,231,39,196,27,192] \ No newline at end of file +[ + 26, 39, 164, 161, 246, 97, 149, 0, 58, 187, 146, 162, 53, 35, 107, 2, 117, + 242, 83, 171, 48, 7, 63, 240, 69, 221, 239, 45, 97, 55, 112, 106, 192, 228, + 214, 205, 123, 71, 58, 23, 62, 229, 166, 213, 149, 122, 96, 145, 35, 150, 16, + 156, 247, 199, 242, 108, 173, 80, 62, 231, 39, 196, 27, 192 +] diff --git a/contracts/programs/write_test/src/lib.rs b/contracts/programs/write_test/src/lib.rs index 4078bca4d..8d8fa3cac 100644 --- a/contracts/programs/write_test/src/lib.rs +++ b/contracts/programs/write_test/src/lib.rs @@ -12,10 +12,9 @@ pub mod write_test { data.administrator = ctx.accounts.admin.key(); data.pending_administrator = Pubkey::default(); data.lookup_table = lookup_table; - + Ok(()) } - } #[derive(Accounts)] diff --git a/go.mod b/go.mod index 70a31342a..6b58adcfe 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/smartcontractkit/chainlink-common v0.4.1-0.20241223143929-db7919d60550 github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 github.com/stretchr/testify v1.9.0 - github.com/test-go/testify v1.1.4 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/sync v0.10.0 diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go new file mode 100644 index 000000000..d931fb6d8 --- /dev/null +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -0,0 +1,690 @@ +package chainwriter_test + +import ( + "bytes" + "errors" + "math/big" + "reflect" + "testing" + + ag_binary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" + "github.com/gagliardetto/solana-go/rpc" + "github.com/google/uuid" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + feemocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees/mocks" + txmMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" +) + +var writeTestIdlJSON = `{"version": "0.1.0","name": "write_test","instructions": [{"name": "initialize","accounts": [{"name": "dataAccount","isMut": true,"isSigner": false,"docs": ["PDA account, derived from seeds and created by the System Program in this instruction"]},{"name": "admin","isMut": true,"isSigner": true,"docs": ["Admin account that pays for PDA creation and signs the transaction"]},{"name": "systemProgram","isMut": false,"isSigner": false,"docs": ["System Program is required for PDA creation"]}],"args": [{"name": "lookupTable","type": "publicKey"}]}],"accounts": [{"name": "DataAccount","type": {"kind": "struct","fields": [{"name": "version","type": "u8"},{"name": "administrator","type": "publicKey"},{"name": "pendingAdministrator","type": "publicKey"},{"name": "lookupTable","type": "publicKey"}]}}]}` + +func TestChainWriter_GetAddresses(t *testing.T) { + ctx := tests.Context(t) + + // mock client + rw := clientmocks.NewReaderWriter(t) + // mock estimator + ge := feemocks.NewEstimator(t) + // mock txm + txm := txmMocks.NewTxManager(t) + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + // expected account meta for constant account + constantAccountMeta := &solana.AccountMeta{ + IsSigner: true, + IsWritable: true, + } + + // expected account meta for account lookup + accountLookupMeta := &solana.AccountMeta{ + IsSigner: true, + IsWritable: false, + } + + // setup pda account address + seed1 := []byte("seed1") + pda1 := mustFindPdaProgramAddress(t, [][]byte{seed1}, solana.SystemProgramID) + // expected account meta for pda lookup + pdaLookupMeta := &solana.AccountMeta{ + PublicKey: pda1, + IsSigner: false, + IsWritable: false, + } + + // setup pda account with inner field lookup + programID := chainwriter.GetRandomPubKey(t) + seed2 := []byte("seed2") + pda2 := mustFindPdaProgramAddress(t, [][]byte{seed2}, programID) + // mock data account response from program + lookupTablePubkey := mockDataAccountLookupTable(t, rw, pda2) + // mock fetch lookup table addresses call + storedPubKeys := chainwriter.CreateTestPubKeys(t, 3) + mockFetchLookupTableAddresses(t, rw, lookupTablePubkey, storedPubKeys) + // expected account meta for derived table lookup + derivedTablePdaLookupMeta := &solana.AccountMeta{ + IsSigner: false, + IsWritable: true, + } + + lookupTableConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + }, + IsSigner: derivedTablePdaLookupMeta.IsSigner, + IsWritable: derivedTablePdaLookupMeta.IsWritable, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: nil, + } + + t.Run("resolve addresses from different types of lookups", func(t *testing.T) { + constantAccountMeta.PublicKey = chainwriter.GetRandomPubKey(t) + accountLookupMeta.PublicKey = chainwriter.GetRandomPubKey(t) + // correlates to DerivedTable index in account lookup config + derivedTablePdaLookupMeta.PublicKey = storedPubKeys[0] + + args := map[string]interface{}{ + "lookup_table": accountLookupMeta.PublicKey.Bytes(), + "seed1": seed1, + "seed2": seed2, + } + + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "Constant", + Address: constantAccountMeta.PublicKey.String(), + IsSigner: constantAccountMeta.IsSigner, + IsWritable: constantAccountMeta.IsWritable, + }, + chainwriter.AccountLookup{ + Name: "LookupTable", + Location: "lookup_table", + IsSigner: accountLookupMeta.IsSigner, + IsWritable: accountLookupMeta.IsWritable, + }, + chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed1 for PDA lookup + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: pdaLookupMeta.IsSigner, + IsWritable: pdaLookupMeta.IsWritable, + // Just get the address of the account, nothing internal. + InternalField: chainwriter.InternalField{}, + }, + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0}, + }, + } + + // Fetch derived table map + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + 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 + require.Len(t, accounts, 4) + + // Validate account constant + require.Equal(t, constantAccountMeta.PublicKey, accounts[0].PublicKey) + require.Equal(t, constantAccountMeta.IsSigner, accounts[0].IsSigner) + require.Equal(t, constantAccountMeta.IsWritable, accounts[0].IsWritable) + + // Validate account lookup + require.Equal(t, accountLookupMeta.PublicKey, accounts[1].PublicKey) + require.Equal(t, accountLookupMeta.IsSigner, accounts[1].IsSigner) + require.Equal(t, accountLookupMeta.IsWritable, accounts[1].IsWritable) + + // Validate pda lookup + require.Equal(t, pdaLookupMeta.PublicKey, accounts[2].PublicKey) + require.Equal(t, pdaLookupMeta.IsSigner, accounts[2].IsSigner) + require.Equal(t, pdaLookupMeta.IsWritable, accounts[2].IsWritable) + + // Validate pda lookup with inner field from derived table + require.Equal(t, derivedTablePdaLookupMeta.PublicKey, accounts[3].PublicKey) + require.Equal(t, derivedTablePdaLookupMeta.IsSigner, accounts[3].IsSigner) + require.Equal(t, derivedTablePdaLookupMeta.IsWritable, accounts[3].IsWritable) + }) + + t.Run("resolve addresses for multiple indices from derived lookup table", func(t *testing.T) { + args := map[string]interface{}{ + "seed2": seed2, + } + + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0, 2}, + }, + } + + // Fetch derived table map + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + require.Len(t, accounts, 2) + require.Equal(t, storedPubKeys[0], accounts[0].PublicKey) + require.Equal(t, storedPubKeys[2], accounts[1].PublicKey) + }) + + t.Run("resolve all addresses from derived lookup table if indices not specified", func(t *testing.T) { + args := map[string]interface{}{ + "seed2": seed2, + } + + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + }, + } + + // Fetch derived table map + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + require.Len(t, accounts, 3) + for i, storedPubkey := range storedPubKeys { + require.Equal(t, storedPubkey, accounts[i].PublicKey) + } + }) +} + +func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { + ctx := tests.Context(t) + + // mock client + rw := clientmocks.NewReaderWriter(t) + // mock estimator + ge := feemocks.NewEstimator(t) + // mock txm + txm := txmMocks.NewTxManager(t) + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + programID := chainwriter.GetRandomPubKey(t) + seed1 := []byte("seed1") + pda1 := mustFindPdaProgramAddress(t, [][]byte{seed1}, programID) + // mock data account response from program + lookupTablePubkey := mockDataAccountLookupTable(t, rw, pda1) + // mock fetch lookup table addresses call + storedPubKey := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, lookupTablePubkey, []solana.PublicKey{storedPubKey}) + + unusedProgramID := chainwriter.GetRandomPubKey(t) + seed2 := []byte("seed2") + unusedPda := mustFindPdaProgramAddress(t, [][]byte{seed2}, unusedProgramID) + // mock data account response from program + unusedLookupTable := mockDataAccountLookupTable(t, rw, unusedPda) + // mock fetch lookup table addresses call + unusedKeys := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, unusedLookupTable, []solana.PublicKey{unusedKeys}) + + // mock static lookup table calls + staticLookupTablePubkey1 := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, staticLookupTablePubkey1, chainwriter.CreateTestPubKeys(t, 2)) + staticLookupTablePubkey2 := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, staticLookupTablePubkey2, chainwriter.CreateTestPubKeys(t, 2)) + + lookupTableConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: true, + IsWritable: true, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + { + Name: "MiscDerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "MiscPDA", + PublicKey: chainwriter.AccountConstant{Name: "UnusedAccount", Address: unusedProgramID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + }, + IsSigner: true, + IsWritable: true, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: []string{staticLookupTablePubkey1.String(), staticLookupTablePubkey2.String()}, + } + + args := map[string]interface{}{ + "seed1": seed1, + "seed2": seed2, + } + + t.Run("returns filtered map with only relevant addresses required by account lookup config", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0}, + }, + } + + // Fetch derived table map + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + 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 + filteredLookupTableMap := cw.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) + + // Filter map should only contain the address for the DerivedTable lookup defined in the account lookup config + require.Len(t, filteredLookupTableMap, len(accounts)) + entry, exists := filteredLookupTableMap[lookupTablePubkey] + require.True(t, exists) + require.Len(t, entry, 1) + require.Equal(t, storedPubKey, entry[0]) + }) + + t.Run("returns empty map if empty account lookup config provided", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{} + + // Fetch derived table map + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + 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 + filteredLookupTableMap := cw.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) + require.Empty(t, filteredLookupTableMap) + }) + + t.Run("returns empty map if only constant account lookup required", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "Constant", + Address: chainwriter.GetRandomPubKey(t).String(), + IsSigner: false, + IsWritable: false, + }, + } + + // Fetch derived table map + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + 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 + filteredLookupTableMap := cw.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) + require.Empty(t, filteredLookupTableMap) + }) +} + +func TestChainWriter_SubmitTransaction(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + // mock client + rw := clientmocks.NewReaderWriter(t) + // mock estimator + ge := feemocks.NewEstimator(t) + // mock txm + txm := txmMocks.NewTxManager(t) + + // setup admin key + adminPk, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + admin := adminPk.PublicKey() + + account1 := chainwriter.GetRandomPubKey(t) + account2 := chainwriter.GetRandomPubKey(t) + + seed1 := []byte("seed1") + account3 := mustFindPdaProgramAddress(t, [][]byte{seed1}, solana.SystemProgramID) + + // create lookup table addresses + seed2 := []byte("seed2") + programID := chainwriter.GetRandomPubKey(t) + derivedTablePda := mustFindPdaProgramAddress(t, [][]byte{seed2}, programID) + // mock data account response from program + derivedLookupTablePubkey := mockDataAccountLookupTable(t, rw, derivedTablePda) + // mock fetch lookup table addresses call + derivedLookupKeys := chainwriter.CreateTestPubKeys(t, 1) + mockFetchLookupTableAddresses(t, rw, derivedLookupTablePubkey, derivedLookupKeys) + + // mock static lookup table call + staticLookupTablePubkey := chainwriter.GetRandomPubKey(t) + staticLookupKeys := chainwriter.CreateTestPubKeys(t, 2) + mockFetchLookupTableAddresses(t, rw, staticLookupTablePubkey, staticLookupKeys) + + cwConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU": { + Methods: map[string]chainwriter.MethodConfig{ + "initialize": { + FromAddress: admin.String(), + ChainSpecificName: "initialize", + LookupTables: chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: []string{staticLookupTablePubkey.String()}, + }, + Accounts: []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "Constant", + Address: account1.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: solana.SystemProgramID.String()}, + 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: writeTestIdlJSON, + }, + }, + } + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, cwConfig) + require.NoError(t, err) + + t.Run("fails with invalid ABI", func(t *testing.T) { + invalidCWConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + "write_test": { + Methods: map[string]chainwriter.MethodConfig{ + "invalid": { + ChainSpecificName: "invalid", + }, + }, + IDL: "", + }, + }, + } + + _, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, invalidCWConfig) + require.Error(t, err) + }) + + t.Run("fails to encode payload if args with missing values provided", func(t *testing.T) { + txID := uuid.NewString() + args := map[string]interface{}{} + submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "initialize", args, txID, programID.String(), nil, nil) + require.Error(t, submitErr) + }) + + t.Run("fails if invalid contract name provided", func(t *testing.T) { + txID := uuid.NewString() + args := map[string]interface{}{} + submitErr := cw.SubmitTransaction(ctx, "write_test", "initialize", args, txID, programID.String(), nil, nil) + require.Error(t, submitErr) + }) + + t.Run("fails if invalid method provided", func(t *testing.T) { + txID := uuid.NewString() + args := map[string]interface{}{} + submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "badMethod", args, txID, programID.String(), nil, nil) + require.Error(t, submitErr) + }) + + t.Run("submits transaction successfully", func(t *testing.T) { + recentBlockHash := solana.Hash{} + rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() + txID := uuid.NewString() + configProgramID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") + + txm.On("Enqueue", mock.Anything, account1.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { + // match transaction fields to ensure it was built as expected + require.Equal(t, recentBlockHash, tx.Message.RecentBlockhash) + require.Len(t, tx.Message.Instructions, 1) + require.Len(t, tx.Message.AccountKeys, 5) // fee payer + derived accounts + require.Equal(t, admin, tx.Message.AccountKeys[0]) // fee payer + require.Equal(t, account1, tx.Message.AccountKeys[1]) // account constant + require.Equal(t, account2, tx.Message.AccountKeys[2]) // account lookup + require.Equal(t, account3, tx.Message.AccountKeys[3]) // pda lookup + require.Equal(t, configProgramID, tx.Message.AccountKeys[4]) // instruction program ID + require.Len(t, tx.Message.AddressTableLookups, 1) // address table look contains entry + require.Equal(t, derivedLookupTablePubkey, tx.Message.AddressTableLookups[0].AccountKey) // address table + return true + }), &txID).Return(nil).Once() + + args := map[string]interface{}{ + "lookupTable": chainwriter.GetRandomPubKey(t).Bytes(), + "lookup_table": account2.Bytes(), + "seed1": seed1, + "seed2": seed2, + } + submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "initialize", args, txID, programID.String(), nil, nil) + require.NoError(t, submitErr) + }) +} + +func TestChainWriter_GetTransactionStatus(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + rw := clientmocks.NewReaderWriter(t) + ge := feemocks.NewEstimator(t) + + // mock txm + txm := txmMocks.NewTxManager(t) + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + t.Run("returns unknown with error if ID not found", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Unknown, errors.New("tx not found")).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.Error(t, err) + require.Equal(t, types.Unknown, status) + }) + + t.Run("returns pending when transaction is pending", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Pending, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Pending, status) + }) + + t.Run("returns unconfirmed when transaction is unconfirmed", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Unconfirmed, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Unconfirmed, status) + }) + + t.Run("returns finalized when transaction is finalized", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Finalized, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Finalized, status) + }) + + t.Run("returns failed when transaction error classfied as failed", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Failed, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Failed, status) + }) + + t.Run("returns fatal when transaction error classfied as fatal", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Fatal, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Fatal, status) + }) +} + +func TestChainWriter_GetFeeComponents(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + rw := clientmocks.NewReaderWriter(t) + ge := feemocks.NewEstimator(t) + ge.On("BaseComputeUnitPrice").Return(uint64(100)) + + // mock txm + txm := txmMocks.NewTxManager(t) + + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + t.Run("returns valid compute unit price", func(t *testing.T) { + feeComponents, err := cw.GetFeeComponents(ctx) + require.NoError(t, err) + require.Equal(t, big.NewInt(100), feeComponents.ExecutionFee) + require.Nil(t, feeComponents.DataAvailabilityFee) // always nil for Solana + }) + + t.Run("fails if gas estimator not set", func(t *testing.T) { + cwNoEstimator, err := chainwriter.NewSolanaChainWriterService(rw, txm, nil, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + _, err = cwNoEstimator.GetFeeComponents(ctx) + require.Error(t, err) + }) +} + +func mustBorshEncodeStruct(t *testing.T, data interface{}) []byte { + buf := new(bytes.Buffer) + err := ag_binary.NewBorshEncoder(buf).Encode(data) + require.NoError(t, err) + return buf.Bytes() +} + +func mustFindPdaProgramAddress(t *testing.T, seeds [][]byte, programID solana.PublicKey) solana.PublicKey { + pda, _, err := solana.FindProgramAddress(seeds, programID) + require.NoError(t, err) + return pda +} + +func mockDataAccountLookupTable(t *testing.T, rw *clientmocks.ReaderWriter, pda solana.PublicKey) solana.PublicKey { + lookupTablePubkey := chainwriter.GetRandomPubKey(t) + dataAccount := DataAccount{ + Discriminator: [8]byte{}, + Version: 1, + Administrator: chainwriter.GetRandomPubKey(t), + PendingAdministrator: chainwriter.GetRandomPubKey(t), + LookupTable: lookupTablePubkey, + } + dataAccountBytes := mustBorshEncodeStruct(t, dataAccount) + rw.On("GetAccountInfoWithOpts", mock.Anything, pda, mock.Anything).Return(&rpc.GetAccountInfoResult{ + RPCContext: rpc.RPCContext{}, + Value: &rpc.Account{Data: rpc.DataBytesOrJSONFromBytes(dataAccountBytes)}, + }, nil) + return lookupTablePubkey +} + +func mockFetchLookupTableAddresses(t *testing.T, rw *clientmocks.ReaderWriter, lookupTablePubkey solana.PublicKey, storedPubkeys []solana.PublicKey) { + var lookupTablePubkeySlice solana.PublicKeySlice + lookupTablePubkeySlice.Append(storedPubkeys...) + lookupTableState := addresslookuptable.AddressLookupTableState{ + Addresses: lookupTablePubkeySlice, + } + lookupTableStateBytes := mustBorshEncodeStruct(t, lookupTableState) + rw.On("GetAccountInfoWithOpts", mock.Anything, lookupTablePubkey, mock.Anything).Return(&rpc.GetAccountInfoResult{ + RPCContext: rpc.RPCContext{}, + Value: &rpc.Account{Data: rpc.DataBytesOrJSONFromBytes(lookupTableStateBytes)}, + }, nil) +} diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index fe4771c5a..2285cd699 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -103,11 +103,11 @@ func GetValueAtLocation(args any, location string) ([][]byte, error) { var values [][]byte for _, value := range valueList { - if byteArray, ok := value.([]byte); ok { - values = append(values, byteArray) - } else { + byteArray, ok := value.([]byte) + if !ok { return nil, fmt.Errorf("invalid value format at path: %s", location) } + values = append(values, byteArray) } return values, nil diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index f875ade3a..1d885adf3 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -163,6 +163,7 @@ func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map Encoding: "base64", Commitment: rpc.CommitmentFinalized, }) + fmt.Printf("Accounts Info: %+v", accountInfo) if err != nil || accountInfo == nil || accountInfo.Value == nil { return nil, fmt.Errorf("error fetching account info for PDA account: %s, error: %w", accountMeta.PublicKey.String(), err) diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go index 2a75814bf..53972feac 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/pkg/solana/chainwriter/lookups_test.go @@ -2,24 +2,24 @@ package chainwriter_test import ( "context" - "crypto/sha256" "reflect" "testing" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" - - commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" - "github.com/test-go/testify/require" ) type TestArgs struct { @@ -40,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, @@ -54,14 +54,15 @@ func TestAccountContant(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := constantConfig.Resolve(nil, nil, nil, nil) + result, err := constantConfig.Resolve(tests.Context(t), nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) } func TestAccountLookups(t *testing.T) { + ctx := tests.Context(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()}, @@ -81,14 +82,14 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) + result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) 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{ @@ -115,7 +116,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) + result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.NoError(t, err) for i, meta := range result { require.Equal(t, expectedMeta[i], meta) @@ -123,7 +124,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{ @@ -136,7 +137,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - _, err := lookupConfig.Resolve(nil, testArgs, nil, nil) + _, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.Error(t, err) }) } @@ -145,7 +146,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) @@ -211,8 +212,6 @@ func TestPDALookups(t *testing.T) { }) t.Run("PDALookup fails with missing seeds", func(t *testing.T) { - programID := solana.SystemProgramID - pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, @@ -234,8 +233,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) @@ -292,22 +291,22 @@ func TestLookupTables(t *testing.T) { txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) - cw, err := chainwriter.NewSolanaChainWriterService(solanaClient, *txm, nil, chainwriter.ChainWriterConfig{}) + 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()}, } - _, staticTableMap, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) - require.NoError(t, err) + _, staticTableMap, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.NoError(t, resolveErr) 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{ { @@ -322,8 +321,8 @@ func TestLookupTables(t *testing.T) { }, StaticLookupTables: nil, } - derivedTableMap, _, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) - require.NoError(t, err) + derivedTableMap, _, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.NoError(t, resolveErr) addresses, ok := derivedTableMap["DerivedTable"][table.String()] require.True(t, ok) @@ -333,7 +332,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{ @@ -356,7 +355,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, @@ -369,8 +368,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{ { @@ -405,10 +404,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"), @@ -446,76 +445,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 -} diff --git a/pkg/solana/utils/utils_test.go b/pkg/solana/utils/utils_test.go index da6166ff0..0f41f80c9 100644 --- a/pkg/solana/utils/utils_test.go +++ b/pkg/solana/utils/utils_test.go @@ -3,7 +3,6 @@ package utils_test import ( "testing" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" "github.com/stretchr/testify/assert" "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils"