Skip to content
Snippets Groups Projects
handler.go 6.96 KiB
package sendtransactionbyjson

import (
	"fmt"
	"net/http"
	"sync"
	"time"

	"github.com/iotaledger/goshimmer/dapps/valuetransfers"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address/signaturescheme"
	"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/iotaledger/hive.go/crypto/ed25519"
	"github.com/iotaledger/hive.go/marshalutil"
	"github.com/labstack/echo"
	"github.com/mr-tron/base58/base58"
)

var (
	sendTxMu           sync.Mutex
	maxBookedAwaitTime = 5 * time.Second

	// ErrMalformedInputs defines a malformed inputs error.
	ErrMalformedInputs = fmt.Errorf("malformed inputs")
	// ErrMalformedOutputs defines a malformed outputs error.
	ErrMalformedOutputs = fmt.Errorf("malformed outputs")
	// ErrMalformedData defines a malformed data error.
	ErrMalformedData = fmt.Errorf("malformed data")
	// ErrMalformedColor defines a malformed color error.
	ErrMalformedColor = fmt.Errorf("malformed color")
	// ErrMalformedPublicKey defines a malformed publicKey error.
	ErrMalformedPublicKey = fmt.Errorf("malformed publicKey")
	// ErrMalformedSignature defines a malformed signature error.
	ErrMalformedSignature = fmt.Errorf("malformed signature")
	// ErrWrongSignature defines a wrong signature error.
	ErrWrongSignature = fmt.Errorf("wrong signature")
	// ErrSignatureVersion defines a unsupported signature version error.
	ErrSignatureVersion = fmt.Errorf("unsupported signature version")
)

// Handler sends a transaction.
func Handler(c echo.Context) error {
	sendTxMu.Lock()
	defer sendTxMu.Unlock()

	var request Request
	if err := c.Bind(&request); err != nil {
		return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
	}

	tx, err := NewTransactionFromJSON(request)
	if err != nil {
		return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
	}

	// validate transaction
	err = valuetransfers.Tangle().ValidateTransactionToAttach(tx)
	if err != nil {
		return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
	}

	// Prepare value payload and send the message to tangle
	payload, err := valuetransfers.ValueObjectFactory().IssueTransaction(tx)
	if err != nil {
		return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
	}
	_, err = issuer.IssuePayload(payload)
	if err != nil {
		return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
	}

	if err := valuetransfers.AwaitTransactionToBeBooked(tx.ID(), maxBookedAwaitTime); err != nil {
		return c.JSON(http.StatusBadRequest, Response{Error: err.Error()})
	}
	return c.JSON(http.StatusOK, Response{TransactionID: tx.ID().String()})
}

// NewTransactionFromJSON returns a new transaction from a given JSON request or an error.
func NewTransactionFromJSON(request Request) (*transaction.Transaction, error) {
	// prepare inputs
	inputs := make([]transaction.OutputID, len(request.Inputs))
	for i, input := range request.Inputs {
		b, err := base58.Decode(input)
		if err != nil || len(b) != transaction.OutputIDLength {
			return nil, ErrMalformedInputs
		}
		copy(inputs[i][:], b)
	}

	// prepare ouputs
	outputs := make(map[address.Address][]*balance.Balance)
	for _, output := range request.Outputs {
		address, err := address.FromBase58(output.Address)
		if err != nil {
			return nil, ErrMalformedOutputs
		}

		balances := []*balance.Balance{}
		for _, b := range output.Balances {
			var color balance.Color
			if b.Color == "IOTA" {
				color = balance.ColorIOTA
			} else {
				colorBytes, err := base58.Decode(b.Color)
				if err != nil || len(colorBytes) != balance.ColorLength {
					return nil, ErrMalformedColor
				}
				copy(color[:], colorBytes)
			}
			balances = append(balances, &balance.Balance{
				Value: b.Value,
				Color: color,
			})
		}

		outputs[address] = balances
	}

	// prepare transaction
	tx := transaction.New(transaction.NewInputs(inputs...), transaction.NewOutputs(outputs))

	// add data payload
	if request.Data != nil {
		tx.SetDataPayload(request.Data)
	}

	// add signatures
	for _, signature := range request.Signatures {
		switch signature.Version {

		case address.VersionED25519:
			pubKeyBytes, err := base58.Decode(signature.PublicKey)
			if err != nil || len(pubKeyBytes) != ed25519.PublicKeySize {
				return nil, ErrMalformedPublicKey
			}

			signatureBytes, err := base58.Decode(signature.Signature)
			if err != nil || len(signatureBytes) != ed25519.SignatureSize {
				return nil, ErrMalformedSignature
			}

			marshalUtil := marshalutil.New(1 + ed25519.PublicKeySize + ed25519.SignatureSize)
			marshalUtil.WriteByte(address.VersionED25519)
			marshalUtil.WriteBytes(pubKeyBytes[:])
			marshalUtil.WriteBytes(signatureBytes[:])

			sign, _, err := signaturescheme.Ed25519SignatureFromBytes(marshalUtil.Bytes())
			if err != nil {
				return nil, ErrWrongSignature
			}
			err = tx.PutSignature(sign)
			if err != nil {
				return nil, ErrWrongSignature
			}

		case address.VersionBLS:
			pubKeyBytes, err := base58.Decode(signature.PublicKey)
			if err != nil || len(pubKeyBytes) != signaturescheme.BLSPublicKeySize {
				return nil, ErrMalformedPublicKey
			}

			signatureBytes, err := base58.Decode(signature.Signature)
			if err != nil || len(signatureBytes) != signaturescheme.BLSSignatureSize {
				return nil, ErrMalformedSignature
			}

			marshalUtil := marshalutil.New(signaturescheme.BLSFullSignatureSize)
			marshalUtil.WriteByte(address.VersionBLS)
			marshalUtil.WriteBytes(pubKeyBytes[:])
			marshalUtil.WriteBytes(signatureBytes[:])

			sign, _, err := signaturescheme.BLSSignatureFromBytes(marshalUtil.Bytes())
			if err != nil {
				return nil, ErrWrongSignature
			}
			err = tx.PutSignature(sign)
			if err != nil {
				return nil, ErrWrongSignature
			}

		default:
			return nil, ErrSignatureVersion
		}
	}

	return tx, nil
}

// Request holds the transaction object(json) to send.
// e.g.,
// {
// 	"inputs": string[],
// 	"outputs": {
// 	   "address": string,
// 	   "balances": {
// 		   "value": number,
// 		   "color": string
// 	   }[];
// 	 }[],
// 	 "data": []byte,
// 	 "signatures": {
// 		"version": number,
// 		"publicKey": string,
// 		"signature": string
// 	   }[]
//  }
type Request struct {
	Inputs     []string    `json:"inputs"`
	Outputs    []Output    `json:"outputs"`
	Data       []byte      `json:"data,omitempty"`
	Signatures []Signature `json:"signatures"`
}

// Output defines the struct of an output.
type Output struct {
	Address  string          `json:"address"`
	Balances []utils.Balance `json:"balances"`
}

// Signature defines the struct of a signature.
type Signature struct {
	Version   byte   `json:"version"`
	PublicKey string `json:"publicKey"`
	Signature string `json:"signature"`
}

// Response is the HTTP response from sending transaction.
type Response struct {
	TransactionID string `json:"transaction_id,omitempty"`
	Error         string `json:"error,omitempty"`
}