smartcontract: add Builder, method invocation helpers and doc

Move the last remaining script-related things out of the rpcclient.
This commit is contained in:
Roman Khimov 2022-07-25 20:04:43 +03:00
parent 1b6f4051d8
commit 32ebb4a90d
6 changed files with 145 additions and 20 deletions

View file

@ -6,14 +6,10 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "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/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
) )
@ -65,17 +61,15 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
// `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string, data interface{}. // `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string, data interface{}.
func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160, func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160,
gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) { gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) {
w := io.NewBufBinWriter() script, err := smartcontract.CreateCallWithAssertScript(tokenHash, "transfer", args...)
emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...) if err != nil {
emit.Opcodes(w.BinWriter, opcode.ASSERT) return nil, fmt.Errorf("failed to create NEP-11 transfer script: %w", err)
if w.Err != nil {
return nil, fmt.Errorf("failed to create NEP-11 transfer script: %w", w.Err)
} }
from, err := address.StringToUint160(acc.Address) from, err := address.StringToUint160(acc.Address)
if err != nil { if err != nil {
return nil, fmt.Errorf("bad account address: %w", err) return nil, fmt.Errorf("bad account address: %w", err)
} }
return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{ return c.CreateTxFromScript(script, acc, -1, gas, append([]SignerAccount{{
Signer: transaction.Signer{ Signer: transaction.Signer{
Account: from, Account: from,
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,

View file

@ -5,12 +5,9 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "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/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
) )
@ -79,16 +76,16 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64,
if err != nil { if err != nil {
return nil, fmt.Errorf("bad account address: %w", err) return nil, fmt.Errorf("bad account address: %w", err)
} }
w := io.NewBufBinWriter() b := smartcontract.NewBuilder()
for i := range recipients { for i := range recipients {
emit.AppCall(w.BinWriter, recipients[i].Token, "transfer", callflag.All, b.InvokeWithAssert(recipients[i].Token, "transfer",
from, recipients[i].Address, recipients[i].Amount, recipients[i].Data) from, recipients[i].Address, recipients[i].Amount, recipients[i].Data)
emit.Opcodes(w.BinWriter, opcode.ASSERT)
} }
if w.Err != nil { script, err := b.Script()
return nil, fmt.Errorf("failed to create transfer script: %w", w.Err) if err != nil {
return nil, fmt.Errorf("failed to create transfer script: %w", err)
} }
return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{ return c.CreateTxFromScript(script, acc, -1, gas, append([]SignerAccount{{
Signer: transaction.Signer{ Signer: transaction.Signer{
Account: from, Account: from,
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,

View file

@ -0,0 +1,68 @@
package smartcontract
import (
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
)
// Builder is used to create arbitrary scripts from the set of methods it provides.
// Each method emits some set of opcodes performing an action and (in most cases)
// returning a result. These chunks of code can be composed together to perform
// several actions in the same script (and therefore in the same transaction), but
// the end result (in terms of state changes and/or resulting items) of the script
// totally depends on what it contains and that's the responsibility of the Builder
// user. Builder is mostly used to create transaction scripts (also known as
// "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).
type Builder struct {
bw *io.BufBinWriter
}
// NewBuilder creates a new Builder instance.
func NewBuilder() *Builder {
return &Builder{bw: io.NewBufBinWriter()}
}
// InvokeMethod is the most generic contract method invoker, the code it produces
// packs all of the arguments given into an array and calls some method of the
// contract. The correctness of this invocation (number and type of parameters) is
// out of scope of this method, as well as return value, if contract's method returns
// something this value just remains on the execution stack.
func (b *Builder) InvokeMethod(contract util.Uint160, method string, params ...interface{}) {
emit.AppCall(b.bw.BinWriter, contract, method, callflag.All, params...)
}
// Assert emits an ASSERT opcode that expects a Boolean value to be on the stack,
// checks if it's true and aborts the transaction if it's not.
func (b *Builder) Assert() {
emit.Opcodes(b.bw.BinWriter, opcode.ASSERT)
}
// InvokeWithAssert emits an invocation of the method (see InvokeMethod) with
// an ASSERT after the invocation. The presumption is that the method called
// returns a Boolean value signalling the success or failure of the operation.
// This pattern is pretty common, NEP-11 or NEP-17 'transfer' methods do exactly
// that as well as NEO's 'vote'. The ASSERT then allow to simplify transaction
// status checking, if action is successful then transaction is successful as
// well, if it went wrong than whole transaction fails (ends with vmstate.FAULT).
func (b *Builder) InvokeWithAssert(contract util.Uint160, method string, params ...interface{}) {
b.InvokeMethod(contract, method, params...)
b.Assert()
}
// Script return current script, you can't use Builder after invoking this method
// unless you Reset it.
func (b *Builder) Script() ([]byte, error) {
err := b.bw.Err
return b.bw.Bytes(), err
}
// Reset resets the Builder, allowing to reuse the same script buffer (but
// previous script will be overwritten there).
func (b *Builder) Reset() {
b.bw.Reset()
}

8
pkg/smartcontract/doc.go Normal file
View file

@ -0,0 +1,8 @@
/*
Package smartcontract contains functions to deal with widely used scripts.
Neo is all about various executed code, verifications and executions of
transactions need some NeoVM code and this package simplifies creating it
for common tasks like multisignature verification scripts or transaction
entry scripts that call previously deployed contracts.
*/
package smartcontract

View file

@ -0,0 +1,49 @@
package smartcontract_test
import (
"context"
"encoding/hex"
"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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
func ExampleBuilder() {
// No error checking done at all, intentionally.
c, _ := rpcclient.New(context.Background(), "url", rpcclient.Options{})
neoHash, _ := c.GetNativeContractHash("NeoToken")
pKey, _ := hex.DecodeString("03d9e8b16bd9b22d3345d6d4cde31be1c3e1d161532e3d0ccecb95ece2eb58336e") // Public key.
b := smartcontract.NewBuilder()
// Single NEO "vote" call with a check
b.InvokeWithAssert(neoHash, "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
}
b.Reset() // Copy the old script above if you need it!
w, _ := wallet.NewWalletFromFile("somewhere")
// Assuming there is one Account inside
acc := w.Accounts[0]
from, _ := address.StringToUint160(acc.Address)
// 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)
script, _ = b.Script()
// The script can then be used to create transaction or to invoke via RPC.
txid, _ := c.SignAndPushInvocationTx(script, acc, -1, 0, nil)
_ = txid
}

View file

@ -76,3 +76,12 @@ func CreateCallAndUnwrapIteratorScript(contract util.Uint160, operation string,
bytes[jmpIfMaxReachedOffset+1] = uint8(loadResultOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position. bytes[jmpIfMaxReachedOffset+1] = uint8(loadResultOffset - jmpIfMaxReachedOffset) // +1 is for JMPIF itself; offset is relative to JMPIF position.
return bytes, nil return bytes, nil
} }
// CreateCallWithAssertScript returns a script that calls contract's method with
// the specified parameters expecting a Boolean value to be return that then is
// used for ASSERT. See also (*Builder).InvokeWithAssert.
func CreateCallWithAssertScript(contract util.Uint160, method string, params ...interface{}) ([]byte, error) {
b := NewBuilder()
b.InvokeWithAssert(contract, method, params...)
return b.Script()
}