From 014cd421a14ae3e5a753591bd8ffde2b144f474c Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Thu, 24 Mar 2016 19:01:11 +0100 Subject: [PATCH] PathPayment --- README.md | 30 ++- .../handlers/request_handler_payment.go | 178 +++++++++++++++--- .../horizon/submit_transaction_response.go | 1 + .../stellar/go-stellar-base/build/main.go | 23 +++ .../go-stellar-base/build/path_payment.go | 119 ++++++++++++ .../stellar/go-stellar-base/build/payment.go | 26 +-- .../go-stellar-base/build/transaction.go | 12 ++ .../stellar/go-stellar-base/build/util.go | 28 +++ 8 files changed, 358 insertions(+), 59 deletions(-) create mode 100644 vendor/src/github.com/stellar/go-stellar-base/build/path_payment.go diff --git a/README.md b/README.md index f0ca098..4bb2791 100644 --- a/README.md +++ b/README.md @@ -68,19 +68,43 @@ Then you can start the server: ### POST /payment -Builds and submits a transaction with a single [`payment`](https://www.stellar.org/developers/learn/concepts/list-of-operations.html#payment) operation built from following parameters. +Builds and submits a transaction with a single [`payment`](https://www.stellar.org/developers/learn/concepts/list-of-operations.html#payment), [`path_payment`](https://www.stellar.org/developers/learn/concepts/list-of-operations.html#path-payment) or [`create_account`](https://www.stellar.org/developers/learn/concepts/list-of-operations.html#create-account) (when sending native asset to account that does not exist) operation built from following parameters. #### Request Parameters +Every request must contain required parameters from the following list. Additionally, depending on `type` parameter, every request must contain required parameters for equivalent operation type (check tables below). + name | | description --- | --- | --- `source` | required | Secret seed of transaction source account `destination` | required | Account ID or Stellar address (ex. `bob*stellar.org`) of payment destination account +`type` | optional | Operation type: `payment` (default) or `path_payment`. +`memo_type` | optional | Memo type, one of: `id`, `text`, `hash` +`memo` | optional | Memo value, when `memo_type` is `id` it must be uint64, when `hash` it must be 32 bytes hex value + +##### CreateAccount / Payment operation parameters + +name | | description +--- | --- | --- `amount` | required | Amount to send `asset_code` | optional | Asset code (XLM when empty) `asset_issuer` | optional | Account ID of asset issuer (XLM when empty) -`memo_type` | optional | Memo type, one of: `id`, `text`, `hash` -`memo` | optional | Memo value, when `memo_type` is `id` it must be uint64, when `hash` it must be 32 bytes hex value + +##### PathPayment operation parameters + +name | | description +--- | --- | --- +`send_max` | required | Maximum amount of send_asset to send +`send_asset_code` | optional | Sending asset code (XLM when empty) +`send_asset_issuer` | optional | Account ID of sending asset issuer (XLM when empty) +`destination_amount` | required | Amount of destination_asset to receiving account will get +`destination_asset_code` | optional | Destination asset code (XLM when empty) +`destination_asset_issuer` | optional | Account ID of destination asset issuer (XLM when empty) +`path[n][asset_code]` | optional | Asset code of `n`th asset on the path (XLM when empty, but empty parameter must be sent!) +`path[n][asset_issuer]` | optional | Account ID of `n`th asset issuer (XLM when empty, but empty parameter must be sent!) +`path[n+1][asset_code]` | optional | Asset code of `n+1`th asset on the path (XLM when empty, but empty parameter must be sent!) +`path[n+1][asset_issuer]` | optional | Account ID of `n+1`th asset issuer (XLM when empty, but empty parameter must be sent!) +... | ... | _Up to 5 assets in the path..._ #### Response diff --git a/src/github.com/stellar/gateway/handlers/request_handler_payment.go b/src/github.com/stellar/gateway/handlers/request_handler_payment.go index bfea03f..798dbec 100644 --- a/src/github.com/stellar/gateway/handlers/request_handler_payment.go +++ b/src/github.com/stellar/gateway/handlers/request_handler_payment.go @@ -2,6 +2,7 @@ package handlers import ( "encoding/hex" + "fmt" log "github.com/Sirupsen/logrus" "net/http" "strconv" @@ -37,41 +38,25 @@ func (rh *RequestHandler) Payment(w http.ResponseWriter, r *http.Request) { return } - amount := r.PostFormValue("amount") - assetCode := r.PostFormValue("asset_code") - assetIssuer := r.PostFormValue("asset_issuer") - var operationBuilder interface{} + var errorResponse *horizon.SubmitTransactionResponseError - if assetCode != "" && assetIssuer != "" { - issuerKeypair, err := keypair.Parse(assetIssuer) - if err != nil { - log.WithFields(log.Fields{"asset_issuer": assetIssuer}).Print("Invalid asset_issuer parameter") - writeError(w, horizon.PaymentInvalidIssuer) + paymentType := r.PostFormValue("type") + switch paymentType { + case "": + case "payment": + log.Println("payment") + operationBuilder, errorResponse = rh.createPaymentOperation(r, destinationObject) + case "path_payment": + log.Println("path_payment") + operationBuilder, errorResponse = rh.createPathPaymentOperation(r, destinationObject) + default: + writeError(w, horizon.PaymentInvalidType) return - } - - operationBuilder = b.Payment( - b.Destination{destinationObject.AccountId}, - b.CreditAmount{assetCode, issuerKeypair.Address(), amount}, - ) - } else if assetCode == "" && assetIssuer == "" { - mutators := []interface{}{ - b.Destination{destinationObject.AccountId}, - b.NativeAmount{amount}, - } + } - // Check if destination account exist - _, err = rh.Horizon.LoadAccount(destinationObject.AccountId) - if err != nil { - log.WithFields(log.Fields{"error": err}).Error("Error loading account") - operationBuilder = b.CreateAccount(mutators...) - } else { - operationBuilder = b.Payment(mutators...) - } - } else { - log.Print("Missing asset param.") - writeError(w, horizon.PaymentMissingParamAsset) + if errorResponse != nil { + writeError(w, errorResponse) return } @@ -187,3 +172,134 @@ func (rh *RequestHandler) Payment(w http.ResponseWriter, r *http.Request) { write(w, submitResponse) } + +func (rh *RequestHandler) createPaymentOperation(r *http.Request, destinationObject StellarDestination) (operationBuilder interface{}, errorResponse *horizon.SubmitTransactionResponseError) { + amount := r.PostFormValue("amount") + assetCode := r.PostFormValue("asset_code") + assetIssuer := r.PostFormValue("asset_issuer") + + if assetCode != "" && assetIssuer != "" { + issuerKeypair, err := keypair.Parse(assetIssuer) + if err != nil { + log.WithFields(log.Fields{"asset_issuer": assetIssuer}).Print("Invalid asset_issuer parameter") + errorResponse = horizon.PaymentInvalidIssuer + return + } + + operationBuilder = b.Payment( + b.Destination{destinationObject.AccountId}, + b.CreditAmount{assetCode, issuerKeypair.Address(), amount}, + ) + + if operationBuilder.(b.PaymentBuilder).Err != nil { + log.WithFields(log.Fields{"err": operationBuilder.(b.PaymentBuilder).Err}).Print("Error building operation") + errorResponse = horizon.ServerError + return + } + } else if assetCode == "" && assetIssuer == "" { + mutators := []interface{}{ + b.Destination{destinationObject.AccountId}, + b.NativeAmount{amount}, + } + + // Check if destination account exist + _, err := rh.Horizon.LoadAccount(destinationObject.AccountId) + if err != nil { + log.WithFields(log.Fields{"error": err}).Error("Error loading account") + operationBuilder = b.CreateAccount(mutators...) + if operationBuilder.(b.CreateAccountBuilder).Err != nil { + log.WithFields(log.Fields{"err": operationBuilder.(b.CreateAccountBuilder).Err}).Print("Error building operation") + errorResponse = horizon.ServerError + return + } + } else { + operationBuilder = b.Payment(mutators...) + if operationBuilder.(b.PaymentBuilder).Err != nil { + log.WithFields(log.Fields{"err": operationBuilder.(b.PaymentBuilder).Err}).Print("Error building operation") + errorResponse = horizon.ServerError + return + } + } + } else { + log.Print("Missing asset param.") + errorResponse = horizon.PaymentMissingParamAsset + return + } + return +} + +func (rh *RequestHandler) createPathPaymentOperation(r *http.Request, destinationObject StellarDestination) (operationBuilder interface{}, errorResponse *horizon.SubmitTransactionResponseError) { + sendMax := r.PostFormValue("send_max") + sendAssetCode := r.PostFormValue("send_asset_code") + sendAssetIssuer := r.PostFormValue("send_asset_issuer") + + var sendAsset b.Asset + if sendAssetCode != "" && sendAssetIssuer != "" { + sendAsset = b.Asset{Code: sendAssetCode, Issuer: sendAssetIssuer} + } else if sendAssetCode == "" && sendAssetIssuer == "" { + sendAsset = b.Asset{Native: true} + } else { + log.Print("Missing send asset param.") + errorResponse = horizon.PaymentMissingParamAsset + return + } + + destinationAmount := r.PostFormValue("destination_amount") + destinationAssetCode := r.PostFormValue("destination_asset_code") + destinationAssetIssuer := r.PostFormValue("destination_asset_issuer") + + var destinationAsset b.Asset + if destinationAssetCode != "" && destinationAssetIssuer != "" { + destinationAsset = b.Asset{Code: destinationAssetCode, Issuer: destinationAssetIssuer} + } else if destinationAssetCode == "" && destinationAssetIssuer == "" { + destinationAsset = b.Asset{Native: true} + } else { + log.Print("Missing destination asset param.") + errorResponse = horizon.PaymentMissingParamAsset + return + } + + // TODO check the fields + + var path []b.Asset + + for i := 0; ; i++ { + codeFieldName := fmt.Sprintf("path[%d][asset_code]", i) + issuerFieldName := fmt.Sprintf("path[%d][asset_issuer]", i) + + // If the element does not exist in PostForm break the loop + if _, exists := r.PostForm[codeFieldName]; !exists { + break + } + + code := r.PostFormValue(codeFieldName) + issuer := r.PostFormValue(issuerFieldName) + + if code == "" && issuer == "" { + path = append(path, b.Asset{Native: true}) + } else { + path = append(path, b.Asset{Code: code, Issuer: issuer}) + } + } + + operationBuilder = b.PathPayment( + b.Destination{destinationObject.AccountId}, + b.PathSend{ + Asset: sendAsset, + MaxAmount: sendMax, + }, + b.PathDestination{ + Asset: destinationAsset, + Amount: destinationAmount, + }, + b.Path{Assets: path}, + ) + + if operationBuilder.(b.PathPaymentBuilder).Err != nil { + log.WithFields(log.Fields{"err": operationBuilder.(b.PathPaymentBuilder).Err}).Print("Error building operation") + errorResponse = horizon.ServerError + return + } + + return +} diff --git a/src/github.com/stellar/gateway/horizon/submit_transaction_response.go b/src/github.com/stellar/gateway/horizon/submit_transaction_response.go index ab82a34..c536c61 100644 --- a/src/github.com/stellar/gateway/horizon/submit_transaction_response.go +++ b/src/github.com/stellar/gateway/horizon/submit_transaction_response.go @@ -31,6 +31,7 @@ var ( // /send & /payment // input errors + PaymentInvalidType = &SubmitTransactionResponseError{Code: "invalid_type", Message: "Invalid operation type.", Status: http.StatusBadRequest} PaymentInvalidSource = &SubmitTransactionResponseError{Code: "invalid_source", Message: "source parameter is invalid.", Status: http.StatusBadRequest} PaymentCannotResolveDestination = &SubmitTransactionResponseError{Code: "cannot_resolve_destination", Message: "Cannot resolve federated Stellar address.", Status: http.StatusBadRequest} PaymentInvalidDestination = &SubmitTransactionResponseError{Code: "invalid_destination", Message: "destination parameter is invalid.", Status: http.StatusBadRequest} diff --git a/vendor/src/github.com/stellar/go-stellar-base/build/main.go b/vendor/src/github.com/stellar/go-stellar-base/build/main.go index 05718b0..b89981b 100644 --- a/vendor/src/github.com/stellar/go-stellar-base/build/main.go +++ b/vendor/src/github.com/stellar/go-stellar-base/build/main.go @@ -34,6 +34,13 @@ var ( DefaultNetwork = TestNetwork ) +// Asset is struct used in path_payment mutators +type Asset struct { + Native bool + Code string + Issuer string +} + // AllowTrustAsset is a mutator capable of setting the asset on // an operations that have one. type AllowTrustAsset struct { @@ -92,6 +99,22 @@ type NativeAmount struct { Amount string } +type Path struct { + Assets []Asset +} + +// PathDestination is a mutator that configures a path_payment's destination asset and amount +type PathDestination struct { + Asset + Amount string +} + +// PathSend is a mutator that configures a path_payment's send asset and max amount +type PathSend struct { + Asset + MaxAmount string +} + // Sequence is a mutator that sets the sequence number on a transaction type Sequence struct { Sequence uint64 diff --git a/vendor/src/github.com/stellar/go-stellar-base/build/path_payment.go b/vendor/src/github.com/stellar/go-stellar-base/build/path_payment.go new file mode 100644 index 0000000..6e69510 --- /dev/null +++ b/vendor/src/github.com/stellar/go-stellar-base/build/path_payment.go @@ -0,0 +1,119 @@ +package build + +import ( + "errors" + + "github.com/stellar/go-stellar-base/amount" + "github.com/stellar/go-stellar-base/xdr" +) + +// PathPayment groups the creation of a new PathPaymentBuilder with a call to Mutate. +func PathPayment(muts ...interface{}) (result PathPaymentBuilder) { + result.Mutate(muts...) + return +} + +// PathPaymentMutator is a interface that wraps the +// MutatePathPayment operation. types may implement this interface to +// specify how they modify an xdr.PathPaymentOp object +type PathPaymentMutator interface { + MutatePathPayment(*xdr.PathPaymentOp) error +} + +// PathPaymentBuilder represents a transaction that is being built. +type PathPaymentBuilder struct { + O xdr.Operation + P xdr.PathPaymentOp + Err error +} + +// Mutate applies the provided mutators to this builder's payment or operation. +func (b *PathPaymentBuilder) Mutate(muts ...interface{}) { + for _, m := range muts { + var err error + switch mut := m.(type) { + case PathPaymentMutator: + err = mut.MutatePathPayment(&b.P) + case OperationMutator: + err = mut.MutateOperation(&b.O) + default: + err = errors.New("Mutator type not allowed") + } + + if err != nil { + b.Err = err + return + } + } +} + +// MutatePayment for Destination sets the PathPaymentOp's Destination field +func (m Destination) MutatePathPayment(o *xdr.PathPaymentOp) error { + return setAccountId(m.AddressOrSeed, &o.Destination) +} + +// MutatePayment for PathDestination sets the PathPaymentOp's DestinationAsset and +// DestinationAmount fields +func (m PathDestination) MutatePathPayment(o *xdr.PathPaymentOp) (err error) { + switch { + case m.Asset.Native: + o.DestAsset, err = xdr.NewAsset(xdr.AssetTypeAssetTypeNative, nil) + case !m.Asset.Native: + o.DestAsset, err = createAlphaNumAsset(m.Asset.Code, m.Asset.Issuer) + default: + err = errors.New("Unknown Asset type") + } + + if err != nil { + return + } + + o.DestAmount, err = amount.Parse(m.Amount) + return +} + +// MutatePayment for PathSend sets the PathPaymentOp's SendAsset and +// SendMax fields +func (m PathSend) MutatePathPayment(o *xdr.PathPaymentOp) (err error) { + switch { + case m.Asset.Native: + o.SendAsset, err = xdr.NewAsset(xdr.AssetTypeAssetTypeNative, nil) + case !m.Asset.Native: + o.SendAsset, err = createAlphaNumAsset(m.Asset.Code, m.Asset.Issuer) + default: + err = errors.New("Unknown Asset type") + } + + if err != nil { + return + } + + o.SendMax, err = amount.Parse(m.MaxAmount) + return +} + +// MutatePayment for Path sets the PathPaymentOp's Path field +func (m Path) MutatePathPayment(o *xdr.PathPaymentOp) (err error) { + var path []xdr.Asset + var xdrAsset xdr.Asset + + for _, asset := range m.Assets { + switch { + case asset.Native: + xdrAsset, err = xdr.NewAsset(xdr.AssetTypeAssetTypeNative, nil) + path = append(path, xdrAsset) + case !asset.Native: + xdrAsset, err = createAlphaNumAsset(asset.Code, asset.Issuer) + path = append(path, xdrAsset) + default: + err = errors.New("Unknown Asset type") + } + + if err != nil { + return err + } + } + + o.Path = path + return +} diff --git a/vendor/src/github.com/stellar/go-stellar-base/build/payment.go b/vendor/src/github.com/stellar/go-stellar-base/build/payment.go index a46a97f..e6c3fbc 100644 --- a/vendor/src/github.com/stellar/go-stellar-base/build/payment.go +++ b/vendor/src/github.com/stellar/go-stellar-base/build/payment.go @@ -54,31 +54,7 @@ func (m CreditAmount) MutatePayment(o *xdr.PaymentOp) (err error) { return } - length := len(m.Code) - - var issuer xdr.AccountId - err = setAccountId(m.Issuer, &issuer) - if err != nil { - return - } - - switch { - case length >= 1 && length <= 4: - var code [4]byte - byteArray := []byte(m.Code) - copy(code[:], byteArray[0:length]) - asset := xdr.AssetAlphaNum4{code, issuer} - o.Asset, err = xdr.NewAsset(xdr.AssetTypeAssetTypeCreditAlphanum4, asset) - case length >= 5 && length <= 12: - var code [12]byte - byteArray := []byte(m.Code) - copy(code[:], byteArray[0:length]) - asset := xdr.AssetAlphaNum12{code, issuer} - o.Asset, err = xdr.NewAsset(xdr.AssetTypeAssetTypeCreditAlphanum12, asset) - default: - err = errors.New("Asset code length is invalid") - } - + o.Asset, err = createAlphaNumAsset(m.Code, m.Issuer) return } diff --git a/vendor/src/github.com/stellar/go-stellar-base/build/transaction.go b/vendor/src/github.com/stellar/go-stellar-base/build/transaction.go index b11c744..b111ea7 100644 --- a/vendor/src/github.com/stellar/go-stellar-base/build/transaction.go +++ b/vendor/src/github.com/stellar/go-stellar-base/build/transaction.go @@ -173,6 +173,18 @@ func (m Network) MutateTransaction(o *TransactionBuilder) error { return nil } +// MutateTransaction for PaymentBuilder causes the underylying PaymentOp +// to be added to the operation list for the provided transaction +func (m PathPaymentBuilder) MutateTransaction(o *TransactionBuilder) error { + if m.Err != nil { + return m.Err + } + + m.O.Body, m.Err = xdr.NewOperationBody(xdr.OperationTypePathPayment, m.P) + o.TX.Operations = append(o.TX.Operations, m.O) + return m.Err +} + // MutateTransaction for PaymentBuilder causes the underylying PaymentOp // to be added to the operation list for the provided transaction func (m PaymentBuilder) MutateTransaction(o *TransactionBuilder) error { diff --git a/vendor/src/github.com/stellar/go-stellar-base/build/util.go b/vendor/src/github.com/stellar/go-stellar-base/build/util.go index beca721..8045172 100644 --- a/vendor/src/github.com/stellar/go-stellar-base/build/util.go +++ b/vendor/src/github.com/stellar/go-stellar-base/build/util.go @@ -1,6 +1,8 @@ package build import ( + "errors" + "github.com/stellar/go-stellar-base/keypair" "github.com/stellar/go-stellar-base/xdr" ) @@ -13,3 +15,29 @@ func setAccountId(addressOrSeed string, aid *xdr.AccountId) error { return aid.SetAddress(kp.Address()) } + +func createAlphaNumAsset(code, issuerAccountId string) (xdr.Asset, error) { + var issuer xdr.AccountId + err := setAccountId(issuerAccountId, &issuer) + if err != nil { + return xdr.Asset{}, err + } + + length := len(code) + switch { + case length >= 1 && length <= 4: + var codeArray [4]byte + byteArray := []byte(code) + copy(codeArray[:], byteArray[0:length]) + asset := xdr.AssetAlphaNum4{codeArray, issuer} + return xdr.NewAsset(xdr.AssetTypeAssetTypeCreditAlphanum4, asset) + case length >= 5 && length <= 12: + var codeArray [12]byte + byteArray := []byte(code) + copy(codeArray[:], byteArray[0:length]) + asset := xdr.AssetAlphaNum12{codeArray, issuer} + return xdr.NewAsset(xdr.AssetTypeAssetTypeCreditAlphanum12, asset) + default: + return xdr.Asset{}, errors.New("Asset code length is invalid") + } +}