forked from TrueCloudLab/neoneo-go
Merge pull request #694 from nspcc-dev/feature/getclaimable
rpc, cli: support gas claim and asset transfer
This commit is contained in:
commit
657f5e46d0
25 changed files with 773 additions and 62 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) {
|
||||
|
|
22
pkg/rpc/response/result/claimable.go
Normal file
22
pkg/rpc/response/result/claimable.go
Normal 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"`
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: "[]",
|
||||
|
|
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
BIN
pkg/rpc/server/testdata/testblocks.acc
vendored
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -214,7 +214,7 @@ func (i *BoolItem) MarshalJSON() ([]byte, error) {
|
|||
}
|
||||
|
||||
func (i *BoolItem) String() string {
|
||||
return "Bool"
|
||||
return "Boolean"
|
||||
}
|
||||
|
||||
// Dup implements StackItem interface.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue