From edeb84a4c520e15f2316d337b5c25b22f70a196e Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Fri, 4 Mar 2022 20:39:46 +0100 Subject: [PATCH 01/10] rename types.go to accounts.go --- types.go => accounts.go | 0 types_test.go => accounts_test.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename types.go => accounts.go (100%) rename types_test.go => accounts_test.go (100%) diff --git a/types.go b/accounts.go similarity index 100% rename from types.go rename to accounts.go diff --git a/types_test.go b/accounts_test.go similarity index 100% rename from types_test.go rename to accounts_test.go From 5b3f53d5cbe3f533af90a7c988bec17017a9c76a Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Fri, 4 Mar 2022 23:50:22 +0100 Subject: [PATCH 02/10] add instructions support --- accounts.go | 76 ++++++++---- builder.go | 264 ++++++++++++++++++++++++++++++++++++++++++ instructions.go | 301 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 619 insertions(+), 22 deletions(-) create mode 100644 builder.go create mode 100644 instructions.go diff --git a/accounts.go b/accounts.go index 54af53f..c78ec0e 100644 --- a/accounts.go +++ b/accounts.go @@ -17,6 +17,8 @@ package pyth import ( "bytes" "errors" + "fmt" + "io" bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" @@ -60,17 +62,61 @@ func PeekAccount(data []byte) uint32 { return header.AccountType } +func unmarshalLPKVs(rd *bytes.Reader) (out map[string]string, n int, err error) { + kvps := make(map[string]string) + for rd.Len() > 0 { + key, n2, err := readLPString(rd) + if err != nil { + return kvps, n, err + } + n += n2 + val, n3, err := readLPString(rd) + if err != nil { + return kvps, n, err + } + n += n3 + kvps[key] = val + } + return kvps, n, nil +} + +func marshalLPKVs(m map[string]string) ([]byte, error) { + var buf bytes.Buffer + for k, v := range m { + if err := writeLPString(&buf, k); err != nil { + return nil, err + } + if err := writeLPString(&buf, v); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + // readLPString returns a length-prefixed string as seen in ProductAccount.Attrs. -func readLPString(rd *bytes.Reader) (string, error) { - strLen, err := rd.ReadByte() +func readLPString(rd *bytes.Reader) (s string, n int, err error) { + var strLen byte + strLen, err = rd.ReadByte() if err != nil { - return "", err + return } val := make([]byte, strLen) - if _, err := rd.Read(val); err != nil { - return "", err + n, err = rd.Read(val) + n += 1 + s = string(val) + return +} + +// writeLPString writes a length-prefixed string as seen in ProductAccount.Attrs. +func writeLPString(wr io.Writer, s string) error { + if len(s) > 0xFF { + return fmt.Errorf("string too long (%d)", len(s)) + } + if _, err := wr.Write([]byte{uint8(len(s))}); err != nil { + return err } - return string(val), nil + _, err := wr.Write([]byte(s)) + return err } // ProductAccount contains metadata for a single product, @@ -98,28 +144,14 @@ func (p *ProductAccount) UnmarshalBinary(buf []byte) error { // GetAttrs returns the parsed set of key-value pairs. func (p *ProductAccount) GetAttrs() (map[string]string, error) { - kvps := make(map[string]string) - attrs := p.Attrs[:] maxSize := int(p.Size) - 48 if maxSize > 0 && len(attrs) > maxSize { attrs = attrs[:maxSize] } - rd := bytes.NewReader(attrs) - for rd.Len() > 0 { - key, err := readLPString(rd) - if err != nil { - return kvps, err - } - val, err := readLPString(rd) - if err != nil { - return kvps, err - } - kvps[key] = val - } - - return kvps, nil + out, _, err := unmarshalLPKVs(rd) + return out, err } // Ema is an exponentially-weighted moving average. diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..8c8ef81 --- /dev/null +++ b/builder.go @@ -0,0 +1,264 @@ +// Copyright 2022 Blockdaemon Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pyth + +import "github.com/gagliardetto/solana-go" + +// InstructionBuilder creates new instructions to interact with the Pyth on-chain program. +type InstructionBuilder struct { + programKey solana.PublicKey +} + +// NewInstructionBuilder creates a new InstructionBuilder targeting the given Pyth program. +func NewInstructionBuilder(programKey solana.PublicKey) *InstructionBuilder { + return &InstructionBuilder{programKey: programKey} +} + +// InitMapping initializes the first mapping list account. +func (i *InstructionBuilder) InitMapping( + fundingKey solana.PublicKey, + mappingKey solana.PublicKey, +) *Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_InitMapping), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(mappingKey).SIGNER().WRITE(), + }, + } +} + +// AddMapping initializes and adds new mapping account to list. +func (i *InstructionBuilder) AddMapping( + fundingKey solana.PublicKey, + tailMappingKey solana.PublicKey, + newMappingKey solana.PublicKey, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_AddMapping), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(tailMappingKey).SIGNER().WRITE(), + solana.Meta(newMappingKey).SIGNER().WRITE(), + }, + } +} + +// AddProduct initializes and adds new product reference data account. +func (i *InstructionBuilder) AddProduct( + fundingKey solana.PublicKey, + mappingKey solana.PublicKey, + productKey solana.PublicKey, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_AddProduct), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(mappingKey).SIGNER().WRITE(), + solana.Meta(productKey).SIGNER().WRITE(), + }, + } +} + +// UpdProduct updates a product account. +func (i *InstructionBuilder) UpdProduct( + fundingKey solana.PublicKey, + productKey solana.PublicKey, + payload CommandUpdProduct, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_UpdProduct), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(productKey).SIGNER().WRITE(), + }, + impl: &payload, + } +} + +// AddPrice adds a new price account to a product account. +func (i *InstructionBuilder) AddPrice( + fundingKey solana.PublicKey, + productKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandAddPrice, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_AddPrice), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(productKey).SIGNER().WRITE(), + solana.Meta(priceKey).SIGNER().WRITE(), + }, + impl: &payload, + } +} + +// AddPublisher adds a publisher to a price account. +func (i *InstructionBuilder) AddPublisher( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandAddPublisher, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_AddPublisher), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(priceKey).SIGNER().WRITE(), + }, + impl: &payload, + } +} + +// DelPublisher deletes a publisher from a price account. +func (i *InstructionBuilder) DelPublisher( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandDelPublisher, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_DelPublisher), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(priceKey).SIGNER().WRITE(), + }, + impl: &payload, + } +} + +func (i *InstructionBuilder) updPrice( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandDelPublisher, + commandID int32, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(commandID), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(priceKey).WRITE(), + solana.Meta(solana.SysVarClockPubkey), + }, + impl: &payload, + } +} + +// UpdPrice publishes a new component price to a price account. +func (i *InstructionBuilder) UpdPrice( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandDelPublisher, +) solana.Instruction { + return i.updPrice(fundingKey, priceKey, payload, Instruction_UpdPrice) +} + +// UpdPriceNoFailOnError is like UpdPrice but never returns an error even if the update failed. +func (i *InstructionBuilder) UpdPriceNoFailOnError( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandDelPublisher, +) solana.Instruction { + return i.updPrice(fundingKey, priceKey, payload, Instruction_UpdPriceNoFailOnError) +} + +// AggPrice computes the aggregate price for a product account. +func (i *InstructionBuilder) AggPrice( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_AggPrice), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(priceKey).WRITE(), + solana.Meta(solana.SysVarClockPubkey), + }, + } +} + +// InitPrice (re)initializes a price account. +func (i *InstructionBuilder) InitPrice( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandInitPrice, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_InitPrice), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(priceKey).SIGNER().WRITE(), + }, + impl: &payload, + } +} + +// InitTest initializes a test account. +func (i *InstructionBuilder) InitTest( + fundingKey solana.PublicKey, + testKey solana.PublicKey, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_InitTest), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(testKey).SIGNER().WRITE(), + }, + } +} + +// UpdTest runs an aggregate price test. +func (i *InstructionBuilder) UpdTest( + fundingKey solana.PublicKey, + testKey solana.PublicKey, + payload CommandUpdTest, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_UpdTest), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(testKey).SIGNER().WRITE(), + }, + impl: &payload, + } +} + +// SetMinPub sets the minimum publishers of a price account. +func (i *InstructionBuilder) SetMinPub( + fundingKey solana.PublicKey, + priceKey solana.PublicKey, + payload CommandSetMinPub, +) solana.Instruction { + return &Instruction{ + programKey: i.programKey, + header: makeCommandHeader(Instruction_SetMinPub), + accounts: []*solana.AccountMeta{ + solana.Meta(fundingKey).SIGNER().WRITE(), + solana.Meta(priceKey).SIGNER().WRITE(), + }, + impl: &payload, + } +} diff --git a/instructions.go b/instructions.go new file mode 100644 index 0000000..06e64ba --- /dev/null +++ b/instructions.go @@ -0,0 +1,301 @@ +// Copyright 2022 Blockdaemon Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pyth + +import ( + "bytes" + "encoding" + "fmt" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" +) + +var ( + ProgramIDDevnet = solana.MustPublicKeyFromBase58("") + ProgramIDTestnet = solana.MustPublicKeyFromBase58("") + ProgramIDMainnet = solana.MustPublicKeyFromBase58("") +) + +func init() { + solana.RegisterInstructionDecoder(ProgramIDDevnet, newInstructionDecoder(ProgramIDDevnet)) + solana.RegisterInstructionDecoder(ProgramIDTestnet, newInstructionDecoder(ProgramIDTestnet)) + solana.RegisterInstructionDecoder(ProgramIDMainnet, newInstructionDecoder(ProgramIDMainnet)) +} + +// Pyth program instructions. +const ( + Instruction_InitMapping = int32(iota) + Instruction_AddMapping + Instruction_AddProduct + Instruction_UpdProduct + Instruction_AddPrice + Instruction_AddPublisher + Instruction_DelPublisher + Instruction_UpdPrice + Instruction_UpdPriceNoFailOnError + Instruction_AggPrice + Instruction_InitPrice + Instruction_InitTest + Instruction_UpdTest + Instruction_SetMinPub + instruction_count // number of different instruction types +) + +func InstructionIDToName(id int32) string { + switch id { + case Instruction_InitMapping: + return "init_mapping" + case Instruction_AddMapping: + return "add_mapping" + case Instruction_AddProduct: + return "add_product" + case Instruction_UpdProduct: + return "upd_product" + case Instruction_AddPrice: + return "add_price" + case Instruction_AddPublisher: + return "add_publisher" + case Instruction_DelPublisher: + return "del_publisher" + case Instruction_UpdPrice: + return "upd_price" + case Instruction_UpdPriceNoFailOnError: + return "upd_price_no_fail_on_error" + case Instruction_AggPrice: + return "agg_price" + case Instruction_InitPrice: + return "init_price" + case Instruction_InitTest: + return "init_test" + case Instruction_UpdTest: + return "upd_test" + case Instruction_SetMinPub: + return "set_min_pub" + default: + return fmt.Sprintf("unsupported (%d)", id) + } +} + +type Instruction struct { + programKey solana.PublicKey + accounts solana.AccountMetaSlice + header CommandHeader + impl interface{} +} + +func (inst *Instruction) ProgramID() solana.PublicKey { + return inst.programKey +} + +func (inst *Instruction) Accounts() []*solana.AccountMeta { + return inst.accounts +} + +func (inst *Instruction) Data() ([]byte, error) { + buf := new(bytes.Buffer) + enc := bin.NewBinEncoder(buf) + if err := enc.Encode(&inst.header); err != nil { + return nil, fmt.Errorf("failed to encode header: %w", err) + } + if inst.impl != nil { + if customMarshal, ok := inst.impl.(encoding.BinaryMarshaler); ok { + buf2, err := customMarshal.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal %s payload: %w", + InstructionIDToName(inst.header.Cmd), err) + } + buf.Write(buf2) + } else { + if err := enc.Encode(inst.impl); err != nil { + return nil, fmt.Errorf("failed to encode %s payload: %w", + InstructionIDToName(inst.header.Cmd), err) + } + } + } + return buf.Bytes(), nil +} + +type CommandHeader struct { + Version uint32 + Cmd int32 +} + +func (h *CommandHeader) Valid() bool { + return h.Version == V2 && h.Cmd >= 0 && h.Cmd < instruction_count +} + +func makeCommandHeader(cmd int32) CommandHeader { + return CommandHeader{ + Version: V2, + Cmd: cmd, + } +} + +type CommandUpdProduct struct { + Attrs map[string]string +} + +func (c *CommandUpdProduct) UnmarshalBinary(data []byte) (err error) { + var n int + c.Attrs, n, err = unmarshalLPKVs(bytes.NewReader(data)) + if err != nil { + return err + } + if n != len(data) { + return fmt.Errorf("unmarshalLPKVs: expected %d bytes got %d", len(data), n) + } + return nil +} + +func (c *CommandUpdProduct) MarshalBinary() ([]byte, error) { + return marshalLPKVs(c.Attrs) +} + +type CommandAddPrice struct { + Exponent int32 + PriceType uint32 +} + +type CommandInitPrice struct { + Exponent int32 + PriceType uint32 +} + +type CommandSetMinPub struct { + MinPub uint8 +} + +type CommandAddPublisher struct { + Publisher solana.PublicKey +} + +type CommandDelPublisher struct { + Publisher solana.PublicKey +} + +type CommandUpdPrice struct { + Status uint32 + Unused uint32 + Price int64 + Conf uint64 + PubSlot uint64 +} + +type CommandUpdTest struct { + Exponent int32 + SlotDiff [32]int8 + Price [32]int64 + Conf [32]uint64 +} + +func newInstructionDecoder(programKey solana.PublicKey) func(accounts []*solana.AccountMeta, data []byte) (interface{}, error) { + return func(accounts []*solana.AccountMeta, data []byte) (interface{}, error) { + return DecodeInstruction(programKey, accounts, data) + } +} + +func DecodeInstruction( + programKey solana.PublicKey, + accounts []*solana.AccountMeta, + data []byte, +) (*Instruction, error) { + dec := bin.NewBinDecoder(data) + + var hdr CommandHeader + if err := dec.Decode(&hdr); err != nil { + return nil, fmt.Errorf("failed to decode header: %w", err) + } + if !hdr.Valid() { + return nil, fmt.Errorf("not a valid Pyth instruction") + } + + var impl interface{} + var numAccounts int + switch hdr.Cmd { + case Instruction_InitMapping: + numAccounts = 2 + case Instruction_AddMapping: + numAccounts = 3 + case Instruction_AddProduct: + numAccounts = 3 + case Instruction_UpdProduct: + impl = new(CommandUpdProduct) + numAccounts = 3 + case Instruction_AddPrice: + impl = new(CommandAddPrice) + numAccounts = 3 + case Instruction_AddPublisher: + impl = new(CommandAddPublisher) + numAccounts = 2 + case Instruction_DelPublisher: + impl = new(CommandDelPublisher) + numAccounts = 2 + case Instruction_UpdPrice: + impl = new(CommandUpdPrice) + numAccounts = 3 + case Instruction_UpdPriceNoFailOnError: + impl = new(CommandUpdPrice) + numAccounts = 3 + case Instruction_AggPrice: + numAccounts = 3 + case Instruction_InitPrice: + numAccounts = 2 + case Instruction_InitTest: + numAccounts = 2 + case Instruction_UpdTest: + impl = new(CommandUpdTest) + numAccounts = 2 + case Instruction_SetMinPub: + impl = new(CommandSetMinPub) + numAccounts = 2 + default: + return nil, fmt.Errorf("unsupported instruction type (%d)", hdr.Cmd) + } + + if len(accounts) != numAccounts { + return nil, fmt.Errorf("expected %d accounts for %s but got %d", + numAccounts, InstructionIDToName(hdr.Cmd), len(accounts)) + } + + // Decode content. + if impl != nil { + if customUnmarshal, ok := impl.(encoding.BinaryUnmarshaler); ok { + // If method overrides UnmarshalBinary(), use that. + err := customUnmarshal.UnmarshalBinary(data[dec.Position():]) + if err != nil { + return nil, fmt.Errorf("while unmarshaling %s: %w", + InstructionIDToName(hdr.Cmd), err) + } + } else { + // Fall back to generic LE deserializer. + if err := dec.Decode(impl); err != nil { + return nil, fmt.Errorf("failed to decode %s: %w", + InstructionIDToName(hdr.Cmd), err) + } + if rem := dec.Remaining(); rem > 0 { + return nil, fmt.Errorf("while unmarshaling %s found %d superfluous bytes", + InstructionIDToName(hdr.Cmd), rem) + } + } + } + + return &Instruction{ + programKey: programKey, + accounts: accounts, + header: hdr, + impl: impl, + }, nil +} From 65981026e3bb04dded04e9e894a606f20a8fe642 Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sat, 5 Mar 2022 03:14:37 +0100 Subject: [PATCH 03/10] define program and mapping keys --- instructions.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/instructions.go b/instructions.go index 06e64ba..ff6fb94 100644 --- a/instructions.go +++ b/instructions.go @@ -23,10 +23,18 @@ import ( "github.com/gagliardetto/solana-go" ) +// Program IDs of the Pyth oracle program. var ( - ProgramIDDevnet = solana.MustPublicKeyFromBase58("") - ProgramIDTestnet = solana.MustPublicKeyFromBase58("") - ProgramIDMainnet = solana.MustPublicKeyFromBase58("") + ProgramIDDevnet = solana.MustPublicKeyFromBase58("gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s") + ProgramIDTestnet = solana.MustPublicKeyFromBase58("8tfDNiaEyrV6Q1U4DEXrEigs9DoDtkugzFbybENEbCDz") + ProgramIDMainnet = solana.MustPublicKeyFromBase58("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH") +) + +// Root mapping account IDs listing the products in the Pyth oracle program. +var ( + MappingKeyDevnet = solana.MustPublicKeyFromBase58("BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2") + MappingKeyTestnet = solana.MustPublicKeyFromBase58("AFmdnt9ng1uVxqCmqwQJDAYC5cKTkw8gJKSM5PnzuF6z") + MappingKeyMainnet = solana.MustPublicKeyFromBase58("AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J") ) func init() { From dec96b0dfa5639e01e6fe3a578146b213a5ecf6e Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sat, 5 Mar 2022 03:14:47 +0100 Subject: [PATCH 04/10] move test cases --- accounts_test.go | 6 +++--- tests/README.md | 1 + ...BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin | Bin ...E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin | Bin ...EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin | Bin 5 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 tests/README.md rename tests/{ => mapping_account}/BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin (100%) rename tests/{ => price_account}/E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin (100%) rename tests/{ => product_account}/EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin (100%) diff --git a/accounts_test.go b/accounts_test.go index 71a2a3b..bbce306 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -24,11 +24,11 @@ import ( ) var ( - //go:embed tests/EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin + //go:embed tests/product_account/EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin caseProductAccount []byte - //go:embed tests/E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin + //go:embed tests/price_account/E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin casePriceAccount []byte - //go:embed tests/BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin + //go:embed tests/mapping_account/BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin caseMappingAccount []byte ) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..d6b1ca1 --- /dev/null +++ b/tests/README.md @@ -0,0 +1 @@ +This directory contains binary test cases of on-chain data used with the Pyth program. diff --git a/tests/BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin b/tests/mapping_account/BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin similarity index 100% rename from tests/BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin rename to tests/mapping_account/BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2.bin diff --git a/tests/E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin b/tests/price_account/E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin similarity index 100% rename from tests/E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin rename to tests/price_account/E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh.bin diff --git a/tests/EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin b/tests/product_account/EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin similarity index 100% rename from tests/EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin rename to tests/product_account/EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko.bin From fafc4048ffce7fa33401c5310bfc9faaa8225f52 Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sat, 5 Mar 2022 03:31:06 +0100 Subject: [PATCH 05/10] test GetPriceAccount --- accounts_test.go | 196 +++++++++++++++++++++++------------------------ client.go | 17 ++-- prices.go | 8 +- prices_test.go | 59 ++++++++++++++ 4 files changed, 173 insertions(+), 107 deletions(-) create mode 100644 prices_test.go diff --git a/accounts_test.go b/accounts_test.go index bbce306..f531c54 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -84,117 +84,117 @@ func TestProductAccount(t *testing.T) { }) } -func TestPriceAccount(t *testing.T) { - expected := PriceAccount{ - AccountHeader: AccountHeader{ - Magic: Magic, - Version: V2, - AccountType: AccountTypePrice, - Size: 1200, +var priceAccount_E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh = PriceAccount{ + AccountHeader: AccountHeader{ + Magic: Magic, + Version: V2, + AccountType: AccountTypePrice, + Size: 1200, + }, + PriceType: 1, + Exponent: -5, + Num: 10, + NumQt: 0, + LastSlot: 117136050, + ValidSlot: 117491486, + Twap: Ema{ + Val: 112674, + Numer: 5644642336, + Denom: 5009691136, + }, + Twac: Ema{ + Val: 4, + Numer: 2033641276, + Denom: 5009691136, + }, + Drv1: 1, + Drv2: 0, + Product: solana.MustPublicKeyFromBase58("EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko"), + Next: solana.PublicKey{}, + PrevSlot: 117491485, + PrevPrice: 112717, + PrevConf: 6, + Drv3: -2413575930482041166, + Agg: PriceInfo{ + Price: 112717, + Conf: 6, + Status: 0, + CorpAct: 0, + PubSlot: 117491487, + }, + Components: [32]PriceComp{ + { + Publisher: solana.MustPublicKeyFromBase58("5U3bH5b6XtG99aVWLqwVzYPVpQiFHytBD68Rz2eFPZd7"), + Agg: PriceInfo{ + PubSlot: 117491484, + }, + Latest: PriceInfo{ + PubSlot: 117491485, + }, }, - PriceType: 1, - Exponent: -5, - Num: 10, - NumQt: 0, - LastSlot: 117136050, - ValidSlot: 117491486, - Twap: Ema{ - Val: 112674, - Numer: 5644642336, - Denom: 5009691136, + { + Publisher: solana.MustPublicKeyFromBase58("4iVm6RJVU4R6kvc3KUDnE6cw4Ffb6769FzbXMu26sJrs"), }, - Twac: Ema{ - Val: 4, - Numer: 2033641276, - Denom: 5009691136, + { + Publisher: solana.MustPublicKeyFromBase58("3djmXHmD9kuAydgFnSnWAjq4Kos5GnEx2KdFR2kvGiUw"), }, - Drv1: 1, - Drv2: 0, - Product: solana.MustPublicKeyFromBase58("EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko"), - Next: solana.PublicKey{}, - PrevSlot: 117491485, - PrevPrice: 112717, - PrevConf: 6, - Drv3: -2413575930482041166, - Agg: PriceInfo{ - Price: 112717, - Conf: 6, - Status: 0, - CorpAct: 0, - PubSlot: 117491487, + { + Publisher: solana.MustPublicKeyFromBase58("86DsXwBCqFoCUiuB1t9oV2inHKQ5h2vFaNZ4GETvTHuz"), }, - Components: [32]PriceComp{ - { - Publisher: solana.MustPublicKeyFromBase58("5U3bH5b6XtG99aVWLqwVzYPVpQiFHytBD68Rz2eFPZd7"), - Agg: PriceInfo{ - PubSlot: 117491484, - }, - Latest: PriceInfo{ - PubSlot: 117491485, - }, - }, - { - Publisher: solana.MustPublicKeyFromBase58("4iVm6RJVU4R6kvc3KUDnE6cw4Ffb6769FzbXMu26sJrs"), - }, - { - Publisher: solana.MustPublicKeyFromBase58("3djmXHmD9kuAydgFnSnWAjq4Kos5GnEx2KdFR2kvGiUw"), - }, - { - Publisher: solana.MustPublicKeyFromBase58("86DsXwBCqFoCUiuB1t9oV2inHKQ5h2vFaNZ4GETvTHuz"), - }, - { - Publisher: solana.MustPublicKeyFromBase58("rkTtobRtTCDLXbADsbVxHcfBr7Z8Z1JDSBM3kyk3LJe"), - }, - { - Publisher: solana.MustPublicKeyFromBase58("2pfE7YYVhM9WaneVVF2kcwArMoconfjtq83oZfSurkkY"), - }, - { - Publisher: solana.MustPublicKeyFromBase58("2vTC3XNpi7ED5T643KxVH5HqM7cSRKuUGnmMtKACY4Ju"), + { + Publisher: solana.MustPublicKeyFromBase58("rkTtobRtTCDLXbADsbVxHcfBr7Z8Z1JDSBM3kyk3LJe"), + }, + { + Publisher: solana.MustPublicKeyFromBase58("2pfE7YYVhM9WaneVVF2kcwArMoconfjtq83oZfSurkkY"), + }, + { + Publisher: solana.MustPublicKeyFromBase58("2vTC3XNpi7ED5T643KxVH5HqM7cSRKuUGnmMtKACY4Ju"), + }, + { + Publisher: solana.MustPublicKeyFromBase58("45FYxKkPM1NhavyAHFTyXG2JCSsy5jD1UwwCz5UtHX5y"), + }, + { + Publisher: solana.MustPublicKeyFromBase58("EevTjv14eGHqsxKvgpastHsuLr9FNPfzkP23wG61pT2U"), + Agg: PriceInfo{ + Price: 113062, + Conf: 1, + Status: 1, + CorpAct: 0, + PubSlot: 116660829, }, - { - Publisher: solana.MustPublicKeyFromBase58("45FYxKkPM1NhavyAHFTyXG2JCSsy5jD1UwwCz5UtHX5y"), + Latest: PriceInfo{ + Price: 113062, + Conf: 1, + Status: 1, + CorpAct: 0, + PubSlot: 116660829, }, - { - Publisher: solana.MustPublicKeyFromBase58("EevTjv14eGHqsxKvgpastHsuLr9FNPfzkP23wG61pT2U"), - Agg: PriceInfo{ - Price: 113062, - Conf: 1, - Status: 1, - CorpAct: 0, - PubSlot: 116660829, - }, - Latest: PriceInfo{ - Price: 113062, - Conf: 1, - Status: 1, - CorpAct: 0, - PubSlot: 116660829, - }, + }, + { + Publisher: solana.MustPublicKeyFromBase58("AKPWGLY5KpxbTx7DaVp4Pve8JweMjKbb1A19MyL2nrYT"), + Agg: PriceInfo{ + Price: 111976, + Conf: 16, + Status: 1, + CorpAct: 0, + PubSlot: 116917242, }, - { - Publisher: solana.MustPublicKeyFromBase58("AKPWGLY5KpxbTx7DaVp4Pve8JweMjKbb1A19MyL2nrYT"), - Agg: PriceInfo{ - Price: 111976, - Conf: 16, - Status: 1, - CorpAct: 0, - PubSlot: 116917242, - }, - Latest: PriceInfo{ - Price: 111976, - Conf: 16, - Status: 1, - CorpAct: 0, - PubSlot: 116917242, - }, + Latest: PriceInfo{ + Price: 111976, + Conf: 16, + Status: 1, + CorpAct: 0, + PubSlot: 116917242, }, }, - } + }, +} +func TestPriceAccount(t *testing.T) { var actual PriceAccount require.NoError(t, actual.UnmarshalBinary(casePriceAccount)) - assert.Equal(t, &expected, &actual) + assert.Equal(t, &priceAccount_E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh, &actual) } func TestMappingAccount(t *testing.T) { diff --git a/client.go b/client.go index 798a9e2..7f346a9 100644 --- a/client.go +++ b/client.go @@ -21,14 +21,21 @@ import ( ) // Client interacts with Pyth via Solana's JSON-RPC API. +// +// Do not instantiate Client directly, use NewClient instead. type Client struct { - Opts - - Log *zap.Logger + ProgramKey solana.PublicKey RPC *rpc.Client WebSocketURL string + Log *zap.Logger } -type Opts struct { - ProgramKey solana.PublicKey +// NewClient creates a new client to the Pyth on-chain program. +func NewClient(programKey solana.PublicKey, rpcURL string, wsURL string) *Client { + return &Client{ + ProgramKey: programKey, + RPC: rpc.New(rpcURL), + WebSocketURL: wsURL, + Log: zap.NewNop(), + } } diff --git a/prices.go b/prices.go index 417c295..5cbd679 100644 --- a/prices.go +++ b/prices.go @@ -13,8 +13,8 @@ import ( ) // GetPriceAccount retrieves a price account from the blockchain. -func (c *Client) GetPriceAccount(ctx context.Context, productKey solana.PublicKey) (*PriceAccount, error) { - accountInfo, err := c.RPC.GetAccountInfo(ctx, productKey) +func (c *Client) GetPriceAccount(ctx context.Context, priceKey solana.PublicKey) (*PriceAccount, error) { + accountInfo, err := c.RPC.GetAccountInfo(ctx, priceKey) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (c *Client) streamPriceAccounts(ctx context.Context, updates chan<- PriceAc defer metricsWsActiveConns.Dec() sub, err := client.ProgramSubscribeWithOpts( - c.Opts.ProgramKey, + c.ProgramKey, rpc.CommitmentConfirmed, solana.EncodingBase64Zstd, []rpc.RPCFilter{ @@ -117,7 +117,7 @@ func (c *Client) readNextUpdate( metricsWsEventsTotal.Inc() // Decode update. - if update.Value.Account.Owner != c.Opts.ProgramKey { + if update.Value.Account.Owner != c.ProgramKey { return nil } accountData := update.Value.Account.Data.GetBinary() diff --git a/prices_test.go b/prices_test.go new file mode 100644 index 0000000..b13c922 --- /dev/null +++ b/prices_test.go @@ -0,0 +1,59 @@ +package pyth + +import ( + "context" + "encoding/base64" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClient_GetPriceAccount(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(wr http.ResponseWriter, req *http.Request) { + buf, err := io.ReadAll(req.Body) + require.NoError(t, err) + assert.JSONEq(t, `{ + "jsonrpc": "2.0", + "id": 0, + "method": "getAccountInfo", + "params": [ + "E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh", + {"encoding": "base64"} + ] + }`, string(buf)) + + _, err = wr.Write([]byte(`{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "context": { + "slot": 118773287 + }, + "value": { + "data": [ + "` + base64.StdEncoding.EncodeToString(casePriceAccount) + `", + "base64" + ], + "executable": false, + "lamports": 23942400, + "owner": "gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s", + "rentEpoch": 274 + } + } + }`)) + require.NoError(t, err) + })) + defer server.Close() + + c := NewClient(ProgramIDDevnet, server.URL, server.URL) + acc, err := c.GetPriceAccount(context.Background(), solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh")) + require.NoError(t, err) + require.NotNil(t, acc) + + assert.Equal(t, &priceAccount_E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh, acc) +} From 0aeeebc97df130be04dfcc6e3d9b27a025cf61a5 Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sat, 5 Mar 2022 03:58:12 +0100 Subject: [PATCH 06/10] test UpdPrice --- accounts.go | 8 ++ builder.go | 48 ++++----- instructions.go | 21 ++-- instructions_test.go | 91 ++++++++++++++++++ tests/instruction/upd_price.bin | Bin 0 -> 40 bytes .../upd_price_no_fail_on_error.bin | Bin 0 -> 40 bytes 6 files changed, 134 insertions(+), 34 deletions(-) create mode 100644 instructions_test.go create mode 100644 tests/instruction/upd_price.bin create mode 100644 tests/instruction/upd_price_no_fail_on_error.bin diff --git a/accounts.go b/accounts.go index c78ec0e..7e09540 100644 --- a/accounts.go +++ b/accounts.go @@ -172,6 +172,14 @@ type PriceInfo struct { PubSlot uint64 // valid publishing slot } +// Price status. +const ( + PriceStatusUnknown = uint32(iota) + PriceStatusTrading + PriceStatusHalted + PriceStatusAuction +) + // PriceComp contains the price and confidence contributed by a specific publisher. type PriceComp struct { Publisher solana.PublicKey // key of contributing publisher diff --git a/builder.go b/builder.go index 8c8ef81..3c20a42 100644 --- a/builder.go +++ b/builder.go @@ -33,7 +33,7 @@ func (i *InstructionBuilder) InitMapping( ) *Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_InitMapping), + Header: makeCommandHeader(Instruction_InitMapping), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(mappingKey).SIGNER().WRITE(), @@ -49,7 +49,7 @@ func (i *InstructionBuilder) AddMapping( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_AddMapping), + Header: makeCommandHeader(Instruction_AddMapping), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(tailMappingKey).SIGNER().WRITE(), @@ -66,7 +66,7 @@ func (i *InstructionBuilder) AddProduct( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_AddProduct), + Header: makeCommandHeader(Instruction_AddProduct), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(mappingKey).SIGNER().WRITE(), @@ -83,12 +83,12 @@ func (i *InstructionBuilder) UpdProduct( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_UpdProduct), + Header: makeCommandHeader(Instruction_UpdProduct), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(productKey).SIGNER().WRITE(), }, - impl: &payload, + Payload: &payload, } } @@ -101,13 +101,13 @@ func (i *InstructionBuilder) AddPrice( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_AddPrice), + Header: makeCommandHeader(Instruction_AddPrice), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(productKey).SIGNER().WRITE(), solana.Meta(priceKey).SIGNER().WRITE(), }, - impl: &payload, + Payload: &payload, } } @@ -119,12 +119,12 @@ func (i *InstructionBuilder) AddPublisher( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_AddPublisher), + Header: makeCommandHeader(Instruction_AddPublisher), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(priceKey).SIGNER().WRITE(), }, - impl: &payload, + Payload: &payload, } } @@ -136,30 +136,30 @@ func (i *InstructionBuilder) DelPublisher( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_DelPublisher), + Header: makeCommandHeader(Instruction_DelPublisher), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(priceKey).SIGNER().WRITE(), }, - impl: &payload, + Payload: &payload, } } func (i *InstructionBuilder) updPrice( fundingKey solana.PublicKey, priceKey solana.PublicKey, - payload CommandDelPublisher, + payload CommandUpdPrice, commandID int32, ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(commandID), + Header: makeCommandHeader(commandID), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(priceKey).WRITE(), solana.Meta(solana.SysVarClockPubkey), }, - impl: &payload, + Payload: &payload, } } @@ -167,7 +167,7 @@ func (i *InstructionBuilder) updPrice( func (i *InstructionBuilder) UpdPrice( fundingKey solana.PublicKey, priceKey solana.PublicKey, - payload CommandDelPublisher, + payload CommandUpdPrice, ) solana.Instruction { return i.updPrice(fundingKey, priceKey, payload, Instruction_UpdPrice) } @@ -176,7 +176,7 @@ func (i *InstructionBuilder) UpdPrice( func (i *InstructionBuilder) UpdPriceNoFailOnError( fundingKey solana.PublicKey, priceKey solana.PublicKey, - payload CommandDelPublisher, + payload CommandUpdPrice, ) solana.Instruction { return i.updPrice(fundingKey, priceKey, payload, Instruction_UpdPriceNoFailOnError) } @@ -188,7 +188,7 @@ func (i *InstructionBuilder) AggPrice( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_AggPrice), + Header: makeCommandHeader(Instruction_AggPrice), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(priceKey).WRITE(), @@ -205,12 +205,12 @@ func (i *InstructionBuilder) InitPrice( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_InitPrice), + Header: makeCommandHeader(Instruction_InitPrice), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(priceKey).SIGNER().WRITE(), }, - impl: &payload, + Payload: &payload, } } @@ -221,7 +221,7 @@ func (i *InstructionBuilder) InitTest( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_InitTest), + Header: makeCommandHeader(Instruction_InitTest), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(testKey).SIGNER().WRITE(), @@ -237,12 +237,12 @@ func (i *InstructionBuilder) UpdTest( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_UpdTest), + Header: makeCommandHeader(Instruction_UpdTest), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(testKey).SIGNER().WRITE(), }, - impl: &payload, + Payload: &payload, } } @@ -254,11 +254,11 @@ func (i *InstructionBuilder) SetMinPub( ) solana.Instruction { return &Instruction{ programKey: i.programKey, - header: makeCommandHeader(Instruction_SetMinPub), + Header: makeCommandHeader(Instruction_SetMinPub), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(priceKey).SIGNER().WRITE(), }, - impl: &payload, + Payload: &payload, } } diff --git a/instructions.go b/instructions.go index ff6fb94..a0311ba 100644 --- a/instructions.go +++ b/instructions.go @@ -100,8 +100,9 @@ func InstructionIDToName(id int32) string { type Instruction struct { programKey solana.PublicKey accounts solana.AccountMetaSlice - header CommandHeader - impl interface{} + + Header CommandHeader + Payload interface{} } func (inst *Instruction) ProgramID() solana.PublicKey { @@ -115,21 +116,21 @@ func (inst *Instruction) Accounts() []*solana.AccountMeta { func (inst *Instruction) Data() ([]byte, error) { buf := new(bytes.Buffer) enc := bin.NewBinEncoder(buf) - if err := enc.Encode(&inst.header); err != nil { + if err := enc.Encode(&inst.Header); err != nil { return nil, fmt.Errorf("failed to encode header: %w", err) } - if inst.impl != nil { - if customMarshal, ok := inst.impl.(encoding.BinaryMarshaler); ok { + if inst.Payload != nil { + if customMarshal, ok := inst.Payload.(encoding.BinaryMarshaler); ok { buf2, err := customMarshal.MarshalBinary() if err != nil { return nil, fmt.Errorf("failed to marshal %s payload: %w", - InstructionIDToName(inst.header.Cmd), err) + InstructionIDToName(inst.Header.Cmd), err) } buf.Write(buf2) } else { - if err := enc.Encode(inst.impl); err != nil { + if err := enc.Encode(inst.Payload); err != nil { return nil, fmt.Errorf("failed to encode %s payload: %w", - InstructionIDToName(inst.header.Cmd), err) + InstructionIDToName(inst.Header.Cmd), err) } } } @@ -303,7 +304,7 @@ func DecodeInstruction( return &Instruction{ programKey: programKey, accounts: accounts, - header: hdr, - impl: impl, + Header: hdr, + Payload: impl, }, nil } diff --git a/instructions_test.go b/instructions_test.go new file mode 100644 index 0000000..8b6b1ea --- /dev/null +++ b/instructions_test.go @@ -0,0 +1,91 @@ +package pyth + +import ( + _ "embed" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + //go:embed tests/instruction/upd_price.bin + caseUpdPrice []byte + //go:embed tests/instruction/upd_price_no_fail_on_error.bin + caseUpdPriceNoFailOnError []byte +) + +func TestInstruction_UpdPrice(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("5U3bH5b6XtG99aVWLqwVzYPVpQiFHytBD68Rz2eFPZd7")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw")).WRITE(), + solana.Meta(solana.SysVarClockPubkey), + } + + actualIns, err := DecodeInstruction(program, accs, caseUpdPrice) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_UpdPrice, + }, actualIns.Header) + require.Equal(t, &CommandUpdPrice{ + Status: PriceStatusTrading, + Unused: 0, + Price: 261253500000, + Conf: 120500000, + PubSlot: 118774432, + }, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + require.Equal(t, caseUpdPrice, data) + + rebuiltIns := NewInstructionBuilder(program).UpdPrice( + accs[0].PublicKey, + accs[1].PublicKey, + *actualIns.Payload.(*CommandUpdPrice), + ) + assert.Equal(t, actualIns, rebuiltIns) +} + +func TestInstruction_UpdPriceNoFailOnError(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("5U3bH5b6XtG99aVWLqwVzYPVpQiFHytBD68Rz2eFPZd7")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw")).WRITE(), + solana.Meta(solana.SysVarClockPubkey), + } + + actualIns, err := DecodeInstruction(program, accs, caseUpdPriceNoFailOnError) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_UpdPriceNoFailOnError, + }, actualIns.Header) + require.Equal(t, &CommandUpdPrice{ + Status: PriceStatusTrading, + Unused: 0, + Price: 261253500000, + Conf: 120500000, + PubSlot: 118774432, + }, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + require.Equal(t, caseUpdPrice, data) + + rebuiltIns := NewInstructionBuilder(program).UpdPrice( + accs[0].PublicKey, + accs[1].PublicKey, + *actualIns.Payload.(*CommandUpdPrice), + ) + assert.Equal(t, actualIns, rebuiltIns) +} diff --git a/tests/instruction/upd_price.bin b/tests/instruction/upd_price.bin new file mode 100644 index 0000000000000000000000000000000000000000..3708e398e8cf1d0123b78e1a91d0df8058c73683 GIT binary patch literal 40 jcmZQ#U|?VeVn!eafdrm6mu-Nw!g@WBFvEf<5ikt^O&|n| literal 0 HcmV?d00001 diff --git a/tests/instruction/upd_price_no_fail_on_error.bin b/tests/instruction/upd_price_no_fail_on_error.bin new file mode 100644 index 0000000000000000000000000000000000000000..2c19908d84a8107b7e96f77b2c77fe5b67bc1b66 GIT binary patch literal 40 jcmZQ#U|`??Vn!eafdrm6mu-Nw!g@V+5Pw0G2$%){O+*BY literal 0 HcmV?d00001 From ef4f5a0c933bdd80d657fa6d08d742de56da6246 Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sat, 5 Mar 2022 04:24:21 +0100 Subject: [PATCH 07/10] add AttrsMap, more godoc --- accounts.go | 80 +++++---------------------------- accounts_test.go | 5 ++- attrs.go | 104 +++++++++++++++++++++++++++++++++++++++++++ instructions.go | 39 +++++++++------- instructions_test.go | 18 +++++++- prices_test.go | 14 ++++++ products.go | 14 ++++++ 7 files changed, 184 insertions(+), 90 deletions(-) create mode 100644 attrs.go diff --git a/accounts.go b/accounts.go index 7e09540..3ce0089 100644 --- a/accounts.go +++ b/accounts.go @@ -15,10 +15,7 @@ package pyth import ( - "bytes" "errors" - "fmt" - "io" bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" @@ -62,69 +59,12 @@ func PeekAccount(data []byte) uint32 { return header.AccountType } -func unmarshalLPKVs(rd *bytes.Reader) (out map[string]string, n int, err error) { - kvps := make(map[string]string) - for rd.Len() > 0 { - key, n2, err := readLPString(rd) - if err != nil { - return kvps, n, err - } - n += n2 - val, n3, err := readLPString(rd) - if err != nil { - return kvps, n, err - } - n += n3 - kvps[key] = val - } - return kvps, n, nil -} - -func marshalLPKVs(m map[string]string) ([]byte, error) { - var buf bytes.Buffer - for k, v := range m { - if err := writeLPString(&buf, k); err != nil { - return nil, err - } - if err := writeLPString(&buf, v); err != nil { - return nil, err - } - } - return buf.Bytes(), nil -} - -// readLPString returns a length-prefixed string as seen in ProductAccount.Attrs. -func readLPString(rd *bytes.Reader) (s string, n int, err error) { - var strLen byte - strLen, err = rd.ReadByte() - if err != nil { - return - } - val := make([]byte, strLen) - n, err = rd.Read(val) - n += 1 - s = string(val) - return -} - -// writeLPString writes a length-prefixed string as seen in ProductAccount.Attrs. -func writeLPString(wr io.Writer, s string) error { - if len(s) > 0xFF { - return fmt.Errorf("string too long (%d)", len(s)) - } - if _, err := wr.Write([]byte{uint8(len(s))}); err != nil { - return err - } - _, err := wr.Write([]byte(s)) - return err -} - // ProductAccount contains metadata for a single product, // such as its symbol and its base/quote currencies. type ProductAccount struct { AccountHeader FirstPrice solana.PublicKey // first price account in list - Attrs [464]byte // key-value string pairs of additional data + AttrsData [464]byte // key-value string pairs of additional data } // UnmarshalBinary decodes the product account from the on-chain format. @@ -142,16 +82,18 @@ func (p *ProductAccount) UnmarshalBinary(buf []byte) error { return nil } -// GetAttrs returns the parsed set of key-value pairs. -func (p *ProductAccount) GetAttrs() (map[string]string, error) { - attrs := p.Attrs[:] +// GetAttrsMap returns the parsed set of key-value pairs. +func (p *ProductAccount) GetAttrsMap() (AttrsMap, error) { + // Length of attrs is determined by size value in header. + data := p.AttrsData[:] maxSize := int(p.Size) - 48 - if maxSize > 0 && len(attrs) > maxSize { - attrs = attrs[:maxSize] + if maxSize > 0 && len(data) > maxSize { + data = data[:maxSize] } - rd := bytes.NewReader(attrs) - out, _, err := unmarshalLPKVs(rd) - return out, err + // Unmarshal attrs. + var attrs AttrsMap + err := attrs.UnmarshalBinary(data) + return attrs, err } // Ema is an exponentially-weighted moving average. diff --git a/accounts_test.go b/accounts_test.go index f531c54..2148e42 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -41,7 +41,7 @@ func TestProductAccount(t *testing.T) { Size: 161, }, FirstPrice: solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh"), - Attrs: [464]byte{ + AttrsData: [464]byte{ 0x06, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x0a, 0x46, 0x58, 0x2e, 0x45, 0x55, 0x52, 0x2f, 0x55, 0x53, 0x44, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, @@ -78,8 +78,9 @@ func TestProductAccount(t *testing.T) { "symbol": "FX.EUR/USD", "tenor": "Spot", } - actual, err := actual.GetAttrs() + actualList, err := actual.GetAttrsMap() assert.NoError(t, err) + actual := actualList.KVs() assert.Equal(t, expected, actual) }) } diff --git a/attrs.go b/attrs.go new file mode 100644 index 0000000..6a265ca --- /dev/null +++ b/attrs.go @@ -0,0 +1,104 @@ +package pyth + +import ( + "bytes" + "fmt" + "io" +) + +// AttrsMap is a list of string key-value pairs with stable order. +type AttrsMap struct { + kvps [][2]string +} + +// NewAttrsMap returns a new attribute map with an initial arbitrary order. +func NewAttrsMap(fromGo map[string]string) (out AttrsMap, err error) { + if fromGo != nil { + for k, v := range fromGo { + if len(k) > 0xFF { + return out, fmt.Errorf("key too long (%d > 0xFF): \"%s\"", len(k), k) + } + if len(v) > 0xFF { + return out, fmt.Errorf("value too long (%d > 0xFF): \"%s\"", len(v), v) + } + out.kvps = append(out.kvps, [2]string{k, v}) + } + } + return +} + +// KVs returns the AttrsMap as an unordered Go map. +func (a AttrsMap) KVs() map[string]string { + m := make(map[string]string, len(a.kvps)) + for _, kv := range a.kvps { + m[kv[0]] = kv[1] + } + return m +} + +// UnmarshalBinary unmarshals AttrsMap from its on-chain format. +// +// Will return an error if it fails to consume the entire provided byte slice. +func (a *AttrsMap) UnmarshalBinary(data []byte) (err error) { + *a, _, err = ReadAttrsMapFromBinary(bytes.NewReader(data)) + return +} + +// ReadAttrsMapFromBinary consumes all bytes from a binary reader, +// returning an AttrsMap and the number of bytes read. +func ReadAttrsMapFromBinary(rd *bytes.Reader) (out AttrsMap, n int, err error) { + for rd.Len() > 0 { + key, n2, err := readLPString(rd) + if err != nil { + return out, n, err + } + n += n2 + val, n3, err := readLPString(rd) + if err != nil { + return out, n, err + } + n += n3 + out.kvps = append(out.kvps, [2]string{key, val}) + } + return out, n, nil +} + +// MarshalBinary marshals AttrsMap to its on-chain format. +func (a AttrsMap) MarshalBinary() ([]byte, error) { + var buf bytes.Buffer + for _, kv := range a.kvps { + if err := writeLPString(&buf, kv[0]); err != nil { + return nil, err + } + if err := writeLPString(&buf, kv[1]); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +// readLPString returns a length-prefixed string as seen in AttrsMap. +func readLPString(rd *bytes.Reader) (s string, n int, err error) { + var strLen byte + strLen, err = rd.ReadByte() + if err != nil { + return + } + val := make([]byte, strLen) + n, err = rd.Read(val) + n += 1 + s = string(val) + return +} + +// writeLPString writes a length-prefixed string as seen in AttrsMap. +func writeLPString(wr io.Writer, s string) error { + if len(s) > 0xFF { + return fmt.Errorf("string too long (%d)", len(s)) + } + if _, err := wr.Write([]byte{uint8(len(s))}); err != nil { + return err + } + _, err := wr.Write([]byte(s)) + return err +} diff --git a/instructions.go b/instructions.go index a0311ba..ed66dea 100644 --- a/instructions.go +++ b/instructions.go @@ -62,6 +62,7 @@ const ( instruction_count // number of different instruction types ) +// InstructionIDToName returns a human-readable name of a Pyth instruction type. func InstructionIDToName(id int32) string { switch id { case Instruction_InitMapping: @@ -137,11 +138,13 @@ func (inst *Instruction) Data() ([]byte, error) { return buf.Bytes(), nil } +// CommandHeader is an 8-byte header at the beginning any instruction data. type CommandHeader struct { - Version uint32 + Version uint32 // currently V2 Cmd int32 } +// Valid performs basic checks on instruction data. func (h *CommandHeader) Valid() bool { return h.Version == V2 && h.Cmd >= 0 && h.Cmd < instruction_count } @@ -153,48 +156,39 @@ func makeCommandHeader(cmd int32) CommandHeader { } } +// CommandUpdProduct is the payload of Instruction_UpdProduct. type CommandUpdProduct struct { Attrs map[string]string } -func (c *CommandUpdProduct) UnmarshalBinary(data []byte) (err error) { - var n int - c.Attrs, n, err = unmarshalLPKVs(bytes.NewReader(data)) - if err != nil { - return err - } - if n != len(data) { - return fmt.Errorf("unmarshalLPKVs: expected %d bytes got %d", len(data), n) - } - return nil -} - -func (c *CommandUpdProduct) MarshalBinary() ([]byte, error) { - return marshalLPKVs(c.Attrs) -} - +// CommandAddPrice is the payload of Instruction_AddPrice. type CommandAddPrice struct { Exponent int32 PriceType uint32 } +// CommandInitPrice is the payload of Instruction_InitPrice. type CommandInitPrice struct { Exponent int32 PriceType uint32 } +// CommandSetMinPub is the payload of Instruction_SetMinPub. type CommandSetMinPub struct { MinPub uint8 } +// CommandAddPublisher is the payload of Instruction_AddPublisher. type CommandAddPublisher struct { Publisher solana.PublicKey } +// CommandDelPublisher is the payload of Instruction_DelPublisher. type CommandDelPublisher struct { Publisher solana.PublicKey } +// CommandUpdPrice is the payload of Instruction_UpdPrice or Instruction_UpdPriceNoFailOnError. type CommandUpdPrice struct { Status uint32 Unused uint32 @@ -203,6 +197,7 @@ type CommandUpdPrice struct { PubSlot uint64 } +// CommandUpdTest is the payload Instruction_UpdTest. type CommandUpdTest struct { Exponent int32 SlotDiff [32]int8 @@ -216,6 +211,16 @@ func newInstructionDecoder(programKey solana.PublicKey) func(accounts []*solana. } } +// DecodeInstruction attempts to reconstruct a Pyth command from an on-chain instruction. +// +// Security +// +// Please note that this function may behave differently than the Pyth on-chain program. +// Especially edge cases and invalid input is handled according to "best effort". +// +// This function also performs no account ownership nor permission checks. +// +// It is best to only use this instruction on successful program executions. func DecodeInstruction( programKey solana.PublicKey, accounts []*solana.AccountMeta, diff --git a/instructions_test.go b/instructions_test.go index 8b6b1ea..631841e 100644 --- a/instructions_test.go +++ b/instructions_test.go @@ -1,3 +1,17 @@ +// Copyright 2022 Blockdaemon Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package pyth import ( @@ -80,9 +94,9 @@ func TestInstruction_UpdPriceNoFailOnError(t *testing.T) { data, err := actualIns.Data() assert.NoError(t, err) - require.Equal(t, caseUpdPrice, data) + require.Equal(t, caseUpdPriceNoFailOnError, data) - rebuiltIns := NewInstructionBuilder(program).UpdPrice( + rebuiltIns := NewInstructionBuilder(program).UpdPriceNoFailOnError( accs[0].PublicKey, accs[1].PublicKey, *actualIns.Payload.(*CommandUpdPrice), diff --git a/prices_test.go b/prices_test.go index b13c922..b9978f6 100644 --- a/prices_test.go +++ b/prices_test.go @@ -1,3 +1,17 @@ +// Copyright 2022 Blockdaemon Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package pyth import ( diff --git a/products.go b/products.go index 9c558ab..848ac69 100644 --- a/products.go +++ b/products.go @@ -1,3 +1,17 @@ +// Copyright 2022 Blockdaemon Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package pyth import ( From 267084aff3e609848be6e551bdffb3855a41fe32 Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sat, 5 Mar 2022 04:27:07 +0100 Subject: [PATCH 08/10] lint --- attrs.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/attrs.go b/attrs.go index 6a265ca..fae4033 100644 --- a/attrs.go +++ b/attrs.go @@ -13,16 +13,14 @@ type AttrsMap struct { // NewAttrsMap returns a new attribute map with an initial arbitrary order. func NewAttrsMap(fromGo map[string]string) (out AttrsMap, err error) { - if fromGo != nil { - for k, v := range fromGo { - if len(k) > 0xFF { - return out, fmt.Errorf("key too long (%d > 0xFF): \"%s\"", len(k), k) - } - if len(v) > 0xFF { - return out, fmt.Errorf("value too long (%d > 0xFF): \"%s\"", len(v), v) - } - out.kvps = append(out.kvps, [2]string{k, v}) + for k, v := range fromGo { + if len(k) > 0xFF { + return out, fmt.Errorf("key too long (%d > 0xFF): \"%s\"", len(k), k) } + if len(v) > 0xFF { + return out, fmt.Errorf("value too long (%d > 0xFF): \"%s\"", len(v), v) + } + out.kvps = append(out.kvps, [2]string{k, v}) } return } From bcf1577d1288552ea2c02677c16b6954ee66f589 Mon Sep 17 00:00:00 2001 From: Richard Patel Date: Sat, 5 Mar 2022 05:06:29 +0100 Subject: [PATCH 09/10] more instruction tests --- accounts_test.go | 3 +- attrs.go | 14 ++- instructions.go | 4 +- instructions_test.go | 190 +++++++++++++++++++++++++++++ tests/instruction/add_mapping.bin | Bin 0 -> 8 bytes tests/instruction/add_price.bin | Bin 0 -> 16 bytes tests/instruction/add_product.bin | Bin 0 -> 8 bytes tests/instruction/init_mapping.bin | Bin 0 -> 8 bytes tests/instruction/upd_product.bin | Bin 0 -> 121 bytes 9 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 tests/instruction/add_mapping.bin create mode 100644 tests/instruction/add_price.bin create mode 100644 tests/instruction/add_product.bin create mode 100644 tests/instruction/init_mapping.bin create mode 100644 tests/instruction/upd_product.bin diff --git a/accounts_test.go b/accounts_test.go index 2148e42..a448751 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -65,10 +65,9 @@ func TestProductAccount(t *testing.T) { var actual ProductAccount require.NoError(t, actual.UnmarshalBinary(caseProductAccount)) - assert.Equal(t, &expected, &actual) - t.Run("GetAttrs", func(t *testing.T) { + t.Run("GetAttrsMap", func(t *testing.T) { expected := map[string]string{ "asset_type": "FX", "base": "EUR", diff --git a/attrs.go b/attrs.go index fae4033..ac57c43 100644 --- a/attrs.go +++ b/attrs.go @@ -8,10 +8,12 @@ import ( // AttrsMap is a list of string key-value pairs with stable order. type AttrsMap struct { - kvps [][2]string + Pairs [][2]string } // NewAttrsMap returns a new attribute map with an initial arbitrary order. +// +// The provided Go map may be nil. func NewAttrsMap(fromGo map[string]string) (out AttrsMap, err error) { for k, v := range fromGo { if len(k) > 0xFF { @@ -20,15 +22,15 @@ func NewAttrsMap(fromGo map[string]string) (out AttrsMap, err error) { if len(v) > 0xFF { return out, fmt.Errorf("value too long (%d > 0xFF): \"%s\"", len(v), v) } - out.kvps = append(out.kvps, [2]string{k, v}) + out.Pairs = append(out.Pairs, [2]string{k, v}) } return } // KVs returns the AttrsMap as an unordered Go map. func (a AttrsMap) KVs() map[string]string { - m := make(map[string]string, len(a.kvps)) - for _, kv := range a.kvps { + m := make(map[string]string, len(a.Pairs)) + for _, kv := range a.Pairs { m[kv[0]] = kv[1] } return m @@ -56,7 +58,7 @@ func ReadAttrsMapFromBinary(rd *bytes.Reader) (out AttrsMap, n int, err error) { return out, n, err } n += n3 - out.kvps = append(out.kvps, [2]string{key, val}) + out.Pairs = append(out.Pairs, [2]string{key, val}) } return out, n, nil } @@ -64,7 +66,7 @@ func ReadAttrsMapFromBinary(rd *bytes.Reader) (out AttrsMap, n int, err error) { // MarshalBinary marshals AttrsMap to its on-chain format. func (a AttrsMap) MarshalBinary() ([]byte, error) { var buf bytes.Buffer - for _, kv := range a.kvps { + for _, kv := range a.Pairs { if err := writeLPString(&buf, kv[0]); err != nil { return nil, err } diff --git a/instructions.go b/instructions.go index ed66dea..54c51ab 100644 --- a/instructions.go +++ b/instructions.go @@ -158,7 +158,7 @@ func makeCommandHeader(cmd int32) CommandHeader { // CommandUpdProduct is the payload of Instruction_UpdProduct. type CommandUpdProduct struct { - Attrs map[string]string + AttrsMap } // CommandAddPrice is the payload of Instruction_AddPrice. @@ -247,7 +247,7 @@ func DecodeInstruction( numAccounts = 3 case Instruction_UpdProduct: impl = new(CommandUpdProduct) - numAccounts = 3 + numAccounts = 2 case Instruction_AddPrice: impl = new(CommandAddPrice) numAccounts = 3 diff --git a/instructions_test.go b/instructions_test.go index 631841e..3579fd7 100644 --- a/instructions_test.go +++ b/instructions_test.go @@ -24,12 +24,198 @@ import ( ) var ( + //go:embed tests/instruction/init_mapping.bin + caseInitMapping []byte + //go:embed tests/instruction/add_mapping.bin + caseAddMapping []byte + //go:embed tests/instruction/add_product.bin + caseAddProduct []byte + //go:embed tests/instruction/upd_product.bin + caseUpdProduct []byte + //go:embed tests/instruction/add_price.bin + caseAddPrice []byte //go:embed tests/instruction/upd_price.bin caseUpdPrice []byte //go:embed tests/instruction/upd_price_no_fail_on_error.bin caseUpdPriceNoFailOnError []byte ) +func TestInstruction_InitMapping(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy")).SIGNER().WRITE(), + solana.Meta(MappingKeyDevnet).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, caseInitMapping) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_InitMapping, + }, actualIns.Header) + assert.Equal(t, "init_mapping", InstructionIDToName(actualIns.Header.Cmd)) + assert.Nil(t, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + assert.Len(t, data, 8) + require.Equal(t, caseInitMapping, data) + + rebuiltIns := NewInstructionBuilder(program).InitMapping( + accs[0].PublicKey, + accs[1].PublicKey, + ) + assert.Equal(t, actualIns, rebuiltIns) +} + +func TestInstruction_AddMapping(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy")).SIGNER().WRITE(), + solana.Meta(MappingKeyDevnet).SIGNER().WRITE(), + solana.Meta(MappingKeyTestnet).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, caseAddMapping) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_AddMapping, + }, actualIns.Header) + assert.Equal(t, "add_mapping", InstructionIDToName(actualIns.Header.Cmd)) + assert.Nil(t, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + assert.Len(t, data, 8) + require.Equal(t, caseAddMapping, data) + + rebuiltIns := NewInstructionBuilder(program).AddMapping( + accs[0].PublicKey, + accs[1].PublicKey, + accs[2].PublicKey, + ) + assert.Equal(t, actualIns, rebuiltIns) +} + +func TestInstruction_AddProduct(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy")).SIGNER().WRITE(), + solana.Meta(MappingKeyDevnet).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko")).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, caseAddProduct) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_AddProduct, + }, actualIns.Header) + assert.Equal(t, "add_product", InstructionIDToName(actualIns.Header.Cmd)) + assert.Nil(t, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + assert.Len(t, data, 8) + require.Equal(t, caseAddProduct, data) + + rebuiltIns := NewInstructionBuilder(program).AddProduct( + accs[0].PublicKey, + accs[1].PublicKey, + accs[2].PublicKey, + ) + assert.Equal(t, actualIns, rebuiltIns) +} + +func TestInstruction_UpdProduct(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko")).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, caseUpdProduct) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_UpdProduct, + }, actualIns.Header) + assert.Equal(t, "upd_product", InstructionIDToName(actualIns.Header.Cmd)) + assert.Equal(t, &CommandUpdProduct{ + AttrsMap{ + Pairs: [][2]string{ + {"symbol", "FX.EUR/USD"}, + {"asset_type", "FX"}, + {"quote_currency", "USD"}, + {"description", "EUR/USD"}, + {"generic_symbol", "EURUSD"}, + {"base", "EUR"}, + {"tenor", "Spot"}, + }, + }, + }, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + require.Equal(t, caseUpdProduct, data) + + rebuiltIns := NewInstructionBuilder(program).UpdProduct( + accs[0].PublicKey, + accs[1].PublicKey, + *actualIns.Payload.(*CommandUpdProduct), + ) + assert.Equal(t, actualIns, rebuiltIns) +} + +func TestInstruction_AddPrice(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("EWxGfxoPQSNA2744AYdAKmsQZ8F9o9M7oKkvL3VM1dko")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh")).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, caseAddPrice) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_AddPrice, + }, actualIns.Header) + assert.Equal(t, "add_price", InstructionIDToName(actualIns.Header.Cmd)) + assert.Equal(t, &CommandAddPrice{ + Exponent: 14099, + PriceType: 1, + }, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + require.Equal(t, caseAddPrice, data) + + rebuiltIns := NewInstructionBuilder(program).AddPrice( + accs[0].PublicKey, + accs[1].PublicKey, + accs[2].PublicKey, + *actualIns.Payload.(*CommandAddPrice), + ) + assert.Equal(t, actualIns, rebuiltIns) +} + func TestInstruction_UpdPrice(t *testing.T) { var program = ProgramIDDevnet var accs = []*solana.AccountMeta{ @@ -47,6 +233,7 @@ func TestInstruction_UpdPrice(t *testing.T) { Version: V2, Cmd: Instruction_UpdPrice, }, actualIns.Header) + assert.Equal(t, "upd_price", InstructionIDToName(actualIns.Header.Cmd)) require.Equal(t, &CommandUpdPrice{ Status: PriceStatusTrading, Unused: 0, @@ -57,6 +244,7 @@ func TestInstruction_UpdPrice(t *testing.T) { data, err := actualIns.Data() assert.NoError(t, err) + assert.Len(t, data, 40) require.Equal(t, caseUpdPrice, data) rebuiltIns := NewInstructionBuilder(program).UpdPrice( @@ -84,6 +272,7 @@ func TestInstruction_UpdPriceNoFailOnError(t *testing.T) { Version: V2, Cmd: Instruction_UpdPriceNoFailOnError, }, actualIns.Header) + assert.Equal(t, "upd_price_no_fail_on_error", InstructionIDToName(actualIns.Header.Cmd)) require.Equal(t, &CommandUpdPrice{ Status: PriceStatusTrading, Unused: 0, @@ -94,6 +283,7 @@ func TestInstruction_UpdPriceNoFailOnError(t *testing.T) { data, err := actualIns.Data() assert.NoError(t, err) + assert.Len(t, data, 40) require.Equal(t, caseUpdPriceNoFailOnError, data) rebuiltIns := NewInstructionBuilder(program).UpdPriceNoFailOnError( diff --git a/tests/instruction/add_mapping.bin b/tests/instruction/add_mapping.bin new file mode 100644 index 0000000000000000000000000000000000000000..658b252278c03092c28c69e25adcc097f9f36097 GIT binary patch literal 8 NcmZQ#U|?VbVgLXf00aO4 literal 0 HcmV?d00001 diff --git a/tests/instruction/add_price.bin b/tests/instruction/add_price.bin new file mode 100644 index 0000000000000000000000000000000000000000..145dde21b315aece8ac760491374ddf50088d40b GIT binary patch literal 16 UcmZQ#U|?VYVqtRz21Xza00MmgQUCw| literal 0 HcmV?d00001 diff --git a/tests/instruction/add_product.bin b/tests/instruction/add_product.bin new file mode 100644 index 0000000000000000000000000000000000000000..f42c613935644a4972cae904ed1b89b01d30cc19 GIT binary patch literal 8 NcmZQ#U|?VZVgLXj00jU5 literal 0 HcmV?d00001 diff --git a/tests/instruction/init_mapping.bin b/tests/instruction/init_mapping.bin new file mode 100644 index 0000000000000000000000000000000000000000..71c2a58453e27ee89ad7d15b1a5f12433409fff4 GIT binary patch literal 8 KcmZQ#fB*mh7yttR literal 0 HcmV?d00001 diff --git a/tests/instruction/upd_product.bin b/tests/instruction/upd_product.bin new file mode 100644 index 0000000000000000000000000000000000000000..5e20c611419d8c95a6d77ba4a8473f4e40e0518f GIT binary patch literal 121 zcmZQ#U|?VdVz%PS+@$;*F1H9h*U%vS&|nv?#Ny)AlK7I!f>b8A2)@G7{F2o8 Date: Sat, 5 Mar 2022 05:25:51 +0100 Subject: [PATCH 10/10] more instruction tests --- builder.go | 24 +--- instructions.go | 9 +- instructions_test.go | 136 +++++++++++++++--- tests/instruction/add_publisher.bin | Bin 0 -> 40 bytes tests/instruction/del_publisher.bin | Bin 0 -> 40 bytes tests/instruction/set_min_pub.bin | Bin 0 -> 12 bytes .../upd_price_no_fail_on_error.bin | Bin 40 -> 0 bytes 7 files changed, 123 insertions(+), 46 deletions(-) create mode 100644 tests/instruction/add_publisher.bin create mode 100644 tests/instruction/del_publisher.bin create mode 100644 tests/instruction/set_min_pub.bin delete mode 100644 tests/instruction/upd_price_no_fail_on_error.bin diff --git a/builder.go b/builder.go index 3c20a42..0f23262 100644 --- a/builder.go +++ b/builder.go @@ -145,15 +145,15 @@ func (i *InstructionBuilder) DelPublisher( } } -func (i *InstructionBuilder) updPrice( +// UpdPrice publishes a new component price to a price account. +func (i *InstructionBuilder) UpdPrice( fundingKey solana.PublicKey, priceKey solana.PublicKey, payload CommandUpdPrice, - commandID int32, ) solana.Instruction { return &Instruction{ programKey: i.programKey, - Header: makeCommandHeader(commandID), + Header: makeCommandHeader(Instruction_UpdPrice), accounts: []*solana.AccountMeta{ solana.Meta(fundingKey).SIGNER().WRITE(), solana.Meta(priceKey).WRITE(), @@ -163,24 +163,6 @@ func (i *InstructionBuilder) updPrice( } } -// UpdPrice publishes a new component price to a price account. -func (i *InstructionBuilder) UpdPrice( - fundingKey solana.PublicKey, - priceKey solana.PublicKey, - payload CommandUpdPrice, -) solana.Instruction { - return i.updPrice(fundingKey, priceKey, payload, Instruction_UpdPrice) -} - -// UpdPriceNoFailOnError is like UpdPrice but never returns an error even if the update failed. -func (i *InstructionBuilder) UpdPriceNoFailOnError( - fundingKey solana.PublicKey, - priceKey solana.PublicKey, - payload CommandUpdPrice, -) solana.Instruction { - return i.updPrice(fundingKey, priceKey, payload, Instruction_UpdPriceNoFailOnError) -} - // AggPrice computes the aggregate price for a product account. func (i *InstructionBuilder) AggPrice( fundingKey solana.PublicKey, diff --git a/instructions.go b/instructions.go index 54c51ab..4e58860 100644 --- a/instructions.go +++ b/instructions.go @@ -53,7 +53,6 @@ const ( Instruction_AddPublisher Instruction_DelPublisher Instruction_UpdPrice - Instruction_UpdPriceNoFailOnError Instruction_AggPrice Instruction_InitPrice Instruction_InitTest @@ -81,8 +80,6 @@ func InstructionIDToName(id int32) string { return "del_publisher" case Instruction_UpdPrice: return "upd_price" - case Instruction_UpdPriceNoFailOnError: - return "upd_price_no_fail_on_error" case Instruction_AggPrice: return "agg_price" case Instruction_InitPrice: @@ -175,7 +172,8 @@ type CommandInitPrice struct { // CommandSetMinPub is the payload of Instruction_SetMinPub. type CommandSetMinPub struct { - MinPub uint8 + MinPub uint8 + Padding [3]byte } // CommandAddPublisher is the payload of Instruction_AddPublisher. @@ -260,9 +258,6 @@ func DecodeInstruction( case Instruction_UpdPrice: impl = new(CommandUpdPrice) numAccounts = 3 - case Instruction_UpdPriceNoFailOnError: - impl = new(CommandUpdPrice) - numAccounts = 3 case Instruction_AggPrice: numAccounts = 3 case Instruction_InitPrice: diff --git a/instructions_test.go b/instructions_test.go index 3579fd7..6cb1ced 100644 --- a/instructions_test.go +++ b/instructions_test.go @@ -36,8 +36,12 @@ var ( caseAddPrice []byte //go:embed tests/instruction/upd_price.bin caseUpdPrice []byte - //go:embed tests/instruction/upd_price_no_fail_on_error.bin - caseUpdPriceNoFailOnError []byte + //go:embed tests/instruction/add_publisher.bin + caseAddPublisher []byte + //go:embed tests/instruction/del_publisher.bin + caseDelPublisher []byte + //go:embed tests/instruction/set_min_pub.bin + caseSetMinPub []byte ) func TestInstruction_InitMapping(t *testing.T) { @@ -170,6 +174,7 @@ func TestInstruction_UpdProduct(t *testing.T) { data, err := actualIns.Data() assert.NoError(t, err) + // no length check since product update is arbitrary length require.Equal(t, caseUpdProduct, data) rebuiltIns := NewInstructionBuilder(program).UpdProduct( @@ -205,6 +210,7 @@ func TestInstruction_AddPrice(t *testing.T) { data, err := actualIns.Data() assert.NoError(t, err) + assert.Len(t, data, 16) require.Equal(t, caseAddPrice, data) rebuiltIns := NewInstructionBuilder(program).AddPrice( @@ -216,6 +222,74 @@ func TestInstruction_AddPrice(t *testing.T) { assert.Equal(t, actualIns, rebuiltIns) } +func TestInstruction_AddPublisher(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh")).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, caseAddPublisher) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_AddPublisher, + }, actualIns.Header) + assert.Equal(t, "add_publisher", InstructionIDToName(actualIns.Header.Cmd)) + assert.Equal(t, &CommandAddPublisher{ + Publisher: solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy"), + }, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + assert.Len(t, data, 40) + require.Equal(t, caseAddPublisher, data) + + rebuiltIns := NewInstructionBuilder(program).AddPublisher( + accs[0].PublicKey, + accs[1].PublicKey, + *actualIns.Payload.(*CommandAddPublisher), + ) + assert.Equal(t, actualIns, rebuiltIns) +} + +func TestInstruction_DelPublisher(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh")).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, caseDelPublisher) + require.NoError(t, err) + + assert.Equal(t, program, actualIns.ProgramID()) + assert.Equal(t, accs, actualIns.Accounts()) + assert.Equal(t, CommandHeader{ + Version: V2, + Cmd: Instruction_DelPublisher, + }, actualIns.Header) + assert.Equal(t, "del_publisher", InstructionIDToName(actualIns.Header.Cmd)) + assert.Equal(t, &CommandDelPublisher{ + Publisher: solana.MustPublicKeyFromBase58("7cVfgArCheMR6Cs4t6vz5rfnqd56vZq4ndaBrY5xkxXy"), + }, actualIns.Payload) + + data, err := actualIns.Data() + assert.NoError(t, err) + assert.Len(t, data, 40) + require.Equal(t, caseDelPublisher, data) + + rebuiltIns := NewInstructionBuilder(program).DelPublisher( + accs[0].PublicKey, + accs[1].PublicKey, + *actualIns.Payload.(*CommandDelPublisher), + ) + assert.Equal(t, actualIns, rebuiltIns) +} + func TestInstruction_UpdPrice(t *testing.T) { var program = ProgramIDDevnet var accs = []*solana.AccountMeta{ @@ -255,41 +329,67 @@ func TestInstruction_UpdPrice(t *testing.T) { assert.Equal(t, actualIns, rebuiltIns) } -func TestInstruction_UpdPriceNoFailOnError(t *testing.T) { +func TestInstruction_SetMinPub(t *testing.T) { var program = ProgramIDDevnet var accs = []*solana.AccountMeta{ solana.Meta(solana.MustPublicKeyFromBase58("5U3bH5b6XtG99aVWLqwVzYPVpQiFHytBD68Rz2eFPZd7")).SIGNER().WRITE(), - solana.Meta(solana.MustPublicKeyFromBase58("EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw")).WRITE(), - solana.Meta(solana.SysVarClockPubkey), + solana.Meta(solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh")).SIGNER().WRITE(), } - actualIns, err := DecodeInstruction(program, accs, caseUpdPriceNoFailOnError) + actualIns, err := DecodeInstruction(program, accs, caseSetMinPub) require.NoError(t, err) assert.Equal(t, program, actualIns.ProgramID()) assert.Equal(t, accs, actualIns.Accounts()) assert.Equal(t, CommandHeader{ Version: V2, - Cmd: Instruction_UpdPriceNoFailOnError, + Cmd: Instruction_SetMinPub, }, actualIns.Header) - assert.Equal(t, "upd_price_no_fail_on_error", InstructionIDToName(actualIns.Header.Cmd)) - require.Equal(t, &CommandUpdPrice{ - Status: PriceStatusTrading, - Unused: 0, - Price: 261253500000, - Conf: 120500000, - PubSlot: 118774432, + assert.Equal(t, "set_min_pub", InstructionIDToName(actualIns.Header.Cmd)) + require.Equal(t, &CommandSetMinPub{ + MinPub: 69, + Padding: [...]byte{0, 0, 0}, }, actualIns.Payload) data, err := actualIns.Data() assert.NoError(t, err) - assert.Len(t, data, 40) - require.Equal(t, caseUpdPriceNoFailOnError, data) + assert.Len(t, data, 12) + require.Equal(t, caseSetMinPub, data) - rebuiltIns := NewInstructionBuilder(program).UpdPriceNoFailOnError( + rebuiltIns := NewInstructionBuilder(program).SetMinPub( accs[0].PublicKey, accs[1].PublicKey, - *actualIns.Payload.(*CommandUpdPrice), + *actualIns.Payload.(*CommandSetMinPub), ) assert.Equal(t, actualIns, rebuiltIns) } + +func TestInstruction_WrongVersion(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("5U3bH5b6XtG99aVWLqwVzYPVpQiFHytBD68Rz2eFPZd7")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh")).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, []byte{ + 0x03, 0x00, 0x00, 0x00, // version + 0x00, 0x00, 0x00, 0x00, // instruction type + }) + require.EqualError(t, err, "not a valid Pyth instruction") + assert.Nil(t, actualIns) +} + +func TestInstruction_Unsupported(t *testing.T) { + var program = ProgramIDDevnet + var accs = []*solana.AccountMeta{ + solana.Meta(solana.MustPublicKeyFromBase58("5U3bH5b6XtG99aVWLqwVzYPVpQiFHytBD68Rz2eFPZd7")).SIGNER().WRITE(), + solana.Meta(solana.MustPublicKeyFromBase58("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh")).SIGNER().WRITE(), + } + + actualIns, err := DecodeInstruction(program, accs, []byte{ + 0x02, 0x00, 0x00, 0x00, // version + 0xfe, 0xff, 0x00, 0x00, // instruction type + }) + require.EqualError(t, err, "not a valid Pyth instruction") + assert.Nil(t, actualIns) +} diff --git a/tests/instruction/add_publisher.bin b/tests/instruction/add_publisher.bin new file mode 100644 index 0000000000000000000000000000000000000000..95f6157f2af8801f84b6d8af42c4833626183a76 GIT binary patch literal 40 vcmZQ#U|?Vc;w0O