mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-01 23:45:50 +00:00
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:
parent
9c605735b7
commit
74f0019df2
9 changed files with 352 additions and 3 deletions
|
@ -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
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
|
@ -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{
|
||||||
|
|
93
pkg/rpc/neoScanBalanceGetter.go
Normal file
93
pkg/rpc/neoScanBalanceGetter.go
Normal 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
49
pkg/rpc/neoScanTypes.go
Normal 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] }
|
|
@ -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
86
pkg/rpc/txBuilder.go
Normal 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
41
pkg/rpc/txTypes.go
Normal 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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue