diff --git a/client/value.go b/client/value.go index 342aea66cc536113b49ead9e465ef6fac1408045..85a560dca5fa2bcd6efd7f642ddb3ac13ab16d48 100644 --- a/client/value.go +++ b/client/value.go @@ -7,6 +7,7 @@ import ( 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_sendtxnbyjson "github.com/iotaledger/goshimmer/plugins/webapi/value/sendtransactionbyjson" webapi_unspentoutputs "github.com/iotaledger/goshimmer/plugins/webapi/value/unspentoutputs" ) @@ -14,6 +15,7 @@ const ( routeAttachments = "value/attachments" routeGetTxnByID = "value/transactionByID" routeSendTxn = "value/sendTransaction" + routeSendTxnByJSON = "value/sendTransactionByJson" routeUnspentOutputs = "value/unspentOutputs" ) @@ -52,7 +54,7 @@ func (api *GoShimmerAPI) GetUnspentOutputs(addresses []string) (*webapi_unspento return res, nil } -// SendTransaction sends the transaction(bytes) to Tangle and returns transaction ID +// SendTransaction sends the transaction(bytes) to the Value Tangle and returns transaction ID. func (api *GoShimmerAPI) SendTransaction(txnBytes []byte) (string, error) { res := &webapi_sendtxn.Response{} if err := api.do(http.MethodPost, routeSendTxn, @@ -62,3 +64,19 @@ func (api *GoShimmerAPI) SendTransaction(txnBytes []byte) (string, error) { return res.TransactionID, nil } + +// SendTransactionByJSON sends the transaction(JSON) to the Value Tangle and returns transaction ID. +func (api *GoShimmerAPI) SendTransactionByJSON(txn webapi_sendtxnbyjson.Request) (string, error) { + res := &webapi_sendtxn.Response{} + if err := api.do(http.MethodPost, routeSendTxnByJSON, + &webapi_sendtxnbyjson.Request{ + Inputs: txn.Inputs, + Outputs: txn.Outputs, + Data: txn.Data, + Signatures: txn.Signatures, + }, res); err != nil { + return "", err + } + + return res.TransactionID, nil +} diff --git a/dapps/valuetransfers/packages/address/signaturescheme/bls.go b/dapps/valuetransfers/packages/address/signaturescheme/bls.go index 7b10349b0419c144fd89151a071864f7f6bb8e0e..89cec265cacf5893709d8cd70fdce04338392356 100644 --- a/dapps/valuetransfers/packages/address/signaturescheme/bls.go +++ b/dapps/valuetransfers/packages/address/signaturescheme/bls.go @@ -227,5 +227,15 @@ func AggregateBLSSignatures(sigs ...Signature) (Signature, error) { return NewBLSSignature(pubKeyBin, sigBin), nil } +// PublicKeySize returns the size of the public key. +func (sig *BLSSignature) PublicKeySize() int { + return BLSPublicKeySize +} + +// SignatureSize returns the size of the signature. +func (sig *BLSSignature) SignatureSize() int { + return BLSPrivateKeySize +} + // interface contract (allow the compiler to check if the implementation has all of the required methods). var _ Signature = &BLSSignature{} diff --git a/dapps/valuetransfers/packages/address/signaturescheme/ed25519.go b/dapps/valuetransfers/packages/address/signaturescheme/ed25519.go index 846d92de594628c9cea3c00d2057abb456cc6495..98e0767b63061f530a27fdb4b64261828e477e0b 100644 --- a/dapps/valuetransfers/packages/address/signaturescheme/ed25519.go +++ b/dapps/valuetransfers/packages/address/signaturescheme/ed25519.go @@ -124,4 +124,14 @@ func (signature *ED25519Signature) Address() address.Address { return address.FromED25519PubKey(signature.publicKey) } +// PublicKeySize returns the size of the public key. +func (signature *ED25519Signature) PublicKeySize() int { + return ed25519.PublicKeySize +} + +// SignatureSize returns the size of the signature. +func (signature *ED25519Signature) SignatureSize() int { + return ed25519.SignatureSize +} + // endregion /////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/dapps/valuetransfers/packages/address/signaturescheme/signature.go b/dapps/valuetransfers/packages/address/signaturescheme/signature.go index eb7d6e47dff9cc57f78b307189d5f690814726e1..541dcf4d0cb52b95ed64fb1b7c148e7431eb7174 100644 --- a/dapps/valuetransfers/packages/address/signaturescheme/signature.go +++ b/dapps/valuetransfers/packages/address/signaturescheme/signature.go @@ -12,4 +12,10 @@ type Signature interface { // Address returns the address that this signature signs. Address() address.Address + + // PublicKeySize returns the size of the public key. + PublicKeySize() int + + // SignatureSize returns the size of the signature. + SignatureSize() int } diff --git a/dapps/valuetransfers/packages/transaction/transaction.go b/dapps/valuetransfers/packages/transaction/transaction.go index 7046fe4a93d76f50fa66dde7a4ea639cc14a7dc3..da41e4a4140bc49d769891ee296f98b86056bfd1 100644 --- a/dapps/valuetransfers/packages/transaction/transaction.go +++ b/dapps/valuetransfers/packages/transaction/transaction.go @@ -177,6 +177,20 @@ func (transaction *Transaction) SignaturesValid() bool { return signaturesValid } +// Signatures returns all the signatures in this transaction. +func (transaction *Transaction) Signatures() (signatures []signaturescheme.Signature) { + transaction.inputs.ForEachAddress(func(address address.Address) bool { + signature, exists := transaction.signatures.Get(address) + if !exists || !signature.IsValid(transaction.EssenceBytes()) { + return false + } + signatures = append(signatures, signature) + return true + }) + + return signatures +} + // InputsCountValid returns true if the number of inputs in this transaction is not greater than MaxTransactionInputCount. func (transaction *Transaction) InputsCountValid() bool { return transaction.inputs.Size() <= MaxTransactionInputCount diff --git a/plugins/webapi/value/plugin.go b/plugins/webapi/value/plugin.go index f9f4cb9b16b70f835a275b30528b27703e9a54d4..f9f8a4ca4169943fb4a50ece68ba5738fac0809f 100644 --- a/plugins/webapi/value/plugin.go +++ b/plugins/webapi/value/plugin.go @@ -7,6 +7,7 @@ import ( "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/sendtransactionbyjson" "github.com/iotaledger/goshimmer/plugins/webapi/value/testsendtxn" "github.com/iotaledger/goshimmer/plugins/webapi/value/unspentoutputs" "github.com/iotaledger/hive.go/node" @@ -33,6 +34,7 @@ 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/sendTransactionByJson", sendtransactionbyjson.Handler) webapi.Server().POST("value/testSendTxn", testsendtxn.Handler) webapi.Server().GET("value/transactionByID", gettransactionbyid.Handler) } diff --git a/plugins/webapi/value/sendtransactionbyjson/handler.go b/plugins/webapi/value/sendtransactionbyjson/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..5300e48db0f2a219e8236d20df35ae128a7436c9 --- /dev/null +++ b/plugins/webapi/value/sendtransactionbyjson/handler.go @@ -0,0 +1,234 @@ +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"` +} diff --git a/plugins/webapi/value/sendtransactionbyjson/transaction_test.go b/plugins/webapi/value/sendtransactionbyjson/transaction_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7ff143bd70aa9b5dbdc2ac54e7f573c6f6cd9b3a --- /dev/null +++ b/plugins/webapi/value/sendtransactionbyjson/transaction_test.go @@ -0,0 +1,90 @@ +package sendtransactionbyjson + +import ( + "testing" + + "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/webapi/value/utils" + "github.com/iotaledger/hive.go/crypto/ed25519" + "github.com/mr-tron/base58/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewTransactionFromJSON(t *testing.T) { + // generate ed25519 keypair + keyPair1 := ed25519.GenerateKeyPair() + sigScheme1 := signaturescheme.ED25519(keyPair1) + // generate BLS keypair + sigScheme2 := signaturescheme.RandBLS() + addr1 := sigScheme1.Address() + addr2 := sigScheme2.Address() + // create first input + o1 := transaction.NewOutputID(addr1, transaction.RandomID()) + // create second input + o2 := transaction.NewOutputID(addr2, transaction.RandomID()) + inputs := transaction.NewInputs(o1, o2) + bal := balance.New(balance.ColorIOTA, 1) + outputs := transaction.NewOutputs(map[address.Address][]*balance.Balance{address.Random(): {bal}}) + tx := transaction.New(inputs, outputs) + tx.SetDataPayload([]byte("TEST")) + // sign with ed25519 + tx.Sign(sigScheme1) + // sign with BLS + tx.Sign(sigScheme2) + + // Parse inputs to base58 + inputsBase58 := []string{} + inputs.ForEach(func(outputId transaction.OutputID) bool { + inputsBase58 = append(inputsBase58, base58.Encode(outputId.Bytes())) + return true + }) + + // Parse outputs to base58 + outputsBase58 := []Output{} + outputs.ForEach(func(address address.Address, balances []*balance.Balance) bool { + var b []utils.Balance + for _, balance := range balances { + b = append(b, utils.Balance{ + Value: balance.Value, + Color: balance.Color.String(), + }) + } + t := Output{ + Address: address.String(), + Balances: b, + } + outputsBase58 = append(outputsBase58, t) + + return true + }) + + // Parse signatures to base58 + signaturesBase58 := []Signature{} + for _, signature := range tx.Signatures() { + signaturesBase58 = append(signaturesBase58, Signature{ + Version: signature.Bytes()[0], + PublicKey: base58.Encode(signature.Bytes()[1 : signature.PublicKeySize()+1]), + Signature: base58.Encode(signature.Bytes()[1+signature.PublicKeySize():]), + }) + } + + // create tx JSON + jsonRequest := Request{ + Inputs: inputsBase58, + Outputs: outputsBase58, + Data: []byte("TEST"), + Signatures: signaturesBase58, + } + txFromJSON, err := NewTransactionFromJSON(jsonRequest) + require.NoError(t, err) + + // compare signatures + assert.Equal(t, tx.SignatureBytes(), txFromJSON.SignatureBytes()) + + // conmpare transactions + assert.Equal(t, tx, txFromJSON) +}