diff --git a/pkg/core/transaction/contract.go b/pkg/core/transaction/contract.go index 649c064ba..28e65cc22 100644 --- a/pkg/core/transaction/contract.go +++ b/pkg/core/transaction/contract.go @@ -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 diff --git a/pkg/rpc/README.md b/pkg/rpc/README.md index a90cbac1a..9633992c5 100644 --- a/pkg/rpc/README.md +++ b/pkg/rpc/README.md @@ -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 | diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index 523da52ae..c9fc7d41f 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -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{ diff --git a/pkg/rpc/neoScanBalanceGetter.go b/pkg/rpc/neoScanBalanceGetter.go new file mode 100644 index 000000000..0c991644a --- /dev/null +++ b/pkg/rpc/neoScanBalanceGetter.go @@ -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 +} diff --git a/pkg/rpc/neoScanTypes.go b/pkg/rpc/neoScanTypes.go new file mode 100644 index 000000000..b0134277a --- /dev/null +++ b/pkg/rpc/neoScanTypes.go @@ -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] } diff --git a/pkg/rpc/rpc.go b/pkg/rpc/rpc.go index 0c336c8b1..ea6a5b325 100644 --- a/pkg/rpc/rpc.go +++ b/pkg/rpc/rpc.go @@ -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) +} diff --git a/pkg/rpc/txBuilder.go b/pkg/rpc/txBuilder.go new file mode 100644 index 000000000..9f93591f7 --- /dev/null +++ b/pkg/rpc/txBuilder.go @@ -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 +} diff --git a/pkg/rpc/txTypes.go b/pkg/rpc/txTypes.go new file mode 100644 index 000000000..bbf0d7c22 --- /dev/null +++ b/pkg/rpc/txTypes.go @@ -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) + } +) + diff --git a/pkg/wallet/wif.go b/pkg/wallet/wif.go index 44f919114..42a1ab585 100644 --- a/pkg/wallet/wif.go +++ b/pkg/wallet/wif.go @@ -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 +} +