From cb1a1f85327f74c2e1c975bd5f3e502ee5a5745a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 7 Sep 2022 15:11:27 +0300 Subject: [PATCH] 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. +}