From aca8ce0d286bb331004d30992859a6e6054f1951 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 6 Sep 2022 16:05:57 +0300 Subject: [PATCH 01/16] unwrap: provide ErrNoSessionID, add some explanations --- pkg/rpcclient/unwrap/unwrap.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/rpcclient/unwrap/unwrap.go b/pkg/rpcclient/unwrap/unwrap.go index 0e91a3d01..c31c74558 100644 --- a/pkg/rpcclient/unwrap/unwrap.go +++ b/pkg/rpcclient/unwrap/unwrap.go @@ -25,6 +25,12 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" ) +// ErrNoSessionID is returned from the SessionIterator when the server does not +// have sessions enabled and does not perform automatic iterator expansion. It +// means you have no way to get the data from returned iterators using this +// server, other than expanding it in the VM script. +var ErrNoSessionID = errors.New("server returned iterator ID, but no session ID") + // BigInt expects correct execution (HALT state) with a single stack item // returned. A big.Int is extracted from this item and returned. func BigInt(r *result.Invoke, err error) (*big.Int, error) { @@ -142,7 +148,10 @@ func Uint256(r *result.Invoke, err error) (util.Uint256, error) { // SessionIterator expects correct execution (HALT state) with a single stack // item returned. If this item is an iterator it's returned to the caller along -// with the session ID. +// with the session ID. Notice that this function also returns successfully +// with zero session ID (but an appropriate Iterator holding all the data +// received) when RPC server performs (limited) iterator expansion which is the +// default behavior for NeoGo servers with SessionEnabled set to false. func SessionIterator(r *result.Invoke, err error) (uuid.UUID, result.Iterator, error) { itm, err := Item(r, err) if err != nil { @@ -156,7 +165,7 @@ func SessionIterator(r *result.Invoke, err error) (uuid.UUID, result.Iterator, e return uuid.UUID{}, result.Iterator{}, errors.New("the item is InteropInterface, but not an Iterator") } if (r.Session == uuid.UUID{}) && iter.ID != nil { - return uuid.UUID{}, result.Iterator{}, errors.New("server returned iterator ID, but no session ID") + return uuid.UUID{}, result.Iterator{}, ErrNoSessionID } return r.Session, iter, nil } From 69176168c3264746a8f870395ae17880c6b8210c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 6 Sep 2022 16:37:21 +0300 Subject: [PATCH 02/16] smartcontract: modernize Builder example And make it a bit more useful. --- pkg/smartcontract/doc_test.go | 50 ++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/pkg/smartcontract/doc_test.go b/pkg/smartcontract/doc_test.go index a74e411f8..39c76dbfb 100644 --- a/pkg/smartcontract/doc_test.go +++ b/pkg/smartcontract/doc_test.go @@ -4,9 +4,10 @@ import ( "context" "encoding/hex" - "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -14,37 +15,48 @@ import ( func ExampleBuilder() { // No error checking done at all, intentionally. + w, _ := wallet.NewWalletFromFile("somewhere") + defer w.Close() + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) - neoHash, _ := c.GetNativeContractHash("NeoToken") + + // Assuming there is one Account inside. + a, _ := actor.NewSimple(c, w.Accounts[0]) pKey, _ := hex.DecodeString("03d9e8b16bd9b22d3345d6d4cde31be1c3e1d161532e3d0ccecb95ece2eb58336e") // Public key. b := smartcontract.NewBuilder() - // Single NEO "vote" call with a check - b.InvokeWithAssert(neoHash, "vote", pKey) + // Transfer + vote in a single script with each action leaving return value on the stack. + b.InvokeMethod(neo.Hash, "transfer", a.Sender(), util.Uint160{0xff}, 1, nil) + b.InvokeMethod(neo.Hash, "vote", pKey) script, _ := b.Script() - // The script can then be used to create transaction or to invoke via RPC. - res, _ := c.InvokeScript(script, []transaction.Signer{{Account: util.Uint160{0x01, 0x02, 0x03}, Scopes: transaction.CalledByEntry}}) - if res.State != "HALT" { - // The script failed + // Actor has an Invoker inside, so we can perform test invocation using the script. + res, _ := a.Run(script) + if res.State != "HALT" || len(res.Stack) != 2 { + // The script failed completely or didn't return proper number of return values. + } + + transferResult, _ := res.Stack[0].TryBool() + voteResult, _ := res.Stack[1].TryBool() + + if !transferResult { + // Transfer failed. + } + if !voteResult { + // Vote failed. } b.Reset() // Copy the old script above if you need it! - w, _ := wallet.NewWalletFromFile("somewhere") - defer w.Close() - // Assuming there is one Account inside - a, _ := actor.NewSimple(c, w.Accounts[0]) - from := w.Accounts[0].Contract.ScriptHash() // Assuming Contract is present. - - // Multiple transfers in a single script. If any of them fail whole script fails. - b.InvokeWithAssert(neoHash, "transfer", from, util.Uint160{0x70}, 1, nil) - b.InvokeWithAssert(neoHash, "transfer", from, util.Uint160{0x71}, 10, []byte("data")) - b.InvokeWithAssert(neoHash, "transfer", from, util.Uint160{0x72}, 1, nil) + // Multiple transfers of different tokens in a single script. If any of + // them fails whole script fails. + b.InvokeWithAssert(neo.Hash, "transfer", a.Sender(), util.Uint160{0x70}, 1, nil) + b.InvokeWithAssert(gas.Hash, "transfer", a.Sender(), util.Uint160{0x71}, 100000, []byte("data")) + b.InvokeWithAssert(neo.Hash, "transfer", a.Sender(), util.Uint160{0x72}, 1, nil) script, _ = b.Script() - // The script can then be used to create transaction or to invoke via RPC. + // Now send a transaction with this script via an RPC node. txid, vub, _ := a.SendRun(script) _ = txid _ = vub From ea92f3d716dafda9e5a57833e7a8e14cdc39d791 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 6 Sep 2022 17:30:56 +0300 Subject: [PATCH 03/16] smartcontract: add some notes on API limitations --- pkg/smartcontract/builder.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/smartcontract/builder.go b/pkg/smartcontract/builder.go index 9ea69f225..f20789652 100644 --- a/pkg/smartcontract/builder.go +++ b/pkg/smartcontract/builder.go @@ -18,6 +18,18 @@ import ( // "entry scripts"), so the set of methods it exposes is tailored to this model // of use and any calls emitted don't limit flags in any way (always use // callflag.All). +// +// When using this API keep in mind that the resulting script can't be larger than +// 64K (transaction.MaxScriptLength) to be used as a transaction entry script and +// it can't have more than 2048 elements on the stack. Technically, this limits +// the number of calls that can be made to a lesser value because invocations use +// the same stack too (the exact number depends on methods and parameters). +// +// This API is not (and won't be) suitable to create complex scripts that use +// returned values as parameters to other calls or perform loops or do any other +// things that can be done in NeoVM. This hardly can be expressed in an API like +// this, so if you need more than that and if you're ready to work with bare +// NeoVM instructions please refer to [emit] and [opcode] packages. type Builder struct { bw *io.BufBinWriter } From 00a9376311c94680e83bdd05f5e9d398869a2bd7 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 22:38:54 +0300 Subject: [PATCH 04/16] invoker: update documentation, add example --- pkg/rpcclient/invoker/doc_test.go | 99 +++++++++++++++++++++++++++++++ pkg/rpcclient/invoker/invoker.go | 15 ++++- 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 pkg/rpcclient/invoker/doc_test.go diff --git a/pkg/rpcclient/invoker/doc_test.go b/pkg/rpcclient/invoker/doc_test.go new file mode 100644 index 000000000..fe01b9490 --- /dev/null +++ b/pkg/rpcclient/invoker/doc_test.go @@ -0,0 +1,99 @@ +package invoker_test + +import ( + "context" + "errors" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" +) + +func ExampleInvoker() { + // No error checking done at all, intentionally. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // A simple invoker with no signers, perfectly fine for reads from safe methods. + inv := invoker.New(c, nil) + + // Get the NEO token supply (notice that unwrap is used to get the result). + supply, _ := unwrap.BigInt(inv.Call(neo.Hash, "totalSupply")) + _ = supply + + acc, _ := address.StringToUint160("NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq") + // Get the NEO balance for account NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq. + balance, _ := unwrap.BigInt(inv.Call(neo.Hash, "balanceOf", acc)) + _ = balance + + // Test-invoke transfer call. + res, _ := inv.Call(neo.Hash, "transfer", acc, util.Uint160{1, 2, 3}, 1, nil) + if res.State == vmstate.Halt.String() { + // NEO is broken! inv has no signers and transfer requires a witness to be performed. + } else { + // OK, this actually should fail. + } + + // A historic invoker with no signers at block 1000000. + inv = invoker.NewHistoricAtHeight(1000000, c, nil) + + // It's the same call as above, but the data is for a state at block 1000000. + balance, _ = unwrap.BigInt(inv.Call(neo.Hash, "balanceOf", acc)) + _ = balance + + // This invoker has a signer for NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq account with + // CalledByEntry scope, which is sufficient for most operation. It uses current + // state which is exactly what you need if you want to then create a transaction + // with the same action. + inv = invoker.New(c, []transaction.Signer{{Account: acc, Scopes: transaction.CalledByEntry}}) + + // Now test invocation should be fine (if NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq has 1 NEO of course). + res, _ = inv.Call(neo.Hash, "transfer", acc, util.Uint160{1, 2, 3}, 1, nil) + if res.State == vmstate.Halt.String() { + // transfer actually returns a value, so check it too. + ok, _ := unwrap.Bool(res, nil) + if ok { + // OK, as expected. res.Script contains the corresponding + // script and res.GasConsumed has an appropriate system fee + // required for a transaction. + } + } + + // Now let's try working with iterators. + nep11Contract := util.Uint160{1, 2, 3} + + var tokens [][]byte + + // Try doing it the right way, by traversing the iterator. + sess, iter, err := unwrap.SessionIterator(inv.Call(nep11Contract, "tokensOf", acc)) + + // The server doesn't support sessions and doesn't perform iterator expansion, + // iterators can't be used. + if err != nil { + if errors.Is(err, unwrap.ErrNoSessionID) { + // But if we expect some low number of elements, CallAndExpandIterator + // can help us in this case. If the account has more than 10 elements, + // some of them will be missing from the response. + tokens, _ = unwrap.ArrayOfBytes(inv.CallAndExpandIterator(nep11Contract, "tokensOf", 10, acc)) + } else { + panic("some error") + } + } else { + items, err := inv.TraverseIterator(sess, &iter, 100) + // Keep going until there are no more elements + for err == nil && len(items) != 0 { + for _, itm := range items { + tokenID, _ := itm.TryBytes() + tokens = append(tokens, tokenID) + } + items, err = inv.TraverseIterator(sess, &iter, 100) + } + // Let the server release the session. + _ = inv.TerminateSession(sess) + } + _ = tokens +} diff --git a/pkg/rpcclient/invoker/invoker.go b/pkg/rpcclient/invoker/invoker.go index 266d09687..55a423805 100644 --- a/pkg/rpcclient/invoker/invoker.go +++ b/pkg/rpcclient/invoker/invoker.go @@ -1,3 +1,12 @@ +/* +Package invoker provides a convenient wrapper to perform test calls via RPC client. + +This layer builds on top of the basic RPC client and simplifies performing +test function invocations and script runs. It also makes historic calls (NeoGo +extension) transparent, allowing to use the same API as for regular calls. +Results of these calls can be interpreted by upper layer packages like actor +(to create transactions) or unwrap (to retrieve data from return values). +*/ package invoker import ( @@ -70,6 +79,9 @@ type historicConverter struct { } // New creates an Invoker to test-execute things at the current blockchain height. +// If you only want to read data from the contract using its safe methods normally +// (but contract-specific in general case) it's OK to pass nil for signers (that +// is, use no signers). func New(client RPCInvoke, signers []transaction.Signer) *Invoker { return &Invoker{client, signers} } @@ -187,7 +199,8 @@ func (v *Invoker) Run(script []byte) (*result.Invoke, error) { } // TerminateSession closes the given session, returning an error if anything -// goes wrong. +// goes wrong. It's not strictly required to close the session (it'll expire on +// the server anyway), but it helps to release server resources earlier. func (v *Invoker) TerminateSession(sessionID uuid.UUID) error { return termSession(v.client, sessionID) } From e1fe76137e8bf2bff47afa5698487cbeb39dac1c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 15:05:25 +0300 Subject: [PATCH 05/16] rpcclient: use separate reader/writer structs in nep11 and nep17 Which greatly simplifies reuse of these packages (and they're expected to be reused since real tokens implement standards and also add something of their own) and allows to avoid effects like doc_test.go:68:28: ambiguous selector neoContract.BalanceOf when neo.Contract is used. Avoids duplication in NEP-11 implementation as well. --- pkg/rpcclient/neo/neo.go | 5 +++-- pkg/rpcclient/nep11/base.go | 21 ++++++++++++++------- pkg/rpcclient/nep11/divisible.go | 23 +++-------------------- pkg/rpcclient/nep11/nondivisible.go | 11 +++-------- pkg/rpcclient/nep17/nep17.go | 29 ++++++++++++++++++----------- 5 files changed, 41 insertions(+), 48 deletions(-) diff --git a/pkg/rpcclient/neo/neo.go b/pkg/rpcclient/neo/neo.go index da2fe0d45..b9b17ed33 100644 --- a/pkg/rpcclient/neo/neo.go +++ b/pkg/rpcclient/neo/neo.go @@ -63,7 +63,7 @@ type ContractReader struct { // Contract provides full NEO interface, both safe and state-changing methods. type Contract struct { ContractReader - nep17.Token + nep17.TokenWriter actor Actor } @@ -102,7 +102,8 @@ func NewReader(invoker Invoker) *ContractReader { // New creates an instance of Contract to perform state-changing actions in the // NEO contract. func New(actor Actor) *Contract { - return &Contract{*NewReader(actor), *nep17.New(actor, Hash), actor} + nep := nep17.New(actor, Hash) + return &Contract{ContractReader{nep.TokenReader, actor}, nep.TokenWriter, actor} } // GetAccountState returns current NEO balance state for the account which diff --git a/pkg/rpcclient/nep11/base.go b/pkg/rpcclient/nep11/base.go index 8aaf06a89..71c75ef7a 100644 --- a/pkg/rpcclient/nep11/base.go +++ b/pkg/rpcclient/nep11/base.go @@ -51,12 +51,19 @@ type BaseReader struct { hash util.Uint160 } +// BaseWriter is a transaction-creating interface for common divisible and +// non-divisible NEP-11 methods. It simplifies reusing this set of methods, +// but a complete Base is expected to be used in other packages. +type BaseWriter struct { + hash util.Uint160 + actor Actor +} + // Base is a state-changing interface for common divisible and non-divisible NEP-11 // methods. type Base struct { BaseReader - - actor Actor + BaseWriter } // TransferEvent represents a Transfer event as defined in the NEP-11 standard. @@ -83,7 +90,7 @@ func NewBaseReader(invoker Invoker, hash util.Uint160) *BaseReader { // NewBase creates an instance of Base for contract with the given // hash using the given actor. func NewBase(actor Actor, hash util.Uint160) *Base { - return &Base{*NewBaseReader(actor, hash), actor} + return &Base{*NewBaseReader(actor, hash), BaseWriter{hash, actor}} } // Properties returns a set of token's properties such as name or URL. The map @@ -142,7 +149,7 @@ func (t *BaseReader) TokensOfExpanded(account util.Uint160, num int) ([][]byte, // transaction if it's not true. It works for divisible NFTs only when there is // one owner for the particular token. The returned values are transaction hash, // its ValidUntilBlock value and an error if any. -func (t *Base) Transfer(to util.Uint160, id []byte, data interface{}) (util.Uint256, uint32, error) { +func (t *BaseWriter) Transfer(to util.Uint160, id []byte, data interface{}) (util.Uint256, uint32, error) { script, err := t.transferScript(to, id, data) if err != nil { return util.Uint256{}, 0, err @@ -155,7 +162,7 @@ func (t *Base) Transfer(to util.Uint160, id []byte, data interface{}) (util.Uint // transaction if it's not true. It works for divisible NFTs only when there is // one owner for the particular token. This transaction is signed, but not sent // to the network, instead it's returned to the caller. -func (t *Base) TransferTransaction(to util.Uint160, id []byte, data interface{}) (*transaction.Transaction, error) { +func (t *BaseWriter) TransferTransaction(to util.Uint160, id []byte, data interface{}) (*transaction.Transaction, error) { script, err := t.transferScript(to, id, data) if err != nil { return nil, err @@ -168,7 +175,7 @@ func (t *Base) TransferTransaction(to util.Uint160, id []byte, data interface{}) // transaction if it's not true. It works for divisible NFTs only when there is // one owner for the particular token. This transaction is not signed and just // returned to the caller. -func (t *Base) TransferUnsigned(to util.Uint160, id []byte, data interface{}) (*transaction.Transaction, error) { +func (t *BaseWriter) TransferUnsigned(to util.Uint160, id []byte, data interface{}) (*transaction.Transaction, error) { script, err := t.transferScript(to, id, data) if err != nil { return nil, err @@ -176,7 +183,7 @@ func (t *Base) TransferUnsigned(to util.Uint160, id []byte, data interface{}) (* return t.actor.MakeUnsignedRun(script, nil) } -func (t *Base) transferScript(params ...interface{}) ([]byte, error) { +func (t *BaseWriter) transferScript(params ...interface{}) ([]byte, error) { return smartcontract.CreateCallWithAssertScript(t.hash, "transfer", params...) } diff --git a/pkg/rpcclient/nep11/divisible.go b/pkg/rpcclient/nep11/divisible.go index 107efb307..29a3b4055 100644 --- a/pkg/rpcclient/nep11/divisible.go +++ b/pkg/rpcclient/nep11/divisible.go @@ -18,7 +18,8 @@ type DivisibleReader struct { // Divisible is a state-changing interface for divisible NEP-11 contract. type Divisible struct { - Base + DivisibleReader + BaseWriter } // OwnerIterator is used for iterating over OwnerOf (for divisible NFTs) results. @@ -37,7 +38,7 @@ func NewDivisibleReader(invoker Invoker, hash util.Uint160) *DivisibleReader { // NewDivisible creates an instance of Divisible for a contract // with the given hash using the given actor. func NewDivisible(actor Actor, hash util.Uint160) *Divisible { - return &Divisible{*NewBase(actor, hash)} + return &Divisible{*NewDivisibleReader(actor, hash), BaseWriter{hash, actor}} } // OwnerOf returns returns an iterator that allows to walk through all owners of @@ -66,24 +67,6 @@ func (t *DivisibleReader) BalanceOfD(owner util.Uint160, token []byte) (*big.Int return unwrap.BigInt(t.invoker.Call(t.hash, "balanceOf", owner, token)) } -// OwnerOf is the same as (*DivisibleReader).OwnerOf. -func (t *Divisible) OwnerOf(token []byte) (*OwnerIterator, error) { - r := DivisibleReader{t.BaseReader} - return r.OwnerOf(token) -} - -// OwnerOfExpanded is the same as (*DivisibleReader).OwnerOfExpanded. -func (t *Divisible) OwnerOfExpanded(token []byte, num int) ([]util.Uint160, error) { - r := DivisibleReader{t.BaseReader} - return r.OwnerOfExpanded(token, num) -} - -// BalanceOfD is the same as (*DivisibleReader).BalanceOfD. -func (t *Divisible) BalanceOfD(owner util.Uint160, token []byte) (*big.Int, error) { - r := DivisibleReader{t.BaseReader} - return r.BalanceOfD(owner, token) -} - // TransferD is a divisible version of (*Base).Transfer, allowing to transfer a // part of NFT. It creates and sends a transaction that performs a `transfer` // method call using the given parameters and checks for this call result, diff --git a/pkg/rpcclient/nep11/nondivisible.go b/pkg/rpcclient/nep11/nondivisible.go index f0b48cf75..1ca28452d 100644 --- a/pkg/rpcclient/nep11/nondivisible.go +++ b/pkg/rpcclient/nep11/nondivisible.go @@ -12,7 +12,8 @@ type NonDivisibleReader struct { // NonDivisible is a state-changing interface for non-divisble NEP-11 contract. type NonDivisible struct { - Base + NonDivisibleReader + BaseWriter } // NewNonDivisibleReader creates an instance of NonDivisibleReader for a contract @@ -24,16 +25,10 @@ func NewNonDivisibleReader(invoker Invoker, hash util.Uint160) *NonDivisibleRead // NewNonDivisible creates an instance of NonDivisible for a contract // with the given hash using the given actor. func NewNonDivisible(actor Actor, hash util.Uint160) *NonDivisible { - return &NonDivisible{*NewBase(actor, hash)} + return &NonDivisible{*NewNonDivisibleReader(actor, hash), BaseWriter{hash, actor}} } // OwnerOf returns the owner of the given NFT. func (t *NonDivisibleReader) OwnerOf(token []byte) (util.Uint160, error) { return unwrap.Uint160(t.invoker.Call(t.hash, "ownerOf", token)) } - -// OwnerOf is the same as (*NonDivisibleReader).OwnerOf. -func (t *NonDivisible) OwnerOf(token []byte) (util.Uint160, error) { - r := NonDivisibleReader{t.BaseReader} - return r.OwnerOf(token) -} diff --git a/pkg/rpcclient/nep17/nep17.go b/pkg/rpcclient/nep17/nep17.go index d6c263747..e2f1d71e4 100644 --- a/pkg/rpcclient/nep17/nep17.go +++ b/pkg/rpcclient/nep17/nep17.go @@ -36,12 +36,19 @@ type TokenReader struct { neptoken.Base } +// TokenWriter contains NEP-17 token methods that change state. It's not meant +// to be used directly (Token that includes it is more convenient) and just +// separates one set of methods from another to simplify reusing this package +// for other contracts that extend NEP-17 interface. +type TokenWriter struct { + hash util.Uint160 + actor Actor +} + // Token provides full NEP-17 interface, both safe and state-changing methods. type Token struct { TokenReader - - hash util.Uint160 - actor Actor + TokenWriter } // TransferEvent represents a Transfer event as defined in the NEP-17 standard. @@ -68,14 +75,14 @@ func NewReader(invoker Invoker, hash util.Uint160) *TokenReader { // 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), hash, actor} + return &Token{*NewReader(actor, hash), TokenWriter{hash, actor}} } // 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) { +func (t *TokenWriter) Transfer(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (util.Uint256, uint32, error) { return t.MultiTransfer([]TransferParameters{{from, to, amount, data}}) } @@ -83,7 +90,7 @@ func (t *Token) Transfer(from util.Uint160, to util.Uint160, amount *big.Int, da // 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) { +func (t *TokenWriter) TransferTransaction(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { return t.MultiTransferTransaction([]TransferParameters{{from, to, amount, data}}) } @@ -91,11 +98,11 @@ func (t *Token) TransferTransaction(from util.Uint160, to util.Uint160, amount * // 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) { +func (t *TokenWriter) TransferUnsigned(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { return t.MultiTransferUnsigned([]TransferParameters{{from, to, amount, data}}) } -func (t *Token) multiTransferScript(params []TransferParameters) ([]byte, error) { +func (t *TokenWriter) multiTransferScript(params []TransferParameters) ([]byte, error) { if len(params) == 0 { return nil, errors.New("at least one transfer parameter required") } @@ -113,7 +120,7 @@ func (t *Token) multiTransferScript(params []TransferParameters) ([]byte, error) // many times as needed (with ASSERTs added, so if any of these transfers fail // whole transaction (with all transfers) fails). The values returned are the // same as in Transfer. -func (t *Token) MultiTransfer(params []TransferParameters) (util.Uint256, uint32, error) { +func (t *TokenWriter) MultiTransfer(params []TransferParameters) (util.Uint256, uint32, error) { script, err := t.multiTransferScript(params) if err != nil { return util.Uint256{}, 0, err @@ -123,7 +130,7 @@ func (t *Token) MultiTransfer(params []TransferParameters) (util.Uint256, uint32 // MultiTransferTransaction is similar to MultiTransfer, but returns the same values // as TransferTransaction (signed transaction that is not yet sent). -func (t *Token) MultiTransferTransaction(params []TransferParameters) (*transaction.Transaction, error) { +func (t *TokenWriter) MultiTransferTransaction(params []TransferParameters) (*transaction.Transaction, error) { script, err := t.multiTransferScript(params) if err != nil { return nil, err @@ -133,7 +140,7 @@ func (t *Token) MultiTransferTransaction(params []TransferParameters) (*transact // MultiTransferUnsigned is similar to MultiTransfer, but returns the same values // as TransferUnsigned (not yet signed transaction). -func (t *Token) MultiTransferUnsigned(params []TransferParameters) (*transaction.Transaction, error) { +func (t *TokenWriter) MultiTransferUnsigned(params []TransferParameters) (*transaction.Transaction, error) { script, err := t.multiTransferScript(params) if err != nil { return nil, err From cb1a1f85327f74c2e1c975bd5f3e502ee5a5745a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 15:11:27 +0300 Subject: [PATCH 06/16] actor: extend documentation, add example --- pkg/rpcclient/actor/actor.go | 17 +++-- pkg/rpcclient/actor/doc_test.go | 122 ++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 pkg/rpcclient/actor/doc_test.go diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go index aed020c7f..59b0ad3a7 100644 --- a/pkg/rpcclient/actor/actor.go +++ b/pkg/rpcclient/actor/actor.go @@ -1,10 +1,11 @@ /* Package actor provides a way to change chain state via RPC client. -This layer builds on top of the basic RPC client and simplifies creating, -signing and sending transactions to the network (since that's the only way chain -state is changed). It's generic enough to be used for any contract that you may -want to invoke and contract-specific functions can build on top of it. +This layer builds on top of the basic RPC client and [invoker] package, it +simplifies creating, signing and sending transactions to the network (since +that's the only way chain state is changed). It's generic enough to be used for +any contract that you may want to invoke and contract-specific functions can +build on top of it. */ package actor @@ -44,6 +45,14 @@ type SignerAccount struct { // state-changing actions (via transactions that can also be created without // sending them to the network) on behalf of a set of signers. It also provides // an Invoker interface to perform test calls with the same set of signers. +// +// Actor-specific APIs follow the naming scheme set by Invoker in method +// suffixes. *Call methods operate with function calls and require a contract +// hash, a method and parameters if any. *Run methods operate with scripts and +// require a NeoVM script that will be used directly. Prefixes denote the +// action to be performed, "Make" prefix is used for methods that create +// transactions in various ways, while "Send" prefix is used by methods that +// directly transmit created transactions to the RPC server. type Actor struct { invoker.Invoker diff --git a/pkg/rpcclient/actor/doc_test.go b/pkg/rpcclient/actor/doc_test.go new file mode 100644 index 000000000..7121cc564 --- /dev/null +++ b/pkg/rpcclient/actor/doc_test.go @@ -0,0 +1,122 @@ +package actor_test + +import ( + "context" + "encoding/json" + "os" + + "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/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/policy" + sccontext "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +func ExampleActor() { + // No error checking done at all, intentionally. + w, _ := wallet.NewWalletFromFile("somewhere") + defer w.Close() + + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Create a simple CalledByEntry-scoped actor (assuming there are accounts + // inside the wallet). + a, _ := actor.NewSimple(c, w.Accounts[0]) + + customContract := util.Uint160{9, 8, 7} + // Actor has an Invoker inside, so we can perform test invocations, it will + // have a signer with the first wallet account and CalledByEntry scope. + res, _ := a.Call(customContract, "method", 1, 2, 3) + if res.State != vmstate.Halt.String() { + // The call failed. + } + // All of the side-effects in res can be analyzed. + + // Now we want to send the same invocation in a transaction, but we already + // have the script and a proper system fee for it, therefore SendUncheckedRun + // can be used. + txid, vub, _ := a.SendUncheckedRun(res.Script, res.GasConsumed, nil, nil) + _ = txid + _ = vub + // You need to wait for it to persist and then check the on-chain result of it. + + // Now we want to send some transaction, but give it a priority by increasing + // its network fee, this can be done with Tuned APIs. + txid, vub, _ = a.SendTunedCall(customContract, "method", nil, func(r *result.Invoke, t *transaction.Transaction) error { + // This code is run after the test-invocation done by *Call methods. + // Reuse the default function to check for HALT execution state. + err := actor.DefaultCheckerModifier(r, t) + if err != nil { + return err + } + // Some additional checks can be performed right here, but we only + // want to raise the network fee by ~20%. + t.NetworkFee += (t.NetworkFee / 5) + return nil + }, 1, 2, 3) + _ = txid + _ = vub + + // Actor can be used for higher-level wrappers as well, if we want to interact with + // NEO then [neo] package can accept our Actor and allow to easily use NEO methods. + neoContract := neo.New(a) + balance, _ := neoContract.BalanceOf(a.Sender()) + _ = balance + + // Now suppose the second wallet account is a committee account. We want to + // create and sign transactions for committee, but use the first account as + // a sender (because committee account has no GAS). We at the same time want + // to make all transactions using this actor high-priority ones, because + // committee can use this attribute. + + // Get the default options to have CheckerModifier/Modifier set up correctly. + opts := actor.NewDefaultOptions() + // And override attributes. + opts.Attributes = []transaction.Attribute{{Type: transaction.HighPriority}} + + // Create an Actor. + a, _ = actor.NewTuned(c, []actor.SignerAccount{{ + // Sender, regular account with None scope. + Signer: transaction.Signer{ + Account: w.Accounts[0].ScriptHash(), + Scopes: transaction.None, + }, + Account: w.Accounts[0], + }, { + // Commmitee. + Signer: transaction.Signer{ + Account: w.Accounts[1].ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: w.Accounts[1], + }}, opts) + + // Use policy contract wrapper to simplify things. All changes in the + // Policy contract are made by the committee. + policyContract := policy.New(a) + + // Create a transaction to set storage price, it'll be high-priority and have two + // signers from above. Committee is a multisignature account, so we can't sign/send + // it right away, w.Accounts[1] has only one public key. Therefore, we need to + // create a partially signed transaction and save it, then collect other signatures + // and send. + tx, _ := policyContract.SetStoragePriceUnsigned(10) + + net := a.GetNetwork() + scCtx := sccontext.NewParameterContext("Neo.Network.P2P.Payloads.Transaction", net, tx) + sign := w.Accounts[0].SignHashable(net, tx) + _ = scCtx.AddSignature(w.Accounts[0].ScriptHash(), w.Accounts[0].Contract, w.Accounts[0].PublicKey(), sign) + + sign = w.Accounts[1].SignHashable(net, tx) + _ = scCtx.AddSignature(w.Accounts[1].ScriptHash(), w.Accounts[1].Contract, w.Accounts[1].PublicKey(), sign) + + data, _ := json.Marshal(scCtx) + _ = os.WriteFile("tx.json", data, 0644) + + // Signature collection is out of scope, usually it's manual for cases like this. +} From 186e5c19b6a69fe72e69d52ac286d2c922c60956 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 22:38:45 +0300 Subject: [PATCH 07/16] rpcclient: update documentation, mention subpackages Drop TODOs (we have relevant GitHub issues), drop verbosity comment (we have *Verbose APIs for that). --- pkg/rpcclient/doc.go | 56 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/pkg/rpcclient/doc.go b/pkg/rpcclient/doc.go index 0684dfcae..8871a5371 100644 --- a/pkg/rpcclient/doc.go +++ b/pkg/rpcclient/doc.go @@ -1,21 +1,57 @@ /* Package rpcclient implements NEO-specific JSON-RPC 2.0 client. -This package is currently in beta and is subject to change. + +This package itself is designed to be a thin layer on top of the regular JSON-RPC +interface provided by Neo implementations. Therefore the set of methods provided +by clients is exactly the same as available from servers and they use data types +that directly correspond to JSON data exchanged. While this is the most powerful +and direct interface, it at the same time is not very convenient for many +purposes, like the most popular one --- performing test invocations and +creating/sending transactions that will do something to the chain. Please check +subpackages for more convenient APIs. + +# Subpackages + +The overall structure can be seen as a number of layers built on top of rpcclient +and on top of each other with each package and each layer solving different +problems. + +These layers are: + + - Basic RPC API, rpcclient package itself. + + - Generic invocation/transaction API represented by invoker, unwrap (auxiliary, + but very convenient) and actor packages. These allow to perform test + invocations with plain Go types, use historic states for these invocations, + get the execution results from reader functions and create/send transactions + that change something on-chain. + + - Standard-specific wrappers that are implemented in nep11 and nep17 packages + (with common methods in neptoken). They implement the respective NEP-11 and + NEP-17 APIs both for safe (read-only) and state-changing methods. Safe methods + require an Invoker to be called, while Actor is used to create/send + transactions. + + - Contract-specific wrappers for native contracts that include management, gas, + neo, oracle, policy and rolemgmt packages for the respective native contracts. + Complete contract functionality is exposed (reusing nep17 package for gas and + neo). + + - Notary actor and contract, a bit special since it's a NeoGo protocol + extension, but notary package provides both the notary native contract wrapper + and a notary-specific actor implementation that allows to easily wrap any + transaction into a notary request. + + - Non-native contract-specific wrappers, currently partially provided only for + NNS contract (it's still in development), at the moment that's mostly an + example of how contract-specific wrappers can be built for other dApps + (reusing invoker/actor layers it's pretty easy). # Client After creating a client instance with or without a ClientConfig you can interact with the NEO blockchain by its exposed methods. -Some of the methods also allow to pass a verbose bool. This will -return a more pretty printed response from the server instead of -a raw hex string. - -TODO: - - Allow client to connect using client cert. - More in-depth examples. - Supported methods calculatenetworkfee From a1c9871d953a1d6f5f80a6dee5fc0d95bc799bc0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 16:41:31 +0300 Subject: [PATCH 08/16] nns: it's NEP-11, so make NEP-11 methods available too --- pkg/rpcclient/nns/contract.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/rpcclient/nns/contract.go b/pkg/rpcclient/nns/contract.go index b358a818c..f51294be7 100644 --- a/pkg/rpcclient/nns/contract.go +++ b/pkg/rpcclient/nns/contract.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -19,14 +20,13 @@ import ( // Invoker is used by ContractReader to call various methods. type Invoker interface { - Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) - CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) - TerminateSession(sessionID uuid.UUID) error - TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) + nep11.Invoker } // ContractReader provides an interface to call read-only NNS contract methods. type ContractReader struct { + nep11.NonDivisibleReader + invoker Invoker hash util.Uint160 } @@ -41,7 +41,7 @@ type RecordIterator struct { // NewReader creates an instance of ContractReader that can be used to read // data from the contract. func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { - return &ContractReader{invoker, hash} + return &ContractReader{*nep11.NewNonDivisibleReader(invoker, hash), invoker, hash} } // GetPrice returns current domain registration price in GAS. From ee55e95c282f77d5d4bac0b05c7d406dd066a27b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 18:26:57 +0300 Subject: [PATCH 09/16] rpcclient: add examples for nep11/nep17/neo GAS doesn't need any, so just mention nep17 package there. --- pkg/rpcclient/gas/gas.go | 3 +- pkg/rpcclient/neo/doc_test.go | 91 +++++++++++++++++ pkg/rpcclient/nep11/doc_test.go | 167 ++++++++++++++++++++++++++++++++ pkg/rpcclient/nep17/doc_test.go | 67 +++++++++++++ 4 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 pkg/rpcclient/neo/doc_test.go create mode 100644 pkg/rpcclient/nep11/doc_test.go create mode 100644 pkg/rpcclient/nep17/doc_test.go diff --git a/pkg/rpcclient/gas/gas.go b/pkg/rpcclient/gas/gas.go index a19dde6bc..0ea2ca5d4 100644 --- a/pkg/rpcclient/gas/gas.go +++ b/pkg/rpcclient/gas/gas.go @@ -2,7 +2,8 @@ Package gas provides a convenience wrapper for GAS contract to use it via RPC. GAS itself only has standard NEP-17 methods, so this package only contains its -hash and allows to create NEP-17 structures in an easier way. +hash and allows to create NEP-17 structures in an easier way. Refer to [nep17] +package for more details on NEP-17 interface. */ package gas diff --git a/pkg/rpcclient/neo/doc_test.go b/pkg/rpcclient/neo/doc_test.go new file mode 100644 index 000000000..46fa77990 --- /dev/null +++ b/pkg/rpcclient/neo/doc_test.go @@ -0,0 +1,91 @@ +package neo_test + +import ( + "context" + "math/big" + "sort" + + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +func ExampleContractReader() { + // No error checking done at all, intentionally. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Safe methods are reachable with just an invoker, no need for an account there. + inv := invoker.New(c, nil) + + // Create a reader interface. + neoToken := neo.NewReader(inv) + + // Account hash we're interested in. + accHash, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Get the account balance. + balance, _ := neoToken.BalanceOf(accHash) + _ = balance + + // Get the extended NEO-specific balance data. + bNeo, _ := neoToken.GetAccountState(accHash) + + // Account can have no associated vote. + if bNeo.VoteTo == nil { + return + } + // Committee keys. + comm, _ := neoToken.GetCommittee() + + // Check if the vote is made for a committee member. + var votedForCommitteeMember bool + for i := range comm { + if bNeo.VoteTo.Equal(comm[i]) { + votedForCommitteeMember = true + break + } + } + _ = votedForCommitteeMember +} + +func ExampleContract() { + // No error checking done at all, intentionally. + w, _ := wallet.NewWalletFromFile("somewhere") + defer w.Close() + + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Create a simple CalledByEntry-scoped actor (assuming there is an account + // inside the wallet). + a, _ := actor.NewSimple(c, w.Accounts[0]) + + // Create a complete contract representation. + neoToken := neo.New(a) + + tgtAcc, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Send a transaction that transfers one token to another account. + txid, vub, _ := neoToken.Transfer(a.Sender(), tgtAcc, big.NewInt(1), nil) + _ = txid + _ = vub + + // Get a list of candidates (it's limited, but should be sufficient in most cases). + cands, _ := neoToken.GetCandidates() + + // Sort by votes. + sort.Slice(cands, func(i, j int) bool { return cands[i].Votes < cands[j].Votes }) + + // Get the extended NEO-specific balance data. + bNeo, _ := neoToken.GetAccountState(a.Sender()) + + // If not yet voted, or voted for suboptimal candidate (we want the one with the least votes), + // send a new voting transaction + if bNeo.VoteTo == nil || !bNeo.VoteTo.Equal(&cands[0].PublicKey) { + txid, vub, _ = neoToken.Vote(a.Sender(), &cands[0].PublicKey) + _ = txid + _ = vub + } +} diff --git a/pkg/rpcclient/nep11/doc_test.go b/pkg/rpcclient/nep11/doc_test.go new file mode 100644 index 000000000..9c8581fde --- /dev/null +++ b/pkg/rpcclient/nep11/doc_test.go @@ -0,0 +1,167 @@ +package nep11_test + +import ( + "context" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +func ExampleNonDivisibleReader() { + // No error checking done at all, intentionally. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Safe methods are reachable with just an invoker, no need for an account there. + inv := invoker.New(c, nil) + + // NEP-11 contract hash. + nep11Hash := util.Uint160{9, 8, 7} + + // Most of the time contracts are non-divisible, create a reader for nep11Hash. + n11 := nep11.NewNonDivisibleReader(inv, nep11Hash) + + // Get the metadata. Even though these methods are implemented in neptoken package, + // they're available for NEP-11 wrappers. + symbol, _ := n11.Symbol() + supply, _ := n11.TotalSupply() + _ = symbol + _ = supply + + // Account hash we're interested in. + accHash, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Get account balance. + balance, _ := n11.BalanceOf(accHash) + if balance.Sign() > 0 { + // There are some tokens there, let's look at them. + tokIter, _ := n11.TokensOf(accHash) + + for toks, err := tokIter.Next(10); err == nil && len(toks) > 0; toks, err = tokIter.Next(10) { + for i := range toks { + // We know the owner of the token, but let's check internal contract consistency. + owner, _ := n11.OwnerOf(toks[i]) + if !owner.Equals(accHash) { + panic("NEP-11 contract is broken!") + } + } + } + } +} + +func ExampleDivisibleReader() { + // No error checking done at all, intentionally. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Safe methods are reachable with just an invoker, no need for an account there. + inv := invoker.New(c, nil) + + // NEP-11 contract hash. + nep11Hash := util.Uint160{9, 8, 7} + + // Divisible contract are more rare, but we can handle them too. + n11 := nep11.NewDivisibleReader(inv, nep11Hash) + + // Get the metadata. Even though these methods are implemented in neptoken package, + // they're available for NEP-11 wrappers. + symbol, _ := n11.Symbol() + supply, _ := n11.TotalSupply() + _ = symbol + _ = supply + + // Account hash we're interested in. + accHash, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Get account balance. + balance, _ := n11.BalanceOf(accHash) + if balance.Sign() > 0 && balance.Cmp(big.NewInt(10)) < 0 { + // We know we have a low number of tokens, so we can use a simple API to get them. + toks, _ := n11.TokensOfExpanded(accHash, 10) + + // We can build a list of all owners of account's tokens. + var owners = make([]util.Uint160, 0) + for i := range toks { + ownIter, _ := n11.OwnerOf(toks[i]) + for ows, err := ownIter.Next(10); err == nil && len(ows) > 0; ows, err = ownIter.Next(10) { + // Notice that it includes accHash too. + owners = append(owners, ows...) + } + } + // The list can be sorted/deduplicated if needed. + _ = owners + } +} + +func ExampleNonDivisible() { + // No error checking done at all, intentionally. + w, _ := wallet.NewWalletFromFile("somewhere") + defer w.Close() + + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Create a simple CalledByEntry-scoped actor (assuming there is an account + // inside the wallet). + a, _ := actor.NewSimple(c, w.Accounts[0]) + + // NEP-11 contract hash. + nep11Hash := util.Uint160{9, 8, 7} + + // Create a complete non-divisible contract representation. + n11 := nep11.NewNonDivisible(a, nep11Hash) + + tgtAcc, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Let's tranfer all of account's tokens to some other account. + tokIter, _ := n11.TokensOf(a.Sender()) + for toks, err := tokIter.Next(10); err == nil && len(toks) > 0; toks, err = tokIter.Next(10) { + for i := range toks { + // This creates a transaction for every token, but you can + // create a script that will move multiple tokens in one + // transaction with Builder from smartcontract package. + txid, vub, _ := n11.Transfer(tgtAcc, toks[i], nil) + _ = txid + _ = vub + } + } +} + +func ExampleDivisible() { + // No error checking done at all, intentionally. + w, _ := wallet.NewWalletFromFile("somewhere") + defer w.Close() + + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Create a simple CalledByEntry-scoped actor (assuming there is an account + // inside the wallet). + a, _ := actor.NewSimple(c, w.Accounts[0]) + + // NEP-11 contract hash. + nep11Hash := util.Uint160{9, 8, 7} + + // Create a complete divisible contract representation. + n11 := nep11.NewDivisible(a, nep11Hash) + + tgtAcc, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Let's tranfer all of account's tokens to some other account. + tokIter, _ := n11.TokensOf(a.Sender()) + for toks, err := tokIter.Next(10); err == nil && len(toks) > 0; toks, err = tokIter.Next(10) { + for i := range toks { + // It's a divisible token, so balance data is required in general case. + balance, _ := n11.BalanceOfD(a.Sender(), toks[i]) + + // This creates a transaction for every token, but you can + // create a script that will move multiple tokens in one + // transaction with Builder from smartcontract package. + txid, vub, _ := n11.TransferD(a.Sender(), tgtAcc, balance, toks[i], nil) + _ = txid + _ = vub + } + } +} diff --git a/pkg/rpcclient/nep17/doc_test.go b/pkg/rpcclient/nep17/doc_test.go new file mode 100644 index 000000000..4bbb2e836 --- /dev/null +++ b/pkg/rpcclient/nep17/doc_test.go @@ -0,0 +1,67 @@ +package nep17_test + +import ( + "context" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +func ExampleTokenReader() { + // No error checking done at all, intentionally. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Safe methods are reachable with just an invoker, no need for an account there. + inv := invoker.New(c, nil) + + // NEP-17 contract hash. + nep17Hash := util.Uint160{9, 8, 7} + + // And a reader interface. + n17 := nep17.NewReader(inv, nep17Hash) + + // Get the metadata. Even though these methods are implemented in neptoken package, + // they're available for NEP-17 wrappers. + symbol, _ := n17.Symbol() + supply, _ := n17.TotalSupply() + _ = symbol + _ = supply + + // Account hash we're interested in. + accHash, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Get account balance. + balance, _ := n17.BalanceOf(accHash) + _ = balance +} + +func ExampleToken() { + // No error checking done at all, intentionally. + w, _ := wallet.NewWalletFromFile("somewhere") + defer w.Close() + + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // Create a simple CalledByEntry-scoped actor (assuming there is an account + // inside the wallet). + a, _ := actor.NewSimple(c, w.Accounts[0]) + + // NEP-17 contract hash. + nep17Hash := util.Uint160{9, 8, 7} + + // Create a complete NEP-17 contract representation. + n17 := nep17.New(a, nep17Hash) + + tgtAcc, _ := address.StringToUint160("NdypBhqkz2CMMnwxBgvoC9X2XjKF5axgKo") + + // Send a transaction that transfers one token to another account. + txid, vub, _ := n17.Transfer(a.Sender(), tgtAcc, big.NewInt(1), nil) + _ = txid + _ = vub +} From c4ddf80742fb5291c4d0004fdb2271a426f84daf Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 19:09:53 +0300 Subject: [PATCH 10/16] rpcclient: correct Init requirement in documentation --- pkg/rpcclient/client.go | 3 ++- pkg/rpcclient/rpc.go | 24 ++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/rpcclient/client.go b/pkg/rpcclient/client.go index 6e1fc7dc1..ba956b0db 100644 --- a/pkg/rpcclient/client.go +++ b/pkg/rpcclient/client.go @@ -86,7 +86,8 @@ type calculateValidUntilBlockCache struct { } // New returns a new Client ready to use. You should call Init method to -// initialize network magic the client is operating on. +// initialize stateroot setting for the network the client is operating on if +// you plan using GetBlock*. func New(ctx context.Context, endpoint string, opts Options) (*Client, error) { cl := new(Client) err := initClient(ctx, cl, endpoint, opts) diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index e1cd3b52c..a2f0d02b1 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -81,14 +81,14 @@ func (c *Client) GetBlockCount() (uint32, error) { return resp, nil } -// GetBlockByIndex returns a block by its height. You should initialize network magic -// with Init before calling GetBlockByIndex. +// GetBlockByIndex returns a block by its height. In-header stateroot option +// must be initialized with Init before calling this method. func (c *Client) GetBlockByIndex(index uint32) (*block.Block, error) { return c.getBlock(index) } -// GetBlockByHash returns a block by its hash. You should initialize network magic -// with Init before calling GetBlockByHash. +// GetBlockByHash returns a block by its hash. In-header stateroot option +// must be initialized with Init before calling this method. func (c *Client) GetBlockByHash(hash util.Uint256) (*block.Block, error) { return c.getBlock(hash.StringLE()) } @@ -116,14 +116,16 @@ func (c *Client) getBlock(param interface{}) (*block.Block, error) { } // GetBlockByIndexVerbose returns a block wrapper with additional metadata by -// its height. You should initialize network magic with Init before calling GetBlockByIndexVerbose. +// its height. In-header stateroot option must be initialized with Init before +// calling this method. // NOTE: to get transaction.ID and transaction.Size, use t.Hash() and io.GetVarSize(t) respectively. func (c *Client) GetBlockByIndexVerbose(index uint32) (*result.Block, error) { return c.getBlockVerbose(index) } // GetBlockByHashVerbose returns a block wrapper with additional metadata by -// its hash. You should initialize network magic with Init before calling GetBlockByHashVerbose. +// its hash. In-header stateroot option must be initialized with Init before +// calling this method. func (c *Client) GetBlockByHashVerbose(hash util.Uint256) (*result.Block, error) { return c.getBlockVerbose(hash.StringLE()) } @@ -158,8 +160,8 @@ func (c *Client) GetBlockHash(index uint32) (util.Uint256, error) { } // GetBlockHeader returns the corresponding block header information from a serialized hex string -// according to the specified script hash. You should initialize network magic -// with Init before calling GetBlockHeader. +// according to the specified script hash. In-header stateroot option must be +// initialized with Init before calling this method. func (c *Client) GetBlockHeader(hash util.Uint256) (*block.Header, error) { var ( params = []interface{}{hash.StringLE()} @@ -193,7 +195,8 @@ func (c *Client) GetBlockHeaderCount() (uint32, error) { } // GetBlockHeaderVerbose returns the corresponding block header information from a Json format string -// according to the specified script hash. +// according to the specified script hash. In-header stateroot option must be +// initialized with Init before calling this method. func (c *Client) GetBlockHeaderVerbose(hash util.Uint256) (*result.Header, error) { var ( params = []interface{}{hash.StringLE(), 1} @@ -1095,7 +1098,8 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs return nil } -// GetNetwork returns the network magic of the RPC node the client connected to. +// GetNetwork returns the network magic of the RPC node the client connected to. It +// requires Init to be done first, otherwise an error is returned. func (c *Client) GetNetwork() (netmode.Magic, error) { c.cacheLock.RLock() defer c.cacheLock.RUnlock() From 3e8e6857e593d0572d960dfc1fe9dbd193b8559c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 19:57:54 +0300 Subject: [PATCH 11/16] rpcclient; deprecate more methods They make little sense now. --- pkg/rpcclient/policy.go | 3 +++ pkg/rpcclient/rpc.go | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/pkg/rpcclient/policy.go b/pkg/rpcclient/policy.go index 84e386a29..4d45ffb09 100644 --- a/pkg/rpcclient/policy.go +++ b/pkg/rpcclient/policy.go @@ -30,6 +30,9 @@ func (c *Client) GetStoragePrice() (int64, error) { } // GetMaxNotValidBeforeDelta invokes `getMaxNotValidBeforeDelta` method on a native Notary contract. +// +// Deprecated: please use notary subpackage. This method will be removed +// in future versions. func (c *Client) GetMaxNotValidBeforeDelta() (int64, error) { notaryHash, err := c.GetNativeContractHash(nativenames.Notary) if err != nil { diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index a2f0d02b1..c61ebb9f2 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -1100,6 +1100,9 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs // GetNetwork returns the network magic of the RPC node the client connected to. It // requires Init to be done first, otherwise an error is returned. +// +// Deprecated: please use GetVersion (it has the same data in the Protocol section) +// or actor subpackage. This method will be removed in future versions. func (c *Client) GetNetwork() (netmode.Magic, error) { c.cacheLock.RLock() defer c.cacheLock.RUnlock() @@ -1112,6 +1115,9 @@ func (c *Client) GetNetwork() (netmode.Magic, error) { // StateRootInHeader returns true if the state root is contained in the block header. // You should initialize Client cache with Init() before calling StateRootInHeader. +// +// Deprecated: please use GetVersion (it has the same data in the Protocol section). +// This method will be removed in future versions. func (c *Client) StateRootInHeader() (bool, error) { c.cacheLock.RLock() defer c.cacheLock.RUnlock() @@ -1123,6 +1129,11 @@ func (c *Client) StateRootInHeader() (bool, error) { } // GetNativeContractHash returns native contract hash by its name. +// +// Deprecated: please use native contract subpackages that have hashes directly +// (gas, management, neo, notary, oracle, policy, rolemgmt) or +// GetContractStateByAddressOrName method that will return hash along with other +// data. func (c *Client) GetNativeContractHash(name string) (util.Uint160, error) { c.cacheLock.RLock() hash, ok := c.cache.nativeHashes[name] From 3c02177d3c63b6f0833f533b39a4afc4c306c70a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 19:59:33 +0300 Subject: [PATCH 12/16] rpcclient: improve comments for some methods --- pkg/rpcclient/rpc.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index c61ebb9f2..d10c505a1 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -209,6 +209,7 @@ func (c *Client) GetBlockHeaderVerbose(hash util.Uint256) (*result.Header, error } // GetBlockSysFee returns the system fees of the block based on the specified index. +// This method is only supported by NeoGo servers. func (c *Client) GetBlockSysFee(index uint32) (fixedn.Fixed8, error) { var ( params = []interface{}{index} @@ -245,12 +246,16 @@ func (c *Client) GetContractStateByHash(hash util.Uint160) (*state.Contract, err return c.getContractState(hash.StringLE()) } -// GetContractStateByAddressOrName queries contract information according to the contract address or name. +// GetContractStateByAddressOrName queries contract information using the contract +// address or name. Notice that name-based queries work only for native contracts, +// non-native ones can't be requested this way. func (c *Client) GetContractStateByAddressOrName(addressOrName string) (*state.Contract, error) { return c.getContractState(addressOrName) } // GetContractStateByID queries contract information according to the contract ID. +// Notice that this is supported by all servers only for native contracts, +// non-native ones can be requested only from NeoGo servers. func (c *Client) GetContractStateByID(id int32) (*state.Contract, error) { return c.getContractState(id) } @@ -997,6 +1002,7 @@ func (c *Client) SubmitP2PNotaryRequest(req *payload.P2PNotaryRequest) (util.Uin } // ValidateAddress verifies that the address is a correct NEO address. +// Consider using [address] package instead to do it locally. func (c *Client) ValidateAddress(address string) error { var ( params = []interface{}{address} From 1e54b422cd666b9c49e108ec737fb029297b2334 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 22:34:46 +0300 Subject: [PATCH 13/16] emit: make *util.Uint160 and *util.Uint256 emittable They can be nil or can be regular uint types we're used to. --- pkg/vm/emit/emit.go | 12 ++++++++++++ pkg/vm/emit/emit_test.go | 21 ++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/pkg/vm/emit/emit.go b/pkg/vm/emit/emit.go index 149cb25bd..8fd6a3655 100644 --- a/pkg/vm/emit/emit.go +++ b/pkg/vm/emit/emit.go @@ -133,6 +133,18 @@ func Array(w *io.BinWriter, es ...interface{}) { Bytes(w, e.BytesBE()) case util.Uint256: Bytes(w, e.BytesBE()) + case *util.Uint160: + if e == nil { + Opcodes(w, opcode.PUSHNULL) + } else { + Bytes(w, e.BytesBE()) + } + case *util.Uint256: + if e == nil { + Opcodes(w, opcode.PUSHNULL) + } else { + Bytes(w, e.BytesBE()) + } case []byte: Bytes(w, e) case bool: diff --git a/pkg/vm/emit/emit_test.go b/pkg/vm/emit/emit_test.go index 09eb3d4ae..f2f135725 100644 --- a/pkg/vm/emit/emit_test.go +++ b/pkg/vm/emit/emit_test.go @@ -10,6 +10,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/assert" @@ -216,9 +217,13 @@ func TestBytes(t *testing.T) { func TestEmitArray(t *testing.T) { t.Run("good", func(t *testing.T) { buf := io.NewBufBinWriter() + var p160 *util.Uint160 + var p256 *util.Uint256 + u160 := util.Uint160{1, 2, 3} + u256 := util.Uint256{1, 2, 3} veryBig := new(big.Int).SetUint64(math.MaxUint64) veryBig.Add(veryBig, big.NewInt(1)) - Array(buf.BinWriter, big.NewInt(0), veryBig, + Array(buf.BinWriter, p160, p256, &u160, &u256, u160, u256, big.NewInt(0), veryBig, []interface{}{int64(1), int64(2)}, nil, int64(1), "str", true, []byte{0xCA, 0xFE}) require.NoError(t, buf.Err) @@ -241,6 +246,20 @@ func TestEmitArray(t *testing.T) { assert.EqualValues(t, opcode.PUSHINT128, res[18]) assert.EqualValues(t, veryBig, bigint.FromBytes(res[19:35])) assert.EqualValues(t, opcode.PUSH0, res[35]) + assert.EqualValues(t, opcode.PUSHDATA1, res[36]) + assert.EqualValues(t, 32, res[37]) + assert.EqualValues(t, u256.BytesBE(), res[38:70]) + assert.EqualValues(t, opcode.PUSHDATA1, res[70]) + assert.EqualValues(t, 20, res[71]) + assert.EqualValues(t, u160.BytesBE(), res[72:92]) + assert.EqualValues(t, opcode.PUSHDATA1, res[92]) + assert.EqualValues(t, 32, res[93]) + assert.EqualValues(t, u256.BytesBE(), res[94:126]) + assert.EqualValues(t, opcode.PUSHDATA1, res[126]) + assert.EqualValues(t, 20, res[127]) + assert.EqualValues(t, u160.BytesBE(), res[128:148]) + assert.EqualValues(t, opcode.PUSHNULL, res[148]) + assert.EqualValues(t, opcode.PUSHNULL, res[149]) }) t.Run("empty", func(t *testing.T) { From 4fb4f5a1ac05e4e0a3be9fe72ba140a2f3cad21b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 22:35:45 +0300 Subject: [PATCH 14/16] smartcontract: make *util.Uint160 and *util.Uint256 usable for parameters Use Any type for NULL. --- pkg/smartcontract/parameter.go | 14 +++++++++++++- pkg/smartcontract/parameter_test.go | 23 ++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index dbd3c5342..e5799b82e 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -311,6 +311,18 @@ func NewParameterFromValue(value interface{}) (Parameter, error) { result.Type = Hash160Type case util.Uint256: result.Type = Hash256Type + case *util.Uint160: + if v != nil { + return NewParameterFromValue(*v) + } + result.Type = AnyType + result.Value = nil + case *util.Uint256: + if v != nil { + return NewParameterFromValue(*v) + } + result.Type = AnyType + result.Value = nil case keys.PublicKey: return NewParameterFromValue(&v) case *keys.PublicKey: @@ -387,7 +399,7 @@ func ExpandParameterToEmitable(param Parameter) (interface{}, error) { } } return res, nil - case MapType, InteropInterfaceType, UnknownType, AnyType, VoidType: + case MapType, InteropInterfaceType, UnknownType, VoidType: return nil, fmt.Errorf("unsupported parameter type: %s", t.String()) default: return param.Value, nil diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index 72b1070f7..01aa977d8 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -484,6 +484,10 @@ func TestExpandParameterToEmitable(t *testing.T) { In: Parameter{Type: SignatureType, Value: []byte{1, 2, 3}}, Expected: []byte{1, 2, 3}, }, + { + In: Parameter{Type: AnyType}, + Expected: nil, + }, { In: Parameter{Type: ArrayType, Value: []Parameter{ { @@ -517,7 +521,6 @@ func TestExpandParameterToEmitable(t *testing.T) { require.NoError(t, bw.Err) } errCases := []Parameter{ - {Type: AnyType}, {Type: UnknownType}, {Type: MapType}, {Type: InteropInterfaceType}, @@ -636,6 +639,24 @@ func TestParameterFromValue(t *testing.T) { expType: Hash256Type, expVal: util.Uint256{3, 2, 1}, }, + { + value: (*util.Uint160)(nil), + expType: AnyType, + }, + { + value: &util.Uint160{1, 2, 3}, + expType: Hash160Type, + expVal: util.Uint160{1, 2, 3}, + }, + { + value: (*util.Uint256)(nil), + expType: AnyType, + }, + { + value: &util.Uint256{3, 2, 1}, + expType: Hash256Type, + expVal: util.Uint256{3, 2, 1}, + }, { value: pk1.PublicKey(), expType: PublicKeyType, From 6be9367f031e2a503e4d1d7984037444588982d3 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 22:37:03 +0300 Subject: [PATCH 15/16] rpcclient/notary: add OnNEP17PaymentData and an example Update documentation as well to mention it and not mention outdated APIs. We can't link them yet, this will be done after the release. --- docs/notary.md | 12 ++--- pkg/rpcclient/notary/contract.go | 9 ++++ pkg/rpcclient/notary/doc_test.go | 89 ++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 pkg/rpcclient/notary/doc_test.go diff --git a/docs/notary.md b/docs/notary.md index e9cfa6fc2..7c5e112dd 100644 --- a/docs/notary.md +++ b/docs/notary.md @@ -292,8 +292,10 @@ server via [`submitnotaryrequest` RPC call](./rpc.md#submitnotaryrequest-call). Note, that all parties must generate the same main transaction while fallbacks can differ. -To create a notary request, you can use [NeoGo RPC client](./rpc.md#Client). Follow -the steps to create a signature request: +To create a notary request, you can use [NeoGo RPC client](./rpc.md#Client). The +procedure below uses only basic RPC client functions and show all of the notary +request internals. You can use much simpler Actor interface in the notary +subpackage with an example written in Go doc. 1. Prepare a list of signers with scopes for the main transaction (i.e. the transaction that signatures are being collected for, that will be `Signers` @@ -307,8 +309,7 @@ the steps to create a signature request: Include Notary native contract in the list of signers with the following constraints: * Notary signer hash is the hash of a native Notary contract that can be fetched - from - [func (*Client) GetNativeContractHash](https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.97.2/pkg/rpcclient#Client.GetNativeContractHash). + from the notary RPC client subpackage (notary.Hash) * A notary signer must have `None` scope. * A notary signer shouldn't be placed at the beginning of the signer list because Notary contract does not pay main transaction fees. Other positions @@ -390,8 +391,7 @@ the steps to create a signature request: tries to push all associated fallbacks. Use the following rules to define `fallbackValidFor`: - `fallbackValidFor` shouldn't be more than `MaxNotValidBeforeDelta` value. - - Use [func (*Client) GetMaxNotValidBeforeDelta](https://pkg.go.dev/github.com/nspcc-dev/neo-go@v0.97.2/pkg/rpcclient#Client.GetMaxNotValidBeforeDelta) - to check `MaxNotValidBefore` value. + - Use notary package's GetMaxNotValidBeforeDelta to check `MaxNotValidBefore` value. 11. Construct a script for the fallback transaction. The script may do something useful, i.g. invoke method of a contract. However, if you don't need to perform anything special on fallback invocation, you can use simple `opcode.RET` script. diff --git a/pkg/rpcclient/notary/contract.go b/pkg/rpcclient/notary/contract.go index eee21bd0a..9bf3b6373 100644 --- a/pkg/rpcclient/notary/contract.go +++ b/pkg/rpcclient/notary/contract.go @@ -59,6 +59,15 @@ type Contract struct { actor ContractActor } +// OnNEP17PaymentData is the data set that is accepted by the notary contract +// onNEP17Payment handler. It's mandatory for GAS tranfers to this contract. +type OnNEP17PaymentData struct { + // Account can be nil, in this case transfer sender (from) account is used. + Account *util.Uint160 + // Till specifies the deposit lock time (in blocks). + Till uint32 +} + // Hash stores the hash of the native Notary contract. var Hash = state.CreateNativeContractHash(nativenames.Notary) diff --git a/pkg/rpcclient/notary/doc_test.go b/pkg/rpcclient/notary/doc_test.go new file mode 100644 index 000000000..33e2524f8 --- /dev/null +++ b/pkg/rpcclient/notary/doc_test.go @@ -0,0 +1,89 @@ +package notary_test + +import ( + "context" + "math/big" + "time" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/notary" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/policy" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +func ExampleActor() { + // No error checking done at all, intentionally. + w, _ := wallet.NewWalletFromFile("somewhere") + defer w.Close() + // We assume there are two accounts in the wallet --- one is a simple signature + // account and another one is committee account. The first one will send notary + // requests, while committee signatures need to be collected. + + // Create an RPC client. + c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{}) + + // An actor for the first account. + single, _ := actor.NewSimple(c, w.Accounts[0]) + + // Transfer some GAS to the Notary contract to be able to send notary requests + // from the first account. + gasSingle := gas.New(single) + txid, vub, _ := gasSingle.Transfer(single.Sender(), notary.Hash, big.NewInt(10_0000_0000), notary.OnNEP17PaymentData{Till: 10000000}) + + var depositOK bool + // Wait for transaction to be persisted, either it gets in and we get + // an application log with some result or it expires. + for height, err := c.GetBlockCount(); err == nil && height <= vub; height, err = c.GetBlockCount() { + appLog, err := c.GetApplicationLog(txid, nil) + // We can't separate "application log missing" from other errors at the moment, see #2248. + if err != nil { + time.Sleep(5 * time.Second) + continue + } + if len(appLog.Executions) == 1 && appLog.Executions[0].VMState == vmstate.Halt { + depositOK = true + } else { + break + } + } + if !depositOK { + panic("deposit failed") + } + + var opts notary.ActorOptions + // Add high priority attribute, we gonna be making committee-signed transactions anyway. + opts.MainAttributes = []transaction.Attribute{{Type: transaction.HighPriority}} + + // Create an Actor with the simple account used for paying fees and committee + // signature to be collected. + multi, _ := notary.NewTunedActor(c, []actor.SignerAccount{{ + // Sender, regular account with None scope. + Signer: transaction.Signer{ + Account: w.Accounts[0].ScriptHash(), + Scopes: transaction.None, + }, + Account: w.Accounts[0], + }, { + // Commmitee. + Signer: transaction.Signer{ + Account: w.Accounts[1].ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: w.Accounts[1], + }}, opts) + + // Use the Policy contract to perform something requiring committee signature. + policyContract := policy.New(multi) + + // Wrap a transaction to set storage price into a notary request. Fallback will + // be create automatically and all appropriate attributes will be added to both + // transactions. + mainTx, fbTx, vub, _ := multi.Notarize(policyContract.SetStoragePriceTransaction(10)) + _ = mainTx + _ = fbTx + _ = vub +} From 541d4b49e1b77df807c603bf7f2c61d2fd2eb19c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 8 Sep 2022 13:07:49 +0300 Subject: [PATCH 16/16] context: define a constant for transaction context type --- cli/paramcontext/context.go | 2 +- pkg/rpcclient/actor/doc_test.go | 2 +- pkg/smartcontract/context/context.go | 8 +++++++- pkg/smartcontract/context/context_test.go | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cli/paramcontext/context.go b/cli/paramcontext/context.go index d8fe06999..3d6429cf4 100644 --- a/cli/paramcontext/context.go +++ b/cli/paramcontext/context.go @@ -15,7 +15,7 @@ import ( // as an input to `multisig sign`. If a wallet.Account is given and can sign, // it's signed as well using it. func InitAndSave(net netmode.Magic, tx *transaction.Transaction, acc *wallet.Account, filename string) error { - scCtx := context.NewParameterContext("Neo.Network.P2P.Payloads.Transaction", net, tx) + scCtx := context.NewParameterContext(context.TransactionType, net, tx) if acc != nil && acc.CanSign() { sign := acc.SignHashable(net, tx) if err := scCtx.AddSignature(acc.ScriptHash(), acc.Contract, acc.PublicKey(), sign); err != nil { diff --git a/pkg/rpcclient/actor/doc_test.go b/pkg/rpcclient/actor/doc_test.go index 7121cc564..a1297b2bc 100644 --- a/pkg/rpcclient/actor/doc_test.go +++ b/pkg/rpcclient/actor/doc_test.go @@ -108,7 +108,7 @@ func ExampleActor() { tx, _ := policyContract.SetStoragePriceUnsigned(10) net := a.GetNetwork() - scCtx := sccontext.NewParameterContext("Neo.Network.P2P.Payloads.Transaction", net, tx) + scCtx := sccontext.NewParameterContext(sccontext.TransactionType, net, tx) sign := w.Accounts[0].SignHashable(net, tx) _ = scCtx.AddSignature(w.Accounts[0].ScriptHash(), w.Accounts[0].Contract, w.Accounts[0].PublicKey(), sign) diff --git a/pkg/smartcontract/context/context.go b/pkg/smartcontract/context/context.go index eb307dbf8..13d2927f7 100644 --- a/pkg/smartcontract/context/context.go +++ b/pkg/smartcontract/context/context.go @@ -21,6 +21,12 @@ import ( "github.com/nspcc-dev/neo-go/pkg/wallet" ) +// TransactionType is the ParameterContext Type used for transactions. +const TransactionType = "Neo.Network.P2P.Payloads.Transaction" + +// compatTransactionType is the old, 2.x type used for transactions. +const compatTransactionType = "Neo.Core.ContractTransaction" + // ParameterContext represents smartcontract parameter's context. type ParameterContext struct { // Type is a type of a verifiable item. @@ -213,7 +219,7 @@ func (c *ParameterContext) UnmarshalJSON(data []byte) error { var verif crypto.VerifiableDecodable switch pc.Type { - case "Neo.Core.ContractTransaction", "Neo.Network.P2P.Payloads.Transaction": + case compatTransactionType, TransactionType: tx := new(transaction.Transaction) verif = tx default: diff --git a/pkg/smartcontract/context/context_test.go b/pkg/smartcontract/context/context_test.go index 5675e0289..9d3468bef 100644 --- a/pkg/smartcontract/context/context_test.go +++ b/pkg/smartcontract/context/context_test.go @@ -102,7 +102,7 @@ func TestParameterContext_AddSignatureMultisig(t *testing.T) { }, } tx := getContractTx(ctr.ScriptHash()) - c := NewParameterContext("Neo.Network.P2P.Payloads.Transaction", netmode.UnitTestNet, tx) + c := NewParameterContext(TransactionType, netmode.UnitTestNet, tx) priv, err := keys.NewPrivateKey() require.NoError(t, err) sig := priv.SignHashable(uint32(c.Network), tx)