SendToAddress RPC call (#114)

* func to get private key from raw bytes

* added function to create raw transfer tx

* fixes

* more fixes

* prettify code and comments; neoscan interaction put in dedicated files
This commit is contained in:
Anastasia Prasolova 2018-12-21 12:32:18 +03:00 committed by Anthony De Meulemeester
parent 9c605735b7
commit 74f0019df2
9 changed files with 352 additions and 3 deletions

View file

@ -8,6 +8,12 @@ import (
// This TX has not special attributes. // This TX has not special attributes.
type ContractTX struct{} type ContractTX struct{}
func NewContractTX() *Transaction {
return &Transaction{
Type: ContractType,
}
}
// DecodeBinary implements the Payload interface. // DecodeBinary implements the Payload interface.
func (tx *ContractTX) DecodeBinary(r io.Reader) error { func (tx *ContractTX) DecodeBinary(r io.Reader) error {
return nil return nil

View file

@ -34,15 +34,15 @@ You can create a new client and start interacting with any NEO node that exposes
| `invokescript` | Yes | - | | `invokescript` | Yes | - |
| `invokefunction` | Yes | - | | `invokefunction` | Yes | - |
| `sendrawtransaction` | Yes | - | | `sendrawtransaction` | Yes | - |
| `invoke` | Yes | - |
| `getrawtransaction` | Yes | - |
| `validateaddress` | No | Handler and result struct | | `validateaddress` | No | Handler and result struct |
| `getblocksysfee` | No | Handler and result struct | | `getblocksysfee` | No | Handler and result struct |
| `getcontractstate` | No | Handler and result struct | | `getcontractstate` | No | Handler and result struct |
| `getrawmempool` | No | Handler and result struct | | `getrawmempool` | No | Handler and result struct |
| `getrawtransaction` | No | Handler and result struct |
| `getstorage` | No | Handler and result struct | | `getstorage` | No | Handler and result struct |
| `submitblock` | No | Handler and result struct | | `submitblock` | No | Handler and result struct |
| `gettxout` | No | Handler and result struct | | `gettxout` | No | Handler and result struct |
| `invoke` | No | Handler and result struct |
| `getassetstate` | No | Handler and result struct | | `getassetstate` | No | Handler and result struct |
| `getpeers` | No | Handler and result struct | | `getpeers` | No | Handler and result struct |
| `getversion` | No | Handler and result struct | | `getversion` | No | Handler and result struct |

View file

@ -9,6 +9,9 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
"github.com/CityOfZion/neo-go/pkg/wallet"
"github.com/pkg/errors"
) )
var ( var (
@ -26,6 +29,8 @@ type Client struct {
endpoint *url.URL endpoint *url.URL
ctx context.Context ctx context.Context
version string version string
Wif *wallet.WIF
Balancer BalanceGetter
} }
// ClientOptions defines options for the RPC client. // ClientOptions defines options for the RPC client.
@ -82,6 +87,21 @@ func NewClient(ctx context.Context, endpoint string, opts ClientOptions) (*Clien
}, nil }, nil
} }
// SetWIF decodes given WIF and adds some wallet
// data to client. Useful for RPC calls that require an open wallet.
func (c *Client) SetWIF(wif string) error {
decodedWif, err := wallet.WIFDecode(wif, 0x00)
if err != nil {
return errors.Wrap(err, "Failed to decode WIF; failed to add WIF to client ")
}
c.Wif = decodedWif
return nil
}
func (c *Client) SetBalancer(b BalanceGetter) {
c.Balancer = b
}
func (c *Client) performRequest(method string, p params, v interface{}) error { func (c *Client) performRequest(method string, p params, v interface{}) error {
var ( var (
r = request{ r = request{

View file

@ -0,0 +1,93 @@
package rpc
import (
"encoding/json"
"errors"
"net/http"
"sort"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/util"
errs "github.com/pkg/errors"
)
func (s NeoScanServer) getBalance(address string) ([]*Unspent, error) {
var (
err error
req *http.Request
res *http.Response
balance NeoScanBalance
client = http.Client{}
balanceURL = s.URL + s.Path
)
if req, err = http.NewRequest(http.MethodGet, balanceURL + address, nil); err != nil {
return nil, errs.Wrap(err, "Failed to compose HTTP request")
}
if res, err = client.Do(req); err != nil {
return nil, errs.Wrap(err, "Failed to perform HTTP request")
}
defer func() error {
if err := res.Body.Close(); err != nil {
return err
}
return nil
}()
if err = json.NewDecoder(res.Body).Decode(&balance); err != nil {
return nil, errs.Wrap(err, "Failed to decode HTTP response")
}
return balance.Balance, nil
}
func filterSpecificAsset(asset string, balance []*Unspent, assetBalance *Unspent) {
for _, us := range balance {
if us.Asset == asset {
assetBalance.Unspent = us.Unspent
assetBalance.Asset = us.Asset
assetBalance.Amount = us.Amount
return
}
}
}
func (s NeoScanServer) CalculateInputs(address string, assetIdUint util.Uint256, cost util.Fixed8) ([]transaction.Input, util.Fixed8, error) {
var (
err error
num, i = uint16(0), uint16(0)
required = cost
selected = util.Fixed8(0)
us []*Unspent
assetUnspent Unspent
assetId = GlobalAssets[assetIdUint.String()]
)
if us, err = s.getBalance(address); err != nil {
return nil, util.Fixed8(0), errs.Wrapf(err, "Cannot get balance for address %v", address)
}
filterSpecificAsset(assetId, us, &assetUnspent)
sort.Sort(assetUnspent.Unspent)
for _, us := range assetUnspent.Unspent {
if selected >= required {
break
}
selected += us.Value
num++
}
if selected < required {
return nil, util.Fixed8(0), errors.New("Cannot compose inputs for transaction; check sender balance")
}
inputs := make([]transaction.Input, 0, num)
for i = 0; i < num; i++ {
inputs = append(inputs, transaction.Input{
PrevHash: assetUnspent.Unspent[i].TxID,
PrevIndex: assetUnspent.Unspent[i].N,
})
}
return inputs, selected, nil
}

49
pkg/rpc/neoScanTypes.go Normal file
View file

@ -0,0 +1,49 @@
package rpc
import "github.com/CityOfZion/neo-go/pkg/util"
/*
Definition of types, helper functions and variables
required for calculation of transaction inputs using
NeoScan API.
*/
type (
NeoScanServer struct {
URL string // "protocol://host:port/"
Path string // path to API endpoint without wallet address
}
UTXO struct {
Value util.Fixed8
TxID util.Uint256
N uint16
}
Unspents []UTXO
// unspent per asset
Unspent struct {
Unspent Unspents
Asset string // "NEO" / "GAS"
Amount util.Fixed8 // total unspent of this asset
}
// struct of NeoScan response to 'get_balance' request
NeoScanBalance struct {
Balance []*Unspent
Address string
}
)
// NeoScan returns asset IDs as strings ("NEO"/"GAS");
// strings might be converted to uint256 assets IDs using this map
var GlobalAssets = map[string]string{
"c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b": "NEO",
"602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7": "GAS",
}
// functions for sorting array of `Unspents`
func (us Unspents) Len() int { return len(us) }
func (us Unspents) Less(i, j int) bool { return us[i].Value < us[j].Value }
func (us Unspents) Swap(i, j int) { us[i], us[j] = us[j], us[i] }

View file

@ -1,6 +1,14 @@
package rpc package rpc
import "github.com/CityOfZion/neo-go/pkg/smartcontract" import (
"bytes"
"encoding/hex"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/smartcontract"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/pkg/errors"
)
// GetBlock returns a block by its hash or index/height. If verbose is true // GetBlock returns a block by its hash or index/height. If verbose is true
// the response will contain a pretty Block object instead of the raw hex string. // the response will contain a pretty Block object instead of the raw hex string.
@ -96,3 +104,31 @@ func (c *Client) SendRawTransaction(rawTX string) (*response, error) {
} }
return resp, nil return resp, nil
} }
// SendToAddress sends an amount of specific asset to a given address.
// This call requires open wallet. (`Wif` key in client struct.)
// If response.Result is `true` then transaction was formed correctly and was written in blockchain.
func (c *Client) SendToAddress(asset util.Uint256, address string, amount util.Fixed8) (*response, error) {
var (
err error
buf = &bytes.Buffer{}
rawTx *transaction.Transaction
rawTxStr string
txParams = ContractTxParams{
assetId: asset,
address: address,
value: amount,
wif: *c.Wif,
balancer: c.Balancer,
}
)
if rawTx, err = CreateRawContractTransaction(txParams); err != nil {
return nil, errors.Wrap(err, "Failed to create raw transaction for `sendtoaddress`")
}
if err = rawTx.EncodeBinary(buf); err != nil {
return nil, errors.Wrap(err, "Failed to encode raw transaction to binary for `sendtoaddress`")
}
rawTxStr = hex.EncodeToString(buf.Bytes())
return c.SendRawTransaction(rawTxStr)
}

86
pkg/rpc/txBuilder.go Normal file
View file

@ -0,0 +1,86 @@
package rpc
import (
"bytes"
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/crypto"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/wallet"
errs "github.com/pkg/errors"
)
func CreateRawContractTransaction(params ContractTxParams) (*transaction.Transaction, error) {
var (
err error
tx = transaction.NewContractTX()
toAddressHash, fromAddressHash util.Uint160
fromAddress string
senderOutput, receiverOutput *transaction.Output
inputs []transaction.Input
spent util.Fixed8
witness transaction.Witness
wif, assetID, address, amount, balancer = params.wif, params.assetId, params.address, params.value, params.balancer
)
if fromAddress, err = wif.PrivateKey.Address(); err != nil {
return nil, errs.Wrapf(err, "Failed to take address from WIF: %v", wif.S)
}
if fromAddressHash, err = crypto.Uint160DecodeAddress(fromAddress); err != nil {
return nil, errs.Wrapf(err, "Failed to take script hash from address: %v", fromAddress)
}
if toAddressHash, err = crypto.Uint160DecodeAddress(address); err != nil {
return nil, errs.Wrapf(err, "Failed to take script hash from address: %v", address)
}
tx.Attributes = append(tx.Attributes,
&transaction.Attribute{
Usage: transaction.Script,
Data: fromAddressHash.Bytes(),
})
if inputs, spent, err = balancer.CalculateInputs(fromAddress, assetID, amount); err != nil {
return nil, errs.Wrap(err, "Failed to get inputs for transaction")
}
for _, input := range inputs {
tx.AddInput(&input)
}
senderOutput = transaction.NewOutput(assetID, spent-amount, fromAddressHash)
tx.AddOutput(senderOutput)
receiverOutput = transaction.NewOutput(assetID, amount, toAddressHash)
tx.AddOutput(receiverOutput)
if witness.InvocationScript, err = getInvocationScript(tx, wif); err != nil {
return nil, errs.Wrap(err, "Failed to create invocation script")
}
if witness.VerificationScript, err = wif.GetVerificationScript(); err != nil {
return nil, errs.Wrap(err, "Failed to create verification script")
}
tx.Scripts = append(tx.Scripts, &witness)
tx.Hash()
return tx, nil
}
func getInvocationScript(tx *transaction.Transaction, wif wallet.WIF) ([]byte, error) {
const (
pushbytes64 = 0x40
)
var (
err error
buf = new(bytes.Buffer)
signature []byte
)
if err = tx.EncodeBinary(buf); err != nil {
return nil, errs.Wrap(err, "Failed to encode transaction to binary")
}
bytes := buf.Bytes()
signature, err = wif.PrivateKey.Sign(bytes[:(len(bytes) - 1)])
if err != nil {
return nil, errs.Wrap(err, "Failed ti sign transaction with private key")
}
return append([]byte{pushbytes64}, signature...), nil
}

41
pkg/rpc/txTypes.go Normal file
View file

@ -0,0 +1,41 @@
package rpc
/*
Definition of types, interfaces and variables
required for raw transaction composing.
*/
import (
"github.com/CityOfZion/neo-go/pkg/core/transaction"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/wallet"
)
type (
// parameters for tx to transfer assets;
// includes parameters duplication `sendtoaddress` RPC call params
// and also some utility data;
ContractTxParams struct {
assetId util.Uint256
address string
value util.Fixed8
wif wallet.WIF // a WIF to send the transaction
// since there are many ways to provide unspents,
// transaction composer stays agnostic to that how
// unspents was got;
balancer BalanceGetter
}
BalanceGetter interface {
// parameters
// address: base58-encoded address assets would be transferred from
// assetId: asset identifier
// amount: an asset amount to spend
// return values
// inputs: UTXO's for the preparing transaction
// total: summarized asset amount from all the `inputs`
// error: error would be considered in the caller function
CalculateInputs(address string, assetId util.Uint256, amount util.Fixed8) (inputs []transaction.Input, total util.Fixed8, err error)
}
)

View file

@ -90,3 +90,21 @@ func WIFDecode(wif string, version byte) (*WIF, error) {
w.Compressed = true w.Compressed = true
return w, nil return w, nil
} }
func (wif WIF) GetVerificationScript() ([]byte, error) {
const (
pushbytes33 = 0x21
checksig = 0xac
)
var (
pubkey, vScript []byte
)
pubkey, err := wif.PrivateKey.PublicKey()
if err != nil {
return nil, err
}
vScript = append([]byte{pushbytes33}, pubkey...)
vScript = append(vScript, checksig)
return vScript, nil
}