mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-25 23:42:23 +00:00
smartcontract: add Builder, method invocation helpers and doc
Move the last remaining script-related things out of the rpcclient.
This commit is contained in:
parent
1b6f4051d8
commit
32ebb4a90d
6 changed files with 145 additions and 20 deletions
|
@ -6,14 +6,10 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"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/io"
|
||||
"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/callflag"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"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/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{}.
|
||||
func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160,
|
||||
gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) {
|
||||
w := io.NewBufBinWriter()
|
||||
emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...)
|
||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||
if w.Err != nil {
|
||||
return nil, fmt.Errorf("failed to create NEP-11 transfer script: %w", w.Err)
|
||||
script, err := smartcontract.CreateCallWithAssertScript(tokenHash, "transfer", args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create NEP-11 transfer script: %w", err)
|
||||
}
|
||||
from, err := address.StringToUint160(acc.Address)
|
||||
if err != nil {
|
||||
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{
|
||||
Account: from,
|
||||
Scopes: transaction.CalledByEntry,
|
||||
|
|
|
@ -5,12 +5,9 @@ import (
|
|||
|
||||
"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/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -79,16 +76,16 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64,
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("bad account address: %w", err)
|
||||
}
|
||||
w := io.NewBufBinWriter()
|
||||
b := smartcontract.NewBuilder()
|
||||
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)
|
||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||
}
|
||||
if w.Err != nil {
|
||||
return nil, fmt.Errorf("failed to create transfer script: %w", w.Err)
|
||||
script, err := b.Script()
|
||||
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{
|
||||
Account: from,
|
||||
Scopes: transaction.CalledByEntry,
|
||||
|
|
68
pkg/smartcontract/builder.go
Normal file
68
pkg/smartcontract/builder.go
Normal 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
8
pkg/smartcontract/doc.go
Normal 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
|
49
pkg/smartcontract/doc_test.go
Normal file
49
pkg/smartcontract/doc_test.go
Normal 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
|
||||
}
|
|
@ -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.
|
||||
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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue