Skip to content
This repository was archived by the owner on May 31, 2023. It is now read-only.

Commit a5f463e

Browse files
author
Richard Patel
authored
More features (#8)
- envs: add Env type and Devnet, Testnet, Mainnet default envs - accounts: add JSON support for ProductAccount - query: add Client.GetMappingAccount - query: add Client.GetAllProductKeys - query: add Client.GetAllProducts - README: add attributions
1 parent ac532ed commit a5f463e

File tree

14 files changed

+567
-279
lines changed

14 files changed

+567
-279
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
<a href="https://pkg.go.dev/go.blockdaemon.com/pyth"><img src="https://pkg.go.dev/badge/go.blockdaemon.com/pyth.svg" alt="Go Reference"></a>
88
<a href="https://github.com/Blockdaemon/pyth-go/actions/workflows/test.yml"><img src="https://github.com/Blockdaemon/pyth-go/actions/workflows/test.yml/badge.svg" alt="unit tests"></a>
99
</p>
10-
<sub>Built with 👿 at <a href="https://blockdaemon.com">Blockdaemon</a></sub>
1110
</div>
1211

1312
### Summary
@@ -27,3 +26,8 @@ go get go.blockdaemon.com/pyth@latest
2726
```
2827

2928
Find docs and usage examples on [pkg.go.dev](https://pkg.go.dev/go.blockdaemon.com/pyth).
29+
30+
### Attributions
31+
32+
- Built with 👿 at [Blockdaemon](https://blockdaemon.com)
33+
- Using [immaterial.ink's](https://twitter.com/immaterial_ink) awesome Solana [Go SDK](https://github.com/gagliardetto/solana-go)

accounts.go

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
package pyth
1616

1717
import (
18+
"encoding/json"
1819
"errors"
20+
"fmt"
1921

2022
bin "github.com/gagliardetto/binary"
2123
"github.com/gagliardetto/solana-go"
@@ -67,6 +69,9 @@ type ProductAccount struct {
6769
AttrsData [464]byte // key-value string pairs of additional data
6870
}
6971

72+
// ProductAccountAttrsDataOffset is the binary offset of the AttrsData field within ProductAccount.
73+
const ProductAccountAttrsDataOffset = 48
74+
7075
// UnmarshalBinary decodes the product account from the on-chain format.
7176
func (p *ProductAccount) UnmarshalBinary(buf []byte) error {
7277
decoder := bin.NewBinDecoder(buf)
@@ -86,7 +91,7 @@ func (p *ProductAccount) UnmarshalBinary(buf []byte) error {
8691
func (p *ProductAccount) GetAttrsMap() (AttrsMap, error) {
8792
// Length of attrs is determined by size value in header.
8893
data := p.AttrsData[:]
89-
maxSize := int(p.Size) - 48
94+
maxSize := int(p.Size) - ProductAccountAttrsDataOffset
9095
if maxSize > 0 && len(data) > maxSize {
9196
data = data[:maxSize]
9297
}
@@ -96,6 +101,50 @@ func (p *ProductAccount) GetAttrsMap() (AttrsMap, error) {
96101
return attrs, err
97102
}
98103

104+
// UnmarshalJSON loads the product account's content from JSON.
105+
func (p *ProductAccount) UnmarshalJSON(data []byte) error {
106+
// Decode JSON as strings map.
107+
var content map[string]string
108+
if err := json.Unmarshal(data, &content); err != nil {
109+
return err
110+
}
111+
// Re-encode as binary data.
112+
attrsMap, err := NewAttrsMap(content)
113+
if err != nil {
114+
return err
115+
}
116+
mapData, err := attrsMap.MarshalBinary()
117+
if err != nil {
118+
return err // unreachable
119+
}
120+
if len(mapData) > len(p.AttrsData) {
121+
return fmt.Errorf("data does not fit in product account")
122+
}
123+
// Copy binary data into product account, zero remaining bytes.
124+
p.AccountHeader = AccountHeader{
125+
Magic: Magic,
126+
Version: V2,
127+
AccountType: AccountTypeProduct,
128+
Size: uint32(ProductAccountAttrsDataOffset + len(mapData)),
129+
}
130+
copy(p.AttrsData[:], mapData)
131+
for i := len(mapData); i < len(p.AttrsData); i++ {
132+
p.AttrsData[i] = 0
133+
}
134+
return nil
135+
}
136+
137+
// MarshalJSON returns a JSON-representation of the product account contents.
138+
func (p *ProductAccount) MarshalJSON() ([]byte, error) {
139+
// Decode binary data from product account.
140+
content, err := p.GetAttrsMap()
141+
if err != nil {
142+
return nil, err
143+
}
144+
// Re-encode as JSON.
145+
return json.Marshal(content.KVs())
146+
}
147+
99148
// Ema is an exponentially-weighted moving average.
100149
type Ema struct {
101150
Val int64
@@ -179,9 +228,9 @@ func (p *PriceAccount) GetComponent(publisher *solana.PublicKey) *PriceComp {
179228
// MappingAccount is a piece of a singly linked-list of all products on Pyth.
180229
type MappingAccount struct {
181230
AccountHeader
182-
Num uint32
183-
Unused uint32
184-
Next solana.PublicKey
231+
Num uint32 // number of keys
232+
Pad1 uint32 // reserved field
233+
Next solana.PublicKey // pubkey of next mapping account
185234
Products [640]solana.PublicKey
186235
}
187236

@@ -199,3 +248,11 @@ func (m *MappingAccount) UnmarshalBinary(buf []byte) error {
199248
}
200249
return nil
201250
}
251+
252+
// ProductKeys returns the slice of product keys referenced by this mapping, excluding empty entries.
253+
func (m *MappingAccount) ProductKeys() []solana.PublicKey {
254+
if m.Num > uint32(len(m.Products)) {
255+
return nil
256+
}
257+
return m.Products[:m.Num]
258+
}

accounts_test.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package pyth
1616

1717
import (
1818
_ "embed"
19+
"encoding/json"
1920
"testing"
2021

2122
"github.com/gagliardetto/solana-go"
@@ -67,20 +68,50 @@ func TestProductAccount(t *testing.T) {
6768
require.NoError(t, actual.UnmarshalBinary(caseProductAccount))
6869
assert.Equal(t, &productAccount_EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko, &actual)
6970

71+
expectedMap := map[string]string{
72+
"asset_type": "FX",
73+
"base": "EUR",
74+
"description": "EUR/USD",
75+
"generic_symbol": "EURUSD",
76+
"quote_currency": "USD",
77+
"symbol": "FX.EUR/USD",
78+
"tenor": "Spot",
79+
}
80+
7081
t.Run("GetAttrsMap", func(t *testing.T) {
71-
expected := map[string]string{
72-
"asset_type": "FX",
73-
"base": "EUR",
74-
"description": "EUR/USD",
75-
"generic_symbol": "EURUSD",
76-
"quote_currency": "USD",
77-
"symbol": "FX.EUR/USD",
78-
"tenor": "Spot",
79-
}
8082
actualList, err := actual.GetAttrsMap()
8183
assert.NoError(t, err)
8284
actual := actualList.KVs()
83-
assert.Equal(t, expected, actual)
85+
assert.Equal(t, expectedMap, actual)
86+
})
87+
88+
t.Run("JSON", func(t *testing.T) {
89+
jsonData, err := json.Marshal(&actual)
90+
require.NoError(t, err)
91+
92+
expected := `{
93+
"asset_type": "FX",
94+
"base": "EUR",
95+
"description": "EUR/USD",
96+
"generic_symbol": "EURUSD",
97+
"quote_currency": "USD",
98+
"symbol": "FX.EUR/USD",
99+
"tenor": "Spot"
100+
}`
101+
assert.JSONEq(t, expected, string(jsonData))
102+
103+
// Deserialize JSON again.
104+
var actual2 ProductAccount
105+
// Write junk into target, so we can ensure the entire account is written.
106+
for i := range actual2.AttrsData {
107+
actual2.AttrsData[i] = 0x41
108+
}
109+
require.NoError(t, json.Unmarshal(jsonData, &actual2))
110+
assert.True(t, actual2.Valid())
111+
assert.Equal(t, actual.Size, actual2.Size)
112+
actual2Map, err := actual2.GetAttrsMap()
113+
require.NoError(t, err)
114+
assert.Equal(t, expectedMap, actual2Map.KVs())
84115
})
85116
}
86117

client.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package pyth
1616

1717
import (
18-
"github.com/gagliardetto/solana-go"
1918
"github.com/gagliardetto/solana-go/rpc"
2019
"go.uber.org/zap"
2120
)
@@ -24,18 +23,22 @@ import (
2423
//
2524
// Do not instantiate Client directly, use NewClient instead.
2625
type Client struct {
27-
ProgramKey solana.PublicKey
26+
Env Env
2827
RPC *rpc.Client
2928
WebSocketURL string
3029
Log *zap.Logger
30+
31+
AccountsBatchSize int // number of accounts to get with getMultipleAccounts()
3132
}
3233

3334
// NewClient creates a new client to the Pyth on-chain program.
34-
func NewClient(programKey solana.PublicKey, rpcURL string, wsURL string) *Client {
35+
func NewClient(env Env, rpcURL string, wsURL string) *Client {
3536
return &Client{
36-
ProgramKey: programKey,
37+
Env: env,
3738
RPC: rpc.New(rpcURL),
3839
WebSocketURL: wsURL,
3940
Log: zap.NewNop(),
41+
42+
AccountsBatchSize: 32,
4043
}
4144
}

cmd/pythian/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package pythian

envs.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2022 Blockdaemon Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package pyth
16+
17+
import "github.com/gagliardetto/solana-go"
18+
19+
// Env identifies deployment of the Pyth on-chain program.
20+
type Env struct {
21+
Program solana.PublicKey // Program ID
22+
Mapping solana.PublicKey // Root mapping key
23+
}
24+
25+
// Devnet is the Pyth program on the Solana devnet cluster.
26+
var Devnet = Env{
27+
Program: solana.MustPublicKeyFromBase58("gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s"),
28+
Mapping: solana.MustPublicKeyFromBase58("BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2"),
29+
}
30+
31+
// Testnet is the Pyth program on the Solana testnet cluster.
32+
var Testnet = Env{
33+
Program: solana.MustPublicKeyFromBase58("8tfDNiaEyrV6Q1U4DEXrEigs9DoDtkugzFbybENEbCDz"),
34+
Mapping: solana.MustPublicKeyFromBase58("AFmdnt9ng1uVxqCmqwQJDAYC5cKTkw8gJKSM5PnzuF6z"),
35+
}
36+
37+
// Mainnet is the Pyth program on the Solana mainnet cluster.
38+
var Mainnet = Env{
39+
Program: solana.MustPublicKeyFromBase58("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH"),
40+
Mapping: solana.MustPublicKeyFromBase58("AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J"),
41+
}

instructions.go

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,10 @@ import (
2323
"github.com/gagliardetto/solana-go"
2424
)
2525

26-
// Program IDs of the Pyth oracle program.
27-
var (
28-
ProgramIDDevnet = solana.MustPublicKeyFromBase58("gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s")
29-
ProgramIDTestnet = solana.MustPublicKeyFromBase58("8tfDNiaEyrV6Q1U4DEXrEigs9DoDtkugzFbybENEbCDz")
30-
ProgramIDMainnet = solana.MustPublicKeyFromBase58("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH")
31-
)
32-
33-
// Root mapping account IDs listing the products in the Pyth oracle program.
34-
var (
35-
MappingKeyDevnet = solana.MustPublicKeyFromBase58("BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2")
36-
MappingKeyTestnet = solana.MustPublicKeyFromBase58("AFmdnt9ng1uVxqCmqwQJDAYC5cKTkw8gJKSM5PnzuF6z")
37-
MappingKeyMainnet = solana.MustPublicKeyFromBase58("AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J")
38-
)
39-
4026
func init() {
41-
solana.RegisterInstructionDecoder(ProgramIDDevnet, newInstructionDecoder(ProgramIDDevnet))
42-
solana.RegisterInstructionDecoder(ProgramIDTestnet, newInstructionDecoder(ProgramIDTestnet))
43-
solana.RegisterInstructionDecoder(ProgramIDMainnet, newInstructionDecoder(ProgramIDMainnet))
27+
solana.RegisterInstructionDecoder(Devnet.Program, newInstructionDecoder(Devnet.Program))
28+
solana.RegisterInstructionDecoder(Testnet.Program, newInstructionDecoder(Testnet.Program))
29+
solana.RegisterInstructionDecoder(Mainnet.Program, newInstructionDecoder(Mainnet.Program))
4430
}
4531

4632
// Pyth program instructions.

0 commit comments

Comments
 (0)