Node/CCQ/Solana: Add sol_pda query (#3782)
* Node/CCQ/Solana: Add sol_pda query * Attempting to fix bigint serialize error in tests * Try backing out sol_pda tests * Put some of solana.test.ts changes back * Add more stuff back * Add more stuff to solana.test.ts * Add more solana.test.ts stuff * Whatever * More sol_pda test debugging * Code review rework * More rework
This commit is contained in:
parent
66f8e85158
commit
c751af3ea3
|
@ -146,6 +146,13 @@
|
|||
"chain": 1,
|
||||
"account": "BVxyYhm498L79r4HMQ9sxZ5bi41DmJmeWZ7SCS7Cyvna"
|
||||
}
|
||||
},
|
||||
{
|
||||
"solPDA": {
|
||||
"note:": "Core Bridge on Devnet",
|
||||
"chain": 1,
|
||||
"programAddress": "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
// As a future enhancement, we may want to specify the allowed seeds.
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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("Solana tests complete!")
|
||||
}
|
||||
|
||||
{
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -482,7 +509,9 @@ func sendSolanaQueryAndGetRsp(queryRequest *query.QueryRequest, sk *ecdsa.Privat
|
|||
for index := range response.PerChainResponses {
|
||||
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))
|
||||
logger.Info("solana account query per chain response", zap.Int("index", index), zap.Any("pcr", r))
|
||||
case *query.SolanaPdaQueryResponse:
|
||||
logger.Info("solana pda 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))
|
||||
}
|
||||
|
|
|
@ -433,11 +433,10 @@ 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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -787,7 +787,65 @@ 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))
|
||||
}
|
||||
|
||||
func TestSolanaPdaQueryUnmarshalFromSDK(t *testing.T) {
|
||||
serialized, err := hex.DecodeString("010000002b010001050000005e0000000966696e616c697a656400000000000008ff000000000000000c00000000000000140102c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000")
|
||||
require.NoError(t, err)
|
||||
|
||||
var solQuery QueryRequest
|
||||
err = solQuery.Unmarshal(serialized)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
///////////// End of Solana PDA Query tests ///////////////////////////
|
||||
|
||||
func TestPostSignedQueryRequestShouldFailIfNoOneIsListening(t *testing.T) {
|
||||
queryRequest := createQueryRequestForTesting(t, vaa.ChainIDPolygon)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 ///////////////////////////
|
||||
|
|
|
@ -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,143 @@ 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 {
|
||||
pub.w.ccqLogger.Error("sol_pda query failed, pcrResp is nil", zap.String("requestId", pub.requestId))
|
||||
pub.w.ccqSendErrorResponse(pub.queryRequest, query.QueryFatalError)
|
||||
return
|
||||
}
|
||||
|
||||
if pcrResp.Status != query.QuerySuccess {
|
||||
// publish() should only get called in success cases.
|
||||
pub.w.ccqLogger.Error("received an unexpected query response for sol_pda query", zap.String("requestId", pub.requestId), zap.Any("pcrResp", pcrResp))
|
||||
pub.w.ccqSendErrorResponse(pub.queryRequest, query.QueryFatalError)
|
||||
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.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -34,6 +34,7 @@
|
|||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@ethersproject/keccak256": "^5.7.0",
|
||||
"@solana/web3.js": "^1.66.2",
|
||||
"@types/elliptic": "^6.4.14",
|
||||
"axios": "^1.6.7",
|
||||
"bs58": "^4.0.1",
|
||||
|
|
|
@ -17,9 +17,14 @@ import {
|
|||
SolanaAccountQueryRequest,
|
||||
SolanaAccountQueryResponse,
|
||||
SolanaAccountResult,
|
||||
SolanaPdaQueryRequest,
|
||||
SolanaPdaQueryResponse,
|
||||
SolanaPdaResult,
|
||||
} from "../query";
|
||||
import { BinaryWriter } from "../query/BinaryWriter";
|
||||
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
// (2**64)-1
|
||||
const SOLANA_MAX_RENT_EPOCH = BigInt("18446744073709551615");
|
||||
|
||||
|
@ -485,6 +490,113 @@ 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<SolanaGetMultipleAccountsResponse>(
|
||||
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) => {
|
||||
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}`);
|
||||
}
|
||||
|
|
|
@ -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(120000);
|
||||
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,4 +6,5 @@ export * from "./ethCall";
|
|||
export * from "./ethCallByTimestamp";
|
||||
export * from "./ethCallWithFinality";
|
||||
export * from "./solanaAccount";
|
||||
export * from "./solanaPda";
|
||||
export * from "./consts";
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import {
|
|||
SolanaAccountQueryRequest,
|
||||
SolanaAccountQueryResponse,
|
||||
SolanaAccountResult,
|
||||
SolanaPdaEntry,
|
||||
SolanaPdaQueryRequest,
|
||||
SolanaPdaQueryResponse,
|
||||
PerChainQueryRequest,
|
||||
QueryRequest,
|
||||
sign,
|
||||
|
@ -27,7 +30,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 +42,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<bigint> {
|
||||
const response = await axios.post(SOLANA_NODE_URL, {
|
||||
jsonrpc: "2.0",
|
||||
|
@ -173,12 +190,12 @@ describe("solana", () => {
|
|||
|
||||
const sar = queryResponse.responses[0]
|
||||
.response as SolanaAccountQueryResponse;
|
||||
expect(sar.slotNumber).not.toEqual(BigInt(0));
|
||||
expect(sar.blockTime).not.toEqual(BigInt(0));
|
||||
expect(Number(sar.slotNumber)).not.toEqual(0);
|
||||
expect(Number(sar.blockTime)).not.toEqual(0);
|
||||
expect(sar.results.length).toEqual(2);
|
||||
|
||||
expect(sar.results[0].lamports).toEqual(BigInt(1461600));
|
||||
expect(sar.results[0].rentEpoch).toEqual(BigInt(0));
|
||||
expect(Number(sar.results[0].lamports)).toEqual(1461600);
|
||||
expect(Number(sar.results[0].rentEpoch)).toEqual(0);
|
||||
expect(sar.results[0].executable).toEqual(false);
|
||||
expect(base58.encode(Buffer.from(sar.results[0].owner))).toEqual(
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
|
@ -187,8 +204,8 @@ describe("solana", () => {
|
|||
"01000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a0901000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
);
|
||||
|
||||
expect(sar.results[1].lamports).toEqual(BigInt(1461600));
|
||||
expect(sar.results[1].rentEpoch).toEqual(BigInt(0));
|
||||
expect(Number(sar.results[1].lamports)).toEqual(1461600);
|
||||
expect(Number(sar.results[1].rentEpoch)).toEqual(0);
|
||||
expect(sar.results[1].executable).toEqual(false);
|
||||
expect(base58.encode(Buffer.from(sar.results[1].owner))).toEqual(
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
|
@ -234,11 +251,11 @@ describe("solana", () => {
|
|||
const sar = queryResponse.responses[0]
|
||||
.response as SolanaAccountQueryResponse;
|
||||
expect(sar.slotNumber).toEqual(minContextSlot);
|
||||
expect(sar.blockTime).not.toEqual(BigInt(0));
|
||||
expect(Number(sar.blockTime)).not.toEqual(0);
|
||||
expect(sar.results.length).toEqual(2);
|
||||
|
||||
expect(sar.results[0].lamports).toEqual(BigInt(1461600));
|
||||
expect(sar.results[0].rentEpoch).toEqual(BigInt(0));
|
||||
expect(Number(sar.results[0].lamports)).toEqual(1461600);
|
||||
expect(Number(sar.results[0].rentEpoch)).toEqual(0);
|
||||
expect(sar.results[0].executable).toEqual(false);
|
||||
expect(base58.encode(Buffer.from(sar.results[0].owner))).toEqual(
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
|
@ -247,8 +264,8 @@ describe("solana", () => {
|
|||
"01000000574108aed69daf7e625a361864b1f74d13702f2ca56de9660e566d1d8691848d0000e8890423c78a0901000000000000000000000000000000000000000000000000000000000000000000000000"
|
||||
);
|
||||
|
||||
expect(sar.results[1].lamports).toEqual(BigInt(1461600));
|
||||
expect(sar.results[1].rentEpoch).toEqual(BigInt(0));
|
||||
expect(Number(sar.results[1].lamports)).toEqual(1461600);
|
||||
expect(Number(sar.results[1].rentEpoch)).toEqual(0);
|
||||
expect(sar.results[1].executable).toEqual(false);
|
||||
expect(base58.encode(Buffer.from(sar.results[1].owner))).toEqual(
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
|
@ -257,4 +274,168 @@ 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(Number(solPdaReq.minContextSlot)).toEqual(123456);
|
||||
expect(Number(solPdaReq.dataSliceOffset)).toEqual(12);
|
||||
expect(Number(solPdaReq.dataSliceLength)).toEqual(20);
|
||||
const serialized = solPdaReq.serialize();
|
||||
expect(Buffer.from(serialized).toString("hex")).toEqual(
|
||||
"0000000966696e616c697a6564000000000001e240000000000000000c00000000000000140102c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000"
|
||||
);
|
||||
const solPdaReq2 = SolanaPdaQueryRequest.from(serialized);
|
||||
expect(solPdaReq2).toEqual(solPdaReq);
|
||||
});
|
||||
|
||||
test("deserialize sol_pda response", () => {
|
||||
const respBytes = Buffer.from(
|
||||
"0100000c8418d81c00aad6283ba3eb30e141ccdd9296e013ca44e5cc713418921253004b93107ba0d858a548ce989e2bca4132e4c2f9a57a9892e3a87a8304cdb36d8f000000006b010000002b010001050000005e0000000966696e616c697a656400000000000008ff000000000000000c00000000000000140102c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa020000000b477561726469616e5365740000000400000000010001050000009b00000000000008ff0006115e3f6d7540e05035785e15056a8559815e71343ce31db2abf23f65b19c982b68aee7bf207b014fa9188b339cfd573a0778c5deaeeee94d4bcfb12b345bf8e417e5119dae773efd0000000000116ac000000000000000000002c806312cbe5b79ef8aa6c17e3f423d8fdfe1d46909fb1f6cdf65ee8e2e6faa0000001457cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65",
|
||||
"hex"
|
||||
);
|
||||
const queryResponse = QueryResponse.from(respBytes);
|
||||
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(Number(sar.slotNumber)).toEqual(2303);
|
||||
expect(Number(sar.blockTime)).toEqual(0x0006115e3f6d7540);
|
||||
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(Number(sar.results[0].lamports)).not.toEqual(0);
|
||||
expect(Number(sar.results[0].rentEpoch)).toEqual(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(
|
||||
"57cd18b7f8a4d91a2da9ab4af05d0fbece2dcd65"
|
||||
);
|
||||
});
|
||||
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(Number(sar.slotNumber)).not.toEqual(0);
|
||||
expect(Number(sar.blockTime)).not.toEqual(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(Number(sar.results[0].lamports)).not.toEqual(0);
|
||||
|
||||
expect(Number(sar.results[0].rentEpoch)).toEqual(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(Number(sar.blockTime)).not.toEqual(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(Number(sar.results[0].lamports)).not.toEqual(0);
|
||||
expect(Number(sar.results[0].rentEpoch)).toEqual(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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Buffer } from "buffer";
|
||||
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 +180,3 @@ export interface SolanaAccountResult {
|
|||
owner: Uint8Array;
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
function bigIntWithDef(val: bigint | undefined): bigint {
|
||||
return BigInt(val !== undefined ? val : BigInt(0));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
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[];
|
||||
}
|
||||
|
||||
// According to the spec, there may be at most 16 seeds.
|
||||
// https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L559
|
||||
export const SolanaMaxSeeds = 16;
|
||||
|
||||
// According to the spec, a seed may be at most 32 bytes.
|
||||
// https://github.com/gagliardetto/solana-go/blob/6fe3aea02e3660d620433444df033fc3fe6e64c1/keys.go#L557
|
||||
export const SolanaMaxSeedLen = 32;
|
||||
|
||||
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")}`
|
||||
);
|
||||
}
|
||||
if (pda.seeds.length > SolanaMaxSeeds) {
|
||||
throw new Error(
|
||||
`Invalid pda, has too many seeds: ${Buffer.from(
|
||||
pda.programAddress
|
||||
).toString("hex")}`
|
||||
);
|
||||
}
|
||||
pda.seeds.forEach((seed) => {
|
||||
if (seed.length == 0) {
|
||||
throw new Error(
|
||||
`Invalid pda, seed is null: ${Buffer.from(
|
||||
pda.programAddress
|
||||
).toString("hex")}`
|
||||
);
|
||||
}
|
||||
if (seed.length > SolanaMaxSeedLen) {
|
||||
throw new Error(
|
||||
`Invalid pda, seed is too long: ${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;
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue