Merge pull request #694 from nspcc-dev/feature/getclaimable

rpc, cli: support gas claim and asset transfer
This commit is contained in:
Roman Khimov 2020-03-02 18:17:49 +03:00 committed by GitHub
commit 657f5e46d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 773 additions and 62 deletions

View file

@ -2,14 +2,20 @@ package wallet
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"strings"
"syscall"
"github.com/CityOfZion/neo-go/pkg/core"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/encoding/address"
"github.com/CityOfZion/neo-go/pkg/rpc/client"
"github.com/CityOfZion/neo-go/pkg/rpc/request"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/wallet"
"github.com/urfave/cli"
"golang.org/x/crypto/ssh/terminal"
@ -33,6 +39,14 @@ var (
Name: "decrypt, d",
Usage: "Decrypt encrypted keys.",
}
rpcFlag = cli.StringFlag{
Name: "rpc, r",
Usage: "RPC node address",
}
timeoutFlag = cli.DurationFlag{
Name: "timeout, t",
Usage: "Timeout for the operation",
}
)
// NewCommands returns 'wallet' command.
@ -41,6 +55,20 @@ func NewCommands() []cli.Command {
Name: "wallet",
Usage: "create, open and manage a NEO wallet",
Subcommands: []cli.Command{
{
Name: "claim",
Usage: "claim GAS",
Action: claimGas,
Flags: []cli.Flag{
walletPathFlag,
rpcFlag,
timeoutFlag,
cli.StringFlag{
Name: "address, a",
Usage: "Address to claim GAS for",
},
},
},
{
Name: "create",
Usage: "create a new wallet",
@ -112,10 +140,106 @@ func NewCommands() []cli.Command {
},
},
},
{
Name: "transfer",
Usage: "transfer NEO/GAS",
UsageText: "transfer --path <path> --from <addr> --to <addr>" +
" --amount <amount> --asset [NEO|GAS|<hex-id>]",
Action: transferAsset,
Flags: []cli.Flag{
walletPathFlag,
rpcFlag,
timeoutFlag,
cli.StringFlag{
Name: "from",
Usage: "Address to send an asset from",
},
cli.StringFlag{
Name: "to",
Usage: "Address to send an asset to",
},
cli.StringFlag{
Name: "amount",
Usage: "Amount of asset to send",
},
cli.StringFlag{
Name: "asset",
Usage: "Asset ID",
},
},
},
},
}}
}
func claimGas(ctx *cli.Context) error {
wall, err := openWallet(ctx.String("path"))
if err != nil {
return cli.NewExitError(err, 1)
}
defer wall.Close()
addr := ctx.String("address")
scriptHash, err := address.StringToUint160(addr)
if err != nil {
return cli.NewExitError(err, 1)
}
acc := wall.GetAccount(scriptHash)
if acc == nil {
return cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", addr), 1)
}
pass, err := readPassword("Enter password > ")
if err != nil {
return cli.NewExitError(err, 1)
} else if err := acc.Decrypt(pass); err != nil {
return cli.NewExitError(err, 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)
}
info, err := c.GetClaimable(addr)
if err != nil {
return cli.NewExitError(err, 1)
} else if info.Unclaimed == 0 || len(info.Spents) == 0 {
fmt.Println("Nothing to claim")
return nil
}
var claim transaction.ClaimTX
for i := range info.Spents {
claim.Claims = append(claim.Claims, transaction.Input{
PrevHash: info.Spents[i].Tx,
PrevIndex: uint16(info.Spents[i].N),
})
}
tx := &transaction.Transaction{
Type: transaction.ClaimType,
Data: &claim,
}
tx.AddOutput(&transaction.Output{
AssetID: core.UtilityTokenID(),
Amount: info.Unclaimed,
ScriptHash: scriptHash,
})
_ = acc.SignTx(tx)
if err := c.SendRawTransaction(tx); err != nil {
return cli.NewExitError(err, 1)
}
fmt.Println(tx.Hash().StringLE())
return nil
}
func addAccount(ctx *cli.Context) error {
wall, err := openWallet(ctx.String("path"))
if err != nil {
@ -249,6 +373,81 @@ func importWallet(ctx *cli.Context) error {
return nil
}
func transferAsset(ctx *cli.Context) error {
wall, err := openWallet(ctx.String("path"))
if err != nil {
return cli.NewExitError(err, 1)
}
defer wall.Close()
from := ctx.String("from")
addr, err := address.StringToUint160(from)
if err != nil {
return cli.NewExitError("invalid address", 1)
}
acc := wall.GetAccount(addr)
if acc == nil {
return cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", addr), 1)
}
asset, err := getAssetID(ctx.String("asset"))
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid asset id: %v", err), 1)
}
amount, err := util.Fixed8FromString(ctx.String("amount"))
if err != nil {
return cli.NewExitError(fmt.Errorf("invalid amount: %v", err), 1)
}
pass, err := readPassword("Enter wallet password > ")
if err != nil {
return cli.NewExitError(err, 1)
} else if err := acc.Decrypt(pass); err != nil {
return cli.NewExitError(err, 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)
}
tx := transaction.NewContractTX()
tx.Data = new(transaction.ContractTX)
if err := request.AddInputsAndUnspentsToTx(tx, from, asset, amount, c); err != nil {
return cli.NewExitError(err, 1)
}
toAddr, err := address.StringToUint160(ctx.String("to"))
if err != nil {
return cli.NewExitError(err, 1)
}
tx.AddOutput(&transaction.Output{
AssetID: asset,
Amount: amount,
ScriptHash: toAddr,
Position: 1,
})
_ = acc.SignTx(tx)
if err := c.SendRawTransaction(tx); err != nil {
return cli.NewExitError(err, 1)
}
fmt.Println(tx.Hash().StringLE())
return nil
}
func getGoContext(ctx *cli.Context) (context.Context, func()) {
if dur := ctx.Duration("timeout"); dur != 0 {
return context.WithTimeout(context.Background(), dur)
}
return context.Background(), func() {}
}
func dumpWallet(ctx *cli.Context) error {
wall, err := openWallet(ctx.String("path"))
if err != nil {
@ -331,6 +530,18 @@ func openWallet(path string) (*wallet.Wallet, error) {
return wallet.NewWalletFromFile(path)
}
func getAssetID(s string) (util.Uint256, error) {
s = strings.ToLower(s)
switch {
case s == "neo":
return core.GoverningTokenID(), nil
case s == "gas":
return core.UtilityTokenID(), nil
default:
return util.Uint256DecodeStringLE(s)
}
}
func newAccountFromWIF(wif string) (*wallet.Account, error) {
// note: NEP2 strings always have length of 58 even though
// base58 strings can have different lengths even if slice lengths are equal

View file

@ -11,7 +11,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/encoding/address"
"github.com/CityOfZion/neo-go/pkg/network"
"github.com/CityOfZion/neo-go/pkg/rpc/request"
"github.com/CityOfZion/neo-go/pkg/wallet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
@ -49,10 +49,12 @@ func prepareData(t *testing.B) []*transaction.Transaction {
var data []*transaction.Transaction
wif := getWif(t)
acc, err := wallet.NewAccountFromWIF(wif.S)
require.NoError(t, err)
for n := 0; n < t.N; n++ {
tx := getTX(t, wif)
require.NoError(t, request.SignTx(tx, wif))
require.NoError(t, acc.SignTx(tx))
data = append(data, tx)
}
return data

View file

@ -476,16 +476,23 @@ func (s *service) getVerifiedTx(count int) []block.Transaction {
}
func (s *service) getValidators(txx ...block.Transaction) []crypto.PublicKey {
var pKeys []*keys.PublicKey
var (
pKeys []*keys.PublicKey
err error
)
if len(txx) == 0 {
pKeys, _ = s.Chain.GetValidators()
pKeys, err = s.Chain.GetValidators()
} else {
ntxx := make([]*transaction.Transaction, len(txx))
for i := range ntxx {
ntxx[i] = txx[i].(*transaction.Transaction)
}
pKeys, _ = s.Chain.GetValidators(ntxx...)
pKeys, err = s.Chain.GetValidators(ntxx...)
}
if err != nil {
s.log.Error("error while trying to get validators", zap.Error(err))
}
pubs := make([]crypto.PublicKey, len(pKeys))

View file

@ -29,7 +29,7 @@ import (
// Tuning parameters.
const (
headerBatchCount = 2000
version = "0.0.3"
version = "0.0.5"
// This one comes from C# code and it's different from the constant used
// when creating an asset with Neo.Asset.Create interop call. It looks
@ -93,6 +93,9 @@ type Blockchain struct {
// Number of headers stored in the chain file.
storedHeaderCount uint32
generationAmount []int
decrementInterval int
// All operations on headerList must be called from an
// headersOp to be routine safe.
headerList *HeaderHashList
@ -154,6 +157,9 @@ func NewBlockchain(s storage.Store, cfg config.ProtocolConfiguration, log *zap.L
memPool: mempool.NewMemPool(cfg.MemPoolSize),
keyCache: make(map[util.Uint160]map[string]*keys.PublicKey),
log: log,
generationAmount: genAmount,
decrementInterval: decrementInterval,
}
if err := bc.init(); err != nil {
@ -408,6 +414,7 @@ func (bc *Blockchain) processHeader(h *block.Header, batch storage.Batch, header
}
buf.Reset()
buf.BinWriter.WriteU32LE(0) // sys fee is yet to be calculated
h.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return buf.Err
@ -420,13 +427,24 @@ func (bc *Blockchain) processHeader(h *block.Header, batch storage.Batch, header
return nil
}
// bc.GetHeaderHash(int(endHeight)) returns sum of all system fees for blocks up to h.
// and 0 if no such block exists.
func (bc *Blockchain) getSystemFeeAmount(h util.Uint256) uint32 {
_, sf, _ := bc.dao.GetBlock(h)
return sf
}
// TODO: storeBlock needs some more love, its implemented as in the original
// project. This for the sake of development speed and understanding of what
// is happening here, quite allot as you can see :). If things are wired together
// and all tests are in place, we can make a more optimized and cleaner implementation.
func (bc *Blockchain) storeBlock(block *block.Block) error {
cache := newCachedDao(bc.dao.store)
if err := cache.StoreAsBlock(block, 0); err != nil {
fee := bc.getSystemFeeAmount(block.PrevHash)
for _, tx := range block.Transactions {
fee += uint32(bc.SystemFee(tx).Int64Value())
}
if err := cache.StoreAsBlock(block, fee); err != nil {
return err
}
@ -473,6 +491,13 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
}
if prevTXOutput.AssetID.Equals(GoverningTokenID()) {
account.Unclaimed = append(account.Unclaimed, state.UnclaimedBalance{
Tx: prevTX.Hash(),
Index: input.PrevIndex,
Start: prevTXHeight,
End: block.Index,
Value: prevTXOutput.Amount,
})
spentCoin.items[input.PrevIndex] = block.Index
if err = processTXWithValidatorsSubtract(&prevTXOutput, account, cache); err != nil {
return err
@ -564,6 +589,37 @@ func (bc *Blockchain) storeBlock(block *block.Block) error {
}
break
}
prevTx, _, err := cache.GetTransaction(input.PrevHash)
if err != nil {
return err
} else if int(input.PrevIndex) > len(prevTx.Outputs) {
return errors.New("invalid input in claim")
}
acc, err := cache.GetAccountState(prevTx.Outputs[input.PrevIndex].ScriptHash)
if err != nil {
return err
}
var changed bool
for i := range acc.Unclaimed {
if acc.Unclaimed[i].Tx == input.PrevHash && acc.Unclaimed[i].Index == input.PrevIndex {
copy(acc.Unclaimed[i:], acc.Unclaimed[i+1:])
acc.Unclaimed = acc.Unclaimed[:len(acc.Unclaimed)-1]
changed = true
break
}
}
if !changed {
bc.log.Warn("no spent coin in the account",
zap.String("tx", tx.Hash().StringLE()),
zap.String("input", input.PrevHash.StringLE()),
zap.String("account", acc.ScriptHash.String()))
} else if err := cache.PutAccountState(acc); err != nil {
return err
}
delete(scs.items, input.PrevIndex)
if len(scs.items) > 0 {
if err = cache.PutSpentCoinState(input.PrevHash, scs); err != nil {
@ -905,7 +961,7 @@ func (bc *Blockchain) GetBlock(hash util.Uint256) (*block.Block, error) {
}
}
block, err := bc.dao.GetBlock(hash)
block, _, err := bc.dao.GetBlock(hash)
if err != nil {
return nil, err
}
@ -930,7 +986,7 @@ func (bc *Blockchain) GetHeader(hash util.Uint256) (*block.Header, error) {
return tb.Header(), nil
}
}
block, err := bc.dao.GetBlock(hash)
block, _, err := bc.dao.GetBlock(hash)
if err != nil {
return nil, err
}
@ -1033,6 +1089,48 @@ func (bc *Blockchain) GetConfig() config.ProtocolConfiguration {
return bc.config
}
// CalculateClaimable calculates the amount of GAS which can be claimed for a transaction with value.
// First return value is GAS generated between startHeight and endHeight.
// Second return value is GAS returned from accumulated SystemFees between startHeight and endHeight.
func (bc *Blockchain) CalculateClaimable(value util.Fixed8, startHeight, endHeight uint32) (util.Fixed8, util.Fixed8, error) {
var amount util.Fixed8
di := uint32(bc.decrementInterval)
ustart := startHeight / di
if genSize := uint32(len(bc.generationAmount)); ustart < genSize {
uend := endHeight / di
iend := endHeight % di
if uend >= genSize {
uend = genSize - 1
iend = di
} else if iend == 0 {
uend--
iend = di
}
istart := startHeight % di
for ustart < uend {
amount += util.Fixed8(di-istart) * util.Fixed8(bc.generationAmount[ustart])
ustart++
istart = 0
}
amount += util.Fixed8(iend-istart) * util.Fixed8(bc.generationAmount[ustart])
}
if startHeight == 0 {
startHeight++
}
h := bc.GetHeaderHash(int(startHeight - 1))
feeStart := bc.getSystemFeeAmount(h)
h = bc.GetHeaderHash(int(endHeight - 1))
feeEnd := bc.getSystemFeeAmount(h)
sysFeeTotal := util.Fixed8(feeEnd - feeStart)
ratio := value / 100000000
return amount * ratio, sysFeeTotal * ratio, nil
}
// References maps transaction's inputs into a slice of InOuts, effectively
// joining each Input with the corresponding Output.
// @TODO: unfortunately we couldn't attach this method to the Transaction struct in the
@ -1070,6 +1168,11 @@ func (bc *Blockchain) FeePerByte(t *transaction.Transaction) util.Fixed8 {
// NetworkFee returns network fee.
func (bc *Blockchain) NetworkFee(t *transaction.Transaction) util.Fixed8 {
// https://github.com/neo-project/neo/blob/master-2.x/neo/Network/P2P/Payloads/ClaimTransaction.cs#L16
if t.Type == transaction.ClaimType || t.Type == transaction.MinerType {
return 0
}
inputAmount := util.Fixed8FromInt64(0)
refs, err := bc.References(t)
if err != nil {
@ -1175,11 +1278,97 @@ func (bc *Blockchain) verifyTx(t *transaction.Transaction, block *block.Block) e
if bc.dao.IsDoubleClaim(claim) {
return errors.New("double claim")
}
if err := bc.verifyClaims(t); err != nil {
return err
}
}
return bc.verifyTxWitnesses(t, block)
}
func (bc *Blockchain) verifyClaims(tx *transaction.Transaction) (err error) {
t := tx.Data.(*transaction.ClaimTX)
var result *transaction.Result
results := bc.GetTransactionResults(tx)
for i := range results {
if results[i].AssetID == UtilityTokenID() {
result = results[i]
break
}
}
if result == nil || result.Amount.GreaterThan(0) {
return errors.New("invalid output in claim tx")
}
bonus, err := bc.calculateBonus(t.Claims)
if err == nil && bonus != -result.Amount {
return fmt.Errorf("wrong bonus calculated in claim tx: %s != %s",
bonus.String(), (-result.Amount).String())
}
return err
}
func (bc *Blockchain) calculateBonus(claims []transaction.Input) (util.Fixed8, error) {
unclaimed := []*spentCoin{}
inputs := transaction.GroupInputsByPrevHash(claims)
for _, group := range inputs {
h := group[0].PrevHash
claimable, err := bc.getUnclaimed(h)
if err != nil || len(claimable) == 0 {
return 0, errors.New("no unclaimed inputs")
}
for _, c := range group {
s, ok := claimable[c.PrevIndex]
if !ok {
return 0, fmt.Errorf("can't find spent coins for %s (%d)", c.PrevHash.StringLE(), c.PrevIndex)
}
unclaimed = append(unclaimed, s)
}
}
return bc.calculateBonusInternal(unclaimed)
}
func (bc *Blockchain) calculateBonusInternal(scs []*spentCoin) (util.Fixed8, error) {
var claimed util.Fixed8
for _, sc := range scs {
gen, sys, err := bc.CalculateClaimable(sc.Output.Amount, sc.StartHeight, sc.EndHeight)
if err != nil {
return 0, err
}
claimed += gen + sys
}
return claimed, nil
}
func (bc *Blockchain) getUnclaimed(h util.Uint256) (map[uint16]*spentCoin, error) {
tx, txHeight, err := bc.GetTransaction(h)
if err != nil {
return nil, err
}
scs, err := bc.dao.GetSpentCoinState(h)
if err != nil {
return nil, err
}
result := make(map[uint16]*spentCoin)
for i, height := range scs.items {
result[i] = &spentCoin{
Output: &tx.Outputs[i],
StartHeight: txHeight,
EndHeight: height,
}
}
return result, nil
}
// isTxStillRelevant is a callback for mempool transaction filtering after the
// new block addition. It returns false for transactions already present in the
// chain (added by the new block), transactions using some inputs that are
@ -1396,7 +1585,7 @@ func (bc *Blockchain) GetValidators(txes ...*transaction.Transaction) ([]*keys.P
for _, tx := range txes {
// iterate through outputs
for index, output := range tx.Outputs {
accountState, err := cache.GetAccountState(output.ScriptHash)
accountState, err := cache.GetAccountStateOrNew(output.ScriptHash)
if err != nil {
return nil, err
}

View file

@ -175,6 +175,43 @@ func TestGetTransaction(t *testing.T) {
}
}
func TestGetClaimable(t *testing.T) {
bc := newTestChain(t)
bc.generationAmount = []int{4, 3, 2, 1}
bc.decrementInterval = 2
_, err := bc.genBlocks(10)
require.NoError(t, err)
t.Run("first generation period", func(t *testing.T) {
amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 0, 2)
require.NoError(t, err)
require.EqualValues(t, 8, amount)
require.EqualValues(t, 0, sysfee)
})
t.Run("a number of full periods", func(t *testing.T) {
amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 0, 6)
require.NoError(t, err)
require.EqualValues(t, 4+4+3+3+2+2, amount)
require.EqualValues(t, 0, sysfee)
})
t.Run("start from the 2-nd block", func(t *testing.T) {
amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 1, 7)
require.NoError(t, err)
require.EqualValues(t, 4+3+3+2+2+1, amount)
require.EqualValues(t, 0, sysfee)
})
t.Run("end height after generation has ended", func(t *testing.T) {
amount, sysfee, err := bc.CalculateClaimable(util.Fixed8FromInt64(1), 1, 10)
require.NoError(t, err)
require.EqualValues(t, 4+3+3+2+2+1+1, amount)
require.EqualValues(t, 0, sysfee)
})
}
func TestClose(t *testing.T) {
defer func() {
r := recover()

View file

@ -20,6 +20,7 @@ type Blockchainer interface {
AddHeaders(...*block.Header) error
AddBlock(*block.Block) error
BlockHeight() uint32
CalculateClaimable(value util.Fixed8, startHeight, endHeight uint32) (util.Fixed8, util.Fixed8, error)
Close()
HeaderHeight() uint32
GetBlock(hash util.Uint256) (*block.Block, error)

View file

@ -375,17 +375,18 @@ func makeStorageItemKey(scripthash util.Uint160, key []byte) []byte {
// -- other.
// GetBlock returns Block by the given hash if it exists in the store.
func (dao *dao) GetBlock(hash util.Uint256) (*block.Block, error) {
func (dao *dao) GetBlock(hash util.Uint256) (*block.Block, uint32, error) {
key := storage.AppendPrefix(storage.DataBlock, hash.BytesLE())
b, err := dao.store.Get(key)
if err != nil {
return nil, err
return nil, 0, err
}
block, err := block.NewBlockFromTrimmedBytes(b)
block, err := block.NewBlockFromTrimmedBytes(b[4:])
if err != nil {
return nil, err
return nil, 0, err
}
return block, err
return block, binary.LittleEndian.Uint32(b[:4]), nil
}
// GetVersion attempts to get the current version stored in the
@ -508,8 +509,7 @@ func (dao *dao) StoreAsBlock(block *block.Block, sysFee uint32) error {
key = storage.AppendPrefix(storage.DataBlock, block.Hash().BytesLE())
buf = io.NewBufBinWriter()
)
// sysFee needs to be handled somehow
// buf.WriteU32LE(sysFee)
buf.WriteU32LE(sysFee)
b, err := block.Trim()
if err != nil {
return err

View file

@ -254,7 +254,7 @@ func TestDeleteStorageItem(t *testing.T) {
func TestGetBlock_NotExists(t *testing.T) {
dao := newDao(storage.NewMemoryStore())
hash := random.Uint256()
block, err := dao.GetBlock(hash)
block, _, err := dao.GetBlock(hash)
require.Error(t, err)
require.Nil(t, block)
}
@ -270,11 +270,12 @@ func TestPutGetBlock(t *testing.T) {
},
}
hash := b.Hash()
err := dao.StoreAsBlock(b, 0)
err := dao.StoreAsBlock(b, 42)
require.NoError(t, err)
gotBlock, err := dao.GetBlock(hash)
gotBlock, sysfee, err := dao.GetBlock(hash)
require.NoError(t, err)
require.NotNil(t, gotBlock)
require.EqualValues(t, 42, sysfee)
}
func TestGetVersion_NoVersion(t *testing.T) {

View file

@ -20,6 +20,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm/emit"
"github.com/CityOfZion/neo-go/pkg/vm/opcode"
"github.com/CityOfZion/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
@ -158,6 +159,11 @@ func newDumbBlock() *block.Block {
}
}
func getInvocationScript(data []byte, priv *keys.PrivateKey) []byte {
signature := priv.Sign(data)
return append([]byte{byte(opcode.PUSHBYTES64)}, signature...)
}
// This function generates "../rpc/testdata/testblocks.acc" file which contains data
// for RPC unit tests.
// To generate new "../rpc/testdata/testblocks.acc", follow the steps:
@ -165,6 +171,8 @@ func newDumbBlock() *block.Block {
// 2. Add specific test-case into "neo-go/pkg/core/blockchain_test.go"
// 3. Run tests with `$ make test`
func _(t *testing.T) {
const prefix = "../rpc/server/testdata/"
bc := newTestChain(t)
n := 50
_, err := bc.genBlocks(n)
@ -172,7 +180,7 @@ func _(t *testing.T) {
tx1 := newMinerTX()
avm, err := ioutil.ReadFile("../rpc/testdata/test_contract.avm")
avm, err := ioutil.ReadFile(prefix + "test_contract.avm")
require.NoError(t, err)
var props smartcontract.PropertyState
@ -206,10 +214,76 @@ func _(t *testing.T) {
emit.AppCall(script.BinWriter, hash.Hash160(avm), false)
tx3 := transaction.NewInvocationTX(script.Bytes(), util.Fixed8FromFloat(100))
b := bc.newBlock(newMinerTX(), tx3)
tx4 := transaction.NewContractTX()
h, err := util.Uint256DecodeStringBE("6da730b566db183bfceb863b780cd92dee2b497e5a023c322c1eaca81cf9ad7a")
require.NoError(t, err)
tx4.AddInput(&transaction.Input{
PrevHash: h,
PrevIndex: 0,
})
// multisig address which possess all NEO
scriptHash, err := util.Uint160DecodeStringBE("be48d3a3f5d10013ab9ffee489706078714f1ea2")
require.NoError(t, err)
priv, err := keys.NewPrivateKeyFromWIF(privNetKeys[0])
require.NoError(t, err)
tx4.AddOutput(&transaction.Output{
AssetID: GoverningTokenID(),
Amount: util.Fixed8FromInt64(1000),
ScriptHash: priv.GetScriptHash(),
Position: 0,
})
tx4.AddOutput(&transaction.Output{
AssetID: GoverningTokenID(),
Amount: util.Fixed8FromInt64(99999000),
ScriptHash: scriptHash,
Position: 1,
})
tx4.Data = new(transaction.ContractTX)
validators, err := getValidators(bc.config)
require.NoError(t, err)
rawScript, err := smartcontract.CreateMultiSigRedeemScript(len(bc.config.StandbyValidators)/2+1, validators)
require.NoError(t, err)
data := tx4.GetSignedPart()
var invoc []byte
for i := range privNetKeys {
priv, err := keys.NewPrivateKeyFromWIF(privNetKeys[i])
require.NoError(t, err)
invoc = append(invoc, getInvocationScript(data, priv)...)
}
tx4.Scripts = []transaction.Witness{{
InvocationScript: invoc,
VerificationScript: rawScript,
}}
b := bc.newBlock(newMinerTX(), tx3, tx4)
require.NoError(t, bc.AddBlock(b))
outStream, err := os.Create("../rpc/testdata/testblocks.acc")
priv1, err := keys.NewPrivateKeyFromWIF(privNetKeys[1])
require.NoError(t, err)
tx5 := transaction.NewContractTX()
tx5.Data = new(transaction.ContractTX)
tx5.AddInput(&transaction.Input{
PrevHash: tx4.Hash(),
PrevIndex: 0,
})
tx5.AddOutput(&transaction.Output{
AssetID: GoverningTokenID(),
Amount: util.Fixed8FromInt64(1000),
ScriptHash: priv1.GetScriptHash(),
})
acc, err := wallet.NewAccountFromWIF(priv.WIF())
require.NoError(t, err)
require.NoError(t, acc.SignTx(tx5))
b = bc.newBlock(newMinerTX(), tx5)
require.NoError(t, bc.AddBlock(b))
outStream, err := os.Create(prefix + "testblocks.acc")
require.NoError(t, err)
defer outStream.Close()
@ -225,6 +299,7 @@ func _(t *testing.T) {
buf := io.NewBufBinWriter()
b.EncodeBinary(buf.BinWriter)
bytes := buf.Bytes()
writer.WriteU32LE(uint32(len(bytes)))
writer.WriteBytes(bytes)
require.NoError(t, writer.Err)
}

View file

@ -1,6 +1,7 @@
package core
import (
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/io"
"github.com/CityOfZion/neo-go/pkg/util"
)
@ -14,6 +15,13 @@ type SpentCoinState struct {
items map[uint16]uint32
}
// spentCoin represents the state of a single spent coin output.
type spentCoin struct {
Output *transaction.Output
StartHeight uint32
EndHeight uint32
}
// NewSpentCoinState returns a new SpentCoinState object.
func NewSpentCoinState(hash util.Uint256, height uint32) *SpentCoinState {
return &SpentCoinState{

View file

@ -14,6 +14,16 @@ type UnspentBalance struct {
Value util.Fixed8 `json:"value"`
}
// UnclaimedBalance represents transaction output which was spent and
// can be claimed.
type UnclaimedBalance struct {
Tx util.Uint256
Index uint16
Start uint32
End uint32
Value util.Fixed8
}
// UnspentBalances is a slice of UnspentBalance (mostly needed to sort them).
type UnspentBalances []UnspentBalance
@ -24,6 +34,7 @@ type Account struct {
IsFrozen bool
Votes []*keys.PublicKey
Balances map[util.Uint256][]UnspentBalance
Unclaimed []UnclaimedBalance
}
// NewAccount returns a new Account object.
@ -34,6 +45,7 @@ func NewAccount(scriptHash util.Uint160) *Account {
IsFrozen: false,
Votes: []*keys.PublicKey{},
Balances: make(map[util.Uint256][]UnspentBalance),
Unclaimed: []UnclaimedBalance{},
}
}
@ -56,6 +68,8 @@ func (s *Account) DecodeBinary(br *io.BinReader) {
}
s.Balances[key] = ubs
}
br.ReadArray(&s.Unclaimed)
}
// EncodeBinary encodes Account to the given BinWriter.
@ -73,6 +87,8 @@ func (s *Account) EncodeBinary(bw *io.BinWriter) {
v[i].EncodeBinary(bw)
}
}
bw.WriteArray(s.Unclaimed)
}
// DecodeBinary implements io.Serializable interface.
@ -89,6 +105,24 @@ func (u *UnspentBalance) EncodeBinary(w *io.BinWriter) {
u.Value.EncodeBinary(w)
}
// DecodeBinary implements io.Serializable interface.
func (u *UnclaimedBalance) DecodeBinary(r *io.BinReader) {
u.Tx.DecodeBinary(r)
u.Index = r.ReadU16LE()
u.Start = r.ReadU32LE()
u.End = r.ReadU32LE()
u.Value.DecodeBinary(r)
}
// EncodeBinary implements io.Serializable interface.
func (u *UnclaimedBalance) EncodeBinary(w *io.BinWriter) {
u.Tx.EncodeBinary(w)
w.WriteU16LE(u.Index)
w.WriteU32LE(u.Start)
w.WriteU32LE(u.End)
u.Value.EncodeBinary(w)
}
// GetBalanceValues sums all unspent outputs and returns a map of asset IDs to
// overall balances.
func (s *Account) GetBalanceValues() map[util.Uint256]util.Fixed8 {

View file

@ -100,6 +100,12 @@ func (t *Transaction) DecodeBinary(br *io.BinReader) {
br.ReadArray(&t.Attributes)
br.ReadArray(&t.Inputs)
br.ReadArray(&t.Outputs)
for i := range t.Outputs {
if t.Outputs[i].Amount.LessThan(0) {
br.Err = errors.New("negative output")
return
}
}
br.ReadArray(&t.Scripts)
// Create the hash of the transaction at decode, so we dont need
@ -197,6 +203,16 @@ func (t Transaction) GroupOutputByAssetID() map[util.Uint256][]*Output {
return m
}
// GetSignedPart returns a part of the transaction which must be signed.
func (t *Transaction) GetSignedPart() []byte {
buf := io.NewBufBinWriter()
t.encodeHashableFields(buf.BinWriter)
if buf.Err != nil {
return nil
}
return buf.Bytes()
}
// Bytes converts the transaction to []byte
func (t *Transaction) Bytes() []byte {
buf := io.NewBufBinWriter()

View file

@ -32,6 +32,9 @@ func (chain testChain) ApplyPolicyToTxSet([]mempool.TxWithFee) []mempool.TxWithF
func (chain testChain) GetConfig() config.ProtocolConfiguration {
panic("TODO")
}
func (chain testChain) CalculateClaimable(util.Fixed8, uint32, uint32) (util.Fixed8, util.Fixed8, error) {
panic("TODO")
}
func (chain testChain) References(t *transaction.Transaction) ([]transaction.InOut, error) {
panic("TODO")

View file

@ -11,6 +11,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/rpc/response/result"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/wallet"
"github.com/pkg/errors"
)
@ -43,6 +44,16 @@ func (c *Client) GetAccountState(address string) (*result.AccountState, error) {
return resp, nil
}
// GetClaimable returns tx outputs which can be claimed.
func (c *Client) GetClaimable(address string) (*result.ClaimableInfo, error) {
params := request.NewRawParams(address)
resp := new(result.ClaimableInfo)
if err := c.performRequest("getclaimable", params, resp); err != nil {
return nil, err
}
return resp, nil
}
// GetUnspents returns UTXOs for the given NEO account.
func (c *Client) GetUnspents(address string) (*result.Unspents, error) {
var (
@ -108,11 +119,11 @@ func (c *Client) Invoke(script string, params []smartcontract.Parameter) (*respo
// return resp, nil
// }
// sendRawTransaction broadcasts a transaction over the NEO network.
// SendRawTransaction broadcasts a transaction over the NEO network.
// The given hex string needs to be signed with a keypair.
// When the result of the response object is true, the TX has successfully
// been broadcasted to the network.
func (c *Client) sendRawTransaction(rawTX *transaction.Transaction) error {
func (c *Client) SendRawTransaction(rawTX *transaction.Transaction) error {
var (
params = request.NewRawParams(hex.EncodeToString(rawTX.Bytes()))
resp bool
@ -146,7 +157,7 @@ func (c *Client) TransferAsset(asset util.Uint256, address string, amount util.F
if rawTx, err = request.CreateRawContractTransaction(txParams); err != nil {
return resp, errors.Wrap(err, "failed to create raw transaction")
}
if err = c.sendRawTransaction(rawTx); err != nil {
if err = c.SendRawTransaction(rawTx); err != nil {
return resp, errors.Wrap(err, "failed to send raw transaction")
}
return rawTx.Hash(), nil
@ -169,11 +180,14 @@ func (c *Client) SignAndPushInvocationTx(script []byte, wif *keys.WIF, gas util.
}
}
if err = request.SignTx(tx, wif); err != nil {
acc, err := wallet.NewAccountFromWIF(wif.S)
if err != nil {
return txHash, err
} else if err = acc.SignTx(tx); err != nil {
return txHash, errors.Wrap(err, "failed to sign tx")
}
txHash = tx.Hash()
err = c.sendRawTransaction(tx)
err = c.SendRawTransaction(tx)
if err != nil {
return txHash, errors.Wrap(err, "failed sendning tx")

View file

@ -13,6 +13,7 @@ import (
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm/emit"
"github.com/CityOfZion/neo-go/pkg/vm/opcode"
"github.com/CityOfZion/neo-go/pkg/wallet"
errs "github.com/pkg/errors"
)
@ -48,7 +49,9 @@ func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transac
}
receiverOutput = transaction.NewOutput(assetID, amount, toAddressHash)
tx.AddOutput(receiverOutput)
if err = SignTx(tx, &wif); err != nil {
if acc, err := wallet.NewAccountFromWIF(wif.S); err != nil {
return nil, err
} else if err = acc.SignTx(tx); err != nil {
return nil, errs.Wrap(err, "failed to sign tx")
}
@ -77,36 +80,6 @@ func AddInputsAndUnspentsToTx(tx *transaction.Transaction, addr string, assetID
return nil
}
// SignTx signs given transaction in-place using given key.
func SignTx(tx *transaction.Transaction, wif *keys.WIF) error {
var witness transaction.Witness
var err error
if witness.InvocationScript, err = GetInvocationScript(tx, wif); err != nil {
return errs.Wrap(err, "failed to create invocation script")
}
witness.VerificationScript = wif.PrivateKey.PublicKey().GetVerificationScript()
tx.Scripts = append(tx.Scripts, witness)
tx.Hash()
return nil
}
// GetInvocationScript returns NEO VM script containing transaction signature.
func GetInvocationScript(tx *transaction.Transaction, wif *keys.WIF) ([]byte, error) {
var (
buf = io.NewBufBinWriter()
signature []byte
)
tx.EncodeBinary(buf.BinWriter)
if buf.Err != nil {
return nil, errs.Wrap(buf.Err, "Failed to encode transaction to binary")
}
data := buf.Bytes()
signature = wif.PrivateKey.Sign(data[:(len(data) - 1)])
return append([]byte{byte(opcode.PUSHBYTES64)}, signature...), nil
}
// CreateDeploymentScript returns a script that deploys given smart contract
// with its metadata.
func CreateDeploymentScript(avm []byte, contract *ContractDetails) ([]byte, error) {

View file

@ -0,0 +1,22 @@
package result
import "github.com/CityOfZion/neo-go/pkg/util"
// ClaimableInfo is a result of a getclaimable RPC call.
type ClaimableInfo struct {
Spents []Claimable `json:"claimable"`
Address string `json:"address"`
Unclaimed util.Fixed8 `json:"unclaimed"`
}
// Claimable represents spent outputs which can be claimed.
type Claimable struct {
Tx util.Uint256 `json:"txid"`
N int `json:"n"`
Value util.Fixed8 `json:"value"`
StartHeight uint32 `json:"start_height"`
EndHeight uint32 `json:"end_height"`
Generated util.Fixed8 `json:"generated"`
SysFee util.Fixed8 `json:"sys_fee"`
Unclaimed util.Fixed8 `json:"unclaimed"`
}

View file

@ -51,6 +51,14 @@ var (
},
)
getclaimableCalled = prometheus.NewCounter(
prometheus.CounterOpts{
Help: "Number of calls to getclaimable rpc endpoint",
Name: "getclaimable_called",
Namespace: "neogo",
},
)
getconnectioncountCalled = prometheus.NewCounter(
prometheus.CounterOpts{
Help: "Number of calls to getconnectioncount rpc endpoint",

View file

@ -190,6 +190,10 @@ Methods:
getblocksysfeeCalled.Inc()
results, resultsErr = s.getBlockSysFee(reqParams)
case "getclaimable":
getclaimableCalled.Inc()
results, resultsErr = s.getClaimable(reqParams)
case "getconnectioncount":
getconnectioncountCalled.Inc()
results = s.coreServer.PeerCount()
@ -322,6 +326,52 @@ func (s *Server) getApplicationLog(reqParams request.Params) (interface{}, error
return result.NewApplicationLog(appExecResult, scriptHash), nil
}
func (s *Server) getClaimable(ps request.Params) (interface{}, error) {
p, ok := ps.ValueWithType(0, request.StringT)
if !ok {
return nil, response.ErrInvalidParams
}
u, err := p.GetUint160FromAddress()
if err != nil {
return nil, response.ErrInvalidParams
}
var unclaimed []state.UnclaimedBalance
if acc := s.chain.GetAccountState(u); acc != nil {
unclaimed = acc.Unclaimed
}
var sum util.Fixed8
claimable := make([]result.Claimable, 0, len(unclaimed))
for _, ub := range unclaimed {
gen, sys, err := s.chain.CalculateClaimable(ub.Value, ub.Start, ub.End)
if err != nil {
s.log.Info("error while calculating claim bonus", zap.Error(err))
continue
}
uc := gen.Add(sys)
sum += uc
claimable = append(claimable, result.Claimable{
Tx: ub.Tx,
N: int(ub.Index),
Value: ub.Value,
StartHeight: ub.Start,
EndHeight: ub.End,
Generated: gen,
SysFee: sys,
Unclaimed: uc,
})
}
return result.ClaimableInfo{
Spents: claimable,
Address: p.String(),
Unclaimed: sum,
}, nil
}
func (s *Server) getStorage(ps request.Params) (interface{}, error) {
param, ok := ps.Value(0)
if !ok {

View file

@ -52,6 +52,7 @@ func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFu
nBlocks = br.ReadU32LE()
require.Nil(t, br.Err)
for i := 0; i < int(nBlocks); i++ {
_ = br.ReadU32LE()
b := &block.Block{}
b.DecodeBinary(br)
require.Nil(t, br.Err)

View file

@ -363,6 +363,40 @@ var rpcTestCases = map[string][]rpcTestCase{
fail: true,
},
},
"getclaimable": {
{
name: "no params",
params: "[]",
fail: true,
},
{
name: "invalid address",
params: `["invalid"]`,
fail: true,
},
{
name: "normal address",
params: `["AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU"]`,
result: func(*executor) interface{} {
// hash of the issueTx
h, _ := util.Uint256DecodeStringBE("6da730b566db183bfceb863b780cd92dee2b497e5a023c322c1eaca81cf9ad7a")
amount := util.Fixed8FromInt64(52 * 8) // (endHeight - startHeight) * genAmount[0]
return &result.ClaimableInfo{
Spents: []result.Claimable{
{
Tx: h,
Value: util.Fixed8FromInt64(100000000),
EndHeight: 52,
Generated: amount,
Unclaimed: amount,
},
},
Address: "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU",
Unclaimed: amount,
}
},
},
},
"getconnectioncount": {
{
params: "[]",

Binary file not shown.

View file

@ -122,7 +122,7 @@ func (pt *ParamType) DecodeBinary(r *io.BinReader) {
// bytes, bytearray -> ByteArrayType
// key, publickey -> PublicKeyType
// string -> StringType
// array -> ArrayType
// array, struct -> ArrayType
// map -> MapType
// interopinterface -> InteropInterfaceType
// void -> VoidType
@ -145,7 +145,7 @@ func ParseParamType(typ string) (ParamType, error) {
return PublicKeyType, nil
case "string":
return StringType, nil
case "array":
case "array", "struct":
return ArrayType, nil
case "map":
return MapType, nil

View file

@ -214,7 +214,7 @@ func (i *BoolItem) MarshalJSON() ([]byte, error) {
}
func (i *BoolItem) String() string {
return "Bool"
return "Boolean"
}
// Dup implements StackItem interface.

View file

@ -7,11 +7,13 @@ import (
"errors"
"fmt"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/crypto/keys"
"github.com/CityOfZion/neo-go/pkg/encoding/address"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm/opcode"
)
// Account represents a NEO account. It holds the private and public key
@ -122,6 +124,29 @@ func NewAccount() (*Account, error) {
return newAccountFromPrivateKey(priv), nil
}
// SignTx signs transaction t and updates it's Witnesses.
func (a *Account) SignTx(t *transaction.Transaction) error {
if a.privateKey == nil {
return errors.New("account is not unlocked")
}
data := t.GetSignedPart()
sign := a.privateKey.Sign(data)
t.Scripts = append(t.Scripts, transaction.Witness{
InvocationScript: append([]byte{byte(opcode.PUSHBYTES64)}, sign...),
VerificationScript: a.getVerificationScript(),
})
return nil
}
func (a *Account) getVerificationScript() []byte {
if a.Contract != nil {
return a.Contract.Script
}
return a.PrivateKey().PublicKey().GetVerificationScript()
}
// Decrypt decrypts the EncryptedWIF with the given passphrase returning error
// if anything goes wrong.
func (a *Account) Decrypt(passphrase string) error {