diff --git a/pkg/rpcclient/nep11.go b/pkg/rpcclient/nep11.go index 4ddc66855..0877a54ef 100644 --- a/pkg/rpcclient/nep11.go +++ b/pkg/rpcclient/nep11.go @@ -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, diff --git a/pkg/rpcclient/nep17.go b/pkg/rpcclient/nep17.go index acdff51eb..06d0a4209 100644 --- a/pkg/rpcclient/nep17.go +++ b/pkg/rpcclient/nep17.go @@ -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, diff --git a/pkg/smartcontract/builder.go b/pkg/smartcontract/builder.go new file mode 100644 index 000000000..9ea69f225 --- /dev/null +++ b/pkg/smartcontract/builder.go @@ -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() +} diff --git a/pkg/smartcontract/doc.go b/pkg/smartcontract/doc.go new file mode 100644 index 000000000..d9292ae85 --- /dev/null +++ b/pkg/smartcontract/doc.go @@ -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 diff --git a/pkg/smartcontract/doc_test.go b/pkg/smartcontract/doc_test.go new file mode 100644 index 000000000..d1b7fe8cc --- /dev/null +++ b/pkg/smartcontract/doc_test.go @@ -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 +} diff --git a/pkg/smartcontract/entry.go b/pkg/smartcontract/entry.go index fda82c0b1..5423495ef 100644 --- a/pkg/smartcontract/entry.go +++ b/pkg/smartcontract/entry.go @@ -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() +}