From 309358c85bd7b832e7a633fd591a666e272ac0c6 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 12 Aug 2022 14:46:50 +0300 Subject: [PATCH] rpcclient: add new NEP-17 wrapper --- pkg/rpcclient/nep17/nep17.go | 111 ++++++++++++++++++++++ pkg/rpcclient/nep17/nep17_test.go | 106 +++++++++++++++++++++ pkg/rpcclient/neptoken/base.go | 63 +++++++++++++ pkg/rpcclient/neptoken/base_test.go | 137 ++++++++++++++++++++++++++++ 4 files changed, 417 insertions(+) create mode 100644 pkg/rpcclient/nep17/nep17.go create mode 100644 pkg/rpcclient/nep17/nep17_test.go create mode 100644 pkg/rpcclient/neptoken/base.go create mode 100644 pkg/rpcclient/neptoken/base_test.go diff --git a/pkg/rpcclient/nep17/nep17.go b/pkg/rpcclient/nep17/nep17.go new file mode 100644 index 000000000..d5f3998bd --- /dev/null +++ b/pkg/rpcclient/nep17/nep17.go @@ -0,0 +1,111 @@ +/* +Package nep17 contains RPC wrappers to work with NEP-17 contracts. + +Safe methods are encapsulated into TokenReader structure while Token provides +various methods to perform the only NEP-17 state-changing call, Transfer. +*/ +package nep17 + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// Invoker is used by TokenReader to call various safe methods. +type Invoker interface { + neptoken.Invoker +} + +// Actor is used by Token to create and send transactions. +type Actor interface { + Invoker + + MakeRun(script []byte) (*transaction.Transaction, error) + MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendRun(script []byte) (util.Uint256, uint32, error) +} + +// TokenReader represents safe (read-only) methods of NEP-17 token. It can be +// used to query various data. +type TokenReader struct { + neptoken.Base + + invoker Invoker + hash util.Uint160 +} + +// Token provides full NEP-17 interface, both safe and state-changing methods. +type Token struct { + TokenReader + + actor Actor +} + +// TransferEvent represents a Transfer event as defined in the NEP-17 standard. +type TransferEvent struct { + From util.Uint160 + To util.Uint160 + Amount *big.Int +} + +// NewReader creates an instance of TokenReader for contract with the given hash +// using the given Invoker. +func NewReader(invoker Invoker, hash util.Uint160) *TokenReader { + return &TokenReader{*neptoken.New(invoker, hash), invoker, hash} +} + +// New creates an instance of Token for contract with the given hash +// using the given Actor. +func New(actor Actor, hash util.Uint160) *Token { + return &Token{*NewReader(actor, hash), actor} +} + +// BalanceOf returns the token balance of the given account. +func (t *TokenReader) BalanceOf(account util.Uint160) (*big.Int, error) { + return unwrap.BigInt(t.invoker.Call(t.hash, "balanceOf", account)) +} + +// Transfer creates and sends a transaction that performs a `transfer` method +// call using the given parameters and checks for this call result, failing the +// transaction if it's not true. The returned values are transaction hash, its +// ValidUntilBlock value and an error if any. +func (t *Token) Transfer(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (util.Uint256, uint32, error) { + script, err := t.transferScript(from, to, amount, data) + if err != nil { + return util.Uint256{}, 0, err + } + return t.actor.SendRun(script) +} + +// TransferTransaction creates a transaction that performs a `transfer` method +// call using the given parameters and checks for this call result, failing the +// transaction if it's not true. This transaction is signed, but not sent to the +// network, instead it's returned to the caller. +func (t *Token) TransferTransaction(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { + script, err := t.transferScript(from, to, amount, data) + if err != nil { + return nil, err + } + return t.actor.MakeRun(script) +} + +// TransferUnsigned creates a transaction that performs a `transfer` method +// call using the given parameters and checks for this call result, failing the +// transaction if it's not true. This transaction is not signed and just returned +// to the caller. +func (t *Token) TransferUnsigned(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { + script, err := t.transferScript(from, to, amount, data) + if err != nil { + return nil, err + } + return t.actor.MakeUnsignedRun(script, nil) +} + +func (t *Token) transferScript(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) ([]byte, error) { + return smartcontract.CreateCallWithAssertScript(t.hash, "transfer", from, to, amount, data) +} diff --git a/pkg/rpcclient/nep17/nep17_test.go b/pkg/rpcclient/nep17/nep17_test.go new file mode 100644 index 000000000..2bcf45d53 --- /dev/null +++ b/pkg/rpcclient/nep17/nep17_test.go @@ -0,0 +1,106 @@ +package nep17 + +import ( + "errors" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +type testAct struct { + err error + res *result.Invoke + tx *transaction.Transaction + txh util.Uint256 + vub uint32 +} + +func (t *testAct) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) { + return t.res, t.err +} +func (t *testAct) MakeRun(script []byte) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendRun(script []byte) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} + +func TestReaderBalanceOf(t *testing.T) { + ta := new(testAct) + tr := NewReader(ta, util.Uint160{1, 2, 3}) + + ta.err = errors.New("") + _, err := tr.BalanceOf(util.Uint160{3, 2, 1}) + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(100500), + }, + } + bal, err := tr.BalanceOf(util.Uint160{3, 2, 1}) + require.NoError(t, err) + require.Equal(t, big.NewInt(100500), bal) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + }, + } + _, err = tr.BalanceOf(util.Uint160{3, 2, 1}) + require.Error(t, err) +} + +func TestTokenTransfer(t *testing.T) { + ta := new(testAct) + tok := New(ta, util.Uint160{1, 2, 3}) + + ta.err = errors.New("") + _, _, err := tok.Transfer(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.Error(t, err) + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + h, vub, err := tok.Transfer(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + + _, _, err = tok.Transfer(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap()) + require.Error(t, err) +} + +func TestTokenTransferTransaction(t *testing.T) { + ta := new(testAct) + tok := New(ta, util.Uint160{1, 2, 3}) + + for _, fun := range []func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error){ + tok.TransferTransaction, + tok.TransferUnsigned, + } { + ta.err = errors.New("") + _, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.Error(t, err) + + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + _, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap()) + require.Error(t, err) + } +} diff --git a/pkg/rpcclient/neptoken/base.go b/pkg/rpcclient/neptoken/base.go new file mode 100644 index 000000000..63e258d6b --- /dev/null +++ b/pkg/rpcclient/neptoken/base.go @@ -0,0 +1,63 @@ +/* +Package neptoken contains RPC wrapper for common NEP-11 and NEP-17 methods. + +All of these methods are safe, read-only. +*/ +package neptoken + +import ( + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +const ( + // MaxValidDecimals is the maximum value 'decimals' contract method can + // return to be considered as valid. It's log10(2^256), higher values + // don't make any sense on a VM with 256-bit integers. This restriction + // is not imposed by NEP-17 or NEP-11, but we do it as a sanity check + // anyway (and return plain int as a result). + MaxValidDecimals = 77 +) + +// Invoker is used by Base to call various methods. +type Invoker interface { + Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) +} + +// Base is a reader interface for common NEP-11 and NEP-17 methods built +// on top of Invoker. +type Base struct { + invoker Invoker + hash util.Uint160 +} + +// New creates an instance of Base for contract with the given hash using the +// given invoker. +func New(invoker Invoker, hash util.Uint160) *Base { + return &Base{invoker, hash} +} + +// Decimals implements `decimals` NEP-17 or NEP-11 method and returns the number +// of decimals used by token. For non-divisible NEP-11 tokens this method always +// returns zero. Values less than 0 or more than MaxValidDecimals are considered +// to be invalid (with an appropriate error) even if returned by the contract. +func (b *Base) Decimals() (int, error) { + r, err := b.invoker.Call(b.hash, "decimals") + dec, err := unwrap.LimitedInt64(r, err, 0, MaxValidDecimals) + return int(dec), err +} + +// Symbol implements `symbol` NEP-17 or NEP-11 method and returns a short token +// identifier (like "NEO" or "GAS"). +func (b *Base) Symbol() (string, error) { + return unwrap.PrintableASCIIString(b.invoker.Call(b.hash, "symbol")) +} + +// TotalSupply returns the total token supply currently available (the amount +// of minted tokens). +func (b *Base) TotalSupply() (*big.Int, error) { + return unwrap.BigInt(b.invoker.Call(b.hash, "totalSupply")) +} diff --git a/pkg/rpcclient/neptoken/base_test.go b/pkg/rpcclient/neptoken/base_test.go new file mode 100644 index 000000000..b251de5f0 --- /dev/null +++ b/pkg/rpcclient/neptoken/base_test.go @@ -0,0 +1,137 @@ +package neptoken + +import ( + "errors" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +type testInv struct { + err error + res *result.Invoke +} + +func (t *testInv) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) { + return t.res, t.err +} + +func TestBaseErrors(t *testing.T) { + ti := new(testInv) + base := New(ti, util.Uint160{1, 2, 3}) + + ti.err = errors.New("") + _, err := base.Decimals() + require.Error(t, err) + _, err = base.Symbol() + require.Error(t, err) + _, err = base.TotalSupply() + require.Error(t, err) + + ti.err = nil + ti.res = &result.Invoke{ + State: "FAULT", + FaultException: "bad thing happened", + } + _, err = base.Decimals() + require.Error(t, err) + _, err = base.Symbol() + require.Error(t, err) + _, err = base.TotalSupply() + require.Error(t, err) + + ti.res = &result.Invoke{ + State: "HALT", + } + _, err = base.Decimals() + require.Error(t, err) + _, err = base.Symbol() + require.Error(t, err) + _, err = base.TotalSupply() + require.Error(t, err) +} + +func TestBaseDecimals(t *testing.T) { + ti := new(testInv) + base := New(ti, util.Uint160{1, 2, 3}) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(0), + }, + } + dec, err := base.Decimals() + require.NoError(t, err) + require.Equal(t, 0, dec) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(-1), + }, + } + _, err = base.Decimals() + require.Error(t, err) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(100500), + }, + } + _, err = base.Decimals() + require.Error(t, err) +} + +func TestBaseSymbol(t *testing.T) { + ti := new(testInv) + base := New(ti, util.Uint160{1, 2, 3}) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make("SYM"), + }, + } + sym, err := base.Symbol() + require.NoError(t, err) + require.Equal(t, "SYM", sym) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make("\xff"), + }, + } + _, err = base.Symbol() + require.Error(t, err) +} + +func TestBaseTotalSupply(t *testing.T) { + ti := new(testInv) + base := New(ti, util.Uint160{1, 2, 3}) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(100500), + }, + } + ts, err := base.TotalSupply() + require.NoError(t, err) + require.Equal(t, big.NewInt(100500), ts) + + ti.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + }, + } + _, err = base.TotalSupply() + require.Error(t, err) +}