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