Skip to content

Commit

Permalink
Batch Contract Read (#926)
Browse files Browse the repository at this point in the history
* Batch Contract Read

Batch contract reads are added and multiple address support was removed. All tests now pass.

* fix tests

* make linter happy
  • Loading branch information
EasterTheBunny authored Nov 18, 2024
1 parent 8b8369c commit b9bf6a3
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 772 deletions.
78 changes: 10 additions & 68 deletions pkg/solana/chainreader/account_read_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,102 +2,44 @@ package chainreader

import (
"context"
"fmt"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"

"github.com/smartcontractkit/chainlink-common/pkg/types"
)

// BinaryDataReader provides an interface for reading bytes from a source. This is likely a wrapper
// for a solana client.
type BinaryDataReader interface {
ReadAll(context.Context, solana.PublicKey, *rpc.GetAccountInfoOpts) ([]byte, error)
}

// accountReadBinding provides decoding and reading Solana Account data using a defined codec. The
// `idlAccount` refers to the account name in the IDL for which the codec has a type mapping.
type accountReadBinding struct {
idlAccount string
codec types.RemoteCodec
reader BinaryDataReader
key solana.PublicKey
opts *rpc.GetAccountInfoOpts
}

func newAccountReadBinding(acct string, codec types.RemoteCodec, reader BinaryDataReader, opts *rpc.GetAccountInfoOpts) *accountReadBinding {
func newAccountReadBinding(acct string, codec types.RemoteCodec, opts *rpc.GetAccountInfoOpts) *accountReadBinding {
return &accountReadBinding{
idlAccount: acct,
codec: codec,
reader: reader,
opts: opts,
}
}

var _ readBinding = &accountReadBinding{}

func (b *accountReadBinding) PreLoad(ctx context.Context, address string, result *loadedResult) {
if result == nil {
return
}

account, err := solana.PublicKeyFromBase58(address)
if err != nil {
result.err <- err

return
}

bts, err := b.reader.ReadAll(ctx, account, b.opts)
if err != nil {
result.err <- fmt.Errorf("%w: failed to get binary data", err)

return
}

select {
case <-ctx.Done():
result.err <- ctx.Err()
default:
result.value <- bts
}
func (b *accountReadBinding) SetAddress(key solana.PublicKey) {
b.key = key
}

func (b *accountReadBinding) GetLatestValue(ctx context.Context, address string, _ any, outVal any, result *loadedResult) error {
var (
bts []byte
err error
)

if result != nil {
// when preloading, the process will wait for one of three conditions:
// 1. the context ends and returns an error
// 2. bytes were loaded in the bytes channel
// 3. an error was loaded in the err channel
select {
case <-ctx.Done():
err = ctx.Err()
case bts = <-result.value:
case err = <-result.err:
}

if err != nil {
return err
}
} else {
account, err := solana.PublicKeyFromBase58(address)
if err != nil {
return err
}

if bts, err = b.reader.ReadAll(ctx, account, b.opts); err != nil {
return fmt.Errorf("%w: failed to get binary data", err)
}
}

return b.codec.Decode(ctx, bts, outVal, b.idlAccount)
func (b *accountReadBinding) GetAddress() solana.PublicKey {
return b.key
}

func (b *accountReadBinding) CreateType(_ bool) (any, error) {
return b.codec.CreateType(b.idlAccount, false)
}

func (b *accountReadBinding) Decode(ctx context.Context, bts []byte, outVal any) error {
return b.codec.Decode(ctx, bts, outVal, b.idlAccount)
}
164 changes: 0 additions & 164 deletions pkg/solana/chainreader/account_read_binding_test.go

This file was deleted.

107 changes: 107 additions & 0 deletions pkg/solana/chainreader/batch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package chainreader

import (
"context"
"errors"

"github.com/gagliardetto/solana-go"

"github.com/smartcontractkit/chainlink-common/pkg/values"
)

type call struct {
ContractName, ReadName string
Params, ReturnVal any
}

type batchResultWithErr struct {
address string
contractName, readName string
returnVal any
err error
}

var (
ErrMissingAccountData = errors.New("account data not found")
)

type MultipleAccountGetter interface {
GetMultipleAccountData(context.Context, ...solana.PublicKey) ([][]byte, error)
}

func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bindings namespaceBindings, batch []call) ([]batchResultWithErr, error) {
// Create the list of public keys to fetch
keys := make([]solana.PublicKey, len(batch))
for idx, call := range batch {
binding, err := bindings.GetReadBinding(call.ContractName, call.ReadName)
if err != nil {
return nil, err
}

keys[idx] = binding.GetAddress()
}

// Fetch the account data
data, err := client.GetMultipleAccountData(ctx, keys...)
if err != nil {
return nil, err
}

results := make([]batchResultWithErr, len(batch))

// decode batch call results
for idx, call := range batch {
results[idx] = batchResultWithErr{
address: keys[idx].String(),
contractName: call.ContractName,
readName: call.ReadName,
returnVal: call.ReturnVal,
}

if data[idx] == nil || len(data[idx]) == 0 {
results[idx].err = ErrMissingAccountData

continue
}

binding, err := bindings.GetReadBinding(results[idx].contractName, results[idx].readName)
if err != nil {
results[idx].err = err

continue
}

ptrToValue, isValue := call.ReturnVal.(*values.Value)
if !isValue {
results[idx].err = errors.Join(
results[idx].err,
binding.Decode(ctx, data[idx], results[idx].returnVal),
)

continue
}

contractType, err := binding.CreateType(false)
if err != nil {
results[idx].err = err

continue
}

results[idx].err = errors.Join(
results[idx].err,
binding.Decode(ctx, data[idx], contractType),
)

value, err := values.Wrap(contractType)
if err != nil {
results[idx].err = errors.Join(results[idx].err, err)

continue
}

*ptrToValue = value
}

return results, nil
}
Loading

0 comments on commit b9bf6a3

Please sign in to comment.