taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

commit a1472480495a815194c5dd55072637f937ac2877
parent c4fff82b71e613069ac7a36575dbb1cb5baa09f2
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Mon, 11 Jul 2022 23:29:42 +0200

start merchant api integration. lots of open issues. tests fail.

Diffstat:
Mpkg/rest/taldir.go | 46++++++++++++++++++++++++++++++++++++----------
Apkg/taler/merchant.go | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/util/helper.go | 45++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 232 insertions(+), 13 deletions(-)

diff --git a/pkg/rest/taldir.go b/pkg/rest/taldir.go @@ -44,6 +44,7 @@ import ( "encoding/base64" "taler.net/taldir/pkg/util" "taler.net/taldir/pkg/gana" + "taler.net/taldir/pkg/taler" "crypto/sha512" "gorm.io/driver/postgres" "gopkg.in/ini.v1" @@ -87,6 +88,9 @@ type Taldir struct { // Challenge length in bytes before encoding ChallengeBytes int + + // Merchant object + Merchant taler.Merchant } type VersionResponse struct { @@ -207,6 +211,9 @@ type Validation struct { // The beginning of the last solution timeframe LastSolutionTimeframeStart time.Time + + // The Taler Merchant Order ID + OrderId string } type ErrorDetail struct { @@ -383,16 +390,6 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ w.Write(resp) return } - if util.AmountIsNonZero(t.Cfg.Section("taldir-" + vars["method"]).Key("challenge_fee").MustString("KUDOS:0")) || - util.AmountIsNonZero(t.Cfg.Section("taldir").Key("monthly_fee").MustString("KUDOS:0")) { - if len(order.Id) == 0 { - w.WriteHeader(http.StatusPaymentRequired) - return - } - // FIXME process order_id - w.WriteHeader(http.StatusNotImplemented) - return - } // Setup validation object. Retrieve object from DB if it already // exists. h := sha512.New() @@ -420,6 +417,34 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ validation.PublicKey = req.PublicKey validation.SolutionAttemptCount = 0 validation.LastSolutionTimeframeStart = time.Now() + amountSum, amountSumStr, _ := util.AmountSum(t.Cfg.Section("taldir-" + vars["method"]).Key("challenge_fee").MustString("KUDOS:0"), + t.Cfg.Section("taldir").Key("monthly_fee").MustString("KUDOS:0")) + if amountSum > 0 { + // FIXME what if provided order ID and validation order ID differ??? + if len(validation.OrderId) == 0 { + // Add new order for new validations + orderId, newOrderErr := t.Merchant.AddNewOrder(amountSumStr) + if newOrderErr != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + validation.OrderId = orderId + } + // Check if order paid + payto, paytoErr := t.Merchant.IsOrderPaid(validation.OrderId) + if paytoErr != nil { + if len(payto) != 0 { + w.WriteHeader(http.StatusPaymentRequired) + w.Header().Set("Location", payto) // FIXME no idea what to do with this. + return + } + w.WriteHeader(http.StatusInternalServerError) + log.Println(paytoErr) + return + } + // In this case, this order was paid + } + if err == nil { // Limit re-initiation attempts validation.InitiationCount++ @@ -733,6 +758,7 @@ func (t *Taldir) Initialize(cfgfile string) { if "" == t.Salt { t.Salt = t.Cfg.Section("taldir").Key("salt").MustString("ChangeMe") } + t.Merchant = taler.NewMerchant("http://localhost:8880", "myInstance") t.setupHandlers() } diff --git a/pkg/taler/merchant.go b/pkg/taler/merchant.go @@ -0,0 +1,154 @@ +package taler + +import ( + "net/http" + "encoding/json" + "bytes" + "fmt" + "errors" +) + +type PostOrderRequest struct { + // The order must at least contain the minimal + // order detail, but can override all. + order MinimalOrderDetail + + // If set, the backend will then set the refund deadline to the current + // time plus the specified delay. If it's not set, refunds will not be + // possible. + RefundDelay int64 `json:"refund_delay,omitempty"` + + // Specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + PaymentTarget string `json:"payment_target,omitempty"` + + // Specifies that some products are to be included in the + // order from the inventory. For these inventory management + // is performed (so the products must be in stock) and + // details are completed from the product data of the backend. + // FIXME: Not sure we actually need this for now + //InventoryProducts []MinimalInventoryProduct `json:"inventory_products,omitempty"` + + // Specifies a lock identifier that was used to + // lock a product in the inventory. Only useful if + // inventory_products is set. Used in case a frontend + // reserved quantities of the individual products while + // the shopping cart was being built. Multiple UUIDs can + // be used in case different UUIDs were used for different + // products (i.e. in case the user started with multiple + // shopping sessions that were combined during checkout). + LockUuids []string `json:"lock_uuids"` + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + CreateToken bool `json:"create_token,omitempty"` + +} + +type MinimalOrderDetail struct { + // Amount to be paid by the customer. + Amount string + + // Short summary of the order. + Summary string; +} + + +// NOTE: Part of the above but optional +type FulfillmentMetadata struct { + // See documentation of fulfillment_url in ContractTerms. + // Either fulfillment_url or fulfillment_message must be specified. + FulfillmentUrl string `json:"fulfillment_url,omitempty"` + + // See documentation of fulfillment_message in ContractTerms. + // Either fulfillment_url or fulfillment_message must be specified. + FulfillmentMessage string `json:"fulfillment_message,omitempty"` +} + +type PostOrderResponse struct { + // Order ID of the response that was just created. + OrderId string `json:"order_id"` +} + +type PostOrderResponseToken struct { + // Token that authorizes the wallet to claim the order. + // Provided only if "create_token" was set to 'true' + // in the request. + Token string +} + +type CheckPaymentStatusResponse struct { + // Status of the order + OrderStatus string `json:"order_status"` +} + +type CheckPaymentPaytoResponse struct { + // Status of the order + TalerPayUri string `json:"taler_pay_uri"` +} + + + +type Merchant struct { + + // The host of this merchant + Host string; + + // The instance of this merchant + Instance string; +} + +func NewMerchant(merchHost string, merchInstance string) Merchant { + return Merchant{ + Host: merchHost, + Instance: merchInstance, + } +} + +func (m *Merchant) IsOrderPaid(orderId string) (string, error) { + var orderPaidResponse CheckPaymentStatusResponse + var paytoResponse CheckPaymentPaytoResponse + resp, err := http.Get(m.Host + "/instances/"+ m.Instance + "/private/orders/" + orderId) + if nil != err { + return "", err + } + defer resp.Body.Close() + if http.StatusOK != resp.StatusCode { + message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) + return "", errors.New(message) + } + err = json.NewDecoder(resp.Body).Decode(&orderPaidResponse) + if err != nil { + return "", err + } + if orderPaidResponse.OrderStatus != "paid" { + err = json.NewDecoder(resp.Body).Decode(&paytoResponse) + return paytoResponse.TalerPayUri, err + } + return "", nil +} + +func (m *Merchant) AddNewOrder(amount string) (string, error) { + var newOrder PostOrderRequest + var orderDetail MinimalOrderDetail + var orderResponse PostOrderResponse + orderDetail.Amount = amount + // FIXME get from cfg + orderDetail.Summary = "This is an order to a TalDir registration" + newOrder.order = orderDetail + reqString, _ := json.Marshal(newOrder) + resp, err := http.Post(m.Host + "/instances/"+ m.Instance + "/private/orders", "application/json", bytes.NewBuffer(reqString)) + + if nil != err { + return "", err + } + defer resp.Body.Close() + if http.StatusOK != resp.StatusCode { + message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) + return "", errors.New(message) + } + err = json.NewDecoder(resp.Body).Decode(&orderResponse) + return orderResponse.OrderId, err +} diff --git a/pkg/util/helper.go b/pkg/util/helper.go @@ -23,6 +23,7 @@ import ( "fmt" "crypto/sha512" "math/rand" + "errors" "strings" "strconv" ) @@ -53,13 +54,51 @@ func GenerateChallenge(bytes int) string { // Check if this is a non-zero, positive amount func AmountIsNonZero(amount string) bool { + amountFloat, _ := AmountToFloat(amount) + return amountFloat > 0 +} + +func AmountCurrency(amount string) (string, error) { + s := strings.Split(amount, ":") + if len(s) != 2 { + return "", errors.New("Malformed amount") + } + return s[0], nil +} + + +func AmountToFloat(amount string) (float64, error) { s := strings.Split(amount, ":") if len(s) != 2 { - return false + return 0.0, errors.New("Malformed amount") } amountFloat, err := strconv.ParseFloat(s[1], 64) if err != nil { - return false + return 0.0, errors.New("Malformed value in amount") } - return amountFloat > 0 + return amountFloat, nil +} + +// Check if this is a non-zero, positive amount +func AmountSum(amountA string, amountB string) (float64, string, error) { + curA, err := AmountCurrency(amountA) + if nil != err { + return 0.0, "", errors.New("Currency in amount malformed") + } + curB, err := AmountCurrency(amountB) + if nil != err { + return 0.0, "", errors.New("Currency in amount malformed") + } + if curA != curB { + return 0.0, "", errors.New("Currency in amounts different") + } + valA, err := AmountToFloat(amountA) + if err != nil { + return 0.0, "", err + } + valB, err := AmountToFloat(amountB) + if err != nil { + return 0.0, "", err + } + return valA + valB, curA, nil }