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

Commit c55145b

Browse files
author
Richard Patel
committed
improve ProductAccount handling
1 parent 769f6b7 commit c55145b

File tree

5 files changed

+131
-120
lines changed

5 files changed

+131
-120
lines changed

accounts.go

Lines changed: 48 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package pyth
1717
import (
1818
"encoding/json"
1919
"errors"
20-
"fmt"
2120

2221
bin "github.com/gagliardetto/binary"
2322
"github.com/gagliardetto/solana-go"
@@ -61,88 +60,74 @@ func PeekAccount(data []byte) uint32 {
6160
return header.AccountType
6261
}
6362

63+
type ProductAccountHeader struct {
64+
AccountHeader `json:"-"`
65+
FirstPrice solana.PublicKey `json:"first_price"` // first price account in list
66+
}
67+
68+
// ProductAccountHeaderLen is the binary offset of the AttrsData field within RawProductAccount.
69+
const ProductAccountHeaderLen = 48
70+
6471
// ProductAccount contains metadata for a single product,
6572
// such as its symbol and its base/quote currencies.
6673
type ProductAccount struct {
67-
AccountHeader
68-
FirstPrice solana.PublicKey // first price account in list
69-
AttrsData [464]byte // key-value string pairs of additional data
74+
ProductAccountHeader
75+
Attrs AttrsMap `json:"attrs"` // key-value string pairs of additional data
7076
}
7177

72-
// ProductAccountAttrsDataOffset is the binary offset of the AttrsData field within ProductAccount.
73-
const ProductAccountAttrsDataOffset = 48
78+
type RawProductAccount struct {
79+
ProductAccountHeader
80+
AttrsData [464]byte
81+
}
82+
83+
// UnmarshalJSON decodes the product account contents from JSON.
84+
func (p *ProductAccount) UnmarshalJSON(buf []byte) error {
85+
var inner struct {
86+
ProductAccountHeader
87+
Attrs AttrsMap `json:"attrs"` // key-value string pairs of additional data
88+
}
89+
if err := json.Unmarshal(buf, &inner); err != nil {
90+
return err
91+
}
92+
*p = ProductAccount{
93+
ProductAccountHeader: ProductAccountHeader{
94+
AccountHeader: AccountHeader{
95+
Magic: Magic,
96+
Version: V2,
97+
AccountType: AccountTypeProduct,
98+
Size: uint32(ProductAccountHeaderLen + inner.Attrs.BinaryLen()),
99+
},
100+
FirstPrice: inner.FirstPrice,
101+
},
102+
Attrs: inner.Attrs,
103+
}
104+
return nil
105+
}
74106

75107
// UnmarshalBinary decodes the product account from the on-chain format.
76108
func (p *ProductAccount) UnmarshalBinary(buf []byte) error {
109+
// Start by decoding the header and raw attrs data byte array.
77110
decoder := bin.NewBinDecoder(buf)
78-
if err := decoder.Decode(p); err != nil {
111+
var raw RawProductAccount
112+
if err := decoder.Decode(&raw); err != nil {
79113
return err
80114
}
81-
if !p.AccountHeader.Valid() {
115+
if !raw.AccountHeader.Valid() {
82116
return errors.New("invalid account")
83117
}
84-
if p.AccountType != AccountTypeProduct {
118+
if raw.AccountType != AccountTypeProduct {
85119
return errors.New("not a product account")
86120
}
87-
return nil
88-
}
89-
90-
// GetAttrsMap returns the parsed set of key-value pairs.
91-
func (p *ProductAccount) GetAttrsMap() (AttrsMap, error) {
121+
p.ProductAccountHeader = raw.ProductAccountHeader
122+
// Now decode AttrsData.
92123
// Length of attrs is determined by size value in header.
93-
data := p.AttrsData[:]
94-
maxSize := int(p.Size) - ProductAccountAttrsDataOffset
124+
data := raw.AttrsData[:]
125+
maxSize := int(p.Size) - ProductAccountHeaderLen
95126
if maxSize > 0 && len(data) > maxSize {
96127
data = data[:maxSize]
97128
}
98129
// Unmarshal attrs.
99-
var attrs AttrsMap
100-
err := attrs.UnmarshalBinary(data)
101-
return attrs, err
102-
}
103-
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())
130+
return p.Attrs.UnmarshalBinary(data)
146131
}
147132

148133
// Ema is an exponentially-weighted moving average.

accounts_test.go

Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,32 +34,25 @@ var (
3434
)
3535

3636
var productAccount_EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko = ProductAccount{
37-
AccountHeader: AccountHeader{
38-
Magic: Magic,
39-
Version: V2,
40-
AccountType: AccountTypeProduct,
41-
Size: 161,
37+
ProductAccountHeader: ProductAccountHeader{
38+
AccountHeader: AccountHeader{
39+
Magic: Magic,
40+
Version: V2,
41+
AccountType: AccountTypeProduct,
42+
Size: 161,
43+
},
44+
FirstPrice: solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh"),
4245
},
43-
FirstPrice: solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh"),
44-
AttrsData: [464]byte{
45-
0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x0a,
46-
0x46, 0x58, 0x2e, 0x45, 0x55, 0x52, 0x2f, 0x55,
47-
0x53, 0x44, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74,
48-
0x5f, 0x74, 0x79, 0x70, 0x65, 0x02, 0x46, 0x58,
49-
0x0e, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x5f, 0x63,
50-
0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x03,
51-
0x55, 0x53, 0x44, 0x0b, 0x64, 0x65, 0x73, 0x63,
52-
0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x07,
53-
0x45, 0x55, 0x52, 0x2f, 0x55, 0x53, 0x44, 0x0e,
54-
0x67, 0x65, 0x6e, 0x65, 0x72, 0x69, 0x63, 0x5f,
55-
0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x06, 0x45,
56-
0x55, 0x52, 0x55, 0x53, 0x44, 0x04, 0x62, 0x61,
57-
0x73, 0x65, 0x03, 0x45, 0x55, 0x52, 0x05, 0x74,
58-
0x65, 0x6e, 0x6f, 0x72, 0x04, 0x53, 0x70, 0x6f,
59-
0x74, 0x53, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00,
60-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
61-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
62-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
46+
Attrs: AttrsMap{
47+
Pairs: [][2]string{
48+
{"symbol", "FX.EUR/USD"},
49+
{"asset_type", "FX"},
50+
{"quote_currency", "USD"},
51+
{"description", "EUR/USD"},
52+
{"generic_symbol", "EURUSD"},
53+
{"base", "EUR"},
54+
{"tenor", "Spot"},
55+
},
6356
},
6457
}
6558

@@ -78,40 +71,31 @@ func TestProductAccount(t *testing.T) {
7871
"tenor": "Spot",
7972
}
8073

81-
t.Run("GetAttrsMap", func(t *testing.T) {
82-
actualList, err := actual.GetAttrsMap()
83-
assert.NoError(t, err)
84-
actual := actualList.KVs()
85-
assert.Equal(t, expectedMap, actual)
86-
})
87-
8874
t.Run("JSON", func(t *testing.T) {
8975
jsonData, err := json.Marshal(&actual)
9076
require.NoError(t, err)
9177

78+
//language=JSON
9279
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"
80+
"first_price": "E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh",
81+
"attrs": {
82+
"asset_type": "FX",
83+
"base": "EUR",
84+
"description": "EUR/USD",
85+
"generic_symbol": "EURUSD",
86+
"quote_currency": "USD",
87+
"symbol": "FX.EUR/USD",
88+
"tenor": "Spot"
89+
}
10090
}`
10191
assert.JSONEq(t, expected, string(jsonData))
10292

10393
// Deserialize JSON again.
10494
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-
}
10995
require.NoError(t, json.Unmarshal(jsonData, &actual2))
11096
assert.True(t, actual2.Valid())
11197
assert.Equal(t, actual.Size, actual2.Size)
112-
actual2Map, err := actual2.GetAttrsMap()
113-
require.NoError(t, err)
114-
assert.Equal(t, expectedMap, actual2Map.KVs())
98+
assert.Equal(t, expectedMap, actual2.Attrs.KVs())
11599
})
116100
}
117101

attrs.go

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

1717
import (
1818
"bytes"
19+
"encoding/json"
1920
"fmt"
2021
"io"
2122
"sort"
@@ -68,6 +69,14 @@ func (a *AttrsMap) UnmarshalBinary(data []byte) (err error) {
6869
return
6970
}
7071

72+
// BinaryLen returns this AttrsMap's length in binary encoding.
73+
func (a AttrsMap) BinaryLen() (size int) {
74+
for _, entry := range a.Pairs {
75+
size += 1 + len(entry[0]) + 1 + len(entry[1])
76+
}
77+
return
78+
}
79+
7180
// ReadAttrsMapFromBinary consumes all bytes from a binary reader,
7281
// returning an AttrsMap and the number of bytes read.
7382
func ReadAttrsMapFromBinary(rd *bytes.Reader) (out AttrsMap, n int, err error) {
@@ -101,6 +110,21 @@ func (a AttrsMap) MarshalBinary() ([]byte, error) {
101110
return buf.Bytes(), nil
102111
}
103112

113+
// UnmarshalJSON loads a JSON string map.
114+
func (a *AttrsMap) UnmarshalJSON(data []byte) (err error) {
115+
var content map[string]string
116+
if err := json.Unmarshal(data, &content); err != nil {
117+
return err
118+
}
119+
*a, err = NewAttrsMap(content)
120+
return
121+
}
122+
123+
// MarshalJSON returns a JSON string map.
124+
func (a *AttrsMap) MarshalJSON() ([]byte, error) {
125+
return json.Marshal(a.KVs())
126+
}
127+
104128
// readLPString returns a length-prefixed string as seen in AttrsMap.
105129
func readLPString(rd *bytes.Reader) (s string, n int, err error) {
106130
var strLen byte

query.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,22 @@ func (c *Client) GetAllProductKeys(ctx context.Context) ([]solana.PublicKey, err
7777
return products, nil
7878
}
7979

80+
// ProductAccountEntry is a product account and its pubkey.
81+
type ProductAccountEntry struct {
82+
ProductAccount
83+
Pubkey solana.PublicKey `json:"pubkey"`
84+
}
85+
8086
// GetAllProducts returns all product accounts.
8187
//
8288
// Aborts and returns an error if any product account failed to fetch.
83-
func (c *Client) GetAllProducts(ctx context.Context) ([]ProductAccount, error) {
89+
func (c *Client) GetAllProducts(ctx context.Context) ([]ProductAccountEntry, error) {
8490
keys, err := c.GetAllProductKeys(ctx)
8591
if err != nil {
8692
return nil, err
8793
}
8894

89-
var accs []ProductAccount
95+
var accs []ProductAccountEntry
9096
for len(keys) > 0 {
9197
// Get next block of keys from list.
9298
nextKeys := keys
@@ -105,7 +111,7 @@ func (c *Client) GetAllProducts(ctx context.Context) ([]ProductAccount, error) {
105111
return accs, nil
106112
}
107113

108-
func (c *Client) getProductsPage(ctx context.Context, accs *[]ProductAccount, keys []solana.PublicKey) error {
114+
func (c *Client) getProductsPage(ctx context.Context, accs *[]ProductAccountEntry, keys []solana.PublicKey) error {
109115
res, err := c.RPC.GetMultipleAccounts(ctx, keys...)
110116
if err != nil {
111117
return err
@@ -121,7 +127,10 @@ func (c *Client) getProductsPage(ctx context.Context, accs *[]ProductAccount, ke
121127
if err := acc.UnmarshalBinary(accountData); err != nil {
122128
return fmt.Errorf("failed to retrieve product account %s: %w", keys[i], err)
123129
}
124-
*accs = append(*accs, acc)
130+
*accs = append(*accs, ProductAccountEntry{
131+
ProductAccount: acc,
132+
Pubkey: keys[i],
133+
})
125134
}
126135

127136
return nil

query_test.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,29 @@ import (
2929
"github.com/stretchr/testify/require"
3030
)
3131

32+
var (
33+
testRPC = "https://api.devnet.solana.com"
34+
testWS = "wss://api.devnet.solana.com"
35+
)
36+
3237
func ExampleClient_GetAllProducts() {
33-
client := NewClient(Devnet, "https://api.devnet.solana.com", "wss://api.devnet.solana.com")
38+
client := NewClient(Devnet, testRPC, testWS)
3439
products, _ := client.GetAllProducts(context.TODO())
3540
// Print first product as JSON.
3641
jsonData, _ := json.MarshalIndent(&products[0], "", " ")
3742
fmt.Println(string(jsonData))
3843
// Output:
3944
// {
40-
// "asset_type": "Crypto",
41-
// "base": "BCH",
42-
// "description": "BCH/USD",
43-
// "generic_symbol": "BCHUSD",
44-
// "quote_currency": "USD",
45-
// "symbol": "Crypto.BCH/USD"
45+
// "first_price": "4EQrNZYk5KR1RnjyzbaaRbHsv8VqZWzSUtvx58wLsZbj",
46+
// "attrs": {
47+
// "asset_type": "Crypto",
48+
// "base": "BCH",
49+
// "description": "BCH/USD",
50+
// "generic_symbol": "BCHUSD",
51+
// "quote_currency": "USD",
52+
// "symbol": "Crypto.BCH/USD"
53+
// },
54+
// "pubkey": "89GseEmvNkzAMMEXcW9oTYzqRPXTsJ3BmNerXmgA1osV"
4655
// }
4756
}
4857

0 commit comments

Comments
 (0)