From ebf867b11d06eb9f775fd5abe94b46837ae7c420 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 5 Mar 2020 20:46:29 +0300 Subject: [PATCH 01/10] state: add a test for NEP5Transfer size It's size is used in NEP5TransferLog so we need to be sure it reflects reality. --- pkg/core/state/nep5_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/core/state/nep5_test.go b/pkg/core/state/nep5_test.go index 03cf14e57e..b4e70f5ea9 100644 --- a/pkg/core/state/nep5_test.go +++ b/pkg/core/state/nep5_test.go @@ -58,6 +58,12 @@ func TestNEP5Transfer_DecodeBinary(t *testing.T) { testEncodeDecode(t, expected, new(NEP5Transfer)) } +func TestNEP5TransferSize(t *testing.T) { + tr := randomTransfer(t, rand.New(rand.NewSource(0))) + size := io.GetVarSize(tr) + require.EqualValues(t, NEP5TransferSize, size) +} + func randomTransfer(t *testing.T, r *rand.Rand) *NEP5Transfer { tr := &NEP5Transfer{ Amount: int64(r.Uint64()), From cdeba6d4173660c8d814aa27c0efcc1c91bfdaed Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 6 Mar 2020 14:56:55 +0300 Subject: [PATCH 02/10] rpc: implement (*Client).NEP5* methods --- pkg/rpc/client/nep5.go | 114 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 pkg/rpc/client/nep5.go diff --git a/pkg/rpc/client/nep5.go b/pkg/rpc/client/nep5.go new file mode 100644 index 0000000000..7970f03cec --- /dev/null +++ b/pkg/rpc/client/nep5.go @@ -0,0 +1,114 @@ +package client + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" +) + +// NEP5Decimals invokes `decimals` NEP5 method on a specified contract. +func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) { + result, err := c.InvokeFunction(tokenHash.StringLE(), "decimals", []smartcontract.Parameter{}) + if err != nil { + return 0, err + } else if result.State != "HALT" || len(result.Stack) == 0 { + return 0, errors.New("invalid VM state") + } + + return topIntFromStack(result.Stack) +} + +// NEP5Name invokes `name` NEP5 method on a specified contract. +func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) { + result, err := c.InvokeFunction(tokenHash.StringLE(), "name", []smartcontract.Parameter{}) + if err != nil { + return "", err + } else if result.State != "HALT" || len(result.Stack) == 0 { + return "", errors.New("invalid VM state") + } + + return topStringFromStack(result.Stack) +} + +// NEP5Symbol invokes `symbol` NEP5 method on a specified contract. +func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) { + result, err := c.InvokeFunction(tokenHash.StringLE(), "symbol", []smartcontract.Parameter{}) + if err != nil { + return "", err + } else if result.State != "HALT" || len(result.Stack) == 0 { + return "", errors.New("invalid VM state") + } + + return topStringFromStack(result.Stack) +} + +// NEP5TotalSupply invokes `totalSupply` NEP5 method on a specified contract. +func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) { + result, err := c.InvokeFunction(tokenHash.StringLE(), "totalSupply", []smartcontract.Parameter{}) + if err != nil { + return 0, err + } else if result.State != "HALT" || len(result.Stack) == 0 { + return 0, errors.New("invalid VM state") + } + + return topIntFromStack(result.Stack) +} + +// NEP5BalanceOf invokes `balanceOf` NEP5 method on a specified contract. +func (c *Client) NEP5BalanceOf(tokenHash util.Uint160) (int64, error) { + result, err := c.InvokeFunction(tokenHash.StringLE(), "balanceOf", []smartcontract.Parameter{}) + if err != nil { + return 0, err + } else if result.State != "HALT" || len(result.Stack) == 0 { + return 0, errors.New("invalid VM state") + } + + return topIntFromStack(result.Stack) +} + +func topIntFromStack(st []smartcontract.Parameter) (int64, error) { + index := len(st) - 1 // top stack element is last in the array + var decimals int64 + switch typ := st[index].Type; typ { + case smartcontract.IntegerType: + var ok bool + decimals, ok = st[index].Value.(int64) + if !ok { + return 0, errors.New("invalid Integer item") + } + case smartcontract.ByteArrayType: + data, ok := st[index].Value.([]byte) + if !ok { + return 0, errors.New("invalid ByteArray item") + } + decimals = emit.BytesToInt(data).Int64() + default: + return 0, fmt.Errorf("invalid stack item type: %s", typ) + } + return decimals, nil +} + +func topStringFromStack(st []smartcontract.Parameter) (string, error) { + index := len(st) - 1 // top stack element is last in the array + var s string + switch typ := st[index].Type; typ { + case smartcontract.StringType: + var ok bool + s, ok = st[index].Value.(string) + if !ok { + return "", errors.New("invalid String item") + } + case smartcontract.ByteArrayType: + data, ok := st[index].Value.([]byte) + if !ok { + return "", errors.New("invalid ByteArray item") + } + s = string(data) + default: + return "", fmt.Errorf("invalid stack item type: %s", typ) + } + return s, nil +} From d447064515d36b8d539e489e28408c1e44d4d434 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 5 Mar 2020 20:05:52 +0300 Subject: [PATCH 03/10] util: implement FixedN from string When working with NEP5 contracts we frequently need to parse fixed-point decimals with arbitrary precision. --- pkg/util/fixed8.go | 20 ++++++++++++++++---- pkg/util/fixed8_test.go | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/pkg/util/fixed8.go b/pkg/util/fixed8.go index c511f76ded..c11badae7b 100644 --- a/pkg/util/fixed8.go +++ b/pkg/util/fixed8.go @@ -3,6 +3,7 @@ package util import ( "encoding/json" "errors" + "math" "strconv" "strings" @@ -71,25 +72,36 @@ func Fixed8FromFloat(val float64) Fixed8 { // Fixed8FromString parses s which must be a fixed point number // with precision up to 10^-8 func Fixed8FromString(s string) (Fixed8, error) { + num, err := FixedNFromString(s, precision) + if err != nil { + return 0, err + } + return Fixed8(num), err +} + +// FixedNFromString parses s which must be a fixed point number +// with precision 10^-d. +func FixedNFromString(s string, precision int) (int64, error) { parts := strings.SplitN(s, ".", 2) + d := int64(math.Pow10(precision)) ip, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { return 0, errInvalidString } else if len(parts) == 1 { - return Fixed8(ip * decimals), nil + return ip * d, nil } fp, err := strconv.ParseInt(parts[1], 10, 64) - if err != nil || fp >= decimals { + if err != nil || fp >= d { return 0, errInvalidString } for i := len(parts[1]); i < precision; i++ { fp *= 10 } if ip < 0 { - return Fixed8(ip*decimals - fp), nil + return ip*d - fp, nil } - return Fixed8(ip*decimals + fp), nil + return ip*d + fp, nil } // UnmarshalJSON implements the json unmarshaller interface. diff --git a/pkg/util/fixed8_test.go b/pkg/util/fixed8_test.go index 5f01dee125..aff92644b6 100644 --- a/pkg/util/fixed8_test.go +++ b/pkg/util/fixed8_test.go @@ -85,6 +85,20 @@ func TestFixed8FromString(t *testing.T) { assert.Error(t, err) } +func TestFixedNFromString(t *testing.T) { + val := "123.456" + num, err := FixedNFromString(val, 3) + require.NoError(t, err) + require.EqualValues(t, 123456, num) + + num, err = FixedNFromString(val, 4) + require.NoError(t, err) + require.EqualValues(t, 1234560, num) + + _, err = FixedNFromString(val, 2) + require.Error(t, err) +} + func TestSatoshi(t *testing.T) { satoshif8 := Satoshi() assert.Equal(t, "0.00000001", satoshif8.String()) From 564a8e429de0d7afac06669e62f65d8a6e76c353 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 6 Mar 2020 15:58:24 +0300 Subject: [PATCH 04/10] wallet: allow to add token contracts to the wallet --- pkg/wallet/token.go | 60 ++++++++++++++++++++++++++++++++++++++++ pkg/wallet/token_test.go | 29 +++++++++++++++++++ pkg/wallet/wallet.go | 13 ++++++++- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 pkg/wallet/token.go create mode 100644 pkg/wallet/token_test.go diff --git a/pkg/wallet/token.go b/pkg/wallet/token.go new file mode 100644 index 0000000000..2c68f467c3 --- /dev/null +++ b/pkg/wallet/token.go @@ -0,0 +1,60 @@ +package wallet + +import ( + "encoding/json" + + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// Token represents imported token contract. +type Token struct { + Name string + Hash util.Uint160 + Decimals int64 + Symbol string + Address string +} + +type tokenAux struct { + Name string `json:"name"` + Hash util.Uint160 `json:"script_hash"` + Decimals int64 `json:"decimals"` + Symbol string `json:"symbol"` +} + +// NewToken returns new token contract info. +func NewToken(tokenHash util.Uint160, name, symbol string, decimals int64) *Token { + return &Token{ + Name: name, + Hash: tokenHash, + Decimals: decimals, + Symbol: symbol, + Address: address.Uint160ToString(tokenHash), + } +} + +// MarshalJSON implements json.Marshaler interface. +func (t *Token) MarshalJSON() ([]byte, error) { + m := &tokenAux{ + Name: t.Name, + Hash: t.Hash.Reverse(), // address should be marshaled in LE but default marshaler uses BE. + Decimals: t.Decimals, + Symbol: t.Symbol, + } + return json.Marshal(m) +} + +// UnmarshalJSON implements json.Unmarshaler interface. +func (t *Token) UnmarshalJSON(data []byte) error { + aux := new(tokenAux) + if err := json.Unmarshal(data, aux); err != nil { + return err + } + t.Name = aux.Name + t.Hash = aux.Hash.Reverse() + t.Decimals = aux.Decimals + t.Symbol = aux.Symbol + t.Address = address.Uint160ToString(t.Hash) + return nil +} diff --git a/pkg/wallet/token_test.go b/pkg/wallet/token_test.go new file mode 100644 index 0000000000..a44d30d18c --- /dev/null +++ b/pkg/wallet/token_test.go @@ -0,0 +1,29 @@ +package wallet + +import ( + "encoding/json" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestToken_MarshalJSON(t *testing.T) { + // From the https://neo-python.readthedocs.io/en/latest/prompt.html#import-nep5-compliant-token + h, err := util.Uint160DecodeStringLE("f8d448b227991cf07cb96a6f9c0322437f1599b9") + require.NoError(t, err) + + tok := NewToken(h, "NEP5 Standard", "NEP5", 8) + require.Equal(t, "NEP5 Standard", tok.Name) + require.Equal(t, "NEP5", tok.Symbol) + require.EqualValues(t, 8, tok.Decimals) + require.Equal(t, h, tok.Hash) + require.Equal(t, "AYhE3Svuqdfh1RtzvE8hUhNR7HSpaSDFQg", tok.Address) + + data, err := json.Marshal(tok) + require.NoError(t, err) + + actual := new(Token) + require.NoError(t, json.Unmarshal(data, actual)) + require.Equal(t, tok, actual) +} diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index e01c22eba0..447df7bff7 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -28,7 +28,7 @@ type Wallet struct { // Extra metadata can be used for storing arbitrary data. // This field can be empty. - Extra interface{} `json:"extra"` + Extra Extra `json:"extra"` // Path where the wallet file is located.. path string @@ -37,6 +37,12 @@ type Wallet struct { rw io.ReadWriter } +// Extra stores imported token contracts. +type Extra struct { + // Tokens is a list of imported token contracts. + Tokens []*Token +} + // NewWallet creates a new NEO wallet at the given location. func NewWallet(location string) (*Wallet, error) { file, err := os.Create(location) @@ -96,6 +102,11 @@ func (w *Wallet) AddAccount(acc *Account) { w.Accounts = append(w.Accounts, acc) } +// AddToken adds new token to a wallet. +func (w *Wallet) AddToken(tok *Token) { + w.Extra.Tokens = append(w.Extra.Tokens, tok) +} + // Path returns the location of the wallet on the filesystem. func (w *Wallet) Path() string { return w.path From 22e99a5b3e2900859db483fe40734d9d900c4cae Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 6 Mar 2020 16:20:23 +0300 Subject: [PATCH 05/10] rpc: implement (*Client).NEP5TokenInfo() It can be useful to receive all NEP5 token info at once. --- pkg/rpc/client/nep5.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/rpc/client/nep5.go b/pkg/rpc/client/nep5.go index 7970f03cec..9f6acbd2e0 100644 --- a/pkg/rpc/client/nep5.go +++ b/pkg/rpc/client/nep5.go @@ -7,6 +7,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/wallet" ) // NEP5Decimals invokes `decimals` NEP5 method on a specified contract. @@ -69,6 +70,23 @@ func (c *Client) NEP5BalanceOf(tokenHash util.Uint160) (int64, error) { return topIntFromStack(result.Stack) } +// NEP5TokenInfo returns full NEP5 token info. +func (c *Client) NEP5TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) { + name, err := c.NEP5Name(tokenHash) + if err != nil { + return nil, err + } + symbol, err := c.NEP5Symbol(tokenHash) + if err != nil { + return nil, err + } + decimals, err := c.NEP5Decimals(tokenHash) + if err != nil { + return nil, err + } + return wallet.NewToken(tokenHash, name, symbol, decimals), nil +} + func topIntFromStack(st []smartcontract.Parameter) (int64, error) { index := len(st) - 1 // top stack element is last in the array var decimals int64 From 519b27fe0eeb9524164dc2413f7d84ab5c4a53c4 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 6 Mar 2020 16:23:41 +0300 Subject: [PATCH 06/10] cli: implement NEP5 token import To work with NEP5 tokens, one need to save some info e.g. token Name. This way we will not make additional RPC just to get Decimals. --- cli/wallet/nep5.go | 77 ++++++++++++++++++++++++++++++++++++++++++++ cli/wallet/wallet.go | 5 +++ 2 files changed, 82 insertions(+) create mode 100644 cli/wallet/nep5.go diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go new file mode 100644 index 0000000000..e138f32f46 --- /dev/null +++ b/cli/wallet/nep5.go @@ -0,0 +1,77 @@ +package wallet + +import ( + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/urfave/cli" +) + +func newNEP5Commands() []cli.Command { + return []cli.Command{ + { + Name: "import", + Usage: "import NEP5 token to a wallet", + UsageText: "import --path --rpc --token ", + Action: importNEP5Token, + Flags: []cli.Flag{ + walletPathFlag, + rpcFlag, + cli.StringFlag{ + Name: "token", + Usage: "Token contract hash in LE", + }, + }, + }, + } +} + +func importNEP5Token(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("path")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + + tokenHash, err := util.Uint160DecodeStringLE(ctx.String("token")) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid token contract hash: %v", err), 1) + } + + for _, t := range wall.Extra.Tokens { + if t.Hash.Equals(tokenHash) { + printTokenInfo(t) + return cli.NewExitError("token already exists", 1) + } + } + + gctx, cancel := getGoContext(ctx) + defer cancel() + + c, err := client.New(gctx, ctx.String("rpc"), client.Options{}) + if err != nil { + return cli.NewExitError(err, 1) + } + + tok, err := c.NEP5TokenInfo(tokenHash) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't receive token info: %v", err), 1) + } + + wall.AddToken(tok) + if err := wall.Save(); err != nil { + return cli.NewExitError(err, 1) + } + printTokenInfo(tok) + return nil +} + +func printTokenInfo(tok *wallet.Token) { + fmt.Printf("Name:\t%s\n", tok.Name) + fmt.Printf("Symbol:\t%s\n", tok.Symbol) + fmt.Printf("Hash:\t%s\n", tok.Hash.StringLE()) + fmt.Printf("Decimals: %d\n", tok.Decimals) + fmt.Printf("Address: %s\n", tok.Address) +} diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 4a0fd7a269..da89a01748 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -186,6 +186,11 @@ func NewCommands() []cli.Command { Usage: "work with multisig address", Subcommands: newMultisigCommands(), }, + { + Name: "nep5", + Usage: "work with NEP5 contracts", + Subcommands: newNEP5Commands(), + }, }, }} } From bcc03e206875e1e46bcdc589a7e779b7f92caa7d Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 5 Mar 2020 19:31:35 +0300 Subject: [PATCH 07/10] cli: implement NEP5 balance querying --- cli/wallet/nep5.go | 114 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index e138f32f46..f3e322cb02 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -1,8 +1,10 @@ package wallet import ( + "errors" "fmt" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -11,6 +13,25 @@ import ( func newNEP5Commands() []cli.Command { return []cli.Command{ + { + Name: "balance", + Usage: "get address balance", + UsageText: "balance --path --rpc --addr [--token ]", + Action: getNEP5Balance, + Flags: []cli.Flag{ + walletPathFlag, + rpcFlag, + timeoutFlag, + cli.StringFlag{ + Name: "addr", + Usage: "Address to use", + }, + cli.StringFlag{ + Name: "token", + Usage: "Token to use", + }, + }, + }, { Name: "import", Usage: "import NEP5 token to a wallet", @@ -28,6 +49,99 @@ func newNEP5Commands() []cli.Command { } } +func getNEP5Balance(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("path")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + + addr := ctx.String("addr") + addrHash, err := address.StringToUint160(addr) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid address: %v", err), 1) + } + acc := wall.GetAccount(addrHash) + if acc == nil { + return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", addr), 1) + } + + gctx, cancel := getGoContext(ctx) + defer cancel() + + c, err := client.New(gctx, ctx.String("rpc"), client.Options{}) + if err != nil { + return cli.NewExitError(err, 1) + } + + var token *wallet.Token + name := ctx.String("token") + if name != "" { + token, err = getMatchingToken(wall, name) + if err != nil { + token, err = getMatchingTokenRPC(c, addrHash, name) + if err != nil { + return cli.NewExitError(err, 1) + } + } + } + + balances, err := c.GetNEP5Balances(addrHash) + if err != nil { + return cli.NewExitError(err, 1) + } + + for i := range balances.Balances { + asset := balances.Balances[i].Asset + if name != "" && !token.Hash.Equals(asset) { + continue + } + fmt.Printf("TokenHash: %s\n", asset) + fmt.Printf("\tAmount : %s\n", balances.Balances[i].Amount) + fmt.Printf("\tUpdated: %d\n", balances.Balances[i].LastUpdated) + } + return nil +} + +func getMatchingToken(w *wallet.Wallet, name string) (*wallet.Token, error) { + return getMatchingTokenAux(func(i int) *wallet.Token { + return w.Extra.Tokens[i] + }, len(w.Extra.Tokens), name) +} + +func getMatchingTokenRPC(c *client.Client, addr util.Uint160, name string) (*wallet.Token, error) { + bs, err := c.GetNEP5Balances(addr) + if err != nil { + return nil, err + } + get := func(i int) *wallet.Token { + t, _ := c.NEP5TokenInfo(bs.Balances[i].Asset) + return t + } + return getMatchingTokenAux(get, len(bs.Balances), name) +} + +func getMatchingTokenAux(get func(i int) *wallet.Token, n int, name string) (*wallet.Token, error) { + var token *wallet.Token + var count int + for i := 0; i < n; i++ { + t := get(i) + if t != nil && (t.Name == name || t.Symbol == name || t.Address == name || t.Hash.StringLE() == name) { + if count == 1 { + printTokenInfo(token) + printTokenInfo(t) + return nil, errors.New("multiple matching tokens found") + } + count++ + token = t + } + } + if count == 0 { + return nil, errors.New("token was not found") + } + return token, nil +} + func importNEP5Token(ctx *cli.Context) error { wall, err := openWallet(ctx.String("path")) if err != nil { From 053b779be7bacfbdb6504a81cff8d13815007deb Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 6 Mar 2020 17:22:14 +0300 Subject: [PATCH 08/10] cli: implement NEP5 transfer --- cli/wallet/nep5.go | 115 +++++++++++++++++++++++++++++++++++++++++++ cli/wallet/wallet.go | 18 ++++--- 2 files changed, 125 insertions(+), 8 deletions(-) diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index f3e322cb02..a166c60cd4 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -4,9 +4,16 @@ import ( "errors" "fmt" + "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/pkg/core" + "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/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/rpc/request" "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/urfave/cli" ) @@ -46,6 +53,31 @@ func newNEP5Commands() []cli.Command { }, }, }, + { + Name: "transfer", + Usage: "transfer NEP5 tokens", + UsageText: "transfer --path --rpc --from --to --token --amount string", + Action: transferNEP5, + Flags: []cli.Flag{ + walletPathFlag, + rpcFlag, + timeoutFlag, + fromAddrFlag, + toAddrFlag, + cli.StringFlag{ + Name: "token", + Usage: "Token to use", + }, + cli.StringFlag{ + Name: "amount", + Usage: "Amount of asset to send", + }, + cli.StringFlag{ + Name: "gas", + Usage: "Amount of GAS to attach to a tx", + }, + }, + }, } } @@ -189,3 +221,86 @@ func printTokenInfo(tok *wallet.Token) { fmt.Printf("Decimals: %d\n", tok.Decimals) fmt.Printf("Address: %s\n", tok.Address) } + +func transferNEP5(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("path")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + + fromFlag := ctx.Generic("from").(*flags.Address) + from := fromFlag.Uint160() + acc := wall.GetAccount(from) + if acc == nil { + return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", fromFlag), 1) + } + + gctx, cancel := getGoContext(ctx) + defer cancel() + c, err := client.New(gctx, ctx.String("rpc"), client.Options{}) + if err != nil { + return cli.NewExitError(err, 1) + } + + toFlag := ctx.Generic("to").(*flags.Address) + to := toFlag.Uint160() + token, err := getMatchingToken(wall, ctx.String("token")) + if err != nil { + fmt.Println("Can't find matching token in the wallet. Querying RPC-node for balances.") + token, err = getMatchingTokenRPC(c, from, ctx.String("token")) + if err != nil { + return cli.NewExitError(err, 1) + } + } + + amount, err := util.FixedNFromString(ctx.String("amount"), int(token.Decimals)) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid amount: %v", err), 1) + } + + // Note: we don't use invoke function here because it requires + // 2 round trips instead of one. + w := io.NewBufBinWriter() + emit.Int(w.BinWriter, amount) + emit.Bytes(w.BinWriter, to.BytesBE()) + emit.Bytes(w.BinWriter, from.BytesBE()) + emit.Int(w.BinWriter, 3) + emit.Opcode(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "transfer") + emit.AppCall(w.BinWriter, token.Hash, false) + emit.Opcode(w.BinWriter, opcode.THROWIFNOT) + + var gas util.Fixed8 + if gasString := ctx.String("gas"); gasString != "" { + gas, err = util.Fixed8FromString(gasString) + if err != nil { + return cli.NewExitError(fmt.Errorf("invalid GAS amount: %v", err), 1) + } + } + + tx := transaction.NewInvocationTX(w.Bytes(), gas) + tx.Attributes = append(tx.Attributes, transaction.Attribute{ + Usage: transaction.Script, + Data: from.BytesBE(), + }) + + if err := request.AddInputsAndUnspentsToTx(tx, fromFlag.String(), core.UtilityTokenID(), gas, c); err != nil { + return cli.NewExitError(fmt.Errorf("can't add GAS to a tx: %v", err), 1) + } + + if pass, err := readPassword("Password > "); err != nil { + return cli.NewExitError(err, 1) + } else if err := acc.Decrypt(pass); err != nil { + return cli.NewExitError(err, 1) + } else if err := acc.SignTx(tx); err != nil { + return cli.NewExitError(fmt.Errorf("can't sign tx: %v", err), 1) + } + + if err := c.SendRawTransaction(tx); err != nil { + return cli.NewExitError(err, 1) + } + + fmt.Println(tx.Hash()) + return nil +} diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index da89a01748..1ad91810cf 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -59,6 +59,14 @@ var ( Name: "in", Usage: "file with JSON transaction", } + fromAddrFlag = flags.AddressFlag{ + Name: "from", + Usage: "Address to send an asset from", + } + toAddrFlag = flags.AddressFlag{ + Name: "to", + Usage: "Address to send an asset to", + } ) // NewCommands returns 'wallet' command. @@ -163,14 +171,8 @@ func NewCommands() []cli.Command { rpcFlag, timeoutFlag, outFlag, - flags.AddressFlag{ - Name: "from", - Usage: "Address to send an asset from", - }, - flags.AddressFlag{ - Name: "to", - Usage: "Address to send an asset to", - }, + fromAddrFlag, + toAddrFlag, cli.StringFlag{ Name: "amount", Usage: "Amount of asset to send", From 97131683d8a91e1e4f4213935e46e0f2a39553a9 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 6 Mar 2020 17:22:28 +0300 Subject: [PATCH 09/10] cli: display info about imported NEP5 token --- cli/wallet/nep5.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index a166c60cd4..dc738325ba 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -53,6 +53,19 @@ func newNEP5Commands() []cli.Command { }, }, }, + { + Name: "info", + Usage: "print imported NEP5 token info", + UsageText: "print --path [--token ]", + Action: printNEP5Info, + Flags: []cli.Flag{ + walletPathFlag, + cli.StringFlag{ + Name: "token", + Usage: "Token name or hash", + }, + }, + }, { Name: "transfer", Usage: "transfer NEP5 tokens", @@ -222,6 +235,31 @@ func printTokenInfo(tok *wallet.Token) { fmt.Printf("Address: %s\n", tok.Address) } +func printNEP5Info(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("path")) + if err != nil { + return cli.NewExitError(err, 1) + } + defer wall.Close() + + if name := ctx.String("token"); name != "" { + token, err := getMatchingToken(wall, name) + if err != nil { + return cli.NewExitError(err, 1) + } + printTokenInfo(token) + return nil + } + + for i, t := range wall.Extra.Tokens { + if i > 0 { + fmt.Println() + } + printTokenInfo(t) + } + return nil +} + func transferNEP5(ctx *cli.Context) error { wall, err := openWallet(ctx.String("path")) if err != nil { From c2924795adf521a4e42300595f7bc14756b753ce Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 10 Mar 2020 10:56:46 +0300 Subject: [PATCH 10/10] cli: fix a bug in `wallet claim` getclaimable RPC expects address as a 1-st argument. --- cli/wallet/wallet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 1ad91810cf..2e5aab42e1 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -228,7 +228,7 @@ func claimGas(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - info, err := c.GetClaimable(scriptHash.String()) + info, err := c.GetClaimable(addrFlag.String()) if err != nil { return cli.NewExitError(err, 1) } else if info.Unclaimed == 0 || len(info.Spents) == 0 {