diff --git a/.mockery.yaml b/.mockery.yaml index 1ef8d4a73..eff9ae451 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -36,4 +36,7 @@ packages: SimpleKeystore: config: filename: simple_keystore.go - case: underscore \ No newline at end of file + case: underscore + Txm: + config: + filename: txm.go diff --git a/go.mod b/go.mod index 85c6898ee..29debbdcd 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/smartcontractkit/chainlink-common v0.3.1-0.20241112140826-0e2daed34ef6 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.8.0 diff --git a/gotest.log b/gotest.log new file mode 100644 index 000000000..2589c5f6a --- /dev/null +++ b/gotest.log @@ -0,0 +1,72 @@ +📦 github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter +exit status 1 + ❌ TestLookupTables (30.07s) + ports.go:37: found open port: 54520 + ports.go:37: found open port: 8536 + test_helpers.go:50: API server not ready yet (attempt 1) + test_helpers.go:50: API server not ready yet (attempt 2) + test_helpers.go:50: API server not ready yet (attempt 3) + test_helpers.go:50: API server not ready yet (attempt 4) + test_helpers.go:50: API server not ready yet (attempt 5) + test_helpers.go:50: API server not ready yet (attempt 6) + test_helpers.go:50: API server not ready yet (attempt 7) + test_helpers.go:50: API server not ready yet (attempt 8) + test_helpers.go:50: API server not ready yet (attempt 9) + test_helpers.go:50: API server not ready yet (attempt 10) + test_helpers.go:50: API server not ready yet (attempt 11) + test_helpers.go:50: API server not ready yet (attempt 12) + test_helpers.go:50: API server not ready yet (attempt 13) + test_helpers.go:50: API server not ready yet (attempt 14) + test_helpers.go:50: API server not ready yet (attempt 15) + test_helpers.go:50: API server not ready yet (attempt 16) + test_helpers.go:50: API server not ready yet (attempt 17) + test_helpers.go:50: API server not ready yet (attempt 18) + test_helpers.go:50: API server not ready yet (attempt 19) + test_helpers.go:50: API server not ready yet (attempt 20) + test_helpers.go:50: API server not ready yet (attempt 21) + test_helpers.go:50: API server not ready yet (attempt 22) + test_helpers.go:50: API server not ready yet (attempt 23) + test_helpers.go:50: API server not ready yet (attempt 24) + test_helpers.go:50: API server not ready yet (attempt 25) + test_helpers.go:50: API server not ready yet (attempt 26) + test_helpers.go:50: API server not ready yet (attempt 27) + test_helpers.go:50: API server not ready yet (attempt 28) + test_helpers.go:50: API server not ready yet (attempt 29) + test_helpers.go:50: API server not ready yet (attempt 30) + test_helpers.go:57: Cmd output: + Notice! No wallet available. `solana airdrop` localnet SOL after creating one + + Ledger location: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001 + Log: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001/validator.log + Initializing... + Error: failed to start validator: Failed to create ledger at /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001: blockstore error + + Cmd error: + test_helpers.go:59: + Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:59 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:148 + Error: Should be true + Test: TestLookupTables + test_helpers.go:37: + Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:37 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1176 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1354 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1684 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/panic.go:629 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1006 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:59 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:148 + Error: "exit status 1" does not contain "signal: killed" + Test: TestLookupTables + Messages: exit status 1 + test_helpers.go:38: solana-test-validator + stdout: + Notice! No wallet available. `solana airdrop` localnet SOL after creating one + + Ledger location: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001 + Log: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001/validator.log + Initializing... + Error: failed to start validator: Failed to create ledger at /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001: blockstore error + + stderr: + diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 93be11058..ef5e3eeff 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -212,7 +212,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Fetch derived and static table maps - derivedTableMap, staticTableMap, err := s.getDerivedTableMap(ctx, methodConfig.LookupTables, debugID) + derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, methodConfig.LookupTables, debugID) if err != nil { return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 520345104..d8f5cddda 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/gagliardetto/solana-go" + addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" ) @@ -181,7 +182,7 @@ func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALo return addresses, nil } -func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { +func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) @@ -211,23 +212,8 @@ func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, looku return nil, nil, errorWithDebugID(fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err), debugID) } - // Fetch the account info for the static table - accountInfo, err := s.reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ - Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, - }) - if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for static table: %s, error: %w", staticTable, err), debugID) - } - - // Decode the account data into an array of public keys - addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error decoding static lookup table data for %s: %w", staticTable, err), debugID) - } - - // Add the static lookup table to the map - staticTableMap[tableAddress] = addresses + addressses, err := getLookupTableAddress(ctx, s.reader, tableAddress, debugID) + staticTableMap[tableAddress] = addressses } return derivedTableMap, staticTableMap, nil @@ -246,18 +232,9 @@ func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context // Iterate over each address of the lookup table for _, addressMeta := range lookupTableAddresses { // Fetch account info - accountInfo, err := reader.GetAccountInfoWithOpts(ctx, addressMeta.PublicKey, &rpc.GetAccountInfoOpts{ - Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, - }) - if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for address %s: %w", addressMeta.PublicKey.String(), err), debugID) - } - - // Decode the account data into an array of public keys - addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) + addresses, err := getLookupTableAddress(ctx, reader, addressMeta.PublicKey, debugID) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error decoding lookup table data for address %s: %w", addressMeta.PublicKey.String(), err), debugID) + return nil, nil, errorWithDebugID(fmt.Errorf("error fetching lookup table address: %w", err), debugID) } // Create the inner map for this lookup table @@ -281,18 +258,19 @@ func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context return resultMap, lookupTableMetas, nil } -func decodeLookupTable(data []byte) (solana.PublicKeySlice, error) { - // Example logic to decode lookup table data; you may need to adjust based on the actual format of the data. - var addresses solana.PublicKeySlice +func getLookupTableAddress(ctx context.Context, reader client.Reader, tableAddress solana.PublicKey, debugID string) (solana.PublicKeySlice, error) { + // Fetch the account info for the static table + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentConfirmed, + }) - // Assuming the data is a list of 32-byte public keys in binary format: - for i := 0; i < len(data); i += solana.PublicKeyLength { - if i+solana.PublicKeyLength > len(data) { - return nil, fmt.Errorf("invalid lookup table data length") - } - address := solana.PublicKeyFromBytes(data[i : i+solana.PublicKeyLength]) - addresses = append(addresses, address) + if err != nil || accountInfo == nil || accountInfo.Value == nil { + return nil, errorWithDebugID(fmt.Errorf("error fetching account info for table: %s, error: %w", tableAddress.String(), err), debugID) } - - return addresses, nil + alt, err := addresslookuptable.DecodeAddressLookupTableState(accountInfo.GetBinary()) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error decoding address lookup table state: %w", err), debugID) + } + return alt.Addresses, nil } diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go new file mode 100644 index 000000000..d1a05171d --- /dev/null +++ b/pkg/solana/chainwriter/lookups_test.go @@ -0,0 +1,369 @@ +package chainwriter_test + +import ( + "context" + "fmt" + "testing" + "time" + + "encoding/binary" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "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-common/pkg/utils" + "github.com/test-go/testify/require" +) + +type TestArgs struct { + Inner []InnerArgs +} + +type InnerArgs struct { + Address []byte +} + +func TestAccountContant(t *testing.T) { + + t.Run("AccountConstant resolves valid address", func(t *testing.T) { + expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + IsSigner: true, + IsWritable: true, + }, + } + constantConfig := chainwriter.AccountConstant{ + Name: "TestAccount", + Address: expectedAddr, + IsSigner: true, + IsWritable: true, + } + result, err := constantConfig.Resolve(nil, nil, nil, "") + require.NoError(t, err) + require.Equal(t, expectedMeta, result) + }) +} +func TestAccountLookups(t *testing.T) { + t.Run("AccountLookup resolves valid address with just one address", func(t *testing.T) { + expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + testArgs := TestArgs{ + Inner: []InnerArgs{ + {Address: solana.MustPublicKeyFromBase58(expectedAddr).Bytes()}, + }, + } + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + IsSigner: true, + IsWritable: true, + }, + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "TestAccount", + Location: "Inner.Address", + IsSigner: true, + IsWritable: true, + } + result, err := lookupConfig.Resolve(nil, testArgs, 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 := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + expectedAddr2 := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6N" + testArgs := TestArgs{ + Inner: []InnerArgs{ + {Address: solana.MustPublicKeyFromBase58(expectedAddr1).Bytes()}, + {Address: solana.MustPublicKeyFromBase58(expectedAddr2).Bytes()}, + }, + } + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr1), + IsSigner: true, + IsWritable: true, + }, + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr2), + IsSigner: true, + IsWritable: true, + }, + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "TestAccount", + Location: "Inner.Address", + IsSigner: true, + IsWritable: true, + } + result, err := lookupConfig.Resolve(nil, testArgs, nil, "") + require.NoError(t, err) + for i, meta := range result { + require.Equal(t, expectedMeta[i], meta) + } + }) + + t.Run("AccountLookup fails when address isn't in args", func(t *testing.T) { + expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + testArgs := TestArgs{ + Inner: []InnerArgs{ + {Address: solana.MustPublicKeyFromBase58(expectedAddr).Bytes()}, + }, + } + lookupConfig := chainwriter.AccountLookup{ + Name: "InvalidAccount", + Location: "Invalid.Directory", + IsSigner: true, + IsWritable: true, + } + _, err := lookupConfig.Resolve(nil, testArgs, nil, "") + require.Error(t, err) + }) +} + +func TestPDALookups(t *testing.T) { + // TODO: May require deploying a program to test + // t.Run("PDALookup resolves valid address", func(t *testing.T) { + // expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + // expectedMeta := []*solana.AccountMeta{ + // { + // PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + // IsSigner: true, + // IsWritable: true, + // }, + // } + // lookupConfig := chainwriter.PDALookups{ + // Name: "TestAccount", + // PublicKey: + // } + + // }) +} + +func TestLookupTables(t *testing.T) { + ctx := tests.Context(t) + url := client.SetupLocalSolNode(t) + c := rpc.New(url) + + sender, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + client.FundTestAccounts(t, []solana.PublicKey{sender.PublicKey()}, url) + + cfg := config.NewDefault() + solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) + + loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) + mkey := keyMocks.NewSimpleKeystore(t) + lggr := logger.Test(t) + + txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) + + chainWriter, err := chainwriter.NewSolanaChainWriterService(solanaClient, *txm, nil, chainwriter.ChainWriterConfig{}) + + t.Run("StaticLookup table resolves properly", func(t *testing.T) { + pubKeys := createTestPubKeys(t, 8) + table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: nil, + StaticLookupTables: []string{table.String()}, + } + _, staticTableMap, err := chainWriter.ResolveLookupTables(ctx, lookupConfig, "test-debug-id") + require.NoError(t, err) + require.Equal(t, pubKeys, staticTableMap[table]) + }) + + t.Run("Derived lookup table resovles properly with static address", func(t *testing.T) { + pubKeys := createTestPubKeys(t, 8) + table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.AccountConstant{ + Name: "TestLookupTable", + Address: table.String(), + IsSigner: true, + IsWritable: true, + }, + }, + }, + StaticLookupTables: nil, + } + derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, lookupConfig, "test-debug-id") + require.NoError(t, err) + require.Equal(t, pubKeys, derivedTableMap["DerivedTable"][table.String()]) + }) +} + +func createTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { + addresses := make([]solana.PublicKey, num) + for i := 0; i < num; i++ { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + addresses[i] = privKey.PublicKey() + } + return addresses +} + +func CreateTestLookupTable(t *testing.T, ctx context.Context, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { + // Create lookup tables + slot, serr := c.GetSlot(ctx, rpc.CommitmentFinalized) + fmt.Println("SLOT: ", slot) + require.NoError(t, serr) + table, instruction, ierr := NewCreateLookupTableInstruction( + sender.PublicKey(), + sender.PublicKey(), + slot, + ) + require.NoError(t, ierr) + SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) + + // add entries to lookup table + SendAndConfirm(ctx, t, c, []solana.Instruction{ + NewExtendLookupTableInstruction( + table, sender.PublicKey(), sender.PublicKey(), + addresses, + ), + }, sender, rpc.CommitmentConfirmed) + + return table +} + +// TxModifier is a dynamic function used to flexibly add components to a transaction such as additional signers, and compute budget parameters +type TxModifier func(tx *solana.Transaction, signers map[solana.PublicKey]solana.PrivateKey) error + +func SendAndConfirm(ctx context.Context, t *testing.T, rpcClient *rpc.Client, instructions []solana.Instruction, + signer solana.PrivateKey, commitment rpc.CommitmentType, opts ...TxModifier) *rpc.GetTransactionResult { + txres := sendTransaction(ctx, rpcClient, t, instructions, signer, commitment, false, opts...) // do not skipPreflight when expected to pass, preflight can help debug + + require.NotNil(t, txres.Meta) + require.Nil(t, txres.Meta.Err, fmt.Sprintf("tx failed with: %+v", txres.Meta)) // tx should not err, print meta if it does (contains logs) + return txres +} + +func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, instructions []solana.Instruction, + signerAndPayer solana.PrivateKey, commitment rpc.CommitmentType, skipPreflight bool, opts ...TxModifier) *rpc.GetTransactionResult { + hashRes, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + require.NoError(t, err) + + tx, err := solana.NewTransaction( + instructions, + hashRes.Value.Blockhash, + solana.TransactionPayer(signerAndPayer.PublicKey()), + ) + require.NoError(t, err) + + // build signers map + signers := map[solana.PublicKey]solana.PrivateKey{} + signers[signerAndPayer.PublicKey()] = signerAndPayer + + // set options before signing transaction + for _, o := range opts { + require.NoError(t, o(tx, signers)) + } + + _, err = tx.Sign(func(pub solana.PublicKey) *solana.PrivateKey { + priv, ok := signers[pub] + require.True(t, ok, fmt.Sprintf("Missing signer private key for %s", pub)) + return &priv + }) + require.NoError(t, err) + + txsig, err := rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{SkipPreflight: skipPreflight, PreflightCommitment: rpc.CommitmentProcessed}) + require.NoError(t, err) + + var txStatus rpc.ConfirmationStatusType + count := 0 + for txStatus != rpc.ConfirmationStatusConfirmed && txStatus != rpc.ConfirmationStatusFinalized { + count++ + statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) + require.NoError(t, sigErr) + if statusRes != nil && len(statusRes.Value) > 0 && statusRes.Value[0] != nil { + txStatus = statusRes.Value[0].ConfirmationStatus + } + time.Sleep(100 * time.Millisecond) + if count > 50 { + require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) + } + } + + txres, err := rpcClient.GetTransaction(ctx, txsig, &rpc.GetTransactionOpts{ + Commitment: commitment, + }) + require.NoError(t, err) + return txres +} + +var ( + AddressLookupTableProgram = solana.MustPublicKeyFromBase58("AddressLookupTab1e1111111111111111111111111") +) + +const ( + InstructionCreateLookupTable uint32 = iota + InstructionFreezeLookupTable + InstructionExtendLookupTable + InstructionDeactiveLookupTable + InstructionCloseLookupTable +) + +func NewCreateLookupTableInstruction( + authority, funder solana.PublicKey, + slot uint64, +) (solana.PublicKey, solana.Instruction, error) { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L274 + slotLE := make([]byte, 8) + binary.LittleEndian.PutUint64(slotLE, slot) + account, bumpSeed, err := solana.FindProgramAddress([][]byte{authority.Bytes(), slotLE}, AddressLookupTableProgram) + if err != nil { + return solana.PublicKey{}, nil, err + } + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionCreateLookupTable) + data = binary.LittleEndian.AppendUint64(data, slot) + data = append(data, bumpSeed) + return account, solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(account).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ), nil +} + +func NewExtendLookupTableInstruction( + table, authority, funder solana.PublicKey, + accounts []solana.PublicKey, +) solana.Instruction { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L113 + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionExtendLookupTable) + data = binary.LittleEndian.AppendUint64(data, uint64(len(accounts))) // note: this is usually u32 + 8 byte buffer + for _, a := range accounts { + data = append(data, a.Bytes()...) + } + + return solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(table).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ) +}