frostfs-node/lib/blockchain/goclient/client.go
alexvanin dadfd90dcd Initial commit
Initial public review release v0.10.0
2020-07-10 17:45:00 +03:00

190 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
}