Merge pull request #1918 from nspcc-dev/cli/nep11
cli: support NEP11-related commands
This commit is contained in:
commit
3acdbbd603
23 changed files with 1191 additions and 127 deletions
|
@ -76,12 +76,14 @@ func GetDataFromContext(ctx *cli.Context) (int, interface{}, *cli.ExitError) {
|
|||
if err != nil {
|
||||
return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1)
|
||||
}
|
||||
if len(params) != 1 {
|
||||
if len(params) > 1 {
|
||||
return offset, nil, cli.NewExitError("'data' should be represented as a single parameter", 1)
|
||||
}
|
||||
data, err = smartcontract.ExpandParameterToEmitable(params[0])
|
||||
if err != nil {
|
||||
return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1)
|
||||
if len(params) != 0 {
|
||||
data, err = smartcontract.ExpandParameterToEmitable(params[0])
|
||||
if err != nil {
|
||||
return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return offset, data, nil
|
||||
|
|
|
@ -227,30 +227,33 @@ func TestContractDeployWithData(t *testing.T) {
|
|||
}
|
||||
|
||||
func deployVerifyContract(t *testing.T, e *executor) util.Uint160 {
|
||||
tmpDir := path.Join(os.TempDir(), "neogo.test.deployverifycontract")
|
||||
require.NoError(t, os.Mkdir(tmpDir, os.ModePerm))
|
||||
return deployContract(t, e, "testdata/verify.go", "testdata/verify.yml", validatorWallet, validatorAddr, "one")
|
||||
}
|
||||
|
||||
func deployContract(t *testing.T, e *executor, inPath, configPath, wallet, address, pass string) util.Uint160 {
|
||||
tmpDir, err := ioutil.TempDir(os.TempDir(), "neogo.test.deploycontract*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
// deploy verification contract
|
||||
nefName := path.Join(tmpDir, "verify.nef")
|
||||
manifestName := path.Join(tmpDir, "verify.manifest.json")
|
||||
nefName := path.Join(tmpDir, "contract.nef")
|
||||
manifestName := path.Join(tmpDir, "contract.manifest.json")
|
||||
e.Run(t, "neo-go", "contract", "compile",
|
||||
"--in", "testdata/verify.go",
|
||||
"--config", "testdata/verify.yml",
|
||||
"--in", inPath,
|
||||
"--config", configPath,
|
||||
"--out", nefName, "--manifest", manifestName)
|
||||
e.In.WriteString("one\r")
|
||||
e.In.WriteString(pass + "\r")
|
||||
e.Run(t, "neo-go", "contract", "deploy",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", validatorWallet, "--address", validatorAddr,
|
||||
"--wallet", wallet, "--address", address,
|
||||
"--in", nefName, "--manifest", manifestName)
|
||||
e.checkTxPersisted(t, "Sent invocation transaction ")
|
||||
line, err := e.Out.ReadString('\n')
|
||||
require.NoError(t, err)
|
||||
line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: "))
|
||||
hVerify, err := util.Uint160DecodeStringLE(line)
|
||||
h, err := util.Uint160DecodeStringLE(line)
|
||||
require.NoError(t, err)
|
||||
return hVerify
|
||||
return h
|
||||
}
|
||||
|
||||
func TestComlileAndInvokeFunction(t *testing.T) {
|
||||
|
|
284
cli/nep11_test.go
Normal file
284
cli/nep11_test.go
Normal file
|
@ -0,0 +1,284 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
// nftOwnerAddr is the owner of NFT-ND HASHY token (../examples/nft-nd/nft.go)
|
||||
nftOwnerAddr = "NX1yL5wDx3inK2qUVLRVaqCLUxYnAbv85S"
|
||||
nftOwnerWallet = "../examples/my_wallet.json"
|
||||
nftOwnerPass = "qwerty"
|
||||
)
|
||||
|
||||
func TestNEP11Import(t *testing.T) {
|
||||
e := newExecutor(t, true)
|
||||
|
||||
tmpDir := os.TempDir()
|
||||
walletPath := path.Join(tmpDir, "walletForImport.json")
|
||||
defer os.Remove(walletPath)
|
||||
|
||||
nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService)
|
||||
require.NoError(t, err)
|
||||
neoContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Neo)
|
||||
require.NoError(t, err)
|
||||
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
|
||||
|
||||
args := []string{
|
||||
"neo-go", "wallet", "nep11", "import",
|
||||
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||
"--wallet", walletPath,
|
||||
}
|
||||
// missing token hash
|
||||
e.RunWithError(t, args...)
|
||||
|
||||
// good
|
||||
e.Run(t, append(args, "--token", nnsContractHash.StringLE())...)
|
||||
|
||||
// already exists
|
||||
e.RunWithError(t, append(args, "--token", nnsContractHash.StringLE())...)
|
||||
|
||||
// not a NEP11 token
|
||||
e.RunWithError(t, append(args, "--token", neoContractHash.StringLE())...)
|
||||
|
||||
t.Run("Info", func(t *testing.T) {
|
||||
checkNNSInfo := func(t *testing.T) {
|
||||
e.checkNextLine(t, "^Name:\\s*NameService")
|
||||
e.checkNextLine(t, "^Symbol:\\s*NNS")
|
||||
e.checkNextLine(t, "^Hash:\\s*"+nnsContractHash.StringLE())
|
||||
e.checkNextLine(t, "^Decimals:\\s*0")
|
||||
e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(nnsContractHash))
|
||||
e.checkNextLine(t, "^Standard:\\s*"+string(manifest.NEP11StandardName))
|
||||
}
|
||||
t.Run("WithToken", func(t *testing.T) {
|
||||
e.Run(t, "neo-go", "wallet", "nep11", "info",
|
||||
"--wallet", walletPath, "--token", nnsContractHash.StringLE())
|
||||
checkNNSInfo(t)
|
||||
})
|
||||
t.Run("NoToken", func(t *testing.T) {
|
||||
e.Run(t, "neo-go", "wallet", "nep11", "info",
|
||||
"--wallet", walletPath)
|
||||
checkNNSInfo(t)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
e.In.WriteString("y\r")
|
||||
e.Run(t, "neo-go", "wallet", "nep11", "remove",
|
||||
"--wallet", walletPath, "--token", nnsContractHash.StringLE())
|
||||
e.Run(t, "neo-go", "wallet", "nep11", "info",
|
||||
"--wallet", walletPath)
|
||||
_, err := e.Out.ReadString('\n')
|
||||
require.Equal(t, err, io.EOF)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNEP11_OwnerOf_BalanceOf_Transfer(t *testing.T) {
|
||||
e := newExecutor(t, true)
|
||||
|
||||
tmpDir, err := ioutil.TempDir(os.TempDir(), "neogo.test.nftwallet*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
// copy wallet to temp dir in order not to overwrite the original file
|
||||
bytesRead, err := ioutil.ReadFile(nftOwnerWallet)
|
||||
require.NoError(t, err)
|
||||
wall := path.Join(tmpDir, "my_wallet.json")
|
||||
err = ioutil.WriteFile(wall, bytesRead, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// transfer funds to contract owner
|
||||
e.In.WriteString("one\r")
|
||||
e.Run(t, "neo-go", "wallet", "nep17", "transfer",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", validatorWallet,
|
||||
"--to", nftOwnerAddr,
|
||||
"--token", "GAS",
|
||||
"--amount", "10000",
|
||||
"--from", validatorAddr)
|
||||
e.checkTxPersisted(t)
|
||||
|
||||
// deploy NFT HASHY contract
|
||||
h := deployNFTContract(t, e)
|
||||
|
||||
mint := func(t *testing.T) []byte {
|
||||
// mint 1 HASHY token by transferring 10 GAS to HASHY contract
|
||||
e.In.WriteString(nftOwnerPass + "\r")
|
||||
e.Run(t, "neo-go", "wallet", "nep17", "transfer",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", wall,
|
||||
"--to", h.StringLE(),
|
||||
"--token", "GAS",
|
||||
"--amount", "10",
|
||||
"--from", nftOwnerAddr)
|
||||
txMint, _ := e.checkTxPersisted(t)
|
||||
|
||||
// get NFT ID from AER
|
||||
aer, err := e.Chain.GetAppExecResults(txMint.Hash(), trigger.Application)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(aer))
|
||||
require.Equal(t, 2, len(aer[0].Events))
|
||||
hashyMintEvent := aer[0].Events[1]
|
||||
require.Equal(t, "Transfer", hashyMintEvent.Name)
|
||||
tokenID, err := hashyMintEvent.Item.Value().([]stackitem.Item)[3].TryBytes()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tokenID)
|
||||
return tokenID
|
||||
}
|
||||
|
||||
tokenID := mint(t)
|
||||
|
||||
// check the balance
|
||||
cmdCheckBalance := []string{"neo-go", "wallet", "nep11", "balance",
|
||||
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||
"--wallet", wall,
|
||||
"--address", nftOwnerAddr}
|
||||
checkBalanceResult := func(t *testing.T, acc string, amount string) {
|
||||
e.checkNextLine(t, "^\\s*Account\\s+"+acc)
|
||||
e.checkNextLine(t, "^\\s*HASHY:\\s+HASHY NFT \\("+h.StringLE()+"\\)")
|
||||
e.checkNextLine(t, "^\\s*Amount\\s*:\\s*"+amount+"$")
|
||||
e.checkEOF(t)
|
||||
}
|
||||
// balance check: by symbol, token is not imported
|
||||
e.RunWithError(t, append(cmdCheckBalance, "--token", "HASHY")...)
|
||||
// balance check: by hash, ok
|
||||
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
|
||||
checkBalanceResult(t, nftOwnerAddr, "1")
|
||||
|
||||
// import token
|
||||
e.Run(t, "neo-go", "wallet", "nep11", "import",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", wall,
|
||||
"--token", h.StringLE())
|
||||
|
||||
// balance check: by symbol, ok
|
||||
e.Run(t, append(cmdCheckBalance, "--token", "HASHY")...)
|
||||
checkBalanceResult(t, nftOwnerAddr, "1")
|
||||
|
||||
// balance check: all accounts
|
||||
e.Run(t, "neo-go", "wallet", "nep11", "balance",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", wall,
|
||||
"--token", h.StringLE())
|
||||
checkBalanceResult(t, nftOwnerAddr, "1")
|
||||
|
||||
// remove token from wallet
|
||||
e.In.WriteString("y\r")
|
||||
e.Run(t, "neo-go", "wallet", "nep11", "remove",
|
||||
"--wallet", wall, "--token", h.StringLE())
|
||||
|
||||
// ownerOf: missing contract hash
|
||||
cmdOwnerOf := []string{"neo-go", "wallet", "nep11", "ownerOf",
|
||||
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||
}
|
||||
e.RunWithError(t, cmdOwnerOf...)
|
||||
cmdOwnerOf = append(cmdOwnerOf, "--token", h.StringLE())
|
||||
|
||||
// ownerOf: missing token ID
|
||||
e.RunWithError(t, cmdOwnerOf...)
|
||||
cmdOwnerOf = append(cmdOwnerOf, "--id", string(tokenID))
|
||||
|
||||
// ownerOf: good
|
||||
e.Run(t, cmdOwnerOf...)
|
||||
e.checkNextLine(t, nftOwnerAddr)
|
||||
|
||||
// tokensOf: missing contract hash
|
||||
cmdTokensOf := []string{"neo-go", "wallet", "nep11", "tokensOf",
|
||||
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||
}
|
||||
e.RunWithError(t, cmdTokensOf...)
|
||||
cmdTokensOf = append(cmdTokensOf, "--token", h.StringLE())
|
||||
|
||||
// tokensOf: missing owner address
|
||||
e.RunWithError(t, cmdTokensOf...)
|
||||
cmdTokensOf = append(cmdTokensOf, "--address", nftOwnerAddr)
|
||||
|
||||
// tokensOf: good
|
||||
e.Run(t, cmdTokensOf...)
|
||||
e.checkNextLine(t, string(tokenID))
|
||||
|
||||
// properties: no contract
|
||||
cmdProperties := []string{
|
||||
"neo-go", "wallet", "nep11", "properties",
|
||||
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||
}
|
||||
e.RunWithError(t, cmdProperties...)
|
||||
cmdProperties = append(cmdProperties, "--token", h.StringLE())
|
||||
|
||||
// properties: no token ID
|
||||
e.RunWithError(t, cmdProperties...)
|
||||
cmdProperties = append(cmdProperties, "--id", string(tokenID))
|
||||
|
||||
// properties: ok
|
||||
e.Run(t, cmdProperties...)
|
||||
e.checkNextLine(t, fmt.Sprintf(`{"name":"HASHY %s"}`, string(tokenID)))
|
||||
|
||||
// tokensOf: good, several tokens
|
||||
tokenID1 := mint(t)
|
||||
e.Run(t, cmdTokensOf...)
|
||||
e.checkNextLine(t, string(tokenID))
|
||||
e.checkNextLine(t, string(tokenID1))
|
||||
|
||||
// tokens: missing contract hash
|
||||
cmdTokens := []string{"neo-go", "wallet", "nep11", "tokens",
|
||||
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||
}
|
||||
e.RunWithError(t, cmdTokens...)
|
||||
cmdTokens = append(cmdTokens, "--token", h.StringLE())
|
||||
|
||||
// tokens: good, several tokens
|
||||
e.Run(t, cmdTokens...)
|
||||
e.checkNextLine(t, string(tokenID))
|
||||
e.checkNextLine(t, string(tokenID1))
|
||||
|
||||
// balance check: several tokens, ok
|
||||
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
|
||||
checkBalanceResult(t, nftOwnerAddr, "2")
|
||||
|
||||
cmdTransfer := []string{
|
||||
"neo-go", "wallet", "nep11", "transfer",
|
||||
"--rpc-endpoint", "http://" + e.RPC.Addr,
|
||||
"--wallet", wall,
|
||||
"--to", validatorAddr,
|
||||
"--from", nftOwnerAddr,
|
||||
}
|
||||
|
||||
// transfer: unimported token with symbol id specified
|
||||
e.In.WriteString(nftOwnerPass + "\r")
|
||||
e.RunWithError(t, append(cmdTransfer,
|
||||
"--token", "HASHY")...)
|
||||
cmdTransfer = append(cmdTransfer, "--token", h.StringLE())
|
||||
|
||||
// transfer: no id specified
|
||||
e.In.WriteString(nftOwnerPass + "\r")
|
||||
e.RunWithError(t, cmdTransfer...)
|
||||
cmdTransfer = append(cmdTransfer, "--id", string(tokenID))
|
||||
|
||||
// transfer: good
|
||||
e.In.WriteString(nftOwnerPass + "\r")
|
||||
e.Run(t, cmdTransfer...)
|
||||
e.checkTxPersisted(t)
|
||||
|
||||
// check balance after transfer
|
||||
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
|
||||
checkBalanceResult(t, nftOwnerAddr, "1") // tokenID1
|
||||
}
|
||||
|
||||
func deployNFTContract(t *testing.T, e *executor) util.Uint160 {
|
||||
return deployContract(t, e, "../examples/nft-nd/nft.go", "../examples/nft-nd/nft.yml", nftOwnerWallet, nftOwnerAddr, nftOwnerPass)
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -133,9 +134,9 @@ func TestNEP17Transfer(t *testing.T) {
|
|||
require.Equal(t, big.NewInt(1), b)
|
||||
|
||||
hVerify := deployVerifyContract(t, e)
|
||||
const validatorDefault = "NTh9TnZTstvAePEYWDGLLxidBikJE24uTo"
|
||||
|
||||
t.Run("default address", func(t *testing.T) {
|
||||
const validatorDefault = "NTh9TnZTstvAePEYWDGLLxidBikJE24uTo"
|
||||
e.In.WriteString("one\r")
|
||||
e.Run(t, "neo-go", "wallet", "nep17", "multitransfer",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
|
@ -161,6 +162,18 @@ func TestNEP17Transfer(t *testing.T) {
|
|||
require.Equal(t, big.NewInt(41), b)
|
||||
})
|
||||
|
||||
t.Run("with signers", func(t *testing.T) {
|
||||
e.In.WriteString("one\r")
|
||||
e.Run(t, "neo-go", "wallet", "nep17", "multitransfer",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", validatorWallet,
|
||||
"--from", validatorAddr,
|
||||
"NEO:"+validatorDefault+":42",
|
||||
"GAS:"+validatorDefault+":7",
|
||||
"--", validatorAddr+":Global")
|
||||
e.checkTxPersisted(t)
|
||||
})
|
||||
|
||||
validTil := e.Chain.BlockHeight() + 100
|
||||
cmd := []string{
|
||||
"neo-go", "wallet", "nep17", "transfer",
|
||||
|
@ -256,6 +269,8 @@ func TestNEP17ImportToken(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
gasContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Gas)
|
||||
require.NoError(t, err)
|
||||
nnsContractHash, err := e.Chain.GetNativeContractScriptHash(nativenames.NameService)
|
||||
require.NoError(t, err)
|
||||
e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath)
|
||||
|
||||
// missing token hash
|
||||
|
@ -272,6 +287,12 @@ func TestNEP17ImportToken(t *testing.T) {
|
|||
"--wallet", walletPath,
|
||||
"--token", address.Uint160ToString(neoContractHash)) // try address instead of sh
|
||||
|
||||
// not a NEP17 token
|
||||
e.RunWithError(t, "neo-go", "wallet", "nep17", "import",
|
||||
"--rpc-endpoint", "http://"+e.RPC.Addr,
|
||||
"--wallet", walletPath,
|
||||
"--token", nnsContractHash.StringLE())
|
||||
|
||||
t.Run("Info", func(t *testing.T) {
|
||||
checkGASInfo := func(t *testing.T) {
|
||||
e.checkNextLine(t, "^Name:\\s*GasToken")
|
||||
|
@ -279,6 +300,7 @@ func TestNEP17ImportToken(t *testing.T) {
|
|||
e.checkNextLine(t, "^Hash:\\s*"+gasContractHash.StringLE())
|
||||
e.checkNextLine(t, "^Decimals:\\s*8")
|
||||
e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(gasContractHash))
|
||||
e.checkNextLine(t, "^Standard:\\s*"+string(manifest.NEP17StandardName))
|
||||
}
|
||||
t.Run("WithToken", func(t *testing.T) {
|
||||
e.Run(t, "neo-go", "wallet", "nep17", "info",
|
||||
|
@ -296,6 +318,7 @@ func TestNEP17ImportToken(t *testing.T) {
|
|||
e.checkNextLine(t, "^Hash:\\s*"+neoContractHash.StringLE())
|
||||
e.checkNextLine(t, "^Decimals:\\s*0")
|
||||
e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(neoContractHash))
|
||||
e.checkNextLine(t, "^Standard:\\s*"+string(manifest.NEP17StandardName))
|
||||
})
|
||||
t.Run("Remove", func(t *testing.T) {
|
||||
e.In.WriteString("y\r")
|
||||
|
|
390
cli/wallet/nep11.go
Normal file
390
cli/wallet/nep11.go
Normal file
|
@ -0,0 +1,390 @@
|
|||
package wallet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/cli/flags"
|
||||
"github.com/nspcc-dev/neo-go/cli/options"
|
||||
"github.com/nspcc-dev/neo-go/cli/paramcontext"
|
||||
"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/fixedn"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
||||
"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/stackitem"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func newNEP11Commands() []cli.Command {
|
||||
tokenAddressFlag := flags.AddressFlag{
|
||||
Name: "token",
|
||||
Usage: "Token contract address or hash in LE",
|
||||
}
|
||||
ownerAddressFlag := flags.AddressFlag{
|
||||
Name: "address",
|
||||
Usage: "NFT owner address or hash in LE",
|
||||
}
|
||||
tokenID := cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: "Token ID",
|
||||
}
|
||||
|
||||
balanceFlags := make([]cli.Flag, len(baseBalanceFlags))
|
||||
copy(balanceFlags, baseBalanceFlags)
|
||||
balanceFlags = append(balanceFlags, tokenID)
|
||||
balanceFlags = append(balanceFlags, options.RPC...)
|
||||
transferFlags := make([]cli.Flag, len(baseTransferFlags))
|
||||
copy(transferFlags, baseTransferFlags)
|
||||
transferFlags = append(transferFlags, tokenID)
|
||||
transferFlags = append(transferFlags, options.RPC...)
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "balance",
|
||||
Usage: "get address balance",
|
||||
UsageText: "balance --wallet <path> --rpc-endpoint <node> [--timeout <time>] [--address <address>] --token <hash-or-name> [--id <token-id>]",
|
||||
Action: getNEP11Balance,
|
||||
Flags: balanceFlags,
|
||||
},
|
||||
{
|
||||
Name: "import",
|
||||
Usage: "import NEP11 token to a wallet",
|
||||
UsageText: "import --wallet <path> --rpc-endpoint <node> --timeout <time> --token <hash>",
|
||||
Action: importNEP11Token,
|
||||
Flags: importFlags,
|
||||
},
|
||||
{
|
||||
Name: "info",
|
||||
Usage: "print imported NEP11 token info",
|
||||
UsageText: "print --wallet <path> [--token <hash-or-name>]",
|
||||
Action: printNEP11Info,
|
||||
Flags: []cli.Flag{
|
||||
walletPathFlag,
|
||||
tokenFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "remove",
|
||||
Usage: "remove NEP11 token from the wallet",
|
||||
UsageText: "remove --wallet <path> --token <hash-or-name>",
|
||||
Action: removeNEP11Token,
|
||||
Flags: []cli.Flag{
|
||||
walletPathFlag,
|
||||
tokenFlag,
|
||||
forceFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "transfer",
|
||||
Usage: "transfer NEP11 tokens",
|
||||
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --id <token-id> [--amount string] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
|
||||
Action: transferNEP11,
|
||||
Flags: transferFlags,
|
||||
Description: `Transfers specified NEP11 token with optional cosigners list attached to
|
||||
the transfer. Amount should be specified for divisible NEP11
|
||||
tokens and omitted for non-divisible NEP11 tokens. See
|
||||
'contract testinvokefunction' documentation for the details
|
||||
about cosigners syntax. If no cosigners are given then the
|
||||
sender with CalledByEntry scope will be used as the only
|
||||
signer.
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "properties",
|
||||
Usage: "print properties of NEP11 token",
|
||||
UsageText: "properties --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
|
||||
Action: printNEP11Properties,
|
||||
Flags: append([]cli.Flag{
|
||||
tokenAddressFlag,
|
||||
tokenID,
|
||||
}, options.RPC...),
|
||||
},
|
||||
{
|
||||
Name: "ownerOf",
|
||||
Usage: "print owner of non-divisible NEP11 token with the specified ID",
|
||||
UsageText: "ownerOf --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
|
||||
Action: printNEP11Owner,
|
||||
Flags: append([]cli.Flag{
|
||||
tokenAddressFlag,
|
||||
tokenID,
|
||||
}, options.RPC...),
|
||||
},
|
||||
{
|
||||
Name: "tokensOf",
|
||||
Usage: "print list of tokens IDs for the specified divisible NFT owner",
|
||||
UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>",
|
||||
Action: printNEP11TokensOf,
|
||||
Flags: append([]cli.Flag{
|
||||
tokenAddressFlag,
|
||||
ownerAddressFlag,
|
||||
}, options.RPC...),
|
||||
},
|
||||
{
|
||||
Name: "tokens",
|
||||
Usage: "print list of tokens IDs minted by the specified NFT (optional method)",
|
||||
UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
|
||||
Action: printNEP11Tokens,
|
||||
Flags: append([]cli.Flag{
|
||||
tokenAddressFlag,
|
||||
}, options.RPC...),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func importNEP11Token(ctx *cli.Context) error {
|
||||
return importNEPToken(ctx, manifest.NEP11StandardName)
|
||||
}
|
||||
|
||||
func printNEP11Info(ctx *cli.Context) error {
|
||||
return printNEPInfo(ctx, manifest.NEP11StandardName)
|
||||
}
|
||||
|
||||
func removeNEP11Token(ctx *cli.Context) error {
|
||||
return removeNEPToken(ctx, manifest.NEP11StandardName)
|
||||
}
|
||||
|
||||
func getNEP11Balance(ctx *cli.Context) error {
|
||||
var accounts []*wallet.Account
|
||||
|
||||
wall, err := openWallet(ctx.String("wallet"))
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("bad wallet: %w", err), 1)
|
||||
}
|
||||
defer wall.Close()
|
||||
|
||||
addrFlag := ctx.Generic("address").(*flags.Address)
|
||||
if addrFlag.IsSet {
|
||||
addrHash := addrFlag.Uint160()
|
||||
acc := wall.GetAccount(addrHash)
|
||||
if acc == nil {
|
||||
return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addrHash)), 1)
|
||||
}
|
||||
accounts = append(accounts, acc)
|
||||
} else {
|
||||
if len(wall.Accounts) == 0 {
|
||||
return cli.NewExitError(errors.New("no accounts in the wallet"), 1)
|
||||
}
|
||||
accounts = wall.Accounts
|
||||
}
|
||||
|
||||
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
c, err := options.GetRPCClient(gctx, ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
name := ctx.String("token")
|
||||
if name == "" {
|
||||
return cli.NewExitError("token hash or name should be specified", 1)
|
||||
}
|
||||
token, err := getMatchingToken(ctx, wall, name, manifest.NEP11StandardName)
|
||||
if err != nil {
|
||||
fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for token info.")
|
||||
tokenHash, err := flags.ParseAddress(name)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("valid token adress or hash in LE should be specified for RPC-node request: %s", err.Error()), 1)
|
||||
}
|
||||
token, err = c.NEP11TokenInfo(tokenHash)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
}
|
||||
|
||||
tokenID := ctx.String("id")
|
||||
for k, acc := range accounts {
|
||||
addrHash, err := address.StringToUint160(acc.Address)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("invalid account address: %w", err), 1)
|
||||
}
|
||||
|
||||
if k != 0 {
|
||||
fmt.Fprintln(ctx.App.Writer)
|
||||
}
|
||||
fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address)
|
||||
|
||||
var amount int64
|
||||
if tokenID == "" {
|
||||
amount, err = c.NEP11BalanceOf(token.Hash, addrHash)
|
||||
} else {
|
||||
amount, err = c.NEP11DBalanceOf(token.Hash, addrHash, tokenID)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
amountStr := fixedn.ToString(big.NewInt(amount), int(token.Decimals))
|
||||
|
||||
format := "%s: %s (%s)\n"
|
||||
formatArgs := []interface{}{token.Symbol, token.Name, token.Hash.StringLE()}
|
||||
if tokenID != "" {
|
||||
format = "%s: %s (%s, %s)\n"
|
||||
formatArgs = append(formatArgs, tokenID)
|
||||
}
|
||||
fmt.Fprintf(ctx.App.Writer, format, formatArgs...)
|
||||
fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", amountStr)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func transferNEP11(ctx *cli.Context) error {
|
||||
return transferNEP(ctx, manifest.NEP11StandardName)
|
||||
}
|
||||
|
||||
func signAndSendNEP11Transfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, token, to util.Uint160, tokenID string, amount *big.Int, cosigners []client.SignerAccount) error {
|
||||
gas := flags.Fixed8FromContext(ctx, "gas")
|
||||
|
||||
var (
|
||||
tx *transaction.Transaction
|
||||
err error
|
||||
)
|
||||
if amount != nil {
|
||||
from, err := address.StringToUint160(acc.Address)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("bad account address: %w", err), 1)
|
||||
}
|
||||
tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, from, to, amount, tokenID)
|
||||
} else {
|
||||
tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, to, tokenID)
|
||||
}
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
if outFile := ctx.String("out"); outFile != "" {
|
||||
if err := paramcontext.InitAndSave(c.GetNetwork(), tx, acc, outFile); err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
} else {
|
||||
_, err := c.SignAndPushTx(tx, acc, cosigners)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNEP11Owner(ctx *cli.Context) error {
|
||||
var err error
|
||||
tokenHash := ctx.Generic("token").(*flags.Address)
|
||||
if !tokenHash.IsSet {
|
||||
return cli.NewExitError("token contract hash was not set", 1)
|
||||
}
|
||||
|
||||
tokenID := ctx.String("id")
|
||||
if tokenID == "" {
|
||||
return cli.NewExitError(errors.New("token ID should be specified"), 1)
|
||||
}
|
||||
|
||||
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
c, err := options.GetRPCClient(gctx, ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
result, err := c.NEP11NDOwnerOf(tokenHash.Uint160(), tokenID)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("failed to call NEP11 `ownerOf` method: %s", err.Error()), 1)
|
||||
}
|
||||
|
||||
fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(result))
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNEP11TokensOf(ctx *cli.Context) error {
|
||||
var err error
|
||||
tokenHash := ctx.Generic("token").(*flags.Address)
|
||||
if !tokenHash.IsSet {
|
||||
return cli.NewExitError("token contract hash was not set", 1)
|
||||
}
|
||||
|
||||
acc := ctx.Generic("address").(*flags.Address)
|
||||
if !acc.IsSet {
|
||||
return cli.NewExitError("owner address flag was not set", 1)
|
||||
}
|
||||
|
||||
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
c, err := options.GetRPCClient(gctx, ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
result, err := c.NEP11TokensOf(tokenHash.Uint160(), acc.Uint160())
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("failed to call NEP11 `tokensOf` method: %s", err.Error()), 1)
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
fmt.Fprintln(ctx.App.Writer, result[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNEP11Tokens(ctx *cli.Context) error {
|
||||
var err error
|
||||
tokenHash := ctx.Generic("token").(*flags.Address)
|
||||
if !tokenHash.IsSet {
|
||||
return cli.NewExitError("token contract hash was not set", 1)
|
||||
}
|
||||
|
||||
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
c, err := options.GetRPCClient(gctx, ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
result, err := c.NEP11Tokens(tokenHash.Uint160())
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("failed to call optional NEP11 `tokens` method: %s", err.Error()), 1)
|
||||
}
|
||||
|
||||
for i := range result {
|
||||
fmt.Fprintln(ctx.App.Writer, result[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNEP11Properties(ctx *cli.Context) error {
|
||||
var err error
|
||||
tokenHash := ctx.Generic("token").(*flags.Address)
|
||||
if !tokenHash.IsSet {
|
||||
return cli.NewExitError("token contract hash was not set", 1)
|
||||
}
|
||||
|
||||
tokenID := ctx.String("id")
|
||||
if tokenID == "" {
|
||||
return cli.NewExitError(errors.New("token ID should be specified"), 1)
|
||||
}
|
||||
|
||||
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
c, err := options.GetRPCClient(gctx, ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
result, err := c.NEP11Properties(tokenHash.Uint160(), tokenID)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("failed to call NEP11 `properties` method: %s", err.Error()), 1)
|
||||
}
|
||||
|
||||
bytes, err := stackitem.ToJSON(result)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Sprintf("failed to convert result to JSON: %s", err), 1)
|
||||
}
|
||||
fmt.Fprintln(ctx.App.Writer, string(bytes))
|
||||
return nil
|
||||
}
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
"github.com/urfave/cli"
|
||||
|
@ -27,10 +28,7 @@ var (
|
|||
Name: "gas",
|
||||
Usage: "Amount of GAS to attach to a tx",
|
||||
}
|
||||
)
|
||||
|
||||
func newNEP17Commands() []cli.Command {
|
||||
balanceFlags := []cli.Flag{
|
||||
baseBalanceFlags = []cli.Flag{
|
||||
walletPathFlag,
|
||||
tokenFlag,
|
||||
flags.AddressFlag{
|
||||
|
@ -38,16 +36,14 @@ func newNEP17Commands() []cli.Command {
|
|||
Usage: "Address to use",
|
||||
},
|
||||
}
|
||||
balanceFlags = append(balanceFlags, options.RPC...)
|
||||
importFlags := []cli.Flag{
|
||||
importFlags = append([]cli.Flag{
|
||||
walletPathFlag,
|
||||
flags.AddressFlag{
|
||||
Name: "token",
|
||||
Usage: "Token contract address or hash in LE",
|
||||
},
|
||||
}
|
||||
importFlags = append(importFlags, options.RPC...)
|
||||
transferFlags := []cli.Flag{
|
||||
}, options.RPC...)
|
||||
baseTransferFlags = []cli.Flag{
|
||||
walletPathFlag,
|
||||
outFlag,
|
||||
fromAddrFlag,
|
||||
|
@ -59,14 +55,21 @@ func newNEP17Commands() []cli.Command {
|
|||
Usage: "Amount of asset to send",
|
||||
},
|
||||
}
|
||||
transferFlags = append(transferFlags, options.RPC...)
|
||||
multiTransferFlags := []cli.Flag{
|
||||
multiTransferFlags = append([]cli.Flag{
|
||||
walletPathFlag,
|
||||
outFlag,
|
||||
fromAddrFlag,
|
||||
gasFlag,
|
||||
}
|
||||
multiTransferFlags = append(multiTransferFlags, options.RPC...)
|
||||
}, options.RPC...)
|
||||
)
|
||||
|
||||
func newNEP17Commands() []cli.Command {
|
||||
balanceFlags := make([]cli.Flag, len(baseBalanceFlags))
|
||||
copy(balanceFlags, baseBalanceFlags)
|
||||
balanceFlags = append(balanceFlags, options.RPC...)
|
||||
transferFlags := make([]cli.Flag, len(baseTransferFlags))
|
||||
copy(transferFlags, baseTransferFlags)
|
||||
transferFlags = append(transferFlags, options.RPC...)
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "balance",
|
||||
|
@ -89,10 +92,7 @@ func newNEP17Commands() []cli.Command {
|
|||
Action: printNEP17Info,
|
||||
Flags: []cli.Flag{
|
||||
walletPathFlag,
|
||||
cli.StringFlag{
|
||||
Name: "token",
|
||||
Usage: "Token name or hash",
|
||||
},
|
||||
tokenFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -102,17 +102,14 @@ func newNEP17Commands() []cli.Command {
|
|||
Action: removeNEP17Token,
|
||||
Flags: []cli.Flag{
|
||||
walletPathFlag,
|
||||
cli.StringFlag{
|
||||
Name: "token",
|
||||
Usage: "Token name or hash",
|
||||
},
|
||||
tokenFlag,
|
||||
forceFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "transfer",
|
||||
Usage: "transfer NEP17 tokens",
|
||||
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash> --amount string [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
|
||||
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --amount string [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
|
||||
Action: transferNEP17,
|
||||
Flags: transferFlags,
|
||||
Description: `Transfers specified NEP17 token amount with optional 'data' parameter and cosigners
|
||||
|
@ -186,7 +183,7 @@ func getNEP17Balance(ctx *cli.Context) error {
|
|||
var tokenName, tokenSymbol string
|
||||
tokenDecimals := 0
|
||||
asset := balances.Balances[i].Asset
|
||||
token, err := getMatchingToken(ctx, wall, asset.StringLE())
|
||||
token, err := getMatchingToken(ctx, wall, asset.StringLE(), manifest.NEP17StandardName)
|
||||
if err != nil {
|
||||
token, err = c.NEP17TokenInfo(asset)
|
||||
}
|
||||
|
@ -218,46 +215,65 @@ func getNEP17Balance(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string) (*wallet.Token, error) {
|
||||
func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, standard string) (*wallet.Token, error) {
|
||||
return getMatchingTokenAux(ctx, func(i int) *wallet.Token {
|
||||
return w.Extra.Tokens[i]
|
||||
}, len(w.Extra.Tokens), name)
|
||||
}, len(w.Extra.Tokens), name, standard)
|
||||
}
|
||||
|
||||
func getMatchingTokenRPC(ctx *cli.Context, c *client.Client, addr util.Uint160, name string) (*wallet.Token, error) {
|
||||
bs, err := c.GetNEP17Balances(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func getMatchingTokenRPC(ctx *cli.Context, c *client.Client, addr util.Uint160, name string, standard string) (*wallet.Token, error) {
|
||||
switch standard {
|
||||
case manifest.NEP17StandardName:
|
||||
bs, err := c.GetNEP17Balances(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
get := func(i int) *wallet.Token {
|
||||
t, _ := c.NEP17TokenInfo(bs.Balances[i].Asset)
|
||||
return t
|
||||
}
|
||||
return getMatchingTokenAux(ctx, get, len(bs.Balances), name, standard)
|
||||
case manifest.NEP11StandardName:
|
||||
tokenHash, err := flags.ParseAddress(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("valid token adress or hash in LE should be specified for %s RPC-node request: %s", standard, err.Error())
|
||||
}
|
||||
get := func(i int) *wallet.Token {
|
||||
t, _ := c.NEP11TokenInfo(tokenHash)
|
||||
return t
|
||||
}
|
||||
return getMatchingTokenAux(ctx, get, 1, name, standard)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported %s token", standard)
|
||||
}
|
||||
get := func(i int) *wallet.Token {
|
||||
t, _ := c.NEP17TokenInfo(bs.Balances[i].Asset)
|
||||
return t
|
||||
}
|
||||
return getMatchingTokenAux(ctx, get, len(bs.Balances), name)
|
||||
}
|
||||
|
||||
func getMatchingTokenAux(ctx *cli.Context, get func(i int) *wallet.Token, n int, name string) (*wallet.Token, error) {
|
||||
func getMatchingTokenAux(ctx *cli.Context, get func(i int) *wallet.Token, n int, name string, standard string) (*wallet.Token, error) {
|
||||
var token *wallet.Token
|
||||
var count int
|
||||
for i := 0; i < n; i++ {
|
||||
t := get(i)
|
||||
if t != nil && (t.Hash.StringLE() == name || t.Address() == name || t.Symbol == name || t.Name == name) {
|
||||
if t != nil && (t.Hash.StringLE() == name || t.Address() == name || t.Symbol == name || t.Name == name) && t.Standard == standard {
|
||||
if count == 1 {
|
||||
printTokenInfo(ctx, token)
|
||||
printTokenInfo(ctx, t)
|
||||
return nil, errors.New("multiple matching tokens found")
|
||||
return nil, fmt.Errorf("multiple matching %s tokens found", standard)
|
||||
}
|
||||
count++
|
||||
token = t
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, errors.New("token was not found")
|
||||
return nil, fmt.Errorf("%s token was not found", standard)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func importNEP17Token(ctx *cli.Context) error {
|
||||
return importNEPToken(ctx, manifest.NEP17StandardName)
|
||||
}
|
||||
|
||||
func importNEPToken(ctx *cli.Context, standard string) error {
|
||||
wall, err := openWallet(ctx.String("wallet"))
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
|
@ -271,9 +287,9 @@ func importNEP17Token(ctx *cli.Context) error {
|
|||
tokenHash := tokenHashFlag.Uint160()
|
||||
|
||||
for _, t := range wall.Extra.Tokens {
|
||||
if t.Hash.Equals(tokenHash) {
|
||||
if t.Hash.Equals(tokenHash) && t.Standard == standard {
|
||||
printTokenInfo(ctx, t)
|
||||
return cli.NewExitError("token already exists", 1)
|
||||
return cli.NewExitError(fmt.Errorf("%s token already exists", standard), 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,7 +301,15 @@ func importNEP17Token(ctx *cli.Context) error {
|
|||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
tok, err := c.NEP17TokenInfo(tokenHash)
|
||||
var tok *wallet.Token
|
||||
switch standard {
|
||||
case manifest.NEP17StandardName:
|
||||
tok, err = c.NEP17TokenInfo(tokenHash)
|
||||
case manifest.NEP11StandardName:
|
||||
tok, err = c.NEP11TokenInfo(tokenHash)
|
||||
default:
|
||||
return cli.NewExitError(fmt.Sprintf("unsupported token standard: %s", standard), 1)
|
||||
}
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("can't receive token info: %w", err), 1)
|
||||
}
|
||||
|
@ -305,9 +329,14 @@ func printTokenInfo(ctx *cli.Context, tok *wallet.Token) {
|
|||
fmt.Fprintf(w, "Hash:\t%s\n", tok.Hash.StringLE())
|
||||
fmt.Fprintf(w, "Decimals: %d\n", tok.Decimals)
|
||||
fmt.Fprintf(w, "Address: %s\n", tok.Address())
|
||||
fmt.Fprintf(w, "Standard:\t%s\n", tok.Standard)
|
||||
}
|
||||
|
||||
func printNEP17Info(ctx *cli.Context) error {
|
||||
return printNEPInfo(ctx, manifest.NEP17StandardName)
|
||||
}
|
||||
|
||||
func printNEPInfo(ctx *cli.Context, standard string) error {
|
||||
wall, err := openWallet(ctx.String("wallet"))
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
|
@ -315,7 +344,7 @@ func printNEP17Info(ctx *cli.Context) error {
|
|||
defer wall.Close()
|
||||
|
||||
if name := ctx.String("token"); name != "" {
|
||||
token, err := getMatchingToken(ctx, wall, name)
|
||||
token, err := getMatchingToken(ctx, wall, name, standard)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
@ -323,23 +352,31 @@ func printNEP17Info(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
for i, t := range wall.Extra.Tokens {
|
||||
if i > 0 {
|
||||
var count int
|
||||
for _, t := range wall.Extra.Tokens {
|
||||
if count > 0 {
|
||||
fmt.Fprintln(ctx.App.Writer)
|
||||
}
|
||||
printTokenInfo(ctx, t)
|
||||
if t.Standard == standard {
|
||||
printTokenInfo(ctx, t)
|
||||
count++
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeNEP17Token(ctx *cli.Context) error {
|
||||
return removeNEPToken(ctx, manifest.NEP17StandardName)
|
||||
}
|
||||
|
||||
func removeNEPToken(ctx *cli.Context, standard string) error {
|
||||
wall, err := openWallet(ctx.String("wallet"))
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
defer wall.Close()
|
||||
|
||||
token, err := getMatchingToken(ctx, wall, ctx.String("token"))
|
||||
token, err := getMatchingToken(ctx, wall, ctx.String("token"), standard)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
@ -401,10 +438,10 @@ func multiTransferNEP17(ctx *cli.Context) error {
|
|||
}
|
||||
token, ok := cache[ss[0]]
|
||||
if !ok {
|
||||
token, err = getMatchingToken(ctx, wall, ss[0])
|
||||
token, err = getMatchingToken(ctx, wall, ss[0], manifest.NEP17StandardName)
|
||||
if err != nil {
|
||||
fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.")
|
||||
token, err = getMatchingTokenRPC(ctx, c, from, ss[0])
|
||||
token, err = getMatchingTokenRPC(ctx, c, from, ss[0], manifest.NEP17StandardName)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
@ -436,10 +473,14 @@ func multiTransferNEP17(ctx *cli.Context) error {
|
|||
return cli.NewExitError(fmt.Errorf("failed to create NEP17 multitransfer transaction: %w", err), 1)
|
||||
}
|
||||
|
||||
return signAndSendTransfer(ctx, c, acc, recipients, cosignersAccounts)
|
||||
return signAndSendNEP17Transfer(ctx, c, acc, recipients, cosignersAccounts)
|
||||
}
|
||||
|
||||
func transferNEP17(ctx *cli.Context) error {
|
||||
return transferNEP(ctx, manifest.NEP17StandardName)
|
||||
}
|
||||
|
||||
func transferNEP(ctx *cli.Context, standard string) error {
|
||||
wall, err := openWallet(ctx.String("wallet"))
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
|
@ -466,20 +507,15 @@ func transferNEP17(ctx *cli.Context) error {
|
|||
|
||||
toFlag := ctx.Generic("to").(*flags.Address)
|
||||
to := toFlag.Uint160()
|
||||
token, err := getMatchingToken(ctx, wall, ctx.String("token"))
|
||||
token, err := getMatchingToken(ctx, wall, ctx.String("token"), standard)
|
||||
if err != nil {
|
||||
fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.")
|
||||
token, err = getMatchingTokenRPC(ctx, c, from, ctx.String("token"))
|
||||
token, err = getMatchingTokenRPC(ctx, c, from, ctx.String("token"), standard)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("failed to get matching token: %w", err), 1)
|
||||
}
|
||||
}
|
||||
|
||||
amount, err := fixedn.FromString(ctx.String("amount"), int(token.Decimals))
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
|
||||
}
|
||||
|
||||
cosignersOffset, data, extErr := cmdargs.GetDataFromContext(ctx)
|
||||
if extErr != nil {
|
||||
return extErr
|
||||
|
@ -494,15 +530,38 @@ func transferNEP17(ctx *cli.Context) error {
|
|||
return cli.NewExitError(fmt.Errorf("failed to create NEP17 transfer transaction: %w", err), 1)
|
||||
}
|
||||
|
||||
return signAndSendTransfer(ctx, c, acc, []client.TransferTarget{{
|
||||
Token: token.Hash,
|
||||
Address: to,
|
||||
Amount: amount.Int64(),
|
||||
Data: data,
|
||||
}}, cosignersAccounts)
|
||||
amountArg := ctx.String("amount")
|
||||
switch standard {
|
||||
case manifest.NEP17StandardName:
|
||||
amount, err := fixedn.FromString(amountArg, int(token.Decimals))
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
|
||||
}
|
||||
return signAndSendNEP17Transfer(ctx, c, acc, []client.TransferTarget{{
|
||||
Token: token.Hash,
|
||||
Address: to,
|
||||
Amount: amount.Int64(),
|
||||
Data: data,
|
||||
}}, cosignersAccounts)
|
||||
case manifest.NEP11StandardName:
|
||||
tokenID := ctx.String("id")
|
||||
if tokenID == "" {
|
||||
return cli.NewExitError(errors.New("token ID should be specified"), 1)
|
||||
}
|
||||
if amountArg == "" {
|
||||
return signAndSendNEP11Transfer(ctx, c, acc, token.Hash, to, tokenID, nil, cosignersAccounts)
|
||||
}
|
||||
amount, err := fixedn.FromString(amountArg, int(token.Decimals))
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
|
||||
}
|
||||
return signAndSendNEP11Transfer(ctx, c, acc, token.Hash, to, tokenID, amount, cosignersAccounts)
|
||||
default:
|
||||
return cli.NewExitError(fmt.Errorf("unsupported token standard %s", standard), 1)
|
||||
}
|
||||
}
|
||||
|
||||
func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget, cosigners []client.SignerAccount) error {
|
||||
func signAndSendNEP17Transfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget, cosigners []client.SignerAccount) error {
|
||||
gas := flags.Fixed8FromContext(ctx, "gas")
|
||||
|
||||
tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, cosigners)
|
||||
|
|
|
@ -234,6 +234,11 @@ func NewCommands() []cli.Command {
|
|||
Usage: "work with NEP17 contracts",
|
||||
Subcommands: newNEP17Commands(),
|
||||
},
|
||||
{
|
||||
Name: "nep11",
|
||||
Usage: "work with NEP11 contracts",
|
||||
Subcommands: newNEP11Commands(),
|
||||
},
|
||||
{
|
||||
Name: "candidate",
|
||||
Usage: "work with candidates",
|
||||
|
|
|
@ -25,6 +25,7 @@ const (
|
|||
totalSupplyPrefix = "s"
|
||||
accountPrefix = "a"
|
||||
tokenPrefix = "t"
|
||||
tokensPrefix = "ts"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -76,6 +77,10 @@ func mkTokenKey(token []byte) []byte {
|
|||
return append(res, token...)
|
||||
}
|
||||
|
||||
func mkTokensKey() []byte {
|
||||
return []byte(tokensPrefix)
|
||||
}
|
||||
|
||||
// BalanceOf returns the number of tokens owned by specified address.
|
||||
func BalanceOf(holder interop.Hash160) int {
|
||||
if len(holder) != 20 {
|
||||
|
@ -112,6 +117,36 @@ func setTokensOf(ctx storage.Context, holder interop.Hash160, tokens []string) {
|
|||
}
|
||||
}
|
||||
|
||||
// setTokens saves minted token if it is not saved yet.
|
||||
func setTokens(ctx storage.Context, newToken string) {
|
||||
key := mkTokensKey()
|
||||
var tokens = []string{}
|
||||
val := storage.Get(ctx, key)
|
||||
if val != nil {
|
||||
tokens = std.Deserialize(val.([]byte)).([]string)
|
||||
}
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
if util.Equals(tokens[i], newToken) {
|
||||
return
|
||||
}
|
||||
}
|
||||
tokens = append(tokens, newToken)
|
||||
val = std.Serialize(tokens)
|
||||
storage.Put(ctx, key, val)
|
||||
}
|
||||
|
||||
// Tokens returns an iterator that contains all of the tokens minted by the contract.
|
||||
func Tokens() iterator.Iterator {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
var arr = []string{}
|
||||
key := mkTokensKey()
|
||||
val := storage.Get(ctx, key)
|
||||
if val != nil {
|
||||
arr = std.Deserialize(val.([]byte)).([]string)
|
||||
}
|
||||
return iterator.Create(arr)
|
||||
}
|
||||
|
||||
// TokensOf returns an iterator with all tokens held by specified address.
|
||||
func TokensOf(holder interop.Hash160) iterator.Iterator {
|
||||
if len(holder) != 20 {
|
||||
|
@ -219,6 +254,7 @@ func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) {
|
|||
toksOf = append(toksOf, token)
|
||||
setTokensOf(ctx, from, toksOf)
|
||||
setOwnerOf(ctx, []byte(token), from)
|
||||
setTokens(ctx, token)
|
||||
|
||||
total++
|
||||
storage.Put(ctx, []byte(totalSupplyPrefix), total)
|
||||
|
@ -248,3 +284,28 @@ func Update(nef, manifest []byte) {
|
|||
}
|
||||
management.Update(nef, manifest)
|
||||
}
|
||||
|
||||
// Properties returns properties of the given NFT.
|
||||
func Properties(id []byte) map[string]string {
|
||||
ctx := storage.GetReadOnlyContext()
|
||||
var tokens = []string{}
|
||||
key := mkTokensKey()
|
||||
val := storage.Get(ctx, key)
|
||||
if val != nil {
|
||||
tokens = std.Deserialize(val.([]byte)).([]string)
|
||||
}
|
||||
var exists bool
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
if util.Equals(tokens[i], id) {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
panic("unknown token")
|
||||
}
|
||||
result := map[string]string{
|
||||
"name": "HASHY " + string(id),
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: "HASHY NFT"
|
||||
supportedstandards: ["NEP-11"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf"]
|
||||
safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens", "properties"]
|
||||
events:
|
||||
- name: Transfer
|
||||
parameters:
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpc"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
@ -49,6 +50,9 @@ func LoadFile(configPath string) (Config, error) {
|
|||
ApplicationConfiguration: ApplicationConfiguration{
|
||||
PingInterval: 30,
|
||||
PingTimeout: 90,
|
||||
RPC: rpc.Config{
|
||||
MaxIteratorResultItems: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -95,3 +95,38 @@ func topMapFromStack(st []stackitem.Item) (*stackitem.Map, error) {
|
|||
}
|
||||
return st[index].(*stackitem.Map), nil
|
||||
}
|
||||
|
||||
// topIterableFromStack returns top list of elements of `resultItemType` type from stack.
|
||||
func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]interface{}, error) {
|
||||
index := len(st) - 1 // top stack element is last in the array
|
||||
if t := st[index].Type(); t != stackitem.InteropT {
|
||||
return nil, fmt.Errorf("invalid return stackitem type: %s (InteropInterface expected)", t.String())
|
||||
}
|
||||
iter, ok := st[index].Value().(result.Iterator)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to deserialize iterable from interop stackitem: invalid value type (Array expected)")
|
||||
}
|
||||
result := make([]interface{}, len(iter.Values))
|
||||
for i := range iter.Values {
|
||||
switch resultItemType.(type) {
|
||||
case string:
|
||||
bytes, err := iter.Values[i].TryBytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deserialize string from stackitem #%d: %w", i, err)
|
||||
}
|
||||
result[i] = string(bytes)
|
||||
case util.Uint160:
|
||||
bytes, err := iter.Values[i].TryBytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deserialize uint160 from stackitem #%d: %w", i, err)
|
||||
}
|
||||
result[i], err = util.Uint160DecodeBytesBE(bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode uint160 from stackitem #%d: %w", i, err)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported iterable type")
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
)
|
||||
|
||||
// nepDecimals invokes `decimals` NEP* method on a specified contract.
|
||||
|
@ -70,3 +73,30 @@ func (c *Client) nepBalanceOf(tokenHash, acc util.Uint160, tokenID *string) (int
|
|||
|
||||
return topIntFromStack(result.Stack)
|
||||
}
|
||||
|
||||
// nepTokenInfo returns full NEP* token info.
|
||||
func (c *Client) nepTokenInfo(tokenHash util.Uint160, standard string) (*wallet.Token, error) {
|
||||
cs, err := c.GetContractStateByHash(tokenHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var isStandardOK bool
|
||||
for _, st := range cs.Manifest.SupportedStandards {
|
||||
if st == standard {
|
||||
isStandardOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isStandardOK {
|
||||
return nil, fmt.Errorf("token %s does not support %s standard", tokenHash.StringLE(), standard)
|
||||
}
|
||||
symbol, err := c.nepSymbol(tokenHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decimals, err := c.nepDecimals(tokenHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wallet.NewToken(tokenHash, cs.Manifest.Name, symbol, decimals, standard), nil
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"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/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||
|
@ -35,6 +36,11 @@ func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) {
|
|||
return c.nepBalanceOf(tokenHash, owner, nil)
|
||||
}
|
||||
|
||||
// NEP11TokenInfo returns full NEP11 token info.
|
||||
func (c *Client) NEP11TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
|
||||
return c.nepTokenInfo(tokenHash, manifest.NEP11StandardName)
|
||||
}
|
||||
|
||||
// TransferNEP11 creates an invocation transaction that invokes 'transfer' method
|
||||
// on a given token to move the whole NEP11 token with the specified token ID to
|
||||
// given account and sends it to the network returning just a hash of it.
|
||||
|
@ -43,7 +49,7 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
|
|||
if !c.initDone {
|
||||
return util.Uint256{}, errNetworkNotInitialized
|
||||
}
|
||||
tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID)
|
||||
tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID)
|
||||
if err != nil {
|
||||
return util.Uint256{}, err
|
||||
}
|
||||
|
@ -51,14 +57,14 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
|
|||
return c.SignAndPushTx(tx, acc, cosigners)
|
||||
}
|
||||
|
||||
// createNEP11TransferTx is an internal helper for TransferNEP11 and
|
||||
// TransferNEP11D which creates an invocation transaction for the
|
||||
// 'transfer' method of a given contract (token) to move the whole (or the
|
||||
// specified amount of) NEP11 token with the specified token ID to given account
|
||||
// and returns it. The returned transaction is not signed.
|
||||
// CreateNEP11TransferTx creates an invocation transaction for the 'transfer'
|
||||
// method of a given contract (token) to move the whole (or the specified amount
|
||||
// of) NEP11 token with the specified token ID to given account and returns it.
|
||||
// The returned transaction is not signed. CreateNEP11TransferTx is also a
|
||||
// helper for TransferNEP11 and TransferNEP11D.
|
||||
// `args` for TransferNEP11: to util.Uint160, tokenID string;
|
||||
// `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string.
|
||||
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) {
|
||||
w := io.NewBufBinWriter()
|
||||
emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...)
|
||||
|
@ -79,6 +85,33 @@ func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1
|
|||
}}, cosigners...))
|
||||
}
|
||||
|
||||
// NEP11TokensOf returns an array of token IDs for the specified owner of the specified NFT token.
|
||||
func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) ([]string, error) {
|
||||
result, err := c.InvokeFunction(tokenHash, "tokensOf", []smartcontract.Parameter{
|
||||
{
|
||||
Type: smartcontract.Hash160Type,
|
||||
Value: owner,
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = getInvocationError(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr, err := topIterableFromStack(result.Stack, string(""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get token IDs from stack: %w", err)
|
||||
}
|
||||
ids := make([]string, len(arr))
|
||||
for i := range ids {
|
||||
ids[i] = arr[i].(string)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// Non-divisible NFT methods section start.
|
||||
|
||||
// NEP11NDOwnerOf invokes `ownerOf` non-devisible NEP11 method with the
|
||||
|
@ -118,7 +151,7 @@ func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160,
|
|||
if err != nil {
|
||||
return util.Uint256{}, fmt.Errorf("bad account address: %w", err)
|
||||
}
|
||||
tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, acc.Address, from, to, amount, tokenID)
|
||||
tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, from, to, amount, tokenID)
|
||||
if err != nil {
|
||||
return util.Uint256{}, err
|
||||
}
|
||||
|
@ -132,6 +165,33 @@ func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID string)
|
|||
return c.nepBalanceOf(tokenHash, owner, &tokenID)
|
||||
}
|
||||
|
||||
// NEP11DOwnerOf returns list of the specified NEP11 divisible token owners.
|
||||
func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID string) ([]util.Uint160, error) {
|
||||
result, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{
|
||||
{
|
||||
Type: smartcontract.StringType,
|
||||
Value: tokenID,
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = getInvocationError(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr, err := topIterableFromStack(result.Stack, util.Uint160{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get token IDs from stack: %w", err)
|
||||
}
|
||||
owners := make([]util.Uint160, len(arr))
|
||||
for i := range owners {
|
||||
owners[i] = arr[i].(util.Uint160)
|
||||
}
|
||||
return owners, nil
|
||||
}
|
||||
|
||||
// Divisible NFT methods section end.
|
||||
|
||||
// Optional NFT methods section start.
|
||||
|
@ -154,4 +214,26 @@ func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID string) (*stack
|
|||
return topMapFromStack(result.Stack)
|
||||
}
|
||||
|
||||
// NEP11Tokens returns list of the tokens minted by the contract.
|
||||
func (c *Client) NEP11Tokens(tokenHash util.Uint160) ([]string, error) {
|
||||
result, err := c.InvokeFunction(tokenHash, "tokens", []smartcontract.Parameter{}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = getInvocationError(result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr, err := topIterableFromStack(result.Stack, string(""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get token IDs from stack: %w", err)
|
||||
}
|
||||
tokens := make([]string, len(arr))
|
||||
for i := range tokens {
|
||||
tokens[i] = arr[i].(string)
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// Optional NFT methods section end.
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"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/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"
|
||||
|
@ -50,19 +51,7 @@ func (c *Client) NEP17BalanceOf(tokenHash, acc util.Uint160) (int64, error) {
|
|||
|
||||
// NEP17TokenInfo returns full NEP17 token info.
|
||||
func (c *Client) NEP17TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
|
||||
cs, err := c.GetContractStateByHash(tokenHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
symbol, err := c.NEP17Symbol(tokenHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decimals, err := c.NEP17Decimals(tokenHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wallet.NewToken(tokenHash, cs.Manifest.Name, symbol, decimals), nil
|
||||
return c.nepTokenInfo(tokenHash, manifest.NEP17StandardName)
|
||||
}
|
||||
|
||||
// CreateNEP17TransferTx creates an invocation transaction for the 'transfer'
|
||||
|
|
|
@ -2,20 +2,35 @@ package result
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
// Invoke represents code invocation result and is used by several RPC calls
|
||||
// that invoke functions, scripts and generic bytecode.
|
||||
type Invoke struct {
|
||||
State string
|
||||
GasConsumed int64
|
||||
Script []byte
|
||||
Stack []stackitem.Item
|
||||
FaultException string
|
||||
Transaction *transaction.Transaction
|
||||
State string
|
||||
GasConsumed int64
|
||||
Script []byte
|
||||
Stack []stackitem.Item
|
||||
FaultException string
|
||||
Transaction *transaction.Transaction
|
||||
maxIteratorResultItems int
|
||||
}
|
||||
|
||||
// NewInvoke returns new Invoke structure with the given fields set.
|
||||
func NewInvoke(vm *vm.VM, script []byte, faultException string, maxIteratorResultItems int) *Invoke {
|
||||
return &Invoke{
|
||||
State: vm.State().String(),
|
||||
GasConsumed: vm.GasConsumed(),
|
||||
Script: script,
|
||||
Stack: vm.Estack().ToArray(),
|
||||
FaultException: faultException,
|
||||
maxIteratorResultItems: maxIteratorResultItems,
|
||||
}
|
||||
}
|
||||
|
||||
type invokeAux struct {
|
||||
|
@ -27,15 +42,51 @@ type invokeAux struct {
|
|||
Transaction []byte `json:"tx,omitempty"`
|
||||
}
|
||||
|
||||
type iteratorAux struct {
|
||||
Type string `json:"type"`
|
||||
Value []json.RawMessage `json:"iterator"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
// Iterator represents deserialized VM iterator values with truncated flag.
|
||||
type Iterator struct {
|
||||
Values []stackitem.Item
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (r Invoke) MarshalJSON() ([]byte, error) {
|
||||
var st json.RawMessage
|
||||
arr := make([]json.RawMessage, len(r.Stack))
|
||||
for i := range arr {
|
||||
data, err := stackitem.ToJSONWithTypes(r.Stack[i])
|
||||
if err != nil {
|
||||
st = []byte(`"error: recursive reference"`)
|
||||
break
|
||||
var (
|
||||
data []byte
|
||||
err error
|
||||
)
|
||||
if (r.Stack[i].Type() == stackitem.InteropT) && vm.IsIterator(r.Stack[i]) {
|
||||
iteratorValues, truncated := vm.IteratorValues(r.Stack[i], r.maxIteratorResultItems)
|
||||
value := make([]json.RawMessage, len(iteratorValues))
|
||||
for j := range iteratorValues {
|
||||
value[j], err = stackitem.ToJSONWithTypes(iteratorValues[j])
|
||||
if err != nil {
|
||||
st = []byte(`"error: recursive reference"`)
|
||||
break
|
||||
}
|
||||
}
|
||||
data, err = json.Marshal(iteratorAux{
|
||||
Type: stackitem.InteropT.String(),
|
||||
Value: value,
|
||||
Truncated: truncated,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal iterator: %w", err)
|
||||
}
|
||||
} else {
|
||||
data, err = stackitem.ToJSONWithTypes(r.Stack[i])
|
||||
if err != nil {
|
||||
st = []byte(`"error: recursive reference"`)
|
||||
break
|
||||
}
|
||||
}
|
||||
arr[i] = data
|
||||
}
|
||||
|
@ -76,6 +127,26 @@ func (r *Invoke) UnmarshalJSON(data []byte) error {
|
|||
if err != nil {
|
||||
break
|
||||
}
|
||||
if st[i].Type() == stackitem.InteropT {
|
||||
iteratorAux := new(iteratorAux)
|
||||
if json.Unmarshal(arr[i], iteratorAux) == nil {
|
||||
iteratorValues := make([]stackitem.Item, len(iteratorAux.Value))
|
||||
for j := range iteratorValues {
|
||||
iteratorValues[j], err = stackitem.FromJSONWithTypes(iteratorAux.Value[j])
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to unmarshal iterator values: %w", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// it's impossible to restore initial iterator type; also iterator is almost
|
||||
// useless outside of the VM, thus let's replace it with a special structure.
|
||||
st[i] = stackitem.NewInterop(Iterator{
|
||||
Values: iteratorValues,
|
||||
Truncated: iteratorAux.Truncated,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
r.Stack = st
|
||||
|
|
|
@ -12,9 +12,10 @@ type (
|
|||
EnableCORSWorkaround bool `yaml:"EnableCORSWorkaround"`
|
||||
// MaxGasInvoke is a maximum amount of gas which
|
||||
// can be spent during RPC call.
|
||||
MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"`
|
||||
Port uint16 `yaml:"Port"`
|
||||
TLSConfig TLSConfig `yaml:"TLSConfig"`
|
||||
MaxGasInvoke fixedn.Fixed8 `yaml:"MaxGasInvoke"`
|
||||
MaxIteratorResultItems int `yaml:"MaxIteratorResultItems"`
|
||||
Port uint16 `yaml:"Port"`
|
||||
TLSConfig TLSConfig `yaml:"TLSConfig"`
|
||||
}
|
||||
|
||||
// TLSConfig describes SSL/TLS configuration.
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
|
||||
"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/trigger"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||
|
@ -800,6 +801,17 @@ func TestClient_NEP11(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.Equal(t, "NNS", sym)
|
||||
})
|
||||
t.Run("TokenInfo", func(t *testing.T) {
|
||||
tok, err := c.NEP11TokenInfo(h)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &wallet.Token{
|
||||
Name: nativenames.NameService,
|
||||
Hash: h,
|
||||
Decimals: 0,
|
||||
Symbol: "NNS",
|
||||
Standard: manifest.NEP11StandardName,
|
||||
}, tok)
|
||||
})
|
||||
t.Run("BalanceOf", func(t *testing.T) {
|
||||
b, err := c.NEP11BalanceOf(h, acc)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -1320,14 +1320,7 @@ func (s *Server) runScriptInVM(t trigger.Type, script []byte, contractScriptHash
|
|||
if err != nil {
|
||||
faultException = err.Error()
|
||||
}
|
||||
result := &result.Invoke{
|
||||
State: vm.State().String(),
|
||||
GasConsumed: vm.GasConsumed(),
|
||||
Script: script,
|
||||
Stack: vm.Estack().ToArray(),
|
||||
FaultException: faultException,
|
||||
}
|
||||
return result, nil
|
||||
return result.NewInvoke(vm, script, faultException, s.config.MaxIteratorResultItems), nil
|
||||
}
|
||||
|
||||
// submitBlock broadcasts a raw block over the NEO network.
|
||||
|
|
|
@ -14,8 +14,6 @@ const (
|
|||
// MaxManifestSize is a max length for a valid contract manifest.
|
||||
MaxManifestSize = math.MaxUint16
|
||||
|
||||
// NEP10StandardName represents the name of NEP10 smartcontract standard.
|
||||
NEP10StandardName = "NEP-10"
|
||||
// NEP11StandardName represents the name of NEP11 smartcontract standard.
|
||||
NEP11StandardName = "NEP-11"
|
||||
// NEP17StandardName represents the name of NEP17 smartcontract standard.
|
||||
|
|
|
@ -71,6 +71,12 @@ func init() {
|
|||
})
|
||||
}
|
||||
|
||||
// IsIterator returns whether stackitem implements iterator interface.
|
||||
func IsIterator(item stackitem.Item) bool {
|
||||
_, ok := item.Value().(iterator)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IteratorNext handles syscall System.Enumerator.Next.
|
||||
func IteratorNext(v *VM) error {
|
||||
iop := v.Estack().Pop().Interop()
|
||||
|
@ -89,6 +95,18 @@ func IteratorValue(v *VM) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// IteratorValues returns an array of up to `max` iterator values. The second
|
||||
// return parameter denotes whether iterator is truncated.
|
||||
func IteratorValues(item stackitem.Item, max int) ([]stackitem.Item, bool) {
|
||||
var result []stackitem.Item
|
||||
arr := item.Value().(iterator)
|
||||
for arr.Next() && max > 0 {
|
||||
result = append(result, arr.Value())
|
||||
max--
|
||||
}
|
||||
return result, arr.Next()
|
||||
}
|
||||
|
||||
// NewIterator creates new iterator from the provided stack item.
|
||||
func NewIterator(item stackitem.Item) (stackitem.Item, error) {
|
||||
switch t := item.(type) {
|
||||
|
|
|
@ -11,15 +11,17 @@ type Token struct {
|
|||
Hash util.Uint160 `json:"script_hash"`
|
||||
Decimals int64 `json:"decimals"`
|
||||
Symbol string `json:"symbol"`
|
||||
Standard string `json:"standard"`
|
||||
}
|
||||
|
||||
// NewToken returns new token contract info.
|
||||
func NewToken(tokenHash util.Uint160, name, symbol string, decimals int64) *Token {
|
||||
func NewToken(tokenHash util.Uint160, name, symbol string, decimals int64, standardName string) *Token {
|
||||
return &Token{
|
||||
Name: name,
|
||||
Hash: tokenHash,
|
||||
Decimals: decimals,
|
||||
Symbol: symbol,
|
||||
Standard: standardName,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -13,7 +14,7 @@ func TestToken_MarshalJSON(t *testing.T) {
|
|||
h, err := util.Uint160DecodeStringLE("f8d448b227991cf07cb96a6f9c0322437f1599b9")
|
||||
require.NoError(t, err)
|
||||
|
||||
tok := NewToken(h, "NEP17 Standard", "NEP17", 8)
|
||||
tok := NewToken(h, "NEP17 Standard", "NEP17", 8, manifest.NEP17StandardName)
|
||||
require.Equal(t, "NEP17 Standard", tok.Name)
|
||||
require.Equal(t, "NEP17", tok.Symbol)
|
||||
require.EqualValues(t, 8, tok.Decimals)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -146,7 +147,7 @@ func removeWallet(t *testing.T, walletPath string) {
|
|||
|
||||
func TestWallet_AddToken(t *testing.T) {
|
||||
w := checkWalletConstructor(t)
|
||||
tok := NewToken(util.Uint160{1, 2, 3}, "Rubl", "RUB", 2)
|
||||
tok := NewToken(util.Uint160{1, 2, 3}, "Rubl", "RUB", 2, manifest.NEP17StandardName)
|
||||
require.Equal(t, 0, len(w.Extra.Tokens))
|
||||
w.AddToken(tok)
|
||||
require.Equal(t, 1, len(w.Extra.Tokens))
|
||||
|
|
Loading…
Reference in a new issue