diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index 8b06b94c5..4346fa930 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -6,7 +6,9 @@ package relayinterface import ( "context" "encoding/binary" + "fmt" "io" + "log" "os" "path/filepath" "sync" @@ -18,20 +20,27 @@ import ( "github.com/gagliardetto/solana-go/rpc/ws" "github.com/gagliardetto/solana-go/text" "github.com/stretchr/testify/require" + "github.com/test-go/testify/mock" "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/logger" commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/types" . "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with . "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" contract "github.com/smartcontractkit/chainlink-solana/contracts/generated/contract_reader_interface" "github.com/smartcontractkit/chainlink-solana/integration-tests/solclient" "github.com/smartcontractkit/chainlink-solana/integration-tests/utils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainreader" + "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" solanautils "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) @@ -124,7 +133,9 @@ type SolanaChainComponentsInterfaceTesterHelper[T TestingT[T]] interface { Context(t T) context.Context Logger(t T) logger.Logger GetJSONEncodedIDL(t T) []byte - CreateAccount(t T, value uint64) solana.PublicKey + CreateAccount(t T, it SolanaChainComponentsInterfaceTester[T], value uint64) solana.PublicKey + TXM() *txm.TxManager + SolanaClient() *client.Client } type SolanaChainComponentsInterfaceTester[T TestingT[T]] struct { @@ -132,6 +143,7 @@ type SolanaChainComponentsInterfaceTester[T TestingT[T]] struct { Helper SolanaChainComponentsInterfaceTesterHelper[T] cr *chainreader.SolanaChainReaderService chainReaderConfig config.ChainReader + chainWriterConfig chainwriter.ChainWriterConfig } func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { @@ -179,6 +191,41 @@ func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { }, }, } + + it.chainWriterConfig = chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + AnyContractName: { + IDL: string(it.Helper.GetJSONEncodedIDL(t)), + Methods: map[string]chainwriter.MethodConfig{ + "initialize": { + FromAddress: solana.MustPrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]).PublicKey().String(), + InputModifications: nil, + ChainSpecificName: "initialize", + LookupTables: chainwriter.LookupTables{}, + Accounts: []chainwriter.Lookup{ + chainwriter.PDALookups{ + Name: "Account", + PublicKey: chainwriter.AccountConstant{ + Name: "ProgramID", + Address: programPubKey, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("data")}, + {Dynamic: chainwriter.AccountLookup{ + Name: "TestIDX", + Location: "testIdx", + }}, + }, + IsWritable: true, + IsSigner: false, + }, + }, + DebugIDLocation: "", + }, + }, + }, + }, + } } func (it *SolanaChainComponentsInterfaceTester[T]) Name() string { @@ -210,14 +257,18 @@ func (it *SolanaChainComponentsInterfaceTester[T]) GetContractReader(t T) types. } func (it *SolanaChainComponentsInterfaceTester[T]) GetContractWriter(t T) types.ContractWriter { - return nil + cw, err := chainwriter.NewSolanaChainWriterService(it.Helper.Logger(t), it.Helper.SolanaClient(), *it.Helper.TXM(), nil, it.chainWriterConfig) + require.NoError(t, err) + + servicetest.Run(t, cw) + return cw } func (it *SolanaChainComponentsInterfaceTester[T]) GetBindings(t T) []types.BoundContract { // Create a new account with fresh state for each test return []types.BoundContract{ - {Name: AnyContractName, Address: it.Helper.CreateAccount(t, AnyValueToReadWithoutAnArgument).String()}, - {Name: AnySecondContractName, Address: it.Helper.CreateAccount(t, AnyDifferentValueToReadWithoutAnArgument).String()}, + {Name: AnyContractName, Address: it.Helper.CreateAccount(t, *it, AnyValueToReadWithoutAnArgument).String()}, + {Name: AnySecondContractName, Address: it.Helper.CreateAccount(t, *it, AnyDifferentValueToReadWithoutAnArgument).String()}, } } @@ -240,6 +291,8 @@ type helper struct { idlBts []byte nonce uint64 nonceMu sync.Mutex + txm txm.TxManager + sc *client.Client } func (h *helper) Init(t *testing.T) { @@ -256,6 +309,26 @@ func (h *helper) Init(t *testing.T) { solanautils.FundAccounts(t, []solana.PrivateKey{privateKey}, h.rpcClient) + cfg := config.NewDefault() + solanaClient, err := client.NewClient(h.rpcURL, cfg, 5*time.Second, nil) + require.NoError(t, err) + + h.sc = solanaClient + + loader := commonutils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) + mkey := keyMocks.NewSimpleKeystore(t) + mkey.On("Sign", mock.Anything, privateKey.PublicKey().String(), mock.Anything).Return(func(_ context.Context, _ string, data []byte) []byte { + sig, _ := privateKey.Sign(data) + verifySignature(privateKey.PublicKey(), sig[:], data) + fmt.Printf("Signed for %s: %x\n", privateKey.PublicKey().String(), sig) + return sig[:] + }, nil) + lggr := logger.Test(t) + + txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) + txm.Start(tests.Context(t)) + h.txm = txm + pubkey, err := solana.PublicKeyFromBase58(programPubKey) require.NoError(t, err) @@ -263,10 +336,28 @@ func (h *helper) Init(t *testing.T) { h.programID = pubkey } +func verifySignature(publicKey solana.PublicKey, signature []byte, message []byte) bool { + valid := publicKey.Verify(message, solana.SignatureFromBytes(signature)) + if valid { + log.Printf("Signature is valid for public key: %s\n", publicKey.String()) + } else { + log.Printf("Signature is invalid for public key: %s\n", publicKey.String()) + } + return valid +} + func (h *helper) RPCClient() *chainreader.RPCClientWrapper { return &chainreader.RPCClientWrapper{Client: h.rpcClient} } +func (h *helper) TXM() *txm.TxManager { + return &h.txm +} + +func (h *helper) SolanaClient() *client.Client { + return h.sc +} + func (h *helper) Context(t *testing.T) context.Context { return tests.Context(t) } @@ -298,7 +389,7 @@ func (h *helper) GetJSONEncodedIDL(t *testing.T) []byte { return h.idlBts } -func (h *helper) CreateAccount(t *testing.T, value uint64) solana.PublicKey { +func (h *helper) CreateAccount(t *testing.T, it SolanaChainComponentsInterfaceTester[*testing.T], value uint64) solana.PublicKey { t.Helper() // avoid collisions in parallel tests @@ -317,7 +408,7 @@ func (h *helper) CreateAccount(t *testing.T, value uint64) solana.PublicKey { privateKey, err := solana.PrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]) require.NoError(t, err) - h.runInitialize(t, nonce, value, pubKey, func(key solana.PublicKey) *solana.PrivateKey { + h.runInitialize(t, it, nonce, value, pubKey, func(key solana.PublicKey) *solana.PrivateKey { return &privateKey }, privateKey.PublicKey()) @@ -326,6 +417,7 @@ func (h *helper) CreateAccount(t *testing.T, value uint64) solana.PublicKey { func (h *helper) runInitialize( t *testing.T, + it SolanaChainComponentsInterfaceTester[*testing.T], nonce uint64, value uint64, data solana.PublicKey, @@ -334,10 +426,33 @@ func (h *helper) runInitialize( ) { t.Helper() - inst, err := contract.NewInitializeInstruction(nonce*value, value, data, payer, solana.SystemProgramID).ValidateAndBuild() + cw := it.GetContractWriter(t) + + args := map[string]interface{}{ + "testIdx": nonce * value, + "value": value, + } + + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, nonce*value) + + data, _, err := solana.FindProgramAddress( + [][]byte{ + []byte("data"), // Seed 1 + buf, // Seed 2 (test_idx) + }, + solana.MustPublicKeyFromBase58(programPubKey), // The program ID + ) require.NoError(t, err) - h.sendInstruction(t, inst, signerFunc, payer) + fmt.Printf("Derived PDA in test: %s\n", data.String()) + + SubmitTransactionToCW(t, &it, cw, "initialize", args, types.BoundContract{Name: AnyContractName, Address: h.programID.String()}, types.Finalized) + + // inst, err := contract.NewInitializeInstruction(nonce*value, value, data, payer, solana.SystemProgramID).ValidateAndBuild() + // require.NoError(t, err) + + // h.sendInstruction(t, inst, signerFunc, payer) } func (h *helper) sendInstruction( diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 1b91dc8df..fd148abff 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -126,7 +126,7 @@ func TestAccountLookups(t *testing.T) { } func TestPDALookups(t *testing.T) { - programID := solana.SystemProgramID + programID := chainwriter.GetRandomPubKey(t) t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { seed := chainwriter.GetRandomPubKey(t) @@ -145,8 +145,8 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountConstant{Name: "seed", Address: seed.String()}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountConstant{Name: "seed", Address: seed.String()}}, }, IsSigner: false, IsWritable: true, @@ -175,9 +175,9 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}, - chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}, }, IsSigner: false, IsWritable: true, @@ -198,8 +198,8 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}}, }, IsSigner: false, IsWritable: true, @@ -233,9 +233,9 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}, - chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}, }, IsSigner: false, IsWritable: true, @@ -281,7 +281,7 @@ func TestLookupTables(t *testing.T) { table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, - StaticLookupTables: []string{table.String()}, + StaticLookupTables: []solana.PublicKey{table}, } _, staticTableMap, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, resolveErr) @@ -342,7 +342,7 @@ func TestLookupTables(t *testing.T) { lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, - StaticLookupTables: []string{invalidTable.String()}, + StaticLookupTables: []solana.PublicKey{invalidTable}, } _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) @@ -403,8 +403,8 @@ func TestLookupTables(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: false, IsWritable: false, diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index acdaf3d35..adbd4d324 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -1,7 +1,7 @@ package chainwriter import ( - "fmt" + "github.com/gagliardetto/solana-go" ) func TestConfig() { @@ -13,8 +13,8 @@ func TestConfig() { systemProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6E" computeBudgetProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6F" sysvarProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6G" - commonAddressesLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H" - routerLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I" + commonAddressesLookupTable := solana.MustPublicKeyFromBase58("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H") + routerLookupTable := solana.MustPublicKeyFromBase58("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I") userAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6J" executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` @@ -47,8 +47,8 @@ func TestConfig() { IsWritable: false, }, // Seeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, }, IsSigner: false, IsWritable: false, @@ -58,7 +58,7 @@ func TestConfig() { // Static lookup tables are the traditional use case (point 2 above) of Lookup tables. These are lookup // tables which contain commonly used addresses in all CCIP execute transactions. The ChainWriter reads // these lookup tables and appends them to the transaction to reduce the size of the transaction. - StaticLookupTables: []string{ + StaticLookupTables: []solana.PublicKey{ commonAddressesLookupTable, routerLookupTable, }, @@ -86,9 +86,9 @@ func TestConfig() { IsWritable: false, }, // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, - AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, }, IsSigner: false, IsWritable: false, @@ -120,13 +120,13 @@ func TestConfig() { IsWritable: false, }, // The seed is the receiver address. - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ Name: "Receiver", Location: "Message.Receiver", IsSigner: false, IsWritable: false, - }, + }}, }, }, // Account constant @@ -146,8 +146,8 @@ func TestConfig() { IsWritable: false, }, // The seed, once again, is the destination token address. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, }, IsSigner: false, IsWritable: false, @@ -175,9 +175,9 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{Location: "Message.Header.DestChainSelector"}, - AccountLookup{Location: "Message.Header.SourceChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, + {Dynamic: AccountLookup{Location: "Message.Header.SourceChainSelector"}}, }, IsSigner: false, IsWritable: false, @@ -191,11 +191,11 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoot", - }, + }}, }, IsSigner: false, IsWritable: false, @@ -211,9 +211,9 @@ func TestConfig() { }, // In this case, the user configured multiple seeds. These will be used in conjunction // with the public key to generate one or multiple PDA accounts. - Seeds: []Lookup{ - AccountLookup{Location: "Message.Receiver"}, - AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.Receiver"}}, + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, }, }, // Account constant @@ -255,7 +255,7 @@ func TestConfig() { InputModifications: nil, ChainSpecificName: "commit", LookupTables: LookupTables{ - StaticLookupTables: []string{ + StaticLookupTables: []solana.PublicKey{ commonAddressesLookupTable, routerLookupTable, }, @@ -284,11 +284,11 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoots", - }, + }}, }, IsSigner: false, IsWritable: false, @@ -329,5 +329,5 @@ func TestConfig() { }, }, } - fmt.Println(chainWriterConfig) + _ = chainWriterConfig } diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 8616b9f62..99cae1f3e 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -225,6 +225,10 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra codec := s.codecs[contractName] encodedPayload, err := codec.Encode(ctx, args, method) + + discriminator := GetDiscriminator(methodConfig.ChainSpecificName) + encodedPayload = append(discriminator[:], encodedPayload...) + if err != nil { return errorWithDebugID(fmt.Errorf("error encoding transaction payload: %w", err), debugID) } @@ -261,6 +265,9 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) } + accounts = append([]*solana.AccountMeta{solana.Meta(feePayer).SIGNER().WRITE()}, accounts...) + accounts = append(accounts, solana.Meta(solana.SystemProgramID)) + tx, err := solana.NewTransaction( []solana.Instruction{ solana.NewInstruction(programID, accounts, encodedPayload), @@ -274,7 +281,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Enqueue transaction - if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx, &transactionID, blockhash.Value.LastValidBlockHeight); err != nil { + if err = s.txm.Enqueue(ctx, methodConfig.FromAddress, tx, &transactionID, blockhash.Value.LastValidBlockHeight); err != nil { return errorWithDebugID(fmt.Errorf("error enqueuing transaction: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index ef7b399af..7a655d438 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -85,9 +85,9 @@ func TestChainWriter_GetAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: derivedTablePdaLookupMeta.IsSigner, IsWritable: derivedTablePdaLookupMeta.IsWritable, @@ -129,9 +129,9 @@ func TestChainWriter_GetAddresses(t *testing.T) { chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: pdaLookupMeta.IsSigner, IsWritable: pdaLookupMeta.IsWritable, @@ -272,9 +272,9 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { 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"}, + Seeds: []chainwriter.Seed{ + // extract seed1 for PDA lookup + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: true, IsWritable: true, @@ -289,9 +289,9 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "MiscPDA", PublicKey: chainwriter.AccountConstant{Name: "UnusedAccount", Address: unusedProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: true, IsWritable: true, @@ -302,7 +302,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { }, }, }, - StaticLookupTables: []string{staticLookupTablePubkey1.String(), staticLookupTablePubkey2.String()}, + StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey1, staticLookupTablePubkey2}, } args := map[string]interface{}{ @@ -428,9 +428,9 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: false, IsWritable: false, @@ -441,7 +441,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { }, }, }, - StaticLookupTables: []string{staticLookupTablePubkey.String()}, + StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey}, }, Accounts: []chainwriter.Lookup{ chainwriter.AccountConstant{ @@ -459,9 +459,9 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: false, IsWritable: false, @@ -528,7 +528,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() txID := uuid.NewString() - txm.On("Enqueue", mock.Anything, account1.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { + txm.On("Enqueue", mock.Anything, admin.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) diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 8b7276276..4618f5db6 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -3,6 +3,7 @@ package chainwriter import ( "context" "crypto/sha256" + "encoding/binary" "errors" "fmt" "reflect" @@ -41,12 +42,15 @@ func GetValuesAtLocation(args any, location string) ([][]byte, error) { if err != nil { return nil, err } - for _, value := range addressList { 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 { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, num) + vals = append(vals, buf) } else { return nil, fmt.Errorf("invalid value format at path: %s", location) } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 9f1071c46..ada2a06f7 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -33,6 +33,11 @@ type AccountLookup struct { IsWritable bool } +type Seed struct { + Static []byte // Static seed value + Dynamic Lookup // Dynamic lookup for seed +} + // PDALookups generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. type PDALookups struct { Name string @@ -40,7 +45,7 @@ type PDALookups struct { // there will be multiple PDAs generated by combining each PublicKey with the seeds. PublicKey Lookup // Seeds to be derived from an additional lookup - Seeds []Lookup + Seeds []Seed IsSigner bool IsWritable bool // OPTIONAL: On-chain location and type of desired data from PDA (e.g. a sub-account of the data account) @@ -52,14 +57,10 @@ type InternalField struct { Location string } -type ValueLookup struct { - Location string -} - // LookupTables represents a list of lookup tables that are used to derive addresses for a program. type LookupTables struct { DerivedLookupTables []DerivedLookupTable - StaticLookupTables []string + StaticLookupTables []solana.PublicKey } // DerivedLookupTable represents a lookup table that is used to derive addresses for a program. @@ -212,32 +213,37 @@ func decodeBorshIntoType(data []byte, typ reflect.Type) (interface{}, error) { // It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from input args). func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([][]byte, error) { var seedBytes [][]byte - maxSeedLength := 32 for _, seed := range lookup.Seeds { - if lookupSeed, ok := seed.(AccountLookup); ok { - // Get value from a location (This doens't have to be an address, it can be any value) - bytes, err := GetValuesAtLocation(args, lookupSeed.Location) - if err != nil { - return nil, fmt.Errorf("error getting address seed: %w", err) - } - // validate seed length - for _, b := range bytes { - if len(b) > maxSeedLength { - return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", maxSeedLength, len(b)) + if seed.Static != nil { + seedBytes = append(seedBytes, seed.Static) + } + if seed.Dynamic != nil { + dynamicSeed := seed.Dynamic + if lookupSeed, ok := dynamicSeed.(AccountLookup); ok { + // Get value from a location (This doens't have to be an address, it can be any value) + bytes, err := GetValuesAtLocation(args, lookupSeed.Location) + if err != nil { + return nil, fmt.Errorf("error getting address seed: %w", err) + } + // validate seed length + for _, b := range bytes { + if len(b) > solana.MaxSeedLength { + return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", solana.MaxSeedLength, len(b)) + } + seedBytes = append(seedBytes, b) + } + } else { + // Get address seeds from the lookup + seedAddresses, err := GetAddresses(ctx, args, []Lookup{dynamicSeed}, derivedTableMap, reader) + if err != nil { + return nil, fmt.Errorf("error getting address seed: %w", err) } - seedBytes = append(seedBytes, b) - } - } else { - // Get address seeds from the lookup - seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, reader) - if err != nil { - return nil, fmt.Errorf("error getting address seed: %w", err) - } - // Add each address seed as bytes - for _, address := range seedAddresses { - seedBytes = append(seedBytes, address.PublicKey.Bytes()) + // Add each address seed as bytes + for _, address := range seedAddresses { + seedBytes = append(seedBytes, address.PublicKey.Bytes()) + } } } } @@ -247,7 +253,7 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups) ([]*solana.AccountMeta, error) { - if len(seeds) > 16 { + if len(seeds) > solana.MaxSeeds { return nil, fmt.Errorf("seed maximum exceeded: %d", len(seeds)) } var addresses []*solana.AccountMeta @@ -271,6 +277,8 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Read derived lookup tables for _, derivedLookup := range lookupTables.DerivedLookupTables { + // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resovles to more + // than one address lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap) if err != nil { return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) @@ -289,17 +297,11 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Read static lookup tables for _, staticTable := range lookupTables.StaticLookupTables { - // Parse the static table address - tableAddress, err := solana.PublicKeyFromBase58(staticTable) - if err != nil { - return nil, nil, fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err) - } - - addressses, err := getLookupTableAddresses(ctx, s.reader, tableAddress) + addressses, err := getLookupTableAddresses(ctx, s.reader, staticTable) if err != nil { return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) } - staticTableMap[tableAddress] = addressses + staticTableMap[staticTable] = addressses } return derivedTableMap, staticTableMap, nil @@ -312,12 +314,13 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt return nil, nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) } + // Nested map in case the lookup table resolves to multiple addresses resultMap := make(map[string]map[string][]*solana.AccountMeta) var lookupTableMetas []*solana.AccountMeta // Iterate over each address of the lookup table for _, addressMeta := range lookupTableAddresses { - // Fetch account info + // Read the full list of addresses from the lookup table addresses, err := getLookupTableAddresses(ctx, reader, addressMeta.PublicKey) if err != nil { return nil, nil, fmt.Errorf("error fetching lookup table address: %w", err) diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index c87089060..06729ca63 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -253,10 +253,24 @@ func (txm *Txm) buildTx(ctx context.Context, msg pendingTx, retryCount int) (sol if err != nil { return solanaGo.Transaction{}, fmt.Errorf("error in Sign: %w", err) } + fmt.Printf("Transaction Message (hex): %x\n", txMsg) + var finalSig [64]byte copy(finalSig[:], sigBytes) newTx.Signatures = append(newTx.Signatures, finalSig) + for i, sig := range newTx.Signatures { + fmt.Printf("Signature[%d]: %x\n", i, sig) + } + + for i, account := range newTx.Message.AccountKeys { + writable, err := newTx.Message.IsWritable(account) + if err != nil { + return solanaGo.Transaction{}, fmt.Errorf("error in IsWritable: %w", err) + } + fmt.Printf("Account[%d]: %s (Signer: %v, Writable: %v)\n", i, account, newTx.Message.IsSigner(account), writable) + } + return newTx, nil }