mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-29 13:41:47 +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.
|
||||
type ContractTX struct{}
|
||||
|
||||
func NewContractTX() *Transaction {
|
||||
return &Transaction{
|
||||
Type: ContractType,
|
||||
}
|
||||
}
|
||||
|
||||
// DecodeBinary implements the Payload interface.
|
||||
func (tx *ContractTX) DecodeBinary(r io.Reader) error {
|
||||
return nil
|
||||
|
|
|
@ -34,15 +34,15 @@ You can create a new client and start interacting with any NEO node that exposes
|
|||
| `invokescript` | Yes | - |
|
||||
| `invokefunction` | Yes | - |
|
||||
| `sendrawtransaction` | Yes | - |
|
||||
| `invoke` | Yes | - |
|
||||
| `getrawtransaction` | Yes | - |
|
||||
| `validateaddress` | No | Handler and result struct |
|
||||
| `getblocksysfee` | No | Handler and result struct |
|
||||
| `getcontractstate` | No | Handler and result struct |
|
||||
| `getrawmempool` | No | Handler and result struct |
|
||||
| `getrawtransaction` | No | Handler and result struct |
|
||||
| `getstorage` | No | Handler and result struct |
|
||||
| `submitblock` | No | Handler and result struct |
|
||||
| `gettxout` | No | Handler and result struct |
|
||||
| `invoke` | No | Handler and result struct |
|
||||
| `getassetstate` | No | Handler and result struct |
|
||||
| `getpeers` | No | Handler and result struct |
|
||||
| `getversion` | No | Handler and result struct |
|
||||
|
|
|
@ -9,6 +9,9 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/CityOfZion/neo-go/pkg/wallet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -26,6 +29,8 @@ type Client struct {
|
|||
endpoint *url.URL
|
||||
ctx context.Context
|
||||
version string
|
||||
Wif *wallet.WIF
|
||||
Balancer BalanceGetter
|
||||
}
|
||||
|
||||
// ClientOptions defines options for the RPC client.
|
||||
|
@ -82,6 +87,21 @@ func NewClient(ctx context.Context, endpoint string, opts ClientOptions) (*Clien
|
|||
}, 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 {
|
||||
var (
|
||||
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
|
||||
|
||||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
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