diff --git a/contracts/Anchor.toml b/contracts/Anchor.toml index 78a2222ad..0f8fd5b99 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -27,6 +27,7 @@ test = "pnpm run test" [programs.localnet] access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW" -log-read-test = "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4" +log_read_test = "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4" ocr_2 = "cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ" # need to rename the idl to satisfy anchor.js... -store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" \ No newline at end of file +store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" +write_test = "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU" \ No newline at end of file diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 953b2b81e..43a9fa6c6 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2664,6 +2664,13 @@ dependencies = [ "memchr", ] +[[package]] +name = "write-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/contracts/artifacts/localnet/write_test-keypair.json b/contracts/artifacts/localnet/write_test-keypair.json new file mode 100644 index 000000000..c4e6e125c --- /dev/null +++ b/contracts/artifacts/localnet/write_test-keypair.json @@ -0,0 +1 @@ +[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 diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index 860108de1..b7cec1551 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -13,13 +13,13 @@ importers: version: link:../ts '@coral-xyz/anchor': specifier: ^0.29.0 - version: 0.29.0 + version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@solana/spl-token': specifier: ^0.3.5 - version: 0.3.11(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22) + version: 0.3.11(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10) '@solana/web3.js': specifier: ^1.50.1 <=1.92.3 - version: 1.92.3 + version: 1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@types/chai': specifier: ^4.2.22 version: 4.3.12 @@ -893,11 +893,11 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@coral-xyz/anchor@0.29.0': + '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: - '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.95.3) + '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@noble/hashes': 1.5.0 - '@solana/web3.js': 1.95.3 + '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 @@ -914,9 +914,9 @@ snapshots: - encoding - utf-8-validate - '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.95.3)': + '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@solana/web3.js': 1.95.3 + '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 buffer-layout: 1.2.2 @@ -926,10 +926,10 @@ snapshots: '@noble/hashes@1.5.0': {} - '@solana/buffer-layout-utils@0.2.0': + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.95.3 + '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) bigint-buffer: 1.1.5 bignumber.js: 9.1.2 transitivePeerDependencies: @@ -963,7 +963,7 @@ snapshots: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-numbers': 2.0.0-experimental.8618508 - '@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)': + '@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-data-structures': 2.0.0-experimental.8618508 @@ -971,16 +971,16 @@ snapshots: '@solana/codecs-strings': 2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22) '@solana/options': 2.0.0-experimental.8618508 '@solana/spl-type-length-value': 0.1.0 - '@solana/web3.js': 1.92.3 + '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/spl-token@0.3.11(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)': + '@solana/spl-token@0.3.11(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0 - '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22) - '@solana/web3.js': 1.92.3 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil @@ -992,7 +992,7 @@ snapshots: dependencies: buffer: 6.0.3 - '@solana/web3.js@1.92.3': + '@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.6 '@noble/curves': 1.6.0 @@ -1005,7 +1005,7 @@ snapshots: bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.1.2 + jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) node-fetch: 2.7.0 rpc-websockets: 8.0.1 superstruct: 1.0.4 @@ -1014,7 +1014,7 @@ snapshots: - encoding - utf-8-validate - '@solana/web3.js@1.95.3': + '@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.6 '@noble/curves': 1.6.0 @@ -1027,7 +1027,7 @@ snapshots: bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.1.2 + jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) node-fetch: 2.7.0 rpc-websockets: 9.0.2 superstruct: 2.0.2 @@ -1185,6 +1185,7 @@ snapshots: bufferutil@4.0.8: dependencies: node-gyp-build: 4.8.2 + optional: true camelcase@6.3.0: {} @@ -1268,6 +1269,7 @@ snapshots: debug@4.3.3(supports-color@8.1.1): dependencies: ms: 2.1.2 + optionalDependencies: supports-color: 8.1.1 decamelize@4.0.0: {} @@ -1433,11 +1435,11 @@ snapshots: isexe@2.0.0: {} - isomorphic-ws@4.0.1(ws@7.5.10): + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) - jayson@4.1.2: + jayson@4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@types/connect': 3.4.38 '@types/node': 12.20.55 @@ -1447,10 +1449,10 @@ snapshots: delay: 5.0.0 es6-promisify: 5.0.0 eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@7.5.10) + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) json-stringify-safe: 5.0.1 uuid: 8.3.2 - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -1767,6 +1769,7 @@ snapshots: utf-8-validate@5.0.10: dependencies: node-gyp-build: 4.8.2 + optional: true util-deprecate@1.0.2: {} @@ -1793,10 +1796,13 @@ snapshots: wrappy@1.0.2: {} - ws@7.5.10: {} + ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): - dependencies: + optionalDependencies: bufferutil: 4.0.8 utf-8-validate: 5.0.10 diff --git a/contracts/programs/write_test/Cargo.toml b/contracts/programs/write_test/Cargo.toml new file mode 100644 index 000000000..ee46888c6 --- /dev/null +++ b/contracts/programs/write_test/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "write-test" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "write_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.29.0" diff --git a/contracts/programs/write_test/Xargo.toml b/contracts/programs/write_test/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/contracts/programs/write_test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/programs/write_test/src/lib.rs b/contracts/programs/write_test/src/lib.rs new file mode 100644 index 000000000..4078bca4d --- /dev/null +++ b/contracts/programs/write_test/src/lib.rs @@ -0,0 +1,52 @@ +use anchor_lang::prelude::*; + +declare_id!("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU"); + +#[program] +pub mod write_test { + use super::*; + + pub fn initialize(ctx: Context, lookup_table: Pubkey) -> Result<()> { + let data = &mut ctx.accounts.data_account; + data.version = 1; + data.administrator = ctx.accounts.admin.key(); + data.pending_administrator = Pubkey::default(); + data.lookup_table = lookup_table; + + Ok(()) + } + +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + /// PDA account, derived from seeds and created by the System Program in this instruction + #[account( + init, // Initialize the account + payer = admin, // Specify the payer + space = DataAccount::SIZE, // Specify the account size + seeds = [b"data"], // Define the PDA seeds + bump // Use the bump seed + )] + pub data_account: Account<'info, DataAccount>, + + /// Admin account that pays for PDA creation and signs the transaction + #[account(mut)] + pub admin: Signer<'info>, + + /// System Program is required for PDA creation + pub system_program: Program<'info, System>, +} + +#[account] +pub struct DataAccount { + pub version: u8, + pub administrator: Pubkey, + pub pending_administrator: Pubkey, + pub lookup_table: Pubkey, +} + +impl DataAccount { + /// The total size of the `DataAccount` struct, including the discriminator + pub const SIZE: usize = 8 + 1 + 32 * 3; // 8 bytes for discriminator + 1 byte for version + 32 bytes * 3 pubkeys +} diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 7dea709aa..608f3c610 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -131,10 +131,10 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program */ // GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list // for Solana transactions. -func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) { var addresses []*solana.AccountMeta for _, accountConfig := range accounts { - meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, debugID) + meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, reader) if err != nil { return nil, err } @@ -147,7 +147,6 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( accounts []*solana.AccountMeta, derivedTableMap map[string]map[string][]*solana.AccountMeta, staticTableMap map[solana.PublicKey]solana.PublicKeySlice, - debugID string, ) map[solana.PublicKey]solana.PublicKeySlice { filteredLookupTables := make(map[solana.PublicKey]solana.PublicKeySlice) @@ -162,7 +161,7 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( for innerIdentifier, metas := range innerMap { tableKey, err := solana.PublicKeyFromBase58(innerIdentifier) if err != nil { - errorWithDebugID(fmt.Errorf("error parsing lookup table key: %w", err), debugID) + fmt.Errorf("error parsing lookup table key: %w", err) } // Collect public keys that are actually used @@ -212,19 +211,19 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Fetch derived and static table maps - derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables, debugID) + derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables) if err != nil { return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } // Resolve account metas - accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, debugID) + accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, s.reader) if err != nil { return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) } // Filter the lookup table addresses based on which accounts are actually used - filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap, debugID) + filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) // Fetch latest blockhash blockhash, err := s.reader.LatestBlockhash(ctx) diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index c2a143d98..4d5d00600 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -10,9 +10,8 @@ import ( ) // GetValuesAtLocation parses through nested types and arrays to find all locations of values -func GetValuesAtLocation(args any, location string, debugID string) ([][]byte, error) { +func GetValuesAtLocation(args any, location string) ([][]byte, error) { var vals [][]byte - path := strings.Split(location, ".") addressList, err := traversePath(args, path) @@ -26,7 +25,7 @@ func GetValuesAtLocation(args any, location string, debugID string) ([][]byte, e } else if address, ok := value.(solana.PublicKey); ok { vals = append(vals, address.Bytes()) } else { - return nil, errorWithDebugID(fmt.Errorf("invalid value format at path: %s", location), debugID) + return nil, fmt.Errorf("invalid value format at path: %s", location) } } @@ -85,7 +84,6 @@ func traversePath(data any, path []string) ([]any, error) { if val.Kind() == reflect.Ptr { val = val.Elem() } - fmt.Printf("Current path: %v, Current value type: %v\n", path, val.Kind()) switch val.Kind() { case reflect.Struct: diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index c16e442b2..1aa9ae92d 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -3,7 +3,9 @@ package chainwriter import ( "context" "fmt" + "reflect" + 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" @@ -11,7 +13,7 @@ import ( ) type Lookup interface { - Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) + Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) } // AccountConstant represents a fixed address, provided in Base58 format, converted into a `solana.PublicKey`. @@ -40,6 +42,13 @@ type PDALookups struct { Seeds []Lookup IsSigner bool IsWritable bool + // OPTIONAL: On-chain location and type of desired data from PDA (e.g. a sub-account of the data account) + InternalField InternalField +} + +type InternalField struct { + Type reflect.Type + Location string } type ValueLookup struct { @@ -64,10 +73,10 @@ type AccountsFromLookupTable struct { IncludeIndexes []int } -func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { address, err := solana.PublicKeyFromBase58(ac.Address) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting account from constant: %w", err), debugID) + return nil, fmt.Errorf("error getting account from constant: %w", err) } return []*solana.AccountMeta{ { @@ -78,10 +87,10 @@ func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[str }, nil } -func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - derivedValues, err := GetValuesAtLocation(args, al.Location, debugID) +func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { + derivedValues, err := GetValuesAtLocation(args, al.Location) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting account from lookup: %w", err), debugID) + return nil, fmt.Errorf("error getting account from lookup: %w", err) } var metas []*solana.AccountMeta @@ -95,11 +104,11 @@ func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[st return metas, nil } -func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +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] if !ok { - return nil, errorWithDebugID(fmt.Errorf("lookup table not found: %s", alt.LookupTablesName), debugID) + return nil, fmt.Errorf("lookup table not found: %s", alt.LookupTablesName) } var result []*solana.AccountMeta @@ -116,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, errorWithDebugID(fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName), debugID) + return nil, fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName) } result = append(result, metas[index]) } @@ -125,39 +134,97 @@ func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTabl return result, nil } -func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, debugID) +func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) { + publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, reader) + if err != nil { + return nil, fmt.Errorf("error getting public key for PDALookups: %w", err) + } + + seeds, err := getSeedBytes(ctx, pda, args, derivedTableMap, reader) + if err != nil { + return nil, fmt.Errorf("error getting seeds for PDALookups: %w", err) + } + + pdas, err := generatePDAs(publicKeys, seeds, pda) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookups: %w", err), debugID) + return nil, fmt.Errorf("error generating PDAs: %w", err) + } + + if pda.InternalField.Location == "" { + return pdas, nil + } + + // If a decoded location is specified, fetch the data at that location + var result []*solana.AccountMeta + for _, accountMeta := range pdas { + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, accountMeta.PublicKey, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentFinalized, + }) + + 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) + } + + decoded, err := decodeBorshIntoType(accountInfo.GetBinary(), pda.InternalField.Type) + if err != nil { + return nil, fmt.Errorf("error decoding Borsh data dynamically: %w", err) + } + + value, err := GetValuesAtLocation(decoded, pda.InternalField.Location) + if err != nil { + return nil, fmt.Errorf("error getting value at location: %w", err) + } + if len(value) > 1 { + return nil, fmt.Errorf("multiple values found at location: %s", pda.InternalField.Location) + } + + result = append(result, &solana.AccountMeta{ + PublicKey: solana.PublicKeyFromBytes(value[0]), + IsSigner: accountMeta.IsSigner, + IsWritable: accountMeta.IsWritable, + }) + } + return result, nil +} + +func decodeBorshIntoType(data []byte, typ reflect.Type) (interface{}, error) { + // Ensure the type is a struct + if typ.Kind() != reflect.Struct { + return nil, fmt.Errorf("provided type is not a struct: %s", typ.Kind()) } - seeds, err := getSeedBytes(ctx, pda, args, derivedTableMap, debugID) + // Create a new instance of the type + instance := reflect.New(typ).Interface() + + // Decode using Borsh + err := ag_binary.NewBorshDecoder(data).Decode(instance) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookups: %w", err), debugID) + return nil, fmt.Errorf("error decoding Borsh data: %w", err) } - return generatePDAs(publicKeys, seeds, pda, debugID) + // Return the underlying value (not a pointer) + return reflect.ValueOf(instance).Elem().Interface(), nil } // getSeedBytes extracts the seeds for the PDALookups. // 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, debugID string) ([][]byte, error) { +func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([][]byte, error) { var seedBytes [][]byte - // Process AddressSeeds first (e.g., public keys) for _, seed := range lookup.Seeds { if lookupSeed, ok := seed.(AccountLookup); ok { - // Get the values at the seed location - bytes, err := GetValuesAtLocation(args, lookupSeed.Location, debugID) + // 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, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + return nil, fmt.Errorf("error getting address seed: %w", err) } seedBytes = append(seedBytes, bytes...) } else { - // Get the address(es) at the seed location - seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, debugID) + // Get address seeds from the lookup + seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, reader) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + return nil, fmt.Errorf("error getting address seed: %w", err) } // Add each address seed as bytes @@ -165,23 +232,22 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable seedBytes = append(seedBytes, address.PublicKey.Bytes()) } } - } return seedBytes, nil } // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. -func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups, debugID string) ([]*solana.AccountMeta, error) { +func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups) ([]*solana.AccountMeta, error) { if len(seeds) > 1 && len(publicKeys) > 1 { - return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) + return nil, fmt.Errorf("multiple public keys and multiple seeds are not allowed") } var addresses []*solana.AccountMeta for _, publicKeyMeta := range publicKeys { address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error finding program address: %w", err), debugID) + return nil, fmt.Errorf("error finding program address: %w", err) } addresses = append(addresses, &solana.AccountMeta{ PublicKey: address, @@ -192,15 +258,15 @@ func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALo return addresses, nil } -func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args any, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { +func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args any, lookupTables LookupTables) (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) // Read derived lookup tables for _, derivedLookup := range lookupTables.DerivedLookupTables { - lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap, debugID) + lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error loading derived lookup table: %w", err), debugID) + return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) } // Merge the loaded table map into the result @@ -219,21 +285,24 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Parse the static table address tableAddress, err := solana.PublicKeyFromBase58(staticTable) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err), debugID) + return nil, nil, fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err) } - addressses, err := getLookupTableAddress(ctx, s.reader, tableAddress, debugID) + addressses, err := getLookupTableAddress(ctx, s.reader, tableAddress) + if err != nil { + return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) + } staticTableMap[tableAddress] = addressses } return derivedTableMap, staticTableMap, nil } -func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt DerivedLookupTable, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { +func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt DerivedLookupTable, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { // Resolve all addresses specified by the identifier - lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, debugID) + lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, reader) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) + return nil, nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) } resultMap := make(map[string]map[string][]*solana.AccountMeta) @@ -242,9 +311,9 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt // Iterate over each address of the lookup table for _, addressMeta := range lookupTableAddresses { // Fetch account info - addresses, err := getLookupTableAddress(ctx, reader, addressMeta.PublicKey, debugID) + addresses, err := getLookupTableAddress(ctx, reader, addressMeta.PublicKey) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching lookup table address: %w", err), debugID) + return nil, nil, fmt.Errorf("error fetching lookup table address: %w", err) } // Create the inner map for this lookup table @@ -268,19 +337,19 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt return resultMap, lookupTableMetas, nil } -func getLookupTableAddress(ctx context.Context, reader client.Reader, tableAddress solana.PublicKey, debugID string) (solana.PublicKeySlice, error) { +func getLookupTableAddress(ctx context.Context, reader client.Reader, tableAddress solana.PublicKey) (solana.PublicKeySlice, error) { // Fetch the account info for the static table accountInfo, err := reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, + Commitment: rpc.CommitmentFinalized, }) 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 nil, fmt.Errorf("error fetching account info for table: %s, error: %w", tableAddress.String(), err) } alt, err := addresslookuptable.DecodeAddressLookupTableState(accountInfo.GetBinary()) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error decoding address lookup table state: %w", err), debugID) + return nil, fmt.Errorf("error decoding address lookup table state: %w", err) } return alt.Addresses, nil } diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go index a196fa2d1..984f3eb92 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/pkg/solana/chainwriter/lookups_test.go @@ -2,6 +2,8 @@ package chainwriter_test import ( "context" + "crypto/sha256" + "reflect" "testing" "time" @@ -28,6 +30,14 @@ type InnerArgs struct { Address []byte } +type DataAccount struct { + Discriminator [8]byte + Version uint8 + Administrator solana.PublicKey + PendingAdministrator solana.PublicKey + LookupTable solana.PublicKey +} + func TestAccountContant(t *testing.T) { t.Run("AccountConstant resolves valid address", func(t *testing.T) { expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" @@ -44,7 +54,7 @@ func TestAccountContant(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := constantConfig.Resolve(nil, nil, nil, "") + result, err := constantConfig.Resolve(nil, nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -71,7 +81,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, "") + result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -104,7 +114,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, "") + result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) require.NoError(t, err) for i, meta := range result { require.Equal(t, expectedMeta[i], meta) @@ -124,7 +134,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - _, err := lookupConfig.Resolve(nil, testArgs, nil, "") + _, err := lookupConfig.Resolve(nil, testArgs, nil, nil) require.Error(t, err) }) } @@ -159,7 +169,7 @@ func TestPDALookups(t *testing.T) { } ctx := context.Background() - result, err := pdaLookup.Resolve(ctx, nil, nil, "") + result, err := pdaLookup.Resolve(ctx, nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -195,11 +205,34 @@ func TestPDALookups(t *testing.T) { "another_seed": seed2, } - result, err := pdaLookup.Resolve(ctx, args, nil, "") + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) + 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()}, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}, + }, + IsSigner: false, + IsWritable: true, + } + + ctx := context.Background() + args := map[string]interface{}{ + "test_seed": []byte("data"), + } + + _, err := pdaLookup.Resolve(ctx, args, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "key not found") + }) + t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { privKey1, err := solana.NewRandomPrivateKey() require.NoError(t, err) @@ -237,7 +270,7 @@ func TestPDALookups(t *testing.T) { "another_seed": seed2, } - result, err := pdaLookup.Resolve(ctx, args, nil, "") + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -245,15 +278,18 @@ func TestPDALookups(t *testing.T) { 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) - utils.FundAccounts(ctx, []solana.PrivateKey{sender}, c, t) + + url := utils.SetupTestValidatorWithAnchorPrograms(t, utils.PathToAnchorConfig, sender.PublicKey().String()) + rpcClient := rpc.New(url) + + utils.FundAccounts(ctx, []solana.PrivateKey{sender}, rpcClient, t) cfg := config.NewDefault() solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) + require.NoError(t, err) loader := commonutils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) mkey := keyMocks.NewSimpleKeystore(t) @@ -261,22 +297,22 @@ func TestLookupTables(t *testing.T) { txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) - chainWriter, 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(t, ctx, c, sender, pubKeys) + table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, StaticLookupTables: []string{table.String()}, } - _, staticTableMap, err := chainWriter.ResolveLookupTables(ctx, nil, lookupConfig, "test-debug-id") + _, staticTableMap, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, err) require.Equal(t, pubKeys, staticTableMap[table]) }) - t.Run("Derived lookup table resovles properly with constant address", func(t *testing.T) { + t.Run("Derived lookup table resolves properly with constant address", func(t *testing.T) { pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -291,7 +327,7 @@ func TestLookupTables(t *testing.T) { }, StaticLookupTables: nil, } - derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, nil, lookupConfig, "test-debug-id") + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, err) addresses, ok := derivedTableMap["DerivedTable"][table.String()] @@ -301,9 +337,49 @@ func TestLookupTables(t *testing.T) { } }) + t.Run("Derived lookup table fails with invalid address", func(t *testing.T) { + pk, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + invalidTable := pk.PublicKey().String() + + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.AccountConstant{ + Name: "InvalidTable", + Address: invalidTable, + IsSigner: true, + IsWritable: true, + }, + }, + }, + StaticLookupTables: nil, + } + + _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.Error(t, err) + require.Contains(t, err.Error(), "error fetching account info for table") // Example error message + }) + + t.Run("Static lookup table fails with invalid address", func(t *testing.T) { + pk, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + invalidTable := pk.PublicKey().String() + + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: nil, + StaticLookupTables: []string{invalidTable}, + } + + _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.Error(t, err) + require.Contains(t, err.Error(), "error fetching account info for table") // Example error message + }) + t.Run("Derived lookup table resolves properly with account lookup address", func(t *testing.T) { pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -324,7 +400,7 @@ func TestLookupTables(t *testing.T) { }, } - derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, testArgs, lookupConfig, "test-debug-id") + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, testArgs, lookupConfig) require.NoError(t, err) addresses, ok := derivedTableMap["DerivedTable"][table.String()] @@ -333,6 +409,87 @@ func TestLookupTables(t *testing.T) { require.Equal(t, pubKeys[i], address.PublicKey) } }) + + t.Run("Derived lookup table resolves properly with PDALookup address", func(t *testing.T) { + // Deployed write_test contract + programID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") + + lookupKeys := createTestPubKeys(t, 5) + lookupTable := CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) + + InitializeDataAccount(ctx, t, rpcClient, programID, sender, lookupTable) + + args := map[string]interface{}{ + "seed1": []byte("data"), + } + + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: nil, + } + + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupConfig) + require.NoError(t, err) + + addresses, ok := derivedTableMap["DerivedTable"][lookupTable.String()] + require.True(t, ok) + for i, address := range addresses { + require.Equal(t, lookupKeys[i], address.PublicKey) + } + }) +} + +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 createTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { @@ -345,7 +502,7 @@ func createTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { return addresses } -func CreateTestLookupTable(t *testing.T, ctx context.Context, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { +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) diff --git a/pkg/solana/client/test_helpers.go b/pkg/solana/client/test_helpers.go index 5bb8b1cde..8d5ab4f88 100644 --- a/pkg/solana/client/test_helpers.go +++ b/pkg/solana/client/test_helpers.go @@ -66,6 +66,7 @@ func SetupLocalSolNodeWithFlags(t *testing.T, flags ...string) (string, string) out, err := client.GetHealth(tests.Context(t)) if err != nil || out != rpc.HealthOk { t.Logf("API server not ready yet (attempt %d)\n", i+1) + t.Logf("Error from API server: %v\n", err) continue } ready = true diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index 3ce1f788e..0c772065b 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -4,15 +4,29 @@ import ( "context" "encoding/binary" "fmt" + "os" + "path/filepath" + "runtime" "testing" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/pelletier/go-toml/v2" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" "github.com/test-go/testify/require" ) +var ( + _, b, _, _ = runtime.Caller(0) + // ProjectRoot Root folder of this project + ProjectRoot = filepath.Join(filepath.Dir(b), "/../../..") + // ContractsDir path to our contracts + ContractsDir = filepath.Join(ProjectRoot, "contracts", "target", "deploy") + PathToAnchorConfig = filepath.Join(ProjectRoot, "contracts", "Anchor.toml") +) + func LamportsToSol(lamports uint64) float64 { return internal.LamportsToSol(lamports) } // TxModifier is a dynamic function used to flexibly add components to a transaction such as additional signers, and compute budget parameters @@ -60,7 +74,7 @@ func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, i var txStatus rpc.ConfirmationStatusType count := 0 - for txStatus != rpc.ConfirmationStatusConfirmed && txStatus != rpc.ConfirmationStatusFinalized { + for txStatus != rpc.ConfirmationStatusType(commitment) && txStatus != rpc.ConfirmationStatusFinalized { count++ statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) require.NoError(t, sigErr) @@ -68,7 +82,7 @@ func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, i txStatus = statusRes.Value[0].ConfirmationStatus } time.Sleep(100 * time.Millisecond) - if count > 50 { + if count > 500 { require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) } } @@ -176,3 +190,27 @@ func FundAccounts(ctx context.Context, accounts []solana.PrivateKey, solanaGoCli } } } + +func DeployAllPrograms(t *testing.T, pathToAnchorConfig string, admin solana.PrivateKey) *rpc.Client { + return rpc.New(SetupTestValidatorWithAnchorPrograms(t, pathToAnchorConfig, admin.PublicKey().String())) +} + +func SetupTestValidatorWithAnchorPrograms(t *testing.T, pathToAnchorConfig string, upgradeAuthority string) string { + anchorData := struct { + Programs struct { + Localnet map[string]string + } + }{} + + // upload programs to validator + anchorBytes, err := os.ReadFile(pathToAnchorConfig) + require.NoError(t, err) + require.NoError(t, toml.Unmarshal(anchorBytes, &anchorData)) + + flags := []string{} + for k, v := range anchorData.Programs.Localnet { + flags = append(flags, "--upgradeable-program", v, filepath.Join(ContractsDir, k+".so"), upgradeAuthority) + } + url, _ := client.SetupLocalSolNodeWithFlags(t, flags...) + return url +}