forked from TrueCloudLab/frostfs-node
191 lines
4.7 KiB
Go
191 lines
4.7 KiB
Go
|
package goclient
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"crypto/ecdsa"
|
||
|
"encoding/hex"
|
||
|
"time"
|
||
|
|
||
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
||
|
sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||
|
"github.com/nspcc-dev/neofs-node/internal"
|
||
|
"github.com/pkg/errors"
|
||
|
"go.uber.org/zap"
|
||
|
)
|
||
|
|
||
|
type (
|
||
|
// Params is a group of Client's constructor parameters.
|
||
|
Params struct {
|
||
|
Log *zap.Logger
|
||
|
Key *ecdsa.PrivateKey
|
||
|
Endpoint string
|
||
|
Magic netmode.Magic
|
||
|
DialTimeout time.Duration
|
||
|
}
|
||
|
|
||
|
// Client is a neo-go wrapper that provides smart-contract invocation interface.
|
||
|
Client struct {
|
||
|
log *zap.Logger
|
||
|
cli *client.Client
|
||
|
acc *wallet.Account
|
||
|
}
|
||
|
)
|
||
|
|
||
|
// ErrNilClient is returned by functions that expect
|
||
|
// a non-nil Client, but received nil.
|
||
|
const ErrNilClient = internal.Error("go client is nil")
|
||
|
|
||
|
// HaltState returned if TestInvoke function processed without panic.
|
||
|
const HaltState = "HALT"
|
||
|
|
||
|
// ErrMissingFee is returned by functions that expect
|
||
|
// a positive invocation fee, but received non-positive.
|
||
|
const ErrMissingFee = internal.Error("invocation fee must be positive")
|
||
|
|
||
|
var (
|
||
|
errNilParams = errors.New("chain/client: config was not provided to the constructor")
|
||
|
|
||
|
errNilLogger = errors.New("chain/client: logger was not provided to the constructor")
|
||
|
|
||
|
errNilKey = errors.New("chain/client: private key was not provided to the constructor")
|
||
|
)
|
||
|
|
||
|
// Invoke invokes contract method by sending transaction into blockchain.
|
||
|
// Supported args types: int64, string, util.Uint160, []byte and bool.
|
||
|
//
|
||
|
// If passed fee is non-positive, ErrMissingFee returns.
|
||
|
func (c *Client) Invoke(contract util.Uint160, fee util.Fixed8, method string, args ...interface{}) error {
|
||
|
var params []sc.Parameter
|
||
|
for i := range args {
|
||
|
param, err := toStackParameter(args[i])
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
params = append(params, param)
|
||
|
}
|
||
|
|
||
|
cosigner := []transaction.Cosigner{
|
||
|
{
|
||
|
Account: c.acc.PrivateKey().PublicKey().GetScriptHash(),
|
||
|
Scopes: transaction.Global,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
resp, err := c.cli.InvokeFunction(contract, method, params, cosigner)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if len(resp.Script) == 0 {
|
||
|
return errors.New("chain/client: got empty invocation script from neo node")
|
||
|
}
|
||
|
|
||
|
script, err := hex.DecodeString(resp.Script)
|
||
|
if err != nil {
|
||
|
return errors.New("chain/client: can't decode invocation script from neo node")
|
||
|
}
|
||
|
|
||
|
txHash, err := c.cli.SignAndPushInvocationTx(script, c.acc, 0, fee, cosigner)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
c.log.Debug("neo client invoke",
|
||
|
zap.String("method", method),
|
||
|
zap.Stringer("tx_hash", txHash))
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// TestInvoke invokes contract method locally in neo-go node. This method should
|
||
|
// be used to read data from smart-contract.
|
||
|
func (c *Client) TestInvoke(contract util.Uint160, method string, args ...interface{}) ([]sc.Parameter, error) {
|
||
|
var params = make([]sc.Parameter, 0, len(args))
|
||
|
|
||
|
for i := range args {
|
||
|
p, err := toStackParameter(args[i])
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
params = append(params, p)
|
||
|
}
|
||
|
|
||
|
cosigner := []transaction.Cosigner{
|
||
|
{
|
||
|
Account: c.acc.PrivateKey().PublicKey().GetScriptHash(),
|
||
|
Scopes: transaction.Global,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
val, err := c.cli.InvokeFunction(contract, method, params, cosigner)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if val.State != HaltState {
|
||
|
return nil, errors.Errorf("chain/client: contract execution finished with state %s", val.State)
|
||
|
}
|
||
|
|
||
|
return val.Stack, nil
|
||
|
}
|
||
|
|
||
|
// New is a Client constructor.
|
||
|
func New(ctx context.Context, p *Params) (*Client, error) {
|
||
|
switch {
|
||
|
case p == nil:
|
||
|
return nil, errNilParams
|
||
|
case p.Log == nil:
|
||
|
return nil, errNilLogger
|
||
|
case p.Key == nil:
|
||
|
return nil, errNilKey
|
||
|
}
|
||
|
|
||
|
privKeyBytes := crypto.MarshalPrivateKey(p.Key)
|
||
|
|
||
|
wif, err := keys.WIFEncode(privKeyBytes, keys.WIFVersion, true)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
account, err := wallet.NewAccountFromWIF(wif)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
cli, err := client.New(ctx, p.Endpoint, client.Options{
|
||
|
DialTimeout: p.DialTimeout,
|
||
|
Network: p.Magic,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &Client{log: p.Log, cli: cli, acc: account}, nil
|
||
|
}
|
||
|
|
||
|
func toStackParameter(value interface{}) (sc.Parameter, error) {
|
||
|
var result = sc.Parameter{
|
||
|
Value: value,
|
||
|
}
|
||
|
|
||
|
// todo: add more types
|
||
|
switch value.(type) {
|
||
|
case []byte:
|
||
|
result.Type = sc.ByteArrayType
|
||
|
case int64: // TODO: add other numerical types
|
||
|
result.Type = sc.IntegerType
|
||
|
default:
|
||
|
return result, errors.Errorf("chain/client: unsupported parameter %v", value)
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|