mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-29 13:41:47 +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/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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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.
|
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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue