Skip to content

Commit

Permalink
Created SubmitTransaction tests
Browse files Browse the repository at this point in the history
  • Loading branch information
silaslenihan committed Dec 3, 2024
1 parent 77bca0f commit 3749bbb
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 116 deletions.
4 changes: 2 additions & 2 deletions pkg/solana/chainwriter/ccip_example_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 3 additions & 11 deletions pkg/solana/chainwriter/chain_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) {
if err := json.Unmarshal([]byte(programConfig.IDL), &idl); err != nil {
return nil, fmt.Errorf("failed to unmarshal IDL: %w", err)
}
idlCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian())
idlCodec, err := codec.NewIDLInstructionsCodec(idl, binary.LittleEndian())
if err != nil {
return nil, fmt.Errorf("failed to create codec from IDL: %w", err)
}
Expand All @@ -79,7 +79,7 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) {
return nil, fmt.Errorf("failed to create input modifications: %w", err)
}
// add mods to codec
idlCodec, err = codec.NewNamedModifierCodec(idlCodec, WrapItemType(program, method, true), modConfig)
idlCodec, err = codec.NewNamedModifierCodec(idlCodec, method, modConfig)
if err != nil {
return nil, fmt.Errorf("failed to create named codec: %w", err)
}
Expand All @@ -90,14 +90,6 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) {
return codecs, nil
}

func WrapItemType(programName, itemType string, isParams bool) string {
if isParams {
return fmt.Sprintf("params.%s.%s", programName, itemType)
}

return fmt.Sprintf("return.%s.%s", programName, itemType)
}

/*
GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list
for Solana transactions. It handles constant addresses, dynamic lookups, program-derived addresses (PDAs), and lookup tables.
Expand Down Expand Up @@ -243,7 +235,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra
}

codec := s.codecs[contractName]
encodedPayload, err := codec.Encode(ctx, args, WrapItemType(contractName, method, true))
encodedPayload, err := codec.Encode(ctx, args, method)
if err != nil {
return errorWithDebugID(fmt.Errorf("error encoding transaction payload: %w", err), debugID)
}
Expand Down
154 changes: 153 additions & 1 deletion pkg/solana/chainwriter/chain_writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package chainwriter_test
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"os"
"reflect"
"sync"
"testing"
"time"
Expand All @@ -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"
Expand All @@ -35,7 +39,155 @@ 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 := chainwriter.GetRandomPubKey(t).String()
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)

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("Submits transaction successfully", func(t *testing.T) {
rw.On("GetAccountInfoWithOpts", mock.Anything, mock.Anything, mock.Anything).Return(&rpc.GetAccountInfoResult{
RPCContext: rpc.RPCContext{},
Value: &rpc.Account{},
}, nil).Maybe()
args := map[string]interface{}{
"lookupTable": chainwriter.GetRandomPubKey(t).String(),
"seed1": []byte("data"),
}
err := cw.SubmitTransaction(ctx, "write_test", "initialize", args, "1", programID, nil, nil)
fmt.Println(err)
})
}

func TestChainWriter_GetTransactionStatus(t *testing.T) {
t.Parallel()
Expand Down
79 changes: 79 additions & 0 deletions pkg/solana/chainwriter/helpers.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
11 changes: 6 additions & 5 deletions pkg/solana/chainwriter/lookups.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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])
}
Expand Down Expand Up @@ -161,6 +161,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)
Expand Down
Loading

0 comments on commit 3749bbb

Please sign in to comment.