From 4a47fc7c3d94fdd063d7c795615c28b03889180c Mon Sep 17 00:00:00 2001 From: jkrvivian <jkrvivian@gmail.com> Date: Fri, 29 May 2020 02:36:57 +0800 Subject: [PATCH] feat: Implement value web api & client library (#438) * feat: Implement value api: Attachment * feat: Implement value api: UnspentOutputs * feat: Implement value api: transactionByID * feat: Implement client lib & value api: sendTransaction * fix: minor tweak * fix: minor fix * refactor: Refactor sendTransaction api * Refactor: Fix :dog: * refactor: Fix :dog: * refactor: Rename api route of testSendTxn to camel case --- client/value.go | 64 ++++++++++++++ .../packages/transaction/outputid.go | 22 +++++ pluginmgr/webapi/plugins.go | 2 + plugins/webapi/value/attachments/handler.go | 68 +++++++++++++++ .../value/gettransactionbyid/handler.go | 44 ++++++++++ plugins/webapi/value/plugin.go | 27 ++++++ .../webapi/value/sendtransaction/handler.go | 48 +++++++++++ plugins/webapi/value/testsendtxn/handler.go | 84 +++++++++++++++++++ .../webapi/value/unspentoutputs/handler.go | 78 +++++++++++++++++ .../webapi/value/utils/transaction_handler.go | 70 ++++++++++++++++ 10 files changed, 507 insertions(+) create mode 100644 client/value.go create mode 100644 plugins/webapi/value/attachments/handler.go create mode 100644 plugins/webapi/value/gettransactionbyid/handler.go create mode 100644 plugins/webapi/value/plugin.go create mode 100644 plugins/webapi/value/sendtransaction/handler.go create mode 100644 plugins/webapi/value/testsendtxn/handler.go create mode 100644 plugins/webapi/value/unspentoutputs/handler.go create mode 100644 plugins/webapi/value/utils/transaction_handler.go diff --git a/client/value.go b/client/value.go new file mode 100644 index 00000000..342aea66 --- /dev/null +++ b/client/value.go @@ -0,0 +1,64 @@ +package client + +import ( + "fmt" + "net/http" + + webapi_attachments "github.com/iotaledger/goshimmer/plugins/webapi/value/attachments" + webapi_gettxn "github.com/iotaledger/goshimmer/plugins/webapi/value/gettransactionbyid" + webapi_sendtxn "github.com/iotaledger/goshimmer/plugins/webapi/value/sendtransaction" + webapi_unspentoutputs "github.com/iotaledger/goshimmer/plugins/webapi/value/unspentoutputs" +) + +const ( + routeAttachments = "value/attachments" + routeGetTxnByID = "value/transactionByID" + routeSendTxn = "value/sendTransaction" + routeUnspentOutputs = "value/unspentOutputs" +) + +// GetAttachments gets the attachments of a transaction ID +func (api *GoShimmerAPI) GetAttachments(base58EncodedTxnID string) (*webapi_attachments.Response, error) { + res := &webapi_attachments.Response{} + if err := api.do(http.MethodGet, func() string { + return fmt.Sprintf("%s?txnID=%s", routeAttachments, base58EncodedTxnID) + }(), nil, res); err != nil { + return nil, err + } + + return res, nil +} + +// GetTransactionByID gets the transaction of a transaction ID +func (api *GoShimmerAPI) GetTransactionByID(base58EncodedTxnID string) (*webapi_gettxn.Response, error) { + res := &webapi_gettxn.Response{} + if err := api.do(http.MethodGet, func() string { + return fmt.Sprintf("%s?txnID=%s", routeGetTxnByID, base58EncodedTxnID) + }(), nil, res); err != nil { + return nil, err + } + + return res, nil +} + +// GetUnspentOutputs return unspent output IDs of addresses +func (api *GoShimmerAPI) GetUnspentOutputs(addresses []string) (*webapi_unspentoutputs.Response, error) { + res := &webapi_unspentoutputs.Response{} + if err := api.do(http.MethodPost, routeUnspentOutputs, + &webapi_unspentoutputs.Request{Addresses: addresses}, res); err != nil { + return nil, err + } + + return res, nil +} + +// SendTransaction sends the transaction(bytes) to Tangle and returns transaction ID +func (api *GoShimmerAPI) SendTransaction(txnBytes []byte) (string, error) { + res := &webapi_sendtxn.Response{} + if err := api.do(http.MethodPost, routeSendTxn, + &webapi_sendtxn.Request{TransactionBytes: txnBytes}, res); err != nil { + return "", err + } + + return res.TransactionID, nil +} diff --git a/dapps/valuetransfers/packages/transaction/outputid.go b/dapps/valuetransfers/packages/transaction/outputid.go index 3eadb097..b8386d60 100644 --- a/dapps/valuetransfers/packages/transaction/outputid.go +++ b/dapps/valuetransfers/packages/transaction/outputid.go @@ -1,6 +1,7 @@ package transaction import ( + "fmt" "github.com/mr-tron/base58" "github.com/iotaledger/hive.go/marshalutil" @@ -19,6 +20,27 @@ func NewOutputID(outputAddress address.Address, transactionID ID) (outputID Outp return } +// OutputIDFromBase58 creates an output id from a base58 encoded string. +func OutputIDFromBase58(base58String string) (outputid OutputID, err error) { + // decode string + bytes, err := base58.Decode(base58String) + if err != nil { + return + } + + // sanitize input + if len(bytes) != OutputIDLength { + err = fmt.Errorf("base58 encoded string does not match the length of a output id") + + return + } + + // copy bytes to result + copy(outputid[:], bytes) + + return +} + // OutputIDFromBytes unmarshals an OutputID from a sequence of bytes. func OutputIDFromBytes(bytes []byte) (result OutputID, consumedBytes int, err error) { // parse the bytes diff --git a/pluginmgr/webapi/plugins.go b/pluginmgr/webapi/plugins.go index dd7a2e0b..48b6ba70 100644 --- a/pluginmgr/webapi/plugins.go +++ b/pluginmgr/webapi/plugins.go @@ -8,6 +8,7 @@ import ( "github.com/iotaledger/goshimmer/plugins/webapi/info" "github.com/iotaledger/goshimmer/plugins/webapi/message" "github.com/iotaledger/goshimmer/plugins/webapi/spammer" + "github.com/iotaledger/goshimmer/plugins/webapi/value" "github.com/iotaledger/goshimmer/plugins/webauth" "github.com/iotaledger/hive.go/node" ) @@ -21,4 +22,5 @@ var PLUGINS = node.Plugins( message.Plugin, autopeering.Plugin, info.Plugin, + value.Plugin, ) diff --git a/plugins/webapi/value/attachments/handler.go b/plugins/webapi/value/attachments/handler.go new file mode 100644 index 00000000..71cca4df --- /dev/null +++ b/plugins/webapi/value/attachments/handler.go @@ -0,0 +1,68 @@ +package attachments + +import ( + "net/http" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" + "github.com/labstack/echo" + "github.com/labstack/gommon/log" +) + +// Handler gets the value attachments. +func Handler(c echo.Context) error { + txnID, err := transaction.IDFromBase58(c.QueryParam("txnID")) + if err != nil { + log.Info(err) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + var valueObjs []ValueObject + + // get txn by txn id + txnObj := valuetransfers.Tangle.Transaction(txnID) + if !txnObj.Exists() { + return c.JSON(http.StatusNotFound, Response{Error: "Transaction not found"}) + } + txn := utils.ParseTransaction(txnObj.Unwrap()) + + // get attachements by txn id + for _, attachmentObj := range valuetransfers.Tangle.Attachments(txnID) { + if !attachmentObj.Exists() { + continue + } + attachment := attachmentObj.Unwrap() + + // get payload by payload id + payloadObj := valuetransfers.Tangle.Payload(attachment.PayloadID()) + if !payloadObj.Exists() { + continue + } + payload := payloadObj.Unwrap() + + // append value object + valueObjs = append(valueObjs, ValueObject{ + ID: payload.ID().String(), + ParentID0: payload.TrunkID().String(), + ParentID1: payload.BranchID().String(), + Transaction: txn, + }) + } + + return c.JSON(http.StatusOK, Response{Attachments: valueObjs}) +} + +// Response is the HTTP response from retreiving value objects. +type Response struct { + Attachments []ValueObject `json:"attachments,omitempty"` + Error string `json:"error,omitempty"` +} + +// ValueObject holds the information of a value object. +type ValueObject struct { + ID string `json:"id"` + ParentID0 string `json:"parent0_id"` + ParentID1 string `json:"parent1_id"` + Transaction utils.Transaction `json:"transaction"` +} diff --git a/plugins/webapi/value/gettransactionbyid/handler.go b/plugins/webapi/value/gettransactionbyid/handler.go new file mode 100644 index 00000000..3ae7e4fd --- /dev/null +++ b/plugins/webapi/value/gettransactionbyid/handler.go @@ -0,0 +1,44 @@ +package gettransactionbyid + +import ( + "net/http" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" + "github.com/labstack/echo" + "github.com/labstack/gommon/log" +) + +// Handler gets the transaction by id. +func Handler(c echo.Context) error { + txnID, err := transaction.IDFromBase58(c.QueryParam("txnID")) + if err != nil { + log.Info(err) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + // get txn by txn id + txnObj := valuetransfers.Tangle.Transaction(txnID) + if !txnObj.Exists() { + return c.JSON(http.StatusNotFound, Response{Error: "Transaction not found"}) + } + txn := utils.ParseTransaction(txnObj.Unwrap()) + + // TODO: get inclusion state + return c.JSON(http.StatusOK, Response{ + Transaction: txn, + InclusionState: utils.InclusionState{ + Confirmed: true, + Conflict: false, + Liked: true, + }, + }) +} + +// Response is the HTTP response from retreiving transaction. +type Response struct { + Transaction utils.Transaction `json:"transaction,omitempty"` + InclusionState utils.InclusionState `json:"inclusion_state,omitempty"` + Error string `json:"error,omitempty"` +} diff --git a/plugins/webapi/value/plugin.go b/plugins/webapi/value/plugin.go new file mode 100644 index 00000000..72facce9 --- /dev/null +++ b/plugins/webapi/value/plugin.go @@ -0,0 +1,27 @@ +package value + +import ( + "github.com/iotaledger/goshimmer/plugins/webapi" + "github.com/iotaledger/goshimmer/plugins/webapi/value/attachments" + "github.com/iotaledger/goshimmer/plugins/webapi/value/gettransactionbyid" + "github.com/iotaledger/goshimmer/plugins/webapi/value/sendtransaction" + "github.com/iotaledger/goshimmer/plugins/webapi/value/testsendtxn" + "github.com/iotaledger/goshimmer/plugins/webapi/value/unspentoutputs" + "github.com/iotaledger/hive.go/node" +) + +// PluginName is the name of the web API DRNG endpoint plugin. +const PluginName = "WebAPI Value Endpoint" + +var ( + // Plugin is the plugin instance of the web API DRNG endpoint plugin. + Plugin = node.NewPlugin(PluginName, node.Enabled, configure) +) + +func configure(_ *node.Plugin) { + webapi.Server.GET("value/attachments", attachments.Handler) + webapi.Server.POST("value/unspentOutputs", unspentoutputs.Handler) + webapi.Server.POST("value/sendTransaction", sendtransaction.Handler) + webapi.Server.POST("value/testSendTxn", testsendtxn.Handler) + webapi.Server.GET("value/transactionByID", gettransactionbyid.Handler) +} diff --git a/plugins/webapi/value/sendtransaction/handler.go b/plugins/webapi/value/sendtransaction/handler.go new file mode 100644 index 00000000..b1dfa63d --- /dev/null +++ b/plugins/webapi/value/sendtransaction/handler.go @@ -0,0 +1,48 @@ +package sendtransaction + +import ( + "net/http" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/plugins/issuer" + "github.com/labstack/echo" + "github.com/labstack/gommon/log" +) + +// Handler sends a transaction. +func Handler(c echo.Context) error { + var request Request + if err := c.Bind(&request); err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + // prepare transaction + tx, _, err := transaction.FromBytes(request.TransactionBytes) + if err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + // Prepare value payload and send the message to tangle + payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + _, err = issuer.IssuePayload(payload) + if err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + return c.JSON(http.StatusOK, Response{TransactionID: tx.ID().String()}) +} + +// Request holds the transaction object(bytes) to send. +type Request struct { + TransactionBytes []byte `json:"txn_bytes"` +} + +// Response is the HTTP response from sending transaction. +type Response struct { + TransactionID string `json:"transaction_id,omitempty"` + Error string `json:"error,omitempty"` +} diff --git a/plugins/webapi/value/testsendtxn/handler.go b/plugins/webapi/value/testsendtxn/handler.go new file mode 100644 index 00000000..927de126 --- /dev/null +++ b/plugins/webapi/value/testsendtxn/handler.go @@ -0,0 +1,84 @@ +package testsendtxn + +import ( + "net/http" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" + "github.com/iotaledger/goshimmer/plugins/issuer" + "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" + "github.com/labstack/echo" + "github.com/labstack/gommon/log" +) + +// Handler sends a transaction. +func Handler(c echo.Context) error { + var request Request + if err := c.Bind(&request); err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + // prepare inputs + outputids := []transaction.OutputID{} + for _, in := range request.Inputs { + id, err := transaction.OutputIDFromBase58(in) + if err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + outputids = append(outputids, id) + } + inputs := transaction.NewInputs(outputids...) + + // prepare outputs + outmap := map[address.Address][]*balance.Balance{} + for _, out := range request.Outputs { + addr, err := address.FromBase58(out.Address) + if err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + // iterate balances + balances := []*balance.Balance{} + for _, b := range out.Balances { + // get token color + color, _, err := balance.ColorFromBytes([]byte(b.Color)) + if err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + balances = append(balances, balance.New(color, b.Value)) + } + outmap[addr] = balances + } + outputs := transaction.NewOutputs(outmap) + + // prepare transaction + // Note: not signed + tx := transaction.New(inputs, outputs) + + // Prepare value payload and send the message to tangle + payload := valuetransfers.ValueObjectFactory().IssueTransaction(tx) + _, err := issuer.IssuePayload(payload) + if err != nil { + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + return c.JSON(http.StatusOK, Response{TransactionID: tx.ID().String()}) +} + +// Request holds the inputs and outputs to send. +type Request struct { + Inputs []string `json:"inputs"` + Outputs []utils.Output `json:"outputs"` +} + +// Response is the HTTP response from sending transaction. +type Response struct { + TransactionID string `json:"transaction_id,omitempty"` + Error string `json:"error,omitempty"` +} diff --git a/plugins/webapi/value/unspentoutputs/handler.go b/plugins/webapi/value/unspentoutputs/handler.go new file mode 100644 index 00000000..7523fcb5 --- /dev/null +++ b/plugins/webapi/value/unspentoutputs/handler.go @@ -0,0 +1,78 @@ +package unspentoutputs + +import ( + "net/http" + + "github.com/iotaledger/goshimmer/dapps/valuetransfers" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/plugins/webapi/value/utils" + "github.com/labstack/echo" + "github.com/labstack/gommon/log" +) + +// Handler gets the unspent outputs. +func Handler(c echo.Context) error { + var request Request + if err := c.Bind(&request); err != nil { + log.Info(err.Error()) + return c.JSON(http.StatusBadRequest, Response{Error: err.Error()}) + } + + var unspents []UnspentOutput + for _, strAddress := range request.Addresses { + address, err := address.FromBase58(strAddress) + if err != nil { + log.Info(err.Error()) + continue + } + + outputids := make([]OutputID, 0) + // get outputids by address + for id, outputObj := range valuetransfers.Tangle.OutputsOnAddress(address) { + output := outputObj.Unwrap() + + // TODO: get inclusion state + if output.ConsumerCount() == 0 { + outputids = append(outputids, OutputID{ + ID: id.String(), + InclusionState: utils.InclusionState{ + Confirmed: true, + Conflict: false, + Liked: true, + }, + }) + } + } + + unspents = append(unspents, UnspentOutput{ + Address: strAddress, + OutputIDs: outputids, + }) + } + + return c.JSON(http.StatusOK, Response{UnspentOutputs: unspents}) +} + +// Request holds the addresses to query. +type Request struct { + Addresses []string `json:"addresses,omitempty"` + Error string `json:"error,omitempty"` +} + +// Response is the HTTP response from retreiving value objects. +type Response struct { + UnspentOutputs []UnspentOutput `json:"unspent_outputs,omitempty"` + Error string `json:"error,omitempty"` +} + +// UnspentOutput holds the address and the corresponding unspent output ids +type UnspentOutput struct { + Address string `json:"address"` + OutputIDs []OutputID `json:"output_ids"` +} + +// OutputID holds the output id and its inclusion state +type OutputID struct { + ID string `json:"id"` + InclusionState utils.InclusionState `json:"inclusion_state"` +} diff --git a/plugins/webapi/value/utils/transaction_handler.go b/plugins/webapi/value/utils/transaction_handler.go new file mode 100644 index 00000000..308904d5 --- /dev/null +++ b/plugins/webapi/value/utils/transaction_handler.go @@ -0,0 +1,70 @@ +package utils + +import ( + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance" + "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction" +) + +// ParseTransaction handle transaction json object. +func ParseTransaction(t *transaction.Transaction) (txn Transaction) { + var inputs []string + var outputs []Output + // process inputs + t.Inputs().ForEachAddress(func(currentAddress address.Address) bool { + inputs = append(inputs, currentAddress.String()) + return true + }) + + // process outputs: address + balance + t.Outputs().ForEach(func(address address.Address, balances []*balance.Balance) bool { + var b []Balance + for _, balance := range balances { + b = append(b, Balance{ + Value: balance.Value(), + Color: balance.Color().String(), + }) + } + t := Output{ + Address: address.String(), + Balances: b, + } + outputs = append(outputs, t) + + return true + }) + + return Transaction{ + Inputs: inputs, + Outputs: outputs, + Signature: t.SignatureBytes(), + DataPayload: t.GetDataPayload(), + } +} + +// Transaction holds the information of a transaction. +type Transaction struct { + Inputs []string `json:"inputs"` + Outputs []Output `json:"outputs"` + Signature []byte `json:"signature"` + DataPayload []byte `json:"data_payload"` +} + +// Output consists an address and balances +type Output struct { + Address string `json:"address"` + Balances []Balance `json:"balances"` +} + +// Balance holds the value and the color of token +type Balance struct { + Value int64 `json:"value"` + Color string `json:"color"` +} + +// InclusionState represents the different states of an OutputID +type InclusionState struct { + Confirmed bool `json:"confirmed,omitempty"` + Conflict bool `json:"conflict,omitempty"` + Liked bool `json:"liked,omitempty"` +} -- GitLab