From 5f42b565dae323fb9d9bd7be7b3c2b3a8c38ab62 Mon Sep 17 00:00:00 2001 From: Bruce Riley Date: Mon, 12 Feb 2024 11:42:09 -0500 Subject: [PATCH] Node/CCQ/Solana: Add sol_pda query --- node/cmd/ccq/devnet.permissions.json | 7 + node/cmd/ccq/parse_config_test.go | 37 +- node/cmd/ccq/permissions.go | 29 +- node/cmd/ccq/utils.go | 18 + node/hack/query/send_req.go | 61 ++- node/pkg/query/query.go | 2 +- node/pkg/query/request.go | 255 ++++++++- node/pkg/query/request_test.go | 51 +- node/pkg/query/response.go | 213 ++++++++ node/pkg/query/response_test.go | 66 ++- node/pkg/watchers/solana/ccq.go | 316 ++++++++++-- sdk/js-query/package-lock.json | 657 +++++++++++++++++++++++- sdk/js-query/package.json | 1 + sdk/js-query/src/mock/index.ts | 113 ++++ sdk/js-query/src/mock/mock.test.ts | 49 ++ sdk/js-query/src/query/index.ts | 1 + sdk/js-query/src/query/request.ts | 4 + sdk/js-query/src/query/response.ts | 3 + sdk/js-query/src/query/solana.test.ts | 146 +++++- sdk/js-query/src/query/solanaAccount.ts | 6 +- sdk/js-query/src/query/solanaPda.ts | 212 ++++++++ sdk/js-query/src/query/utils.ts | 8 + whitepapers/0013_ccq.md | 73 ++- 23 files changed, 2231 insertions(+), 97 deletions(-) create mode 100644 sdk/js-query/src/query/solanaPda.ts diff --git a/node/cmd/ccq/devnet.permissions.json b/node/cmd/ccq/devnet.permissions.json index d31f438b01..d16559604c 100644 --- a/node/cmd/ccq/devnet.permissions.json +++ b/node/cmd/ccq/devnet.permissions.json @@ -146,6 +146,13 @@ "chain": 1, "account": "BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna" } + }, + { + "solPDA": { + "note:": "Core Bridge on Devnet", + "chain": 1, + "programAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" + } } ] }, diff --git a/node/cmd/ccq/parse_config_test.go b/node/cmd/ccq/parse_config_test.go index b207b50b0b..b460c7c847 100644 --- a/node/cmd/ccq/parse_config_test.go +++ b/node/cmd/ccq/parse_config_test.go @@ -157,7 +157,7 @@ func TestParseConfigUnsupportedCallType(t *testing.T) { _, err := parseConfig([]byte(str)) require.Error(t, err) - assert.Equal(t, `unsupported call type for user "Test User", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality" or "solAccount"`, err.Error()) + assert.Equal(t, `unsupported call type for user "Test User", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality", "solAccount" or "solPDA"`, err.Error()) } func TestParseConfigInvalidContractAddress(t *testing.T) { @@ -295,7 +295,29 @@ func TestParseConfigSuccess(t *testing.T) { "contractAddress": "B4FBF271143F4FBf7B91A5ded31805e42b2208d7", "call": "0x06fdde03" } - } + }, + { + "ethCallWithFinality": { + "note:": "Decimals of WETH on Devnet", + "chain": 2, + "contractAddress": "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E", + "call": "0x313ce567" + } + }, + { + "solAccount": { + "note:": "Example NFT on Devnet", + "chain": 1, + "account": "BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna" + } + }, + { + "solPDA": { + "note:": "Core Bridge on Devnet", + "chain": 1, + "programAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o" + } + } ] } ] @@ -308,9 +330,20 @@ func TestParseConfigSuccess(t *testing.T) { perm, exists := perms["my_secret_key"] require.True(t, exists) + assert.Equal(t, 5, len(perm.allowedCalls)) + _, exists = perm.allowedCalls["ethCall:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6:06fdde03"] assert.True(t, exists) _, exists = perm.allowedCalls["ethCallByTimestamp:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d7:06fdde03"] assert.True(t, exists) + + _, exists = perm.allowedCalls["ethCallWithFinality:2:000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e:313ce567"] + assert.True(t, exists) + + _, exists = perm.allowedCalls["solAccount:1:BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna"] + assert.True(t, exists) + + _, exists = perm.allowedCalls["solPDA:1:Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"] + assert.True(t, exists) } diff --git a/node/cmd/ccq/permissions.go b/node/cmd/ccq/permissions.go index f0eacd5423..032db53370 100644 --- a/node/cmd/ccq/permissions.go +++ b/node/cmd/ccq/permissions.go @@ -37,6 +37,7 @@ type ( EthCallByTimestamp *EthCallByTimestamp `json:"ethCallByTimestamp"` EthCallWithFinality *EthCallWithFinality `json:"ethCallWithFinality"` SolanaAccount *SolanaAccount `json:"solAccount"` + SolanaPda *SolanaPda `json:"solPDA"` } EthCall struct { @@ -62,6 +63,12 @@ type ( Account string `json:"account"` } + SolanaPda struct { + Chain int `json:"chain"` + ProgramAddress string `json:"programAddress"` + // TODO: Should we make them specify seeds as well? + } + PermissionsMap map[string]*permissionEntry permissionEntry struct { @@ -234,8 +241,28 @@ func parseConfig(byteValue []byte) (PermissionsMap, error) { } } callKey = fmt.Sprintf("solAccount:%d:%s", ac.SolanaAccount.Chain, account) + } else if ac.SolanaPda != nil { + // We assume the account is base58, but if it starts with "0x" it should be 32 bytes of hex. + pa := ac.SolanaPda.ProgramAddress + if strings.HasPrefix(pa, "0x") { + buf, err := hex.DecodeString(pa[2:]) + if err != nil { + return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s": %w`, pa, user.UserName, err) + } + if len(buf) != query.SolanaPublicKeyLength { + return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s, must be %d bytes`, pa, user.UserName, query.SolanaPublicKeyLength) + } + pa = solana.PublicKey(buf).String() + } else { + // Make sure it is valid base58. + _, err := solana.PublicKeyFromBase58(pa) + if err != nil { + return nil, fmt.Errorf(`solana program address string "%s" for user "%s" is not valid base58: %w`, pa, user.UserName, err) + } + } + callKey = fmt.Sprintf("solPDA:%d:%s", ac.SolanaPda.Chain, pa) } else { - return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality" or "solAccount"`, user.UserName) + return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality", "solAccount" or "solPDA"`, user.UserName) } if callKey == "" { diff --git a/node/cmd/ccq/utils.go b/node/cmd/ccq/utils.go index f35bf7299a..774de64527 100644 --- a/node/cmd/ccq/utils.go +++ b/node/cmd/ccq/utils.go @@ -112,6 +112,8 @@ func validateRequest(logger *zap.Logger, env common.Environment, perms *Permissi status, err = validateCallData(logger, permsForUser, "ethCallWithFinality", pcq.ChainId, q.CallData) case *query.SolanaAccountQueryRequest: status, err = validateSolanaAccountQuery(logger, permsForUser, "solAccount", pcq.ChainId, q) + case *query.SolanaPdaQueryRequest: + status, err = validateSolanaPdaQuery(logger, permsForUser, "solPDA", pcq.ChainId, q) default: logger.Debug("unsupported query type", zap.String("userName", permsForUser.userName), zap.Any("type", pcq.Query)) invalidQueryRequestReceived.WithLabelValues("unsupported_query_type").Inc() @@ -171,3 +173,19 @@ func validateSolanaAccountQuery(logger *zap.Logger, permsForUser *permissionEntr return http.StatusOK, nil } + +// validateSolanaPdaQuery performs verification on a Solana sol_account query. +func validateSolanaPdaQuery(logger *zap.Logger, permsForUser *permissionEntry, callTag string, chainId vaa.ChainID, q *query.SolanaPdaQueryRequest) (int, error) { + for _, acct := range q.PDAs { + callKey := fmt.Sprintf("%s:%d:%s", callTag, chainId, solana.PublicKey(acct.ProgramAddress).String()) + if _, exists := permsForUser.allowedCalls[callKey]; !exists { + logger.Debug("requested call not authorized", zap.String("userName", permsForUser.userName), zap.String("callKey", callKey)) + invalidQueryRequestReceived.WithLabelValues("call_not_authorized").Inc() + return http.StatusForbidden, fmt.Errorf(`call "%s" not authorized`, callKey) + } + + totalRequestedCallsByChain.WithLabelValues(chainId.String()).Inc() + } + + return http.StatusOK, nil +} diff --git a/node/hack/query/send_req.go b/node/hack/query/send_req.go index 98175c2291..041a3479a2 100644 --- a/node/hack/query/send_req.go +++ b/node/hack/query/send_req.go @@ -19,6 +19,7 @@ import ( gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" "github.com/certusone/wormhole/node/pkg/query" "github.com/ethereum/go-ethereum/accounts/abi" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethCrypto "github.com/ethereum/go-ethereum/crypto" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -124,7 +125,7 @@ func main() { // { - logger.Info("Running Solana tests") + logger.Info("Running Solana account test") // Start of query creation... account1, err := solana.PublicKeyFromBase58("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o") @@ -142,11 +143,50 @@ func main() { Accounts: [][query.SolanaPublicKeyLength]byte{account1, account2}, } - queryRequest := createSolanaQueryRequest(callRequest) + queryRequest := &query.QueryRequest{ + Nonce: rand.Uint32(), + PerChainQueries: []*query.PerChainQueryRequest{ + { + ChainId: 1, + Query: callRequest, + }, + }, + } sendSolanaQueryAndGetRsp(queryRequest, sk, th_req, ctx, logger, sub) + } + + { + logger.Info("Running Solana PDA test") + + // Start of query creation... + callRequest := &query.SolanaPdaQueryRequest{ + Commitment: "finalized", + DataSliceOffset: 0, + DataSliceLength: 100, + PDAs: []query.SolanaPDAEntry{ + query.SolanaPDAEntry{ + ProgramAddress: ethCommon.HexToHash("0x02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"), // Devnet core bridge + Seeds: [][]byte{ + []byte("GuardianSet"), + make([]byte, 4), + }, + }, + }, + } - logger.Info("Solana tests complete!") + queryRequest := &query.QueryRequest{ + Nonce: rand.Uint32(), + PerChainQueries: []*query.PerChainQueryRequest{ + { + ChainId: 1, + Query: callRequest, + }, + }, + } + sendSolanaQueryAndGetRsp(queryRequest, sk, th_req, ctx, logger, sub) } + + logger.Info("Solana tests complete!") // return // @@ -392,19 +432,6 @@ func sendQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.PrivateKey, } } -func createSolanaQueryRequest(callRequest *query.SolanaAccountQueryRequest) *query.QueryRequest { - queryRequest := &query.QueryRequest{ - Nonce: rand.Uint32(), - PerChainQueries: []*query.PerChainQueryRequest{ - { - ChainId: 1, - Query: callRequest, - }, - }, - } - return queryRequest -} - func sendSolanaQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.PrivateKey, th *pubsub.Topic, ctx context.Context, logger *zap.Logger, sub *pubsub.Subscription) { queryRequestBytes, err := queryRequest.Marshal() if err != nil { @@ -483,6 +510,8 @@ func sendSolanaQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.Privat switch r := response.PerChainResponses[index].Response.(type) { case *query.SolanaAccountQueryResponse: logger.Info("solana query per chain response", zap.Int("index", index), zap.Any("pcr", r)) + case *query.SolanaPdaQueryResponse: + logger.Info("solana query per chain response", zap.Int("index", index), zap.Any("pcr", r)) default: panic(fmt.Sprintf("unsupported query type, should be solana, index: %d", index)) } diff --git a/node/pkg/query/query.go b/node/pkg/query/query.go index 636912433e..e244b43d2f 100644 --- a/node/pkg/query/query.go +++ b/node/pkg/query/query.go @@ -433,11 +433,11 @@ func (pcq *perChainQuery) ccqForwardToWatcher(qLogger *zap.Logger, receiveTime t case pcq.channel <- pcq.req: qLogger.Debug("forwarded query request to watcher", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chainID", pcq.req.Request.ChainId)) totalRequestsByChain.WithLabelValues(pcq.req.Request.ChainId.String()).Inc() - pcq.lastUpdateTime = receiveTime default: // By leaving lastUpdateTime unset, we will retry next interval. qLogger.Warn("failed to send query request to watcher, will retry next interval", zap.String("requestID", pcq.req.RequestID), zap.Stringer("chain_id", pcq.req.Request.ChainId)) } + pcq.lastUpdateTime = receiveTime } // numPendingRequests returns the number of per chain queries in a request that are still awaiting responses. Zero means the request can now be published. diff --git a/node/pkg/query/request.go b/node/pkg/query/request.go index 7c4c2c4aa7..854707049c 100644 --- a/node/pkg/query/request.go +++ b/node/pkg/query/request.go @@ -138,7 +138,7 @@ type SolanaAccountQueryRequest struct { // The length of the data to be returned. Zero means all data is returned. DataSliceLength uint64 - // Accounts is an array of accounts to be queried, in base58 representation. + // Accounts is an array of accounts to be queried. Accounts [][SolanaPublicKeyLength]byte } @@ -157,6 +157,48 @@ func (saq *SolanaAccountQueryRequest) AccountList() [][SolanaPublicKeyLength]byt return saq.Accounts } +// SolanaPdaQueryRequestType is the type of a Solana sol_pda query request. +const SolanaPdaQueryRequestType ChainSpecificQueryType = 5 + +// SolanaPdaQueryRequest implements ChainSpecificQuery for a Solana sol_pda query request. +type SolanaPdaQueryRequest struct { + // Commitment identifies the commitment level to be used in the queried. Currently it may only "finalized". + // Before we can support "confirmed", we need a way to read the account data and the block information atomically. + // We would also need to deal with the fact that queries are only handled in the finalized watcher and it does not + // have access to the latest confirmed slot needed for MinContextSlot retries. + Commitment string + + // The minimum slot that the request can be evaluated at. Zero means unused. + MinContextSlot uint64 + + // The offset of the start of data to be returned. Unused if DataSliceLength is zero. + DataSliceOffset uint64 + + // The length of the data to be returned. Zero means all data is returned. + DataSliceLength uint64 + + // PDAs is an array of PDAs to be queried. + PDAs []SolanaPDAEntry +} + +// SolanaPDAEntry defines a single Solana Program derived address (PDA). +type SolanaPDAEntry struct { + ProgramAddress [SolanaPublicKeyLength]byte + Seeds [][]byte +} + +// According to the spec, there may be at most 16 seeds. +// https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L559 +const SolanaMaxSeeds = solana.MaxSeeds + +// According to the spec, a seed may be at most 32 bytes. +// https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L557 +const SolanaMaxSeedLen = solana.MaxSeedLength + +func (spda *SolanaPdaQueryRequest) PDAList() []SolanaPDAEntry { + return spda.PDAs +} + // PerChainQueryInternal is an internal representation of a query request that is passed to the watcher. type PerChainQueryInternal struct { RequestID string @@ -192,6 +234,16 @@ func PostSignedQueryRequest(signedQueryReqSendC chan<- *gossipv1.SignedQueryRequ } } +func SignedQueryRequestEqual(left *gossipv1.SignedQueryRequest, right *gossipv1.SignedQueryRequest) bool { + if !bytes.Equal(left.QueryRequest, right.QueryRequest) { + return false + } + if !bytes.Equal(left.Signature, right.Signature) { + return false + } + return true +} + // // Implementation of QueryRequest. // @@ -382,6 +434,12 @@ func (perChainQuery *PerChainQueryRequest) UnmarshalFromReader(reader *bytes.Rea return fmt.Errorf("failed to unmarshal solana account query request: %w", err) } perChainQuery.Query = &q + case SolanaPdaQueryRequestType: + q := SolanaPdaQueryRequest{} + if err := q.UnmarshalFromReader(reader); err != nil { + return fmt.Errorf("failed to unmarshal solana PDA query request: %w", err) + } + perChainQuery.Query = &q default: return fmt.Errorf("unsupported query type: %d", queryType) } @@ -411,6 +469,14 @@ func (perChainQuery *PerChainQueryRequest) Validate() error { return nil } +func ValidatePerChainQueryRequestType(qt ChainSpecificQueryType) error { + if qt != EthCallQueryRequestType && qt != EthCallByTimestampQueryRequestType && qt != EthCallWithFinalityQueryRequestType && + qt != SolanaAccountQueryRequestType && qt != SolanaPdaQueryRequestType { + return fmt.Errorf("invalid query request type: %d", qt) + } + return nil +} + // Equal verifies that two query requests are equal. func (left *PerChainQueryRequest) Equal(right *PerChainQueryRequest) bool { if left.ChainId != right.ChainId { @@ -458,6 +524,13 @@ func (left *PerChainQueryRequest) Equal(right *PerChainQueryRequest) bool { default: panic("unsupported query type on right, must be sol_account") } + case *SolanaPdaQueryRequest: + switch rightQuery := right.Query.(type) { + case *SolanaPdaQueryRequest: + return leftQuery.Equal(rightQuery) + default: + panic("unsupported query type on right, must be sol_pda") + } default: panic("unsupported query type on left") } @@ -1052,19 +1125,187 @@ func (left *SolanaAccountQueryRequest) Equal(right *SolanaAccountQueryRequest) b return true } -func ValidatePerChainQueryRequestType(qt ChainSpecificQueryType) error { - if qt != EthCallQueryRequestType && qt != EthCallByTimestampQueryRequestType && qt != EthCallWithFinalityQueryRequestType && qt != SolanaAccountQueryRequestType { - return fmt.Errorf("invalid query request type: %d", qt) +// +// Implementation of SolanaPdaQueryRequest, which implements the ChainSpecificQuery interface. +// + +func (e *SolanaPdaQueryRequest) Type() ChainSpecificQueryType { + return SolanaPdaQueryRequestType +} + +// Marshal serializes the binary representation of a Solana sol_pda request. +// This method calls Validate() and relies on it to range checks lengths, etc. +func (spda *SolanaPdaQueryRequest) Marshal() ([]byte, error) { + if err := spda.Validate(); err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + + vaa.MustWrite(buf, binary.BigEndian, uint32(len(spda.Commitment))) + buf.Write([]byte(spda.Commitment)) + + vaa.MustWrite(buf, binary.BigEndian, spda.MinContextSlot) + vaa.MustWrite(buf, binary.BigEndian, spda.DataSliceOffset) + vaa.MustWrite(buf, binary.BigEndian, spda.DataSliceLength) + + vaa.MustWrite(buf, binary.BigEndian, uint8(len(spda.PDAs))) + for _, pda := range spda.PDAs { + buf.Write(pda.ProgramAddress[:]) + vaa.MustWrite(buf, binary.BigEndian, uint8(len(pda.Seeds))) + for _, seed := range pda.Seeds { + vaa.MustWrite(buf, binary.BigEndian, uint32(len(seed))) + buf.Write(seed) + } + } + return buf.Bytes(), nil +} + +// Unmarshal deserializes a Solana sol_pda query from a byte array +func (spda *SolanaPdaQueryRequest) Unmarshal(data []byte) error { + reader := bytes.NewReader(data[:]) + return spda.UnmarshalFromReader(reader) +} + +// UnmarshalFromReader deserializes a Solana sol_pda query from a byte array +func (spda *SolanaPdaQueryRequest) UnmarshalFromReader(reader *bytes.Reader) error { + len := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &len); err != nil { + return fmt.Errorf("failed to read commitment len: %w", err) + } + + if len > SolanaMaxCommitmentLength { + return fmt.Errorf("commitment string is too long, may not be more than %d characters", SolanaMaxCommitmentLength) + } + + commitment := make([]byte, len) + if n, err := reader.Read(commitment[:]); err != nil || n != int(len) { + return fmt.Errorf("failed to read commitment [%d]: %w", n, err) + } + spda.Commitment = string(commitment) + + if err := binary.Read(reader, binary.BigEndian, &spda.MinContextSlot); err != nil { + return fmt.Errorf("failed to read min slot: %w", err) + } + + if err := binary.Read(reader, binary.BigEndian, &spda.DataSliceOffset); err != nil { + return fmt.Errorf("failed to read data slice offset: %w", err) + } + + if err := binary.Read(reader, binary.BigEndian, &spda.DataSliceLength); err != nil { + return fmt.Errorf("failed to read data slice length: %w", err) + } + + numPDAs := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numPDAs); err != nil { + return fmt.Errorf("failed to read number of PDAs: %w", err) + } + + for count := 0; count < int(numPDAs); count++ { + programAddress := [SolanaPublicKeyLength]byte{} + if n, err := reader.Read(programAddress[:]); err != nil || n != SolanaPublicKeyLength { + return fmt.Errorf("failed to read program address [%d]: %w", n, err) + } + + pda := SolanaPDAEntry{ProgramAddress: programAddress} + numSeeds := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numSeeds); err != nil { + return fmt.Errorf("failed to read number of seeds: %w", err) + } + + for count := 0; count < int(numSeeds); count++ { + seedLen := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &seedLen); err != nil { + return fmt.Errorf("failed to read call Data len: %w", err) + } + seed := make([]byte, seedLen) + if n, err := reader.Read(seed[:]); err != nil || n != int(seedLen) { + return fmt.Errorf("failed to read seed [%d]: %w", n, err) + } + + pda.Seeds = append(pda.Seeds, seed) + } + + spda.PDAs = append(spda.PDAs, pda) } + return nil } -func SignedQueryRequestEqual(left *gossipv1.SignedQueryRequest, right *gossipv1.SignedQueryRequest) bool { - if !bytes.Equal(left.QueryRequest, right.QueryRequest) { +// Validate does basic validation on a Solana sol_pda query. +func (spda *SolanaPdaQueryRequest) Validate() error { + if len(spda.Commitment) > SolanaMaxCommitmentLength { + return fmt.Errorf("commitment too long") + } + if spda.Commitment != "finalized" { + return fmt.Errorf(`commitment must be "finalized"`) + } + + if spda.DataSliceLength == 0 && spda.DataSliceOffset != 0 { + return fmt.Errorf("data slice offset may not be set if data slice length is zero") + } + + if len(spda.PDAs) <= 0 { + return fmt.Errorf("does not contain any PDAs entries") + } + if len(spda.PDAs) > SolanaMaxAccountsPerQuery { + return fmt.Errorf("too many PDA entries, may not be more than %d", SolanaMaxAccountsPerQuery) + } + for _, pda := range spda.PDAs { + // The program address is fixed length, so don't need to check for nil. + if len(pda.ProgramAddress) != SolanaPublicKeyLength { + return fmt.Errorf("invalid program address length") + } + + if len(pda.Seeds) == 0 { + return fmt.Errorf("PDA does not contain any seeds") + } + + if len(pda.Seeds) > SolanaMaxSeeds { + return fmt.Errorf("PDA contains too many seeds") + } + + for _, seed := range pda.Seeds { + if len(seed) == 0 { + return fmt.Errorf("seed is null") + } + + if len(seed) > SolanaMaxSeedLen { + return fmt.Errorf("seed is too long") + } + } + } + + return nil +} + +// Equal verifies that two Solana sol_pda queries are equal. +func (left *SolanaPdaQueryRequest) Equal(right *SolanaPdaQueryRequest) bool { + if left.Commitment != right.Commitment || + left.MinContextSlot != right.MinContextSlot || + left.DataSliceOffset != right.DataSliceOffset || + left.DataSliceLength != right.DataSliceLength { return false } - if !bytes.Equal(left.Signature, right.Signature) { + + if len(left.PDAs) != len(right.PDAs) { return false } + for idx := range left.PDAs { + if !bytes.Equal(left.PDAs[idx].ProgramAddress[:], right.PDAs[idx].ProgramAddress[:]) { + return false + } + + if len(left.PDAs[idx].Seeds) != len(right.PDAs[idx].Seeds) { + return false + } + + for idx2 := range left.PDAs[idx].Seeds { + if !bytes.Equal(left.PDAs[idx].Seeds[idx2][:], right.PDAs[idx].Seeds[idx2][:]) { + return false + } + } + } + return true } diff --git a/node/pkg/query/request_test.go b/node/pkg/query/request_test.go index dcb90efaf3..491875a473 100644 --- a/node/pkg/query/request_test.go +++ b/node/pkg/query/request_test.go @@ -787,7 +787,56 @@ func TestSolanaPublicKeyLengthIsAsExpected(t *testing.T) { require.Equal(t, 32, SolanaPublicKeyLength) } -///////////// End of Solana Account Query tests /////////////////////////// +///////////// Solana PDA Query tests ///////////////////////////////// + +func TestSolanaSeedConstsAreAsExpected(t *testing.T) { + // It might break the spec if these ever changes! + require.Equal(t, 16, SolanaMaxSeeds) + require.Equal(t, 32, SolanaMaxSeedLen) +} + +func createSolanaPdaQueryRequestForTesting(t *testing.T) *QueryRequest { + t.Helper() + + callRequest1 := &SolanaPdaQueryRequest{ + Commitment: "finalized", + PDAs: []SolanaPDAEntry{ + SolanaPDAEntry{ + ProgramAddress: ethCommon.HexToHash("0x02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa"), // Devnet core bridge + Seeds: [][]byte{ + []byte("GuardianSet"), + make([]byte, 4), + }, + }, + }, + } + + perChainQuery1 := &PerChainQueryRequest{ + ChainId: vaa.ChainIDSolana, + Query: callRequest1, + } + + queryRequest := &QueryRequest{ + Nonce: 1, + PerChainQueries: []*PerChainQueryRequest{perChainQuery1}, + } + + return queryRequest +} + +func TestSolanaPdaQueryRequestMarshalUnmarshal(t *testing.T) { + queryRequest := createSolanaPdaQueryRequestForTesting(t) + queryRequestBytes, err := queryRequest.Marshal() + require.NoError(t, err) + + var queryRequest2 QueryRequest + err = queryRequest2.Unmarshal(queryRequestBytes) + require.NoError(t, err) + + assert.True(t, queryRequest.Equal(&queryRequest2)) +} + +///////////// End of Solana PDA Query tests /////////////////////////// func TestPostSignedQueryRequestShouldFailIfNoOneIsListening(t *testing.T) { queryRequest := createQueryRequestForTesting(t, vaa.ChainIDPolygon) diff --git a/node/pkg/query/response.go b/node/pkg/query/response.go index 63b77b6089..7d4d4fa292 100644 --- a/node/pkg/query/response.go +++ b/node/pkg/query/response.go @@ -138,6 +138,43 @@ type SolanaAccountResult struct { Data []byte } +// SolanaPdaQueryResponse implements ChainSpecificResponse for a Solana sol_pda query response. +type SolanaPdaQueryResponse struct { + // SlotNumber is the slot number returned by the sol_pda query + SlotNumber uint64 + + // BlockTime is the block time associated with the slot. + BlockTime time.Time + + // BlockHash is the block hash associated with the slot. + BlockHash [SolanaPublicKeyLength]byte + + Results []SolanaPdaResult +} + +type SolanaPdaResult struct { + // Account is the public key of the account derived from the PDA. + Account [SolanaPublicKeyLength]byte + + // Bump is the bump value returned by the solana derivation function. + Bump uint8 + + // Lamports is the number of lamports assigned to the account. + Lamports uint64 + + // RentEpoch is the epoch at which this account will next owe rent. + RentEpoch uint64 + + // Executable is a boolean indicating if the account contains a program (and is strictly read-only). + Executable bool + + // Owner is the public key of the owner of the account. + Owner [SolanaPublicKeyLength]byte + + // Data is the data returned by the sol_pda query. + Data []byte +} + // // Implementation of QueryResponsePublication. // @@ -413,6 +450,12 @@ func (perChainResponse *PerChainQueryResponse) UnmarshalFromReader(reader *bytes return fmt.Errorf("failed to unmarshal sol_account response: %w", err) } perChainResponse.Response = &r + case SolanaPdaQueryRequestType: + r := SolanaPdaQueryResponse{} + if err := r.UnmarshalFromReader(reader); err != nil { + return fmt.Errorf("failed to unmarshal sol_account response: %w", err) + } + perChainResponse.Response = &r default: return fmt.Errorf("unsupported query type: %d", queryType) } @@ -489,6 +532,13 @@ func (left *PerChainQueryResponse) Equal(right *PerChainQueryResponse) bool { default: panic("unsupported query type on right") // We checked this above! } + case *SolanaPdaQueryResponse: + switch rightResp := right.Response.(type) { + case *SolanaPdaQueryResponse: + return leftResp.Equal(rightResp) + default: + panic("unsupported query type on right") // We checked this above! + } default: panic("unsupported query type on left") // We checked this above! } @@ -1042,3 +1092,166 @@ func (left *SolanaAccountQueryResponse) Equal(right *SolanaAccountQueryResponse) return true } + +// +// Implementation of SolanaPdaQueryResponse, which implements the ChainSpecificResponse for a Solana sol_pda query response. +// + +func (sar *SolanaPdaQueryResponse) Type() ChainSpecificQueryType { + return SolanaPdaQueryRequestType +} + +// Marshal serializes the binary representation of a Solana sol_pda response. +// This method calls Validate() and relies on it to range check lengths, etc. +func (sar *SolanaPdaQueryResponse) Marshal() ([]byte, error) { + if err := sar.Validate(); err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + vaa.MustWrite(buf, binary.BigEndian, sar.SlotNumber) + vaa.MustWrite(buf, binary.BigEndian, sar.BlockTime.UnixMicro()) + buf.Write(sar.BlockHash[:]) + + vaa.MustWrite(buf, binary.BigEndian, uint8(len(sar.Results))) + for _, res := range sar.Results { + buf.Write(res.Account[:]) + vaa.MustWrite(buf, binary.BigEndian, res.Bump) + vaa.MustWrite(buf, binary.BigEndian, res.Lamports) + vaa.MustWrite(buf, binary.BigEndian, res.RentEpoch) + vaa.MustWrite(buf, binary.BigEndian, res.Executable) + buf.Write(res.Owner[:]) + + vaa.MustWrite(buf, binary.BigEndian, uint32(len(res.Data))) + buf.Write(res.Data) + } + + return buf.Bytes(), nil +} + +// Unmarshal deserializes a Solana sol_pda response from a byte array +func (sar *SolanaPdaQueryResponse) Unmarshal(data []byte) error { + reader := bytes.NewReader(data[:]) + return sar.UnmarshalFromReader(reader) +} + +// UnmarshalFromReader deserializes a Solana sol_pda response from a byte array +func (sar *SolanaPdaQueryResponse) UnmarshalFromReader(reader *bytes.Reader) error { + if err := binary.Read(reader, binary.BigEndian, &sar.SlotNumber); err != nil { + return fmt.Errorf("failed to read slot number: %w", err) + } + + blockTime := int64(0) + if err := binary.Read(reader, binary.BigEndian, &blockTime); err != nil { + return fmt.Errorf("failed to read block time: %w", err) + } + sar.BlockTime = time.UnixMicro(blockTime) + if n, err := reader.Read(sar.BlockHash[:]); err != nil || n != SolanaPublicKeyLength { + return fmt.Errorf("failed to read block hash [%d]: %w", n, err) + } + + numResults := uint8(0) + if err := binary.Read(reader, binary.BigEndian, &numResults); err != nil { + return fmt.Errorf("failed to read number of results: %w", err) + } + + for count := 0; count < int(numResults); count++ { + var result SolanaPdaResult + + if n, err := reader.Read(result.Account[:]); err != nil || n != SolanaPublicKeyLength { + return fmt.Errorf("failed to read account [%d]: %w", n, err) + } + + if err := binary.Read(reader, binary.BigEndian, &result.Bump); err != nil { + return fmt.Errorf("failed to read bump: %w", err) + } + + if err := binary.Read(reader, binary.BigEndian, &result.Lamports); err != nil { + return fmt.Errorf("failed to read lamports: %w", err) + } + + if err := binary.Read(reader, binary.BigEndian, &result.RentEpoch); err != nil { + return fmt.Errorf("failed to read rent epoch: %w", err) + } + + if err := binary.Read(reader, binary.BigEndian, &result.Executable); err != nil { + return fmt.Errorf("failed to read executable flag: %w", err) + } + + if n, err := reader.Read(result.Owner[:]); err != nil || n != SolanaPublicKeyLength { + return fmt.Errorf("failed to read owner [%d]: %w", n, err) + } + + len := uint32(0) + if err := binary.Read(reader, binary.BigEndian, &len); err != nil { + return fmt.Errorf("failed to read data len: %w", err) + } + result.Data = make([]byte, len) + if n, err := reader.Read(result.Data[:]); err != nil || n != int(len) { + return fmt.Errorf("failed to read data [%d]: %w", n, err) + } + + sar.Results = append(sar.Results, result) + } + + return nil +} + +// Validate does basic validation on a Solana sol_pda response. +func (sar *SolanaPdaQueryResponse) Validate() error { + // Not checking for SlotNumber == 0, because maybe that could happen?? + // Not checking for BlockTime == 0, because maybe that could happen?? + + // The block hash is fixed length, so don't need to check for nil. + if len(sar.BlockHash) != SolanaPublicKeyLength { + return fmt.Errorf("invalid block hash length") + } + + if len(sar.Results) <= 0 { + return fmt.Errorf("does not contain any results") + } + if len(sar.Results) > math.MaxUint8 { + return fmt.Errorf("too many results") + } + for _, result := range sar.Results { + // Account is fixed length, so don't need to check for nil. + if len(result.Account) != SolanaPublicKeyLength { + return fmt.Errorf("invalid account length") + } + // Owner is fixed length, so don't need to check for nil. + if len(result.Owner) != SolanaPublicKeyLength { + return fmt.Errorf("invalid owner length") + } + if len(result.Data) > math.MaxUint32 { + return fmt.Errorf("data too long") + } + } + + return nil +} + +// Equal verifies that two Solana sol_pda responses are equal. +func (left *SolanaPdaQueryResponse) Equal(right *SolanaPdaQueryResponse) bool { + if left.SlotNumber != right.SlotNumber || + left.BlockTime != right.BlockTime || + !bytes.Equal(left.BlockHash[:], right.BlockHash[:]) { + return false + } + + if len(left.Results) != len(right.Results) { + return false + } + for idx := range left.Results { + if !bytes.Equal(left.Results[idx].Account[:], right.Results[idx].Account[:]) || + left.Results[idx].Bump != right.Results[idx].Bump || + left.Results[idx].Lamports != right.Results[idx].Lamports || + left.Results[idx].RentEpoch != right.Results[idx].RentEpoch || + left.Results[idx].Executable != right.Results[idx].Executable || + !bytes.Equal(left.Results[idx].Owner[:], right.Results[idx].Owner[:]) || + !bytes.Equal(left.Results[idx].Data, right.Results[idx].Data) { + return false + } + } + + return true +} diff --git a/node/pkg/query/response_test.go b/node/pkg/query/response_test.go index b2844eb40e..9b39d2ea45 100644 --- a/node/pkg/query/response_test.go +++ b/node/pkg/query/response_test.go @@ -325,4 +325,68 @@ func TestSolanaAccountQueryResponseMarshalUnmarshal(t *testing.T) { assert.True(t, respPub.Equal(&respPub2)) } -///////////// End of Solana Account Query tests /////////////////////////// +///////////// Solana PDA Query tests ///////////////////////////////// + +func createSolanaPdaQueryResponseFromRequest(t *testing.T, queryRequest *QueryRequest) *QueryResponsePublication { + queryRequestBytes, err := queryRequest.Marshal() + require.NoError(t, err) + + sig := [65]byte{} + signedQueryRequest := &gossipv1.SignedQueryRequest{ + QueryRequest: queryRequestBytes, + Signature: sig[:], + } + + perChainResponses := []*PerChainQueryResponse{} + for idx, pcr := range queryRequest.PerChainQueries { + switch req := pcr.Query.(type) { + case *SolanaPdaQueryRequest: + results := []SolanaPdaResult{} + for idx := range req.PDAs { + results = append(results, SolanaPdaResult{ + Account: ethCommon.HexToHash("4fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773e"), + Bump: uint8(255 - idx), + Lamports: uint64(2000 + idx), + RentEpoch: uint64(3000 + idx), + Executable: (idx%2 == 0), + Owner: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e2"), + Data: []byte([]byte(fmt.Sprintf("Result %d", idx))), + }) + } + perChainResponses = append(perChainResponses, &PerChainQueryResponse{ + ChainId: pcr.ChainId, + Response: &SolanaPdaQueryResponse{ + SlotNumber: uint64(1000 + idx), + BlockTime: timeForTest(t, time.Now()), + BlockHash: ethCommon.HexToHash("0x9999bac44d09a7f69ee7941819b0a19c59ccb1969640cc513be09ef95ed2d8e3"), + Results: results, + }, + }) + default: + panic("invalid query type!") + } + + } + + return &QueryResponsePublication{ + Request: signedQueryRequest, + PerChainResponses: perChainResponses, + } +} + +func TestSolanaPdaQueryResponseMarshalUnmarshal(t *testing.T) { + queryRequest := createSolanaPdaQueryRequestForTesting(t) + respPub := createSolanaPdaQueryResponseFromRequest(t, queryRequest) + + respPubBytes, err := respPub.Marshal() + require.NoError(t, err) + + var respPub2 QueryResponsePublication + err = respPub2.Unmarshal(respPubBytes) + require.NoError(t, err) + require.NotNil(t, respPub2) + + assert.True(t, respPub.Equal(&respPub2)) +} + +///////////// End of Solana PDA Query tests /////////////////////////// diff --git a/node/pkg/watchers/solana/ccq.go b/node/pkg/watchers/solana/ccq.go index 2332cf3711..2db6747214 100644 --- a/node/pkg/watchers/solana/ccq.go +++ b/node/pkg/watchers/solana/ccq.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" "strconv" "time" @@ -28,9 +29,8 @@ const ( CCQ_FAST_RETRY_INTERVAL = 200 * time.Millisecond ) -// ccqSendQueryResponse sends a response back to the query handler. In the case of an error, the response parameter may be nil. -func (w *SolanaWatcher) ccqSendQueryResponse(req *query.PerChainQueryInternal, status query.QueryStatus, response query.ChainSpecificResponse) { - queryResponse := query.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, response) +// ccqSendQueryResponse sends a response back to the query handler. +func (w *SolanaWatcher) ccqSendQueryResponse(queryResponse *query.PerChainQueryResponseInternal) { select { case w.queryResponseC <- queryResponse: w.ccqLogger.Debug("published query response to handler") @@ -39,9 +39,14 @@ func (w *SolanaWatcher) ccqSendQueryResponse(req *query.PerChainQueryInternal, s } } +// ccqSendErrorResponse creates an error query response and sends it back to the query handler. It sets the response field to nil. +func (w *SolanaWatcher) ccqSendErrorResponse(req *query.PerChainQueryInternal, status query.QueryStatus) { + queryResponse := query.CreatePerChainQueryResponseInternal(req.RequestID, req.RequestIdx, req.Request.ChainId, status, nil) + w.ccqSendQueryResponse(queryResponse) +} + // ccqHandleQuery is the top-level query handler. It breaks out the requests based on the type and calls the appropriate handler. func (w *SolanaWatcher) ccqHandleQuery(ctx context.Context, queryRequest *query.PerChainQueryInternal) { - // This can't happen unless there is a programming error - the caller // is expected to send us only requests for our chainID. if queryRequest.Request.ChainId != w.chainID { @@ -50,33 +55,40 @@ func (w *SolanaWatcher) ccqHandleQuery(ctx context.Context, queryRequest *query. start := time.Now() + giveUpTime := start.Add(query.RetryInterval).Add(-CCQ_RETRY_SLOP) switch req := queryRequest.Request.Query.(type) { case *query.SolanaAccountQueryRequest: - giveUpTime := start.Add(query.RetryInterval).Add(-CCQ_RETRY_SLOP) - w.ccqHandleSolanaAccountQueryRequest(ctx, queryRequest, req, giveUpTime, false) + w.ccqHandleSolanaAccountQueryRequest(ctx, queryRequest, req, giveUpTime) + case *query.SolanaPdaQueryRequest: + w.ccqHandleSolanaPdaQueryRequest(ctx, queryRequest, req, giveUpTime) default: w.ccqLogger.Warn("received unsupported request type", zap.Uint8("payload", uint8(queryRequest.Request.Query.Type())), ) - w.ccqSendQueryResponse(queryRequest, query.QueryFatalError, nil) + w.ccqSendErrorResponse(queryRequest, query.QueryFatalError) } query.TotalWatcherTime.WithLabelValues(w.chainID.String()).Observe(float64(time.Since(start).Milliseconds())) } -// ccqHandleSolanaAccountQueryRequest is the query handler for a sol_account request. -func (w *SolanaWatcher) ccqHandleSolanaAccountQueryRequest(ctx context.Context, queryRequest *query.PerChainQueryInternal, req *query.SolanaAccountQueryRequest, giveUpTime time.Time, isRetry bool) { - requestId := "sol_account:" + queryRequest.ID() - if !isRetry { - w.ccqLogger.Info("received a sol_account query", - zap.Uint64("minContextSlot", req.MinContextSlot), - zap.Uint64("dataSliceOffset", req.DataSliceOffset), - zap.Uint64("dataSliceLength", req.DataSliceLength), - zap.Int("numAccounts", len(req.Accounts)), - zap.String("requestId", requestId), - ) - } +// ccqCustomPublisher is an interface used by ccqBaseHandleSolanaAccountQueryRequest to specify how to publish the response from a query. +type ccqCustomPublisher interface { + // publish should take a sol_account query response and publish it as the appropriate response type. + publish(*query.PerChainQueryResponseInternal, *query.SolanaAccountQueryResponse) +} +// ccqBaseHandleSolanaAccountQueryRequest is the base Solana Account query handler. It does the actual account queries, and if necessary does fast retries +// until the minimum context slot is reached. It does not publish the response, but instead invokes the query specific publisher that is passed in. +func (w *SolanaWatcher) ccqBaseHandleSolanaAccountQueryRequest( + ctx context.Context, + queryRequest *query.PerChainQueryInternal, + req *query.SolanaAccountQueryRequest, + giveUpTime time.Time, + tag string, + requestId string, + isRetry bool, + publisher ccqCustomPublisher, +) { rCtx, cancel := context.WithTimeout(ctx, rpcTimeout) defer cancel() @@ -106,18 +118,18 @@ func (w *SolanaWatcher) ccqHandleSolanaAccountQueryRequest(ctx context.Context, // Read the accounts. info, err := w.getMultipleAccountsWithOpts(rCtx, accounts, ¶ms) if err != nil { - if w.ccqCheckForMinSlotContext(ctx, queryRequest, req, requestId, err, giveUpTime, !isRetry) { + if w.ccqCheckForMinSlotContext(ctx, queryRequest, req, requestId, err, giveUpTime, !isRetry, tag, publisher) { // Return without posting a response because a go routine was created to handle it. return } - w.ccqLogger.Error("read failed for sol_account query request", + w.ccqLogger.Error(fmt.Sprintf("read failed for %s query request", tag), zap.String("requestId", requestId), zap.Any("accounts", accounts), zap.Any("params", params), zap.Error(err), ) - w.ccqSendQueryResponse(queryRequest, query.QueryRetryNeeded, nil) + w.ccqSendErrorResponse(queryRequest, query.QueryRetryNeeded) return } @@ -130,36 +142,36 @@ func (w *SolanaWatcher) ccqHandleSolanaAccountQueryRequest(ctx context.Context, MaxSupportedTransactionVersion: &maxSupportedTransactionVersion, }) if err != nil { - w.ccqLogger.Error("failed to read block time for sol_account query request", + w.ccqLogger.Error(fmt.Sprintf("failed to read block time for %s query request", tag), zap.String("requestId", requestId), zap.Uint64("slotNumber", info.Context.Slot), zap.Error(err), ) - w.ccqSendQueryResponse(queryRequest, query.QueryRetryNeeded, nil) + w.ccqSendErrorResponse(queryRequest, query.QueryRetryNeeded) return } if info == nil { - w.ccqLogger.Error("read for sol_account query request returned nil info", zap.String("requestId", requestId)) - w.ccqSendQueryResponse(queryRequest, query.QueryFatalError, nil) + w.ccqLogger.Error(fmt.Sprintf("read for %s query request returned nil info", tag), zap.String("requestId", requestId)) + w.ccqSendErrorResponse(queryRequest, query.QueryFatalError) return } if info.Value == nil { - w.ccqLogger.Error("read for sol_account query request returned nil value", zap.String("requestId", requestId)) - w.ccqSendQueryResponse(queryRequest, query.QueryFatalError, nil) + w.ccqLogger.Error(fmt.Sprintf("read for %s query request returned nil value", tag), zap.String("requestId", requestId)) + w.ccqSendErrorResponse(queryRequest, query.QueryFatalError) return } if len(info.Value) != len(req.Accounts) { - w.ccqLogger.Error("read for sol_account query request returned unexpected number of results", + w.ccqLogger.Error(fmt.Sprintf("read for %s query request returned unexpected number of results", tag), zap.String("requestId", requestId), zap.Int("numAccounts", len(req.Accounts)), zap.Int("numValues", len(info.Value)), ) - w.ccqSendQueryResponse(queryRequest, query.QueryFatalError, nil) + w.ccqSendErrorResponse(queryRequest, query.QueryFatalError) return } @@ -167,13 +179,13 @@ func (w *SolanaWatcher) ccqHandleSolanaAccountQueryRequest(ctx context.Context, results := make([]query.SolanaAccountResult, 0, len(req.Accounts)) for idx, val := range info.Value { if val == nil { // This can happen for an invalid account. - w.ccqLogger.Error("read of account for sol_account query request failed, val is nil", zap.String("requestId", requestId), zap.Any("account", req.Accounts[idx])) - w.ccqSendQueryResponse(queryRequest, query.QueryFatalError, nil) + w.ccqLogger.Error(fmt.Sprintf("read of account for %s query request failed, val is nil", tag), zap.String("requestId", requestId), zap.Any("account", req.Accounts[idx])) + w.ccqSendErrorResponse(queryRequest, query.QueryFatalError) return } if val.Data == nil { - w.ccqLogger.Error("read of account for sol_account query request failed, data is nil", zap.String("requestId", requestId), zap.Any("account", req.Accounts[idx])) - w.ccqSendQueryResponse(queryRequest, query.QueryFatalError, nil) + w.ccqLogger.Error(fmt.Sprintf("read of account for %s query request failed, data is nil", tag), zap.String("requestId", requestId), zap.Any("account", req.Accounts[idx])) + w.ccqSendErrorResponse(queryRequest, query.QueryFatalError) return } results = append(results, query.SolanaAccountResult{ @@ -193,7 +205,7 @@ func (w *SolanaWatcher) ccqHandleSolanaAccountQueryRequest(ctx context.Context, Results: results, } - w.ccqLogger.Info("account read for sol_account_query succeeded", + w.ccqLogger.Info(fmt.Sprintf("account read for %s query succeeded", tag), zap.String("requestId", requestId), zap.Uint64("slotNumber", info.Context.Slot), zap.Uint64("blockTime", uint64(*block.BlockTime)), @@ -201,7 +213,8 @@ func (w *SolanaWatcher) ccqHandleSolanaAccountQueryRequest(ctx context.Context, zap.Uint64("blockHeight", *block.BlockHeight), ) - w.ccqSendQueryResponse(queryRequest, query.QuerySuccess, resp) + // Publish the response using the custom publisher. + publisher.publish(query.CreatePerChainQueryResponseInternal(queryRequest.RequestID, queryRequest.RequestIdx, queryRequest.Request.ChainId, query.QuerySuccess, resp), resp) } // ccqCheckForMinSlotContext checks to see if the returned error was due to the min context slot not being reached. @@ -216,6 +229,8 @@ func (w *SolanaWatcher) ccqCheckForMinSlotContext( err error, giveUpTime time.Time, log bool, + tag string, + publisher ccqCustomPublisher, ) bool { if req.MinContextSlot == 0 { return false @@ -254,7 +269,7 @@ func (w *SolanaWatcher) ccqCheckForMinSlotContext( } // Kick off the retry after a short delay. - go w.ccqSleepAndRetryAccountQuery(ctx, queryRequest, req, requestId, currentSlot, currentSlotFromError, giveUpTime, log) + go w.ccqSleepAndRetryAccountQuery(ctx, queryRequest, req, requestId, currentSlot, currentSlotFromError, giveUpTime, log, tag, publisher) return true } @@ -268,6 +283,8 @@ func (w *SolanaWatcher) ccqSleepAndRetryAccountQuery( currentSlotFromError uint64, giveUpTime time.Time, log bool, + tag string, + publisher ccqCustomPublisher, ) { if log { w.ccqLogger.Info("minimum context slot has not been reached, will retry shortly", @@ -285,7 +302,7 @@ func (w *SolanaWatcher) ccqSleepAndRetryAccountQuery( w.ccqLogger.Info("initiating fast retry", zap.String("requestId", requestId)) } - w.ccqHandleSolanaAccountQueryRequest(ctx, queryRequest, req, giveUpTime, true) + w.ccqBaseHandleSolanaAccountQueryRequest(ctx, queryRequest, req, giveUpTime, tag, requestId, true, publisher) } // ccqIsMinContextSlotError parses an error to see if it is "Minimum context slot has not been reached". If it is, it returns the slot number @@ -331,6 +348,140 @@ func ccqIsMinContextSlotError(err error) (bool, uint64) { return true, currentSlot } +// ccqHandleSolanaAccountQueryRequest is the query handler for a sol_account request. +func (w *SolanaWatcher) ccqHandleSolanaAccountQueryRequest(ctx context.Context, queryRequest *query.PerChainQueryInternal, req *query.SolanaAccountQueryRequest, giveUpTime time.Time) { + requestId := "sol_account" + ":" + queryRequest.ID() + w.ccqLogger.Info("received a sol_account query", + zap.Uint64("minContextSlot", req.MinContextSlot), + zap.Uint64("dataSliceOffset", req.DataSliceOffset), + zap.Uint64("dataSliceLength", req.DataSliceLength), + zap.Int("numAccounts", len(req.Accounts)), + zap.String("requestId", requestId), + ) + + publisher := ccqSolanaAccountPublisher{w} + w.ccqBaseHandleSolanaAccountQueryRequest(ctx, queryRequest, req, giveUpTime, "sol_account", requestId, false, publisher) +} + +// ccqSolanaAccountPublisher is the publisher for the sol_account query. All it has to do is forward the response passed in to the watcher, as is. +type ccqSolanaAccountPublisher struct { + w *SolanaWatcher +} + +func (impl ccqSolanaAccountPublisher) publish(resp *query.PerChainQueryResponseInternal, _ *query.SolanaAccountQueryResponse) { + impl.w.ccqSendQueryResponse(resp) +} + +// ccqHandleSolanaPdaQueryRequest is the query handler for a sol_pda request. +func (w *SolanaWatcher) ccqHandleSolanaPdaQueryRequest(ctx context.Context, queryRequest *query.PerChainQueryInternal, req *query.SolanaPdaQueryRequest, giveUpTime time.Time) { + requestId := "sol_pda:" + queryRequest.ID() + w.ccqLogger.Info("received a sol_pda query", + zap.Uint64("minContextSlot", req.MinContextSlot), + zap.Uint64("dataSliceOffset", req.DataSliceOffset), + zap.Uint64("dataSliceLength", req.DataSliceLength), + zap.Int("numPdas", len(req.PDAs)), + zap.String("requestId", requestId), + ) + + // Derive the list of accounts from the PDAs and save those along with the bumps. + accounts := make([][query.SolanaPublicKeyLength]byte, 0, len(req.PDAs)) + bumps := make([]uint8, 0, len(req.PDAs)) + for _, pda := range req.PDAs { + account, bump, err := solana.FindProgramAddress(pda.Seeds, pda.ProgramAddress) + if err != nil { + w.ccqLogger.Error("failed to derive account from pda for sol_pda query", + zap.String("requestId", requestId), + zap.String("programAddress", hex.EncodeToString(pda.ProgramAddress[:])), + zap.Any("seeds", pda.Seeds), + zap.Error(err), + ) + + w.ccqSendErrorResponse(queryRequest, query.QueryFatalError) + return + } + + accounts = append(accounts, account) + bumps = append(bumps, bump) + } + + // Build a standard sol_account query using the derived accounts. + acctReq := &query.SolanaAccountQueryRequest{ + Commitment: req.Commitment, + MinContextSlot: req.MinContextSlot, + DataSliceOffset: req.DataSliceOffset, + DataSliceLength: req.DataSliceLength, + Accounts: accounts, + } + + publisher := ccqPdaPublisher{ + w: w, + queryRequest: queryRequest, + requestId: requestId, + accounts: accounts, + bumps: bumps, + } + + // Execute the standard sol_account query passing in the publisher to publish a sol_pda response. + w.ccqBaseHandleSolanaAccountQueryRequest(ctx, queryRequest, acctReq, giveUpTime, "sol_pda", requestId, false, publisher) +} + +// ccqPdaPublisher is a custom publisher that publishes a sol_pda response. +type ccqPdaPublisher struct { + w *SolanaWatcher + queryRequest *query.PerChainQueryInternal + requestId string + accounts [][query.SolanaPublicKeyLength]byte + bumps []uint8 +} + +func (pub ccqPdaPublisher) publish(pcrResp *query.PerChainQueryResponseInternal, acctResp *query.SolanaAccountQueryResponse) { + if pcrResp == nil { + // This is the case where we are doing a fast retry, so we don't want to publish anything yet. + return + } + + if pcrResp.Status != query.QuerySuccess { + pub.w.ccqLogger.Error("received an unexpected query response for sol_pda query", zap.String("requestId", pub.requestId), zap.Any("pcrResp", pcrResp)) + return + } + + if acctResp == nil { + pub.w.ccqLogger.Error("sol_pda query failed, acctResp is nil", zap.String("requestId", pub.requestId)) + pub.w.ccqSendErrorResponse(pub.queryRequest, query.QueryFatalError) + return + } + + if len(acctResp.Results) != len(pub.accounts) { + pub.w.ccqLogger.Error("sol_pda query failed, unexpected number of results", zap.String("requestId", pub.requestId), zap.Int("numResults", len(acctResp.Results)), zap.Int("expectedResults", len(pub.accounts))) + pub.w.ccqSendErrorResponse(pub.queryRequest, query.QueryFatalError) + return + } + + // Build the PDA response from the base response. + results := make([]query.SolanaPdaResult, 0, len(pub.accounts)) + for idx, acctResult := range acctResp.Results { + results = append(results, query.SolanaPdaResult{ + Account: pub.accounts[idx], + Bump: pub.bumps[idx], + Lamports: acctResult.Lamports, + RentEpoch: acctResult.RentEpoch, + Executable: acctResult.Executable, + Owner: acctResult.Owner, + Data: acctResult.Data, + }) + } + + resp := &query.SolanaPdaQueryResponse{ + SlotNumber: acctResp.SlotNumber, + BlockTime: acctResp.BlockTime, + BlockHash: acctResp.BlockHash, + Results: results, + } + + // Finally, publish the result. + pub.w.ccqSendQueryResponse(query.CreatePerChainQueryResponseInternal(pub.queryRequest.RequestID, pub.queryRequest.RequestIdx, pub.queryRequest.Request.ChainId, query.QuerySuccess, resp)) +} + type M map[string]interface{} // getMultipleAccountsWithOpts is a work-around for the fact that the library call doesn't honor MinContextSlot. @@ -376,3 +527,92 @@ func (w *SolanaWatcher) getMultipleAccountsWithOpts( } return } + +// TODO: Delete this!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +/* +https://pkg.go.dev/github.com/gagliardetto/solana-go#FindProgramAddress + + [ + Buffer.from("GuardianSet"), + (() => { + const buf = Buffer.alloc(4); + buf.writeUInt32BE(index); + return buf; + })(), + ], + wormholeProgramId +*/ + +func (w *SolanaWatcher) ccqTest(ctx context.Context) { + programID, err := solana.PublicKeyFromBase58("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o") // Core bridge address + if err != nil { + w.ccqLogger.Error("TEST: failed to decode core bridge address", zap.Error(err)) + return + } + + guardianSetIndex, err := hex.DecodeString("00000000") + if err != nil { + w.ccqLogger.Error("TEST: failed to decode guardian set index", zap.Error(err)) + return + } + seeds := [][]byte{[]byte("GuardianSet"), guardianSetIndex} + + account, bump, err := solana.FindProgramAddress(seeds, programID) + if err != nil { + w.ccqLogger.Error("TEST: failed to find guardian set account", zap.Any("programID", programID), zap.Any("seeds", seeds), zap.Error(err)) + return + } + + w.ccqLogger.Info("TEST: found guardian set account", + zap.String("programID", hex.EncodeToString(programID.Bytes())), + zap.Any("seeds", seeds), + zap.Any("account", hex.EncodeToString(account.Bytes())), + zap.Any("bump", bump), + ) + + // Convert the accounts from byte arrays to public keys. + accounts := []solana.PublicKey{account} + + // Create the parameters needed for the account read and add any optional parameters. + params := rpc.GetMultipleAccountsOpts{ + Encoding: solana.EncodingBase64, + Commitment: rpc.CommitmentType("finalized"), + } + + // Read the account. + rCtx, cancel := context.WithTimeout(ctx, rpcTimeout) + defer cancel() + + info, err := w.getMultipleAccountsWithOpts(rCtx, accounts, ¶ms) + if err != nil { + w.ccqLogger.Error("TEST: failed to read account", zap.Error(err)) + return + } + + w.ccqLogger.Info("TEST: read guardian set account", zap.Any("info", info)) + + if len(info.Value) == 0 { + w.ccqLogger.Error("TEST: account read did not return any values") + return + } + + if info.Value[0] == nil { + w.ccqLogger.Error("TEST: account read returned nil value") + return + } + val := info.Value[0] + + if val.Data == nil { + w.ccqLogger.Error("TEST: account read returned nil data") + return + } + + w.ccqLogger.Info("TEST: guardian set account results", + zap.Uint64("Lamports", val.Lamports), + zap.Uint64("RentEpoch", val.RentEpoch), + zap.Bool("Executable", val.Executable), + zap.String("Owner", val.Owner.String()), + zap.String("Data", hex.EncodeToString(val.Data.GetBinary())), + ) +} diff --git a/sdk/js-query/package-lock.json b/sdk/js-query/package-lock.json index 1e481112c6..8b4509ca05 100644 --- a/sdk/js-query/package-lock.json +++ b/sdk/js-query/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@ethersproject/keccak256": "^5.7.0", + "@solana/web3.js": "^1.90.0", "@types/elliptic": "^6.4.14", "bs58": "^4.0.1", "buffer": "^6.0.3", @@ -621,6 +622,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -1540,6 +1552,61 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.90.0.tgz", + "integrity": "sha512-p0cb/COXb8NNVSMkGMPwqQ6NvObZgUitN80uOedMB+jbYWOKOeJBuPnzhenkIV9RX0krGwyuY1Ltn5O8MGFsEw==", + "dependencies": { + "@babel/runtime": "^7.23.4", + "@noble/curves": "^1.2.0", + "@noble/hashes": "^1.3.2", + "@solana/buffer-layout": "^4.0.1", + "agentkeepalive": "^4.5.0", + "bigint-buffer": "^1.1.5", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.0", + "node-fetch": "^2.7.0", + "rpc-websockets": "^7.5.1", + "superstruct": "^0.14.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@solana/web3.js/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1623,6 +1690,14 @@ "base-x": "^3.0.6" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/elliptic": { "version": "6.4.14", "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", @@ -1726,6 +1801,17 @@ "node": ">=0.4.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1946,11 +2032,40 @@ } ] }, + "node_modules/bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.3.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -2069,6 +2184,19 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2226,6 +2354,11 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2311,6 +2444,17 @@ "node": ">=0.10.0" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2399,6 +2543,19 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2442,6 +2599,11 @@ "@scure/bip39": "1.2.1" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2490,12 +2652,25 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2505,6 +2680,11 @@ "bser": "2.1.1" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2794,6 +2974,14 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3052,6 +3240,72 @@ "node": ">=8" } }, + "node_modules/jayson": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "JSONStream": "^1.3.5", + "uuid": "^8.3.2", + "ws": "^7.4.5" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "node_modules/jayson/node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/jayson/node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", @@ -3685,6 +3939,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3697,6 +3956,29 @@ "node": ">=6" } }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -3852,8 +4134,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -3865,7 +4146,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -3881,6 +4161,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4158,6 +4449,11 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4214,6 +4510,25 @@ "node": ">=10" } }, + "node_modules/rpc-websockets": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.9.0.tgz", + "integrity": "sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "eventemitter3": "^4.0.7", + "uuid": "^8.3.2", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4396,6 +4711,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superstruct": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4434,6 +4754,16 @@ "node": ">=8" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -4464,8 +4794,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-jest": { "version": "29.1.0", @@ -4650,6 +4979,19 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -4663,6 +5005,14 @@ "which-typed-array": "^1.1.2" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -5021,14 +5371,12 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -5108,7 +5456,6 @@ "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, "engines": { "node": ">=10.0.0" }, @@ -5649,6 +5996,14 @@ "@babel/helper-plugin-utils": "^7.22.5" } }, + "@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, "@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -6283,6 +6638,51 @@ "@sinonjs/commons": "^3.0.0" } }, + "@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "requires": { + "buffer": "~6.0.3" + } + }, + "@solana/web3.js": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.90.0.tgz", + "integrity": "sha512-p0cb/COXb8NNVSMkGMPwqQ6NvObZgUitN80uOedMB+jbYWOKOeJBuPnzhenkIV9RX0krGwyuY1Ltn5O8MGFsEw==", + "requires": { + "@babel/runtime": "^7.23.4", + "@noble/curves": "^1.2.0", + "@noble/hashes": "^1.3.2", + "@solana/buffer-layout": "^4.0.1", + "agentkeepalive": "^4.5.0", + "bigint-buffer": "^1.1.5", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.0", + "node-fetch": "^2.7.0", + "rpc-websockets": "^7.5.1", + "superstruct": "^0.14.2" + }, + "dependencies": { + "@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "requires": { + "@noble/hashes": "1.3.3" + } + }, + "@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + } + } + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -6366,6 +6766,14 @@ "base-x": "^3.0.6" } }, + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "requires": { + "@types/node": "*" + } + }, "@types/elliptic": { "version": "6.4.14", "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", @@ -6460,6 +6868,14 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, + "agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "requires": { + "humanize-ms": "^1.2.1" + } + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -6621,11 +7037,36 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "requires": { + "bindings": "^1.3.0" + } + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "requires": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } }, "brace-expansion": { "version": "1.1.11", @@ -6704,6 +7145,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "optional": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -6807,6 +7257,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -6872,6 +7327,11 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true }, + "delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6944,6 +7404,19 @@ "is-arrayish": "^0.2.1" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "requires": { + "es6-promise": "^4.0.3" + } + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -6974,6 +7447,11 @@ "@scure/bip39": "1.2.1" } }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7010,12 +7488,22 @@ "jest-util": "^29.5.0" } }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==" + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" + }, "fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -7025,6 +7513,11 @@ "bser": "2.1.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7227,6 +7720,14 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "requires": { + "ms": "^2.0.0" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7400,6 +7901,52 @@ "istanbul-lib-report": "^3.0.0" } }, + "jayson": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", + "integrity": "sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==", + "requires": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "JSONStream": "^1.3.5", + "uuid": "^8.3.2", + "ws": "^7.4.5" + }, + "dependencies": { + "@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "requires": { + "@types/node": "*" + } + }, + "isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "requires": {} + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + } + } + }, "jest": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", @@ -7888,12 +8435,31 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==" + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -8019,8 +8585,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "natural-compare": { "version": "1.4.0", @@ -8032,11 +8597,16 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "requires": { "whatwg-url": "^5.0.0" } }, + "node-gyp-build": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "optional": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8233,6 +8803,11 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8271,6 +8846,19 @@ "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true }, + "rpc-websockets": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.9.0.tgz", + "integrity": "sha512-DwKewQz1IUA5wfLvgM8wDpPRcr+nWSxuFxx5CbrI2z/MyyZ4nXLM86TvIA+cI1ZAdqC8JIBR1mZR55dzaLU+Hw==", + "requires": { + "@babel/runtime": "^7.17.2", + "bufferutil": "^4.0.1", + "eventemitter3": "^4.0.7", + "utf-8-validate": "^5.0.2", + "uuid": "^8.3.2", + "ws": "^8.5.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -8400,6 +8988,11 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "superstruct": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8426,6 +9019,16 @@ "minimatch": "^3.0.4" } }, + "text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -8450,8 +9053,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "ts-jest": { "version": "29.1.0", @@ -8544,6 +9146,15 @@ "picocolors": "^1.0.0" } }, + "utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "optional": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -8557,6 +9168,11 @@ "which-typed-array": "^1.1.2" } }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8840,14 +9456,12 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -8906,7 +9520,6 @@ "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, "requires": {} }, "y18n": { diff --git a/sdk/js-query/package.json b/sdk/js-query/package.json index 39496cbdac..70ce480b33 100644 --- a/sdk/js-query/package.json +++ b/sdk/js-query/package.json @@ -35,6 +35,7 @@ "sideEffects": false, "dependencies": { "@ethersproject/keccak256": "^5.7.0", + "@solana/web3.js": "^1.90.0", "@types/elliptic": "^6.4.14", "bs58": "^4.0.1", "buffer": "^6.0.3", diff --git a/sdk/js-query/src/mock/index.ts b/sdk/js-query/src/mock/index.ts index b8b8c5b032..0e0e91b9bf 100644 --- a/sdk/js-query/src/mock/index.ts +++ b/sdk/js-query/src/mock/index.ts @@ -17,9 +17,14 @@ import { SolanaAccountQueryRequest, SolanaAccountQueryResponse, SolanaAccountResult, + SolanaPdaQueryRequest, + SolanaPdaQueryResponse, + SolanaPdaResult, } from "../query"; import { BinaryWriter } from "../query/BinaryWriter"; +import { PublicKey } from "@solana/web3.js"; + interface SolanaGetMultipleAccountsOpts { commitment: string; minContextSlot?: number; @@ -475,6 +480,114 @@ export class QueryProxyMock { ) ) ); + } else if (type === ChainQueryType.SolanaPda) { + const query = perChainRequest.query as SolanaPdaQueryRequest; + // Validate the request and convert the PDAs into accounts. + if (query.commitment !== "finalized") { + throw new Error( + `Invalid commitment in sol_account query request, must be "finalized"` + ); + } + if ( + query.dataSliceLength === BigInt(0) && + query.dataSliceOffset !== BigInt(0) + ) { + throw new Error( + `data slice offset may not be set if data slice length is zero` + ); + } + if (query.pdas.length <= 0) { + throw new Error(`does not contain any account entries`); + } + if (query.pdas.length > 255) { + throw new Error(`too many account entries`); + } + + let accounts: string[] = []; + let bumps: number[] = []; + query.pdas.forEach((pda) => { + if (pda.programAddress.length != 32) { + throw new Error(`invalid program address length`); + } + + const [acct, bump] = PublicKey.findProgramAddressSync( + pda.seeds, + new PublicKey(pda.programAddress) + ); + accounts.push(acct.toString()); + bumps.push(bump); + }); + + let opts: SolanaGetMultipleAccountsOpts = { + commitment: query.commitment, + }; + if (query.minContextSlot != BigInt(0)) { + opts.minContextSlot = Number(query.minContextSlot); + } + if (query.dataSliceLength !== BigInt(0)) { + opts.dataSlice = { + offset: Number(query.dataSliceOffset), + length: Number(query.dataSliceLength), + }; + } + + const response = await axios.post( + rpc, + { + jsonrpc: "2.0", + id: 1, + method: "getMultipleAccounts", + params: [accounts, opts], + } + ); + + if (!response.data.result) { + throw new Error("Invalid result for getMultipleAccounts"); + } + + const slotNumber = response.data.result.context.slot; + let results: SolanaPdaResult[] = []; + let idx = 0; + response.data.result.value.forEach((val) => { + const account = Buffer.from(base58.decode(accounts[idx].toString())); + results.push({ + account: Uint8Array.from(base58.decode(accounts[idx].toString())), + bump: bumps[idx], + lamports: BigInt(val.lamports), + rentEpoch: BigInt(val.rentEpoch), + executable: Boolean(val.executable), + owner: Uint8Array.from(base58.decode(val.owner.toString())), + data: Uint8Array.from( + Buffer.from(val.data[0].toString(), "base64") + ), + }); + idx += 1; + }); + + const response2 = await axios.post(rpc, { + jsonrpc: "2.0", + id: 1, + method: "getBlock", + params: [ + slotNumber, + { commitment: query.commitment, transactionDetails: "none" }, + ], + }); + + const blockTime = response2.data.result.blockTime; + const blockHash = base58.decode(response2.data.result.blockhash); + + queryResponse.responses.push( + new PerChainQueryResponse( + perChainRequest.chainId, + new SolanaPdaQueryResponse( + BigInt(slotNumber), + BigInt(blockTime) * BigInt(1000000), // time in seconds -> microseconds, + blockHash, + results + ) + ) + ); } else { throw new Error(`Unsupported query type for mock: ${type}`); } diff --git a/sdk/js-query/src/mock/mock.test.ts b/sdk/js-query/src/mock/mock.test.ts index c3c53b83ea..342637db0f 100644 --- a/sdk/js-query/src/mock/mock.test.ts +++ b/sdk/js-query/src/mock/mock.test.ts @@ -7,6 +7,7 @@ import { test, } from "@jest/globals"; import axios from "axios"; +import base58 from "bs58"; import { eth } from "web3"; import { EthCallByTimestampQueryRequest, @@ -19,6 +20,9 @@ import { QueryResponse, SolanaAccountQueryRequest, SolanaAccountQueryResponse, + SolanaPdaEntry, + SolanaPdaQueryRequest, + SolanaPdaQueryResponse, } from ".."; jest.setTimeout(60000); @@ -28,6 +32,18 @@ const POLYGON_NODE_URL = "https://polygon-mumbai-bor.publicnode.com"; const ARBITRUM_NODE_URL = "https://arbitrum-goerli.publicnode.com"; const QUERY_URL = "https://testnet.ccq.vaa.dev/v1/query"; +const SOL_PDAS: SolanaPdaEntry[] = [ + { + programAddress: Uint8Array.from( + base58.decode("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o") + ), // Core Bridge address + seeds: [ + new Uint8Array(Buffer.from("GuardianSet")), + new Uint8Array(Buffer.alloc(4)), + ], // Use index zero in tilt. + }, +]; + let mock: QueryProxyMock; beforeAll(() => { @@ -341,4 +357,37 @@ describe.skip("mocks match testnet", () => { "000000574108aed69daf" ); }); + test("SolanaPda to devnet", async () => { + const query = new QueryRequest(42, [ + new PerChainQueryRequest( + 1, + new SolanaPdaQueryRequest( + "finalized", + SOL_PDAS, + BigInt(0), + BigInt(12), + BigInt(16) // After this, things can change. + ) + ), + ]); + const resp = await mock.mock(query); + const queryResponse = QueryResponse.from(resp.bytes); + const sar = queryResponse.responses[0].response as SolanaPdaQueryResponse; + expect(sar.blockTime).not.toEqual(BigInt(0)); + expect(sar.results.length).toEqual(1); + + expect(Buffer.from(sar.results[0].account).toString("hex")).toEqual( + "4fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773e" + ); + expect(sar.results[0].bump).toEqual(253); + expect(sar.results[0].lamports).toEqual(BigInt(1141440)); + expect(sar.results[0].rentEpoch).toEqual(BigInt(0)); + expect(sar.results[0].executable).toEqual(false); + expect(Buffer.from(sar.results[0].owner).toString("hex")).toEqual( + "02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa" + ); + expect(Buffer.from(sar.results[0].data).toString("hex")).toEqual( + "57cd18b7f8a4d91a2da9ab4af05d0fbe" + ); + }); }); diff --git a/sdk/js-query/src/query/index.ts b/sdk/js-query/src/query/index.ts index c2aa4d4597..0454871d8c 100644 --- a/sdk/js-query/src/query/index.ts +++ b/sdk/js-query/src/query/index.ts @@ -6,4 +6,5 @@ export * from "./ethCall"; export * from "./ethCallByTimestamp"; export * from "./ethCallWithFinality"; export * from "./solanaAccount"; +export * from "./solanaPda"; export * from "./consts"; diff --git a/sdk/js-query/src/query/request.ts b/sdk/js-query/src/query/request.ts index 846b4567c9..e803d7428e 100644 --- a/sdk/js-query/src/query/request.ts +++ b/sdk/js-query/src/query/request.ts @@ -8,6 +8,7 @@ import { EthCallQueryRequest } from "./ethCall"; import { EthCallByTimestampQueryRequest } from "./ethCallByTimestamp"; import { EthCallWithFinalityQueryRequest } from "./ethCallWithFinality"; import { SolanaAccountQueryRequest } from "./solanaAccount"; +import { SolanaPdaQueryRequest } from "./solanaPda"; export const MAINNET_QUERY_REQUEST_PREFIX = "mainnet_query_request_000000000000|"; @@ -104,6 +105,8 @@ export class PerChainQueryRequest { query = EthCallWithFinalityQueryRequest.fromReader(reader); } else if (queryType === ChainQueryType.SolanaAccount) { query = SolanaAccountQueryRequest.fromReader(reader); + } else if (queryType === ChainQueryType.SolanaPda) { + query = SolanaPdaQueryRequest.fromReader(reader); } else { throw new Error(`Unsupported query type: ${queryType}`); } @@ -121,4 +124,5 @@ export enum ChainQueryType { EthCallByTimeStamp = 2, EthCallWithFinality = 3, SolanaAccount = 4, + SolanaPda = 5, } diff --git a/sdk/js-query/src/query/response.ts b/sdk/js-query/src/query/response.ts index 38bfa5dd17..07772d2764 100644 --- a/sdk/js-query/src/query/response.ts +++ b/sdk/js-query/src/query/response.ts @@ -8,6 +8,7 @@ import { EthCallQueryResponse } from "./ethCall"; import { EthCallByTimestampQueryResponse } from "./ethCallByTimestamp"; import { EthCallWithFinalityQueryResponse } from "./ethCallWithFinality"; import { SolanaAccountQueryResponse } from "./solanaAccount"; +import { SolanaPdaQueryResponse } from "./solanaPda"; export const QUERY_RESPONSE_PREFIX = "query_response_0000000000000000000|"; @@ -112,6 +113,8 @@ export class PerChainQueryResponse { response = EthCallWithFinalityQueryResponse.fromReader(reader); } else if (queryType === ChainQueryType.SolanaAccount) { response = SolanaAccountQueryResponse.fromReader(reader); + } else if (queryType === ChainQueryType.SolanaPda) { + response = SolanaPdaQueryResponse.fromReader(reader); } else { throw new Error(`Unsupported response type: ${queryType}`); } diff --git a/sdk/js-query/src/query/solana.test.ts b/sdk/js-query/src/query/solana.test.ts index e788db6e5b..2189e0ec1a 100644 --- a/sdk/js-query/src/query/solana.test.ts +++ b/sdk/js-query/src/query/solana.test.ts @@ -14,6 +14,10 @@ import { SolanaAccountQueryRequest, SolanaAccountQueryResponse, SolanaAccountResult, + SolanaPdaEntry, + SolanaPdaQueryRequest, + SolanaPdaQueryResponse, + SolanaPdaResult, PerChainQueryRequest, QueryRequest, sign, @@ -27,7 +31,9 @@ const ENV = "DEVNET"; const SERVER_URL = CI ? "http://query-server:" : "http://localhost:"; const CCQ_SERVER_URL = SERVER_URL + "6069/v1"; const QUERY_URL = CCQ_SERVER_URL + "/query"; -const SOLANA_NODE_URL = CI ? "http://solana-devnet:8899" : "http://localhost:8899"; +const SOLANA_NODE_URL = CI + ? "http://solana-devnet:8899" + : "http://localhost:8899"; const PRIVATE_KEY = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; @@ -37,6 +43,18 @@ const ACCOUNTS = [ "BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna", // Example NFT in devnet ]; +const PDAS: SolanaPdaEntry[] = [ + { + programAddress: Uint8Array.from( + base58.decode("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o") + ), // Core Bridge address + seeds: [ + new Uint8Array(Buffer.from("GuardianSet")), + new Uint8Array(Buffer.alloc(4)), + ], // Use index zero in tilt. + }, +]; + async function getSolanaSlot(comm: string): Promise { const response = await axios.post(SOLANA_NODE_URL, { jsonrpc: "2.0", @@ -257,4 +275,130 @@ describe("solana", () => { "01000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d01000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000" ); }); + test("serialize and deserialize sol_pda request with defaults", () => { + const solPdaReq = new SolanaPdaQueryRequest( + "finalized", + PDAS, + BigInt(123456), + BigInt(12), + BigInt(20) + ); + expect(solPdaReq.minContextSlot).toEqual(BigInt(123456)); + expect(solPdaReq.dataSliceOffset).toEqual(BigInt(12)); + expect(solPdaReq.dataSliceLength).toEqual(BigInt(20)); + const serialized = solPdaReq.serialize(); + expect(Buffer.from(serialized).toString("hex")).toEqual( + "0000000966696e616c697a6564000000000001e240000000000000000c00000000000000140102c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000" + ); + const solPdaReq2 = SolanaPdaQueryRequest.from(serialized); + expect(solPdaReq2).toEqual(solPdaReq); + }); + test("successful sol_pda query", async () => { + const solPdaReq = new SolanaPdaQueryRequest( + "finalized", + PDAS, + BigInt(0), + BigInt(12), + BigInt(16) // After this, things can change. + ); + const nonce = 43; + const query = new PerChainQueryRequest(1, solPdaReq); + const request = new QueryRequest(nonce, [query]); + const serialized = request.serialize(); + const digest = QueryRequest.digest(ENV, serialized); + const signature = sign(PRIVATE_KEY, digest); + const response = await axios.put( + QUERY_URL, + { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "my_secret_key" } } + ); + expect(response.status).toBe(200); + + const queryResponse = QueryResponse.from(response.data.bytes); + expect(queryResponse.version).toEqual(1); + expect(queryResponse.requestChainId).toEqual(0); + expect(queryResponse.request.version).toEqual(1); + expect(queryResponse.request.requests.length).toEqual(1); + expect(queryResponse.request.requests[0].chainId).toEqual(1); + expect(queryResponse.request.requests[0].query.type()).toEqual( + ChainQueryType.SolanaPda + ); + + const sar = queryResponse.responses[0].response as SolanaPdaQueryResponse; + expect(sar.slotNumber).not.toEqual(BigInt(0)); + expect(sar.blockTime).not.toEqual(BigInt(0)); + expect(sar.results.length).toEqual(1); + + expect(Buffer.from(sar.results[0].account).toString("hex")).toEqual( + "4fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773e" + ); + expect(sar.results[0].bump).toEqual(253); + expect(sar.results[0].lamports).toEqual(BigInt(1141440)); + expect(sar.results[0].rentEpoch).toEqual(BigInt(0)); + expect(sar.results[0].executable).toEqual(false); + expect(Buffer.from(sar.results[0].owner).toString("hex")).toEqual( + "02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa" + ); + expect(Buffer.from(sar.results[0].data).toString("hex")).toEqual( + "57cd18b7f8a4d91a2da9ab4af05d0fbe" + ); + }); + test("successful sol_pda query with future min context slot", async () => { + const currSlot = await getSolanaSlot("finalized"); + const minContextSlot = BigInt(currSlot) + BigInt(10); + const solPdaReq = new SolanaPdaQueryRequest( + "finalized", + PDAS, + minContextSlot, + BigInt(12), + BigInt(16) // After this, things can change. + ); + const nonce = 43; + const query = new PerChainQueryRequest(1, solPdaReq); + const request = new QueryRequest(nonce, [query]); + const serialized = request.serialize(); + const digest = QueryRequest.digest(ENV, serialized); + const signature = sign(PRIVATE_KEY, digest); + const response = await axios.put( + QUERY_URL, + { + signature, + bytes: Buffer.from(serialized).toString("hex"), + }, + { headers: { "X-API-Key": "my_secret_key" } } + ); + expect(response.status).toBe(200); + + const queryResponse = QueryResponse.from(response.data.bytes); + expect(queryResponse.version).toEqual(1); + expect(queryResponse.requestChainId).toEqual(0); + expect(queryResponse.request.version).toEqual(1); + expect(queryResponse.request.requests.length).toEqual(1); + expect(queryResponse.request.requests[0].chainId).toEqual(1); + expect(queryResponse.request.requests[0].query.type()).toEqual( + ChainQueryType.SolanaPda + ); + + const sar = queryResponse.responses[0].response as SolanaPdaQueryResponse; + expect(sar.slotNumber).toEqual(minContextSlot); + expect(sar.blockTime).not.toEqual(BigInt(0)); + expect(sar.results.length).toEqual(1); + + expect(Buffer.from(sar.results[0].account).toString("hex")).toEqual( + "4fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773e" + ); + expect(sar.results[0].bump).toEqual(253); + expect(sar.results[0].lamports).toEqual(BigInt(1141440)); + expect(sar.results[0].rentEpoch).toEqual(BigInt(0)); + expect(sar.results[0].executable).toEqual(false); + expect(Buffer.from(sar.results[0].owner).toString("hex")).toEqual( + "02c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa" + ); + expect(Buffer.from(sar.results[0].data).toString("hex")).toEqual( + "57cd18b7f8a4d91a2da9ab4af05d0fbe" + ); + }); }); diff --git a/sdk/js-query/src/query/solanaAccount.ts b/sdk/js-query/src/query/solanaAccount.ts index 45b80d5641..c3f528fad9 100644 --- a/sdk/js-query/src/query/solanaAccount.ts +++ b/sdk/js-query/src/query/solanaAccount.ts @@ -3,7 +3,7 @@ import base58 from "bs58"; import { BinaryWriter } from "./BinaryWriter"; import { HexString } from "./consts"; import { ChainQueryType, ChainSpecificQuery } from "./request"; -import { coalesceUint8Array, hexToUint8Array, isValidHexString } from "./utils"; +import { bigIntWithDef, coalesceUint8Array } from "./utils"; import { BinaryReader } from "./BinaryReader"; import { ChainSpecificResponse } from "./response"; @@ -181,7 +181,3 @@ export interface SolanaAccountResult { owner: Uint8Array; data: Uint8Array; } - -function bigIntWithDef(val: bigint | undefined): bigint { - return BigInt(val !== undefined ? val : BigInt(0)); -} diff --git a/sdk/js-query/src/query/solanaPda.ts b/sdk/js-query/src/query/solanaPda.ts new file mode 100644 index 0000000000..c948a4e260 --- /dev/null +++ b/sdk/js-query/src/query/solanaPda.ts @@ -0,0 +1,212 @@ +import { Buffer } from "buffer"; +import { BinaryWriter } from "./BinaryWriter"; +import { ChainQueryType, ChainSpecificQuery } from "./request"; +import { bigIntWithDef, coalesceUint8Array } from "./utils"; +import { BinaryReader } from "./BinaryReader"; +import { ChainSpecificResponse } from "./response"; + +export interface SolanaPdaEntry { + programAddress: Uint8Array; + seeds: Uint8Array[]; +} + +export class SolanaPdaQueryRequest implements ChainSpecificQuery { + commitment: string; + minContextSlot: bigint; + dataSliceOffset: bigint; + dataSliceLength: bigint; + + constructor( + commitment: "finalized", + public pdas: SolanaPdaEntry[], + minContextSlot?: bigint, + dataSliceOffset?: bigint, + dataSliceLength?: bigint + ) { + pdas.forEach((pda) => { + if (pda.programAddress.length != 32) { + throw new Error( + `Invalid program address, must be 32 bytes: ${pda.programAddress}` + ); + } + if (pda.seeds.length == 0) { + throw new Error( + `Invalid pda, has no seeds: ${Buffer.from( + pda.programAddress + ).toString("hex")}` + ); + } + }); + + this.commitment = commitment; + this.minContextSlot = bigIntWithDef(minContextSlot); + this.dataSliceOffset = bigIntWithDef(dataSliceOffset); + this.dataSliceLength = bigIntWithDef(dataSliceLength); + } + + type(): ChainQueryType { + return ChainQueryType.SolanaPda; + } + + serialize(): Uint8Array { + const writer = new BinaryWriter() + .writeUint32(this.commitment.length) + .writeUint8Array(Buffer.from(this.commitment)) + .writeUint64(this.minContextSlot) + .writeUint64(this.dataSliceOffset) + .writeUint64(this.dataSliceLength) + .writeUint8(this.pdas.length); + this.pdas.forEach((pda) => { + writer.writeUint8Array(pda.programAddress).writeUint8(pda.seeds.length); + pda.seeds.forEach((seed) => { + writer.writeUint32(seed.length).writeUint8Array(seed); + }); + }); + return writer.data(); + } + + static from(bytes: string | Uint8Array): SolanaPdaQueryRequest { + const reader = new BinaryReader(coalesceUint8Array(bytes)); + return this.fromReader(reader); + } + + static fromReader(reader: BinaryReader): SolanaPdaQueryRequest { + const commitmentLength = reader.readUint32(); + const commitment = reader.readString(commitmentLength); + if (commitment !== "finalized") { + throw new Error(`Invalid commitment: ${commitment}`); + } + const minContextSlot = reader.readUint64(); + const dataSliceOffset = reader.readUint64(); + const dataSliceLength = reader.readUint64(); + const numPdas = reader.readUint8(); + const pdas: SolanaPdaEntry[] = []; + for (let idx = 0; idx < numPdas; idx++) { + const programAddress = reader.readUint8Array(32); + let seeds: Uint8Array[] = []; + const numSeeds = reader.readUint8(); + for (let idx2 = 0; idx2 < numSeeds; idx2++) { + const seedLen = reader.readUint32(); + const seed = reader.readUint8Array(seedLen); + seeds.push(seed); + } + pdas.push({ programAddress, seeds }); + } + return new SolanaPdaQueryRequest( + commitment, + pdas, + minContextSlot, + dataSliceOffset, + dataSliceLength + ); + } +} + +export class SolanaPdaQueryResponse implements ChainSpecificResponse { + slotNumber: bigint; + blockTime: bigint; + blockHash: Uint8Array; + results: SolanaPdaResult[]; + + constructor( + slotNumber: bigint, + blockTime: bigint, + blockHash: Uint8Array, + results: SolanaPdaResult[] + ) { + if (blockHash.length != 32) { + throw new Error( + `Invalid block hash, should be 32 bytes long: ${blockHash}` + ); + } + for (const result of results) { + if (result.account.length != 32) { + throw new Error( + `Invalid account, should be 32 bytes long: ${result.account}` + ); + } + if (result.owner.length != 32) { + throw new Error( + `Invalid owner, should be 32 bytes long: ${result.owner}` + ); + } + } + this.slotNumber = slotNumber; + this.blockTime = blockTime; + this.blockHash = blockHash; + this.results = results; + } + + type(): ChainQueryType { + return ChainQueryType.SolanaPda; + } + + serialize(): Uint8Array { + const writer = new BinaryWriter() + .writeUint64(this.slotNumber) + .writeUint64(this.blockTime) + .writeUint8Array(this.blockHash) + .writeUint8(this.results.length); + for (const result of this.results) { + writer + .writeUint8Array(result.account) + .writeUint8(result.bump) + .writeUint64(result.lamports) + .writeUint64(result.rentEpoch) + .writeUint8(result.executable ? 1 : 0) + .writeUint8Array(result.owner) + .writeUint32(result.data.length) + .writeUint8Array(result.data); + } + return writer.data(); + } + + static from(bytes: string | Uint8Array): SolanaPdaQueryResponse { + const reader = new BinaryReader(coalesceUint8Array(bytes)); + return this.fromReader(reader); + } + + static fromReader(reader: BinaryReader): SolanaPdaQueryResponse { + const slotNumber = reader.readUint64(); + const blockTime = reader.readUint64(); + const blockHash = reader.readUint8Array(32); + const resultsLength = reader.readUint8(); + const results: SolanaPdaResult[] = []; + for (let idx = 0; idx < resultsLength; idx++) { + const account = reader.readUint8Array(32); + const bump = reader.readUint8(); + const lamports = reader.readUint64(); + const rentEpoch = reader.readUint64(); + const executableU8 = reader.readUint8(); + const executable = executableU8 != 0; + const owner = reader.readUint8Array(32); + const dataLength = reader.readUint32(); + const data = reader.readUint8Array(dataLength); + results.push({ + account, + bump, + lamports, + rentEpoch, + executable, + owner, + data, + }); + } + return new SolanaPdaQueryResponse( + slotNumber, + blockTime, + blockHash, + results + ); + } +} + +export interface SolanaPdaResult { + account: Uint8Array; + bump: number; + lamports: bigint; + rentEpoch: bigint; + executable: boolean; + owner: Uint8Array; + data: Uint8Array; +} diff --git a/sdk/js-query/src/query/utils.ts b/sdk/js-query/src/query/utils.ts index 7c17c5d0db..61d72f3703 100644 --- a/sdk/js-query/src/query/utils.ts +++ b/sdk/js-query/src/query/utils.ts @@ -48,3 +48,11 @@ export function sign(key: string, data: Uint8Array): string { Buffer.from([signature.recoveryParam ?? 0]).toString("hex"); return packed; } + +/** + * @param val value to be converted to a big int + * @returns the value or zero as a bigint + */ +export function bigIntWithDef(val: bigint | undefined): bigint { + return BigInt(val !== undefined ? val : BigInt(0)); +} diff --git a/whitepapers/0013_ccq.md b/whitepapers/0013_ccq.md index 98714740d5..bb3f278e20 100644 --- a/whitepapers/0013_ccq.md +++ b/whitepapers/0013_ccq.md @@ -54,7 +54,7 @@ CCQ will run as an optional component in `guardiand`. If it is not configured, i The request format is extensible in order to support querying data across heterogeneous chains and batching of requests to minimize gossip traffic and RPC overhead. -The initial release of CCQ will only support EVM chains. However, the software is extensible to other chains, such as Solana, CosmWasm, etc. +The current release of CCQ, as of February 2024, supports EVM chains and Solana. However, the software is extensible to other chains, such as CosmWasm, etc. #### Off-Chain Requests @@ -317,7 +317,42 @@ Currently the only supported query type on Solana is `sol_account`. - The `data_slice_offset` and `data_slice_length` are optional and specify the portion of the account data that should be returned. - - The `account_list` specifies a list of accounts to be batched into a single call. Each account in the list is a Solana `PublicKey` + - The `account_list` specifies a list of accounts to be batched into a single query. Each account in the list is a Solana `PublicKey` + +2. sol_pda (query type 5) - this query is used to read data for one or more accounts on Solana based on their Program Derived Addresses. + + ```go + u32 commitment_len + []byte commitment + u64 min_context_slot + u64 data_slice_offset + u64 data_slice_length + u8 num_pdas + []PdaList pda_list + ``` + + - The `commitment` is required and currently must be `finalized`. + + - The `min_context_slot` is optional and specifies the minimum slot at which the request may be evaluated. + + - The `data_slice_offset` and `data_slice_length` are optional and specify the portion of the account data that should be returned. + + - The `pda_list` specifies a list of program derived addresses batched into a single query. + + `PdaList` is defined as follows: + + ```go + [32]byte program_address + u8 num_seeds + []Seed seed_data (max of 16, per the Solana code) + ``` + + Each `Seed` is defined as follows: + + ```go + u32 seed_len (max of 32, per the Solana code) + []byte seed + ``` ## Query Response @@ -421,6 +456,40 @@ uint32 response_len - The `owner` is the public key of the owner of the account. - The `result` is the data returned by the account query. +2. sol_pda (query type 5) Response Body + + ```go + u64 slot_number + u64 block_time_us + [32]byte block_hash + u8 num_results + []byte results + ``` + + - The `slot_number` is the slot number returned by the query. + - The `block_time_us` is the timestamp of the block associated with the slot. + - The `block_hash` is the block hash associated with the slot. + - The `results` array returns the data for each PDA queried + + ```go + [32]byte account + u8 bump + u64 lamports + u64 rent_epoch + u8 executable + [32]byte owner + u32 result_len + []byte result + ``` + + - The `account` is the account address derived from the PDA. + - The `bump` is the bump value returned by the Solana derivation function. + - The `lamports` is the number of lamports assigned to the account. + - The `rent_epoch` is the epoch at which this account will next owe rent. + - The `executable` is a boolean indicating if the account contains a program (and is strictly read-only). + - The `owner` is the public key of the owner of the account. + - The `result` is the data returned by the account query. + ## REST Service ### Request