diff --git a/examples/uma-server/currencies.go b/examples/uma-server/currencies.go new file mode 100644 index 0000000..bd7508b --- /dev/null +++ b/examples/uma-server/currencies.go @@ -0,0 +1,27 @@ +package main + +import "github.com/uma-universal-money-address/uma-go-sdk/uma" + +var SatsCurrency = uma.Currency{ + Code: "SAT", + Name: "Satoshis", + Symbol: "SAT", + MillisatoshiPerUnit: 1000, + Convertible: uma.ConvertibleCurrency{ + MinSendable: 1, + MaxSendable: 100_000_000, + }, + Decimals: 0, +} + +var UsdCurrency = uma.Currency{ + Code: "USD", + Name: "US Dollars", + Symbol: "$", + MillisatoshiPerUnit: MillisatoshiPerUsd, + Convertible: uma.ConvertibleCurrency{ + MinSendable: 1, + MaxSendable: 1_000, + }, + Decimals: 2, +} diff --git a/examples/uma-server/go.mod b/examples/uma-server/go.mod index 9151c57..c7b1447 100644 --- a/examples/uma-server/go.mod +++ b/examples/uma-server/go.mod @@ -9,7 +9,7 @@ require ( github.com/google/uuid v1.3.1 github.com/lightsparkdev/go-sdk v0.10.0 // Run go get github.com/uma-universal-money-address/uma-go-sdk@release/v1.0 to update: - github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240301061227-c82cc51dc509 + github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240308075308-61c0e50f5a95 ) require ( diff --git a/examples/uma-server/go.sum b/examples/uma-server/go.sum index a0d7854..c92a77b 100644 --- a/examples/uma-server/go.sum +++ b/examples/uma-server/go.sum @@ -955,6 +955,10 @@ github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240223211106-661bd4 github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240223211106-661bd445f2fd/go.mod h1:OimSKjRNT7Wm2lA0Q9C0DmxsMvqBickucUjtQmB8Cl8= github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240301061227-c82cc51dc509 h1:rfS4bg6ov5V602mikSQTCU3FcWDjCNrViW+ft5Q3Zb8= github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240301061227-c82cc51dc509/go.mod h1:OimSKjRNT7Wm2lA0Q9C0DmxsMvqBickucUjtQmB8Cl8= +github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240308063219-e424ad19886b h1:q144FZyk9kIIJyO2vlD+aYMRDTF0EzfwkSq1z0gihas= +github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240308063219-e424ad19886b/go.mod h1:OimSKjRNT7Wm2lA0Q9C0DmxsMvqBickucUjtQmB8Cl8= +github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240308075308-61c0e50f5a95 h1:8mWh0f4U6guzhbd/YxDuUrhAgGOkT+PU76lBh9swz8U= +github.com/uma-universal-money-address/uma-go-sdk v0.6.3-0.20240308075308-61c0e50f5a95/go.mod h1:OimSKjRNT7Wm2lA0Q9C0DmxsMvqBickucUjtQmB8Cl8= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= diff --git a/examples/uma-server/raw_lnurl.go b/examples/uma-server/raw_lnurl.go deleted file mode 100644 index 87e04de..0000000 --- a/examples/uma-server/raw_lnurl.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -type NonUmaLnurlpResponse struct { - Tag string `json:"tag"` - Callback string `json:"callback"` - MinSendable int64 `json:"minSendable"` - MaxSendable int64 `json:"maxSendable"` - Metadata string `json:"metadata"` -} - -type NonUmaPayReqResponse struct { - // EncodedInvoice is the BOLT11 invoice that the sender will pay. - EncodedInvoice string `json:"pr"` -} diff --git a/examples/uma-server/vasp1.go b/examples/uma-server/vasp1.go index af19b5f..11b5bd0 100644 --- a/examples/uma-server/vasp1.go +++ b/examples/uma-server/vasp1.go @@ -97,9 +97,16 @@ func (v *Vasp1) handleClientUmaLookup(context *gin.Context) { } lnurlpResponse, err := uma.ParseLnurlpResponse(responseBodyBytes) - if err != nil || lnurlpResponse.UmaVersion == "" || lnurlpResponse.Compliance.Signature == "" { + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to parse lnurlp response", + }) + return + } + if !lnurlpResponse.IsUmaResponse() { // Try to fall back to a non-UMA lnurlp response. - v.attemptToParseAsNonUmaLnurlpResponse(responseBodyBytes, receiverId, receiverVasp, context) + v.handleNonUmaLnurlpResponse(*lnurlpResponse, receiverId, receiverVasp, context) return } @@ -120,7 +127,7 @@ func (v *Vasp1) handleClientUmaLookup(context *gin.Context) { }) return } - err = uma.VerifyUmaLnurlpResponseSignature(lnurlpResponse, receiverSigningPubKey, v.nonceCache) + err = uma.VerifyUmaLnurlpResponseSignature(*lnurlpResponse.AsUmaResponse(), receiverSigningPubKey, v.nonceCache) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ "status": "ERROR", @@ -225,24 +232,17 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { return } - if initialRequestData.umaLnurlpResponse == nil { - if initialRequestData.nonUmaLnurlpResponse == nil { - context.JSON(http.StatusBadRequest, gin.H{ - "status": "ERROR", - "reason": "Invalid callback UUID", - }) - return - } - - // Fall back to non-UMA LNURL payreq flow. - v.handleNonUmaPayReq(context, initialRequestData, amountInt64, callbackUuid) - return - } - currencyCode := context.Query("currencyCode") + if currencyCode == "" { + currencyCode = "SAT" + } currencySupported := false - for i := range initialRequestData.umaLnurlpResponse.Currencies { - if initialRequestData.umaLnurlpResponse.Currencies[i].Code == currencyCode { + receiverCurrencies := initialRequestData.lnurlpResponse.Currencies + if receiverCurrencies == nil { + receiverCurrencies = &[]uma.Currency{SatsCurrency} + } + for i := range *receiverCurrencies { + if (*receiverCurrencies)[i].Code == currencyCode { currencySupported = true break } @@ -254,6 +254,25 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { }) return } + var payerInfo *PayerInfo + if initialRequestData.lnurlpResponse.RequiredPayerData != nil { + payerInfoVal := v.getPayerInfo(*initialRequestData.lnurlpResponse.RequiredPayerData, context) + payerInfo = &payerInfoVal + } + isAmountInMsats := strings.ToLower(context.Query("isAmountInMsats")) == "true" + if !initialRequestData.lnurlpResponse.IsUmaResponse() { + isAmountInMsats = strings.ToLower(context.Query("isAmountInMsats")) != "false" + } + var comment *string + if commentVal, ok := context.GetQuery("comment"); ok { + comment = &commentVal + } + + if !initialRequestData.lnurlpResponse.IsUmaResponse() { + v.handleNonUmaPayReq( + context, initialRequestData, amountInt64, callbackUuid, payerInfo, currencyCode, isAmountInMsats, comment) + return + } umaSigningPrivateKey, err := v.config.UmaSigningPrivKeyBytes() if err != nil { @@ -273,7 +292,6 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { return } - payerInfo := v.getPayerInfo(initialRequestData.umaLnurlpResponse.RequiredPayerData, context) trInfo := "Here is some fake travel rule info. It's up to you to actually implement this." senderUtxos, err := v.client.GetNodeChannelUtxos(v.config.NodeUUID) if err != nil { @@ -284,7 +302,6 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { }) return } - isAmountInMsats := strings.ToLower(context.Query("isAmountInMsats")) == "true" vasp2EncryptionPubKey, err := vasp2PubKeys.EncryptionPubKey() if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -293,6 +310,7 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { }) return } + senderNode, err := GetNode(v.client, v.config.NodeUUID) if err != nil || senderNode == nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -305,12 +323,12 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { // If you are using a standardized travel rule format, you can set this to something like: // "IVMS@101.2023". var trFormat *uma.TravelRuleFormat - payReq, err := uma.GetPayRequest( + payReq, err := uma.GetUmaPayRequest( + amountInt64, vasp2EncryptionPubKey, umaSigningPrivateKey, currencyCode, !isAmountInMsats, - amountInt64, payerInfo.Identifier, payerInfo.Name, payerInfo.Email, @@ -325,6 +343,7 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { uma.CounterPartyDataFieldEmail.String(): {Mandatory: false}, // Compliance and Identifier are mandatory fields added automatically. }, + nil, ) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -342,7 +361,7 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { }) return } - payreqResult, err := http.Post(initialRequestData.umaLnurlpResponse.Callback, "application/json", bytes.NewBuffer(payReqBytes)) + payreqResult, err := http.Post(initialRequestData.lnurlpResponse.Callback, "application/json", bytes.NewBuffer(payReqBytes)) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ "status": "ERROR", @@ -392,14 +411,14 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { } invoiceData := (*invoice).(objects.InvoiceData) compliance, err := payreqResponse.PayeeData.Compliance() - var utxoCallback = "" - if compliance != nil && compliance.UtxoCallback != "" { + var utxoCallback *string + if compliance != nil && compliance.UtxoCallback != nil && *compliance.UtxoCallback != "" { utxoCallback = compliance.UtxoCallback } v.requestCache.SavePayReqData( callbackUuid, payreqResponse.EncodedInvoice, - &utxoCallback, + utxoCallback, &invoiceData, ) @@ -564,29 +583,41 @@ func (v *Vasp1) handlePubKeyRequest(context *gin.Context) { context.JSON(http.StatusOK, response) } -func (v *Vasp1) attemptToParseAsNonUmaLnurlpResponse( - bodyBytes []byte, receiverId string, receiverDomain string, context *gin.Context) { - var nonUmaLnurlpResponse NonUmaLnurlpResponse - err := json.Unmarshal(bodyBytes, &nonUmaLnurlpResponse) - if err != nil { - context.JSON(http.StatusInternalServerError, gin.H{ - "status": "ERROR", - "reason": "Failed to parse lnurlp response", - }) - return +func (v *Vasp1) handleNonUmaLnurlpResponse( + lnurlpResponse uma.LnurlpResponse, receiverId string, receiverDomain string, context *gin.Context) { + callbackUuid := v.requestCache.SaveLnurlpResponseData(lnurlpResponse, receiverId, receiverDomain) + var serializedCurrencies = []byte("[]") + if lnurlpResponse.Currencies != nil && len(*lnurlpResponse.Currencies) == 0 { + var err error + serializedCurrencies, err = json.Marshal(lnurlpResponse.Currencies) + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to serialize currencies", + }) + return + } } - callbackUuid := v.requestCache.SaveNonUmaLnurlpResponseData(nonUmaLnurlpResponse, receiverId, receiverDomain) context.JSON(http.StatusOK, gin.H{ - "callbackUuid": callbackUuid, - "maxSendSats": nonUmaLnurlpResponse.MaxSendable, - "minSendSats": nonUmaLnurlpResponse.MinSendable, - "receiverKYCStatus": uma.KycStatusNotVerified, + "receiverCurrencies": serializedCurrencies, + "callbackUuid": callbackUuid, + "maxSendSats": lnurlpResponse.MaxSendable, + "minSendSats": lnurlpResponse.MinSendable, + "receiverKYCStatus": uma.KycStatusNotVerified, }) } func (v *Vasp1) handleNonUmaPayReq( - context *gin.Context, initialRequestData Vasp1InitialRequestData, amountInt64 int64, callbackUuid string) { - callbackUrl, err := url.Parse(initialRequestData.nonUmaLnurlpResponse.Callback) + context *gin.Context, + initialRequestData Vasp1InitialRequestData, + amountInt64 int64, + callbackUuid string, + payerInfo *PayerInfo, + currencyCode string, + isAmountInMsats bool, + comment *string, +) { + callbackUrl, err := url.Parse(initialRequestData.lnurlpResponse.Callback) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ "status": "ERROR", @@ -594,8 +625,41 @@ func (v *Vasp1) handleNonUmaPayReq( }) return } + var payerData *uma.PayerData + if payerInfo != nil { + payerData = &uma.PayerData{ + uma.CounterPartyDataFieldName.String(): payerInfo.Name, + uma.CounterPartyDataFieldEmail.String(): payerInfo.Email, + uma.CounterPartyDataFieldIdentifier.String(): payerInfo.Identifier, + } + } + var sendingAmountCurrencyCode *string + if !isAmountInMsats { + *sendingAmountCurrencyCode = currencyCode + } + payreq := uma.PayRequest{ + SendingAmountCurrencyCode: sendingAmountCurrencyCode, + ReceivingCurrencyCode: ¤cyCode, + Amount: amountInt64, + PayerData: payerData, + RequestedPayeeData: nil, + Comment: comment, + } + + payreqParams, err := payreq.EncodeAsUrlParams() + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to encode payreq as URL params", + }) + return + } queryParams := callbackUrl.Query() - queryParams.Add("amount", strconv.FormatInt(amountInt64, 10)) + for key, values := range *payreqParams { + for _, value := range values { + queryParams.Add(key, value) + } + } callbackUrl.RawQuery = queryParams.Encode() payreqResult, err := http.Get(callbackUrl.String()) @@ -626,7 +690,7 @@ func (v *Vasp1) handleNonUmaPayReq( return } - var payreqResponse NonUmaPayReqResponse + var payreqResponse uma.PayReqResponse err = json.Unmarshal(payreqResultBytes, &payreqResponse) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -653,14 +717,20 @@ func (v *Vasp1) handleNonUmaPayReq( &invoiceData, ) - context.JSON(http.StatusOK, gin.H{ + resp := gin.H{ "encodedInvoice": payreqResponse.EncodedInvoice, "callbackUuid": callbackUuid, - "amount": invoiceData.Amount, - "conversionRate": 1, - "currencyCode": "mSAT", + "amountMsats": invoiceData.Amount, + "currencyCode": currencyCode, "expiresAt": invoiceData.ExpiresAt.Unix(), - }) + } + if payreqResponse.PaymentInfo != nil { + resp["amountReceivingCurrency"] = payreqResponse.PaymentInfo.Amount + resp["conversionRate"] = payreqResponse.PaymentInfo.Multiplier + resp["exchangeFeesMsats"] = payreqResponse.PaymentInfo.ExchangeFeesMillisatoshi + resp["receivingCurrencyDecimals"] = payreqResponse.PaymentInfo.Decimals + } + context.JSON(http.StatusOK, resp) } func (v *Vasp1) getVaspDomain(context *gin.Context) string { diff --git a/examples/uma-server/vasp1_request_cache.go b/examples/uma-server/vasp1_request_cache.go index c3a3e4e..14bd11f 100644 --- a/examples/uma-server/vasp1_request_cache.go +++ b/examples/uma-server/vasp1_request_cache.go @@ -7,10 +7,9 @@ import ( ) type Vasp1InitialRequestData struct { - umaLnurlpResponse *uma.LnurlpResponse - nonUmaLnurlpResponse *NonUmaLnurlpResponse - receiverId string - vasp2Domain string + lnurlpResponse uma.LnurlpResponse + receiverId string + vasp2Domain string } type Vasp1PayReqData struct { @@ -48,20 +47,9 @@ func (c *Vasp1RequestCache) SaveLnurlpResponseData(lnurlpResponse uma.LnurlpResp // Generate a UUID for this request requestUUID := uuid.New().String() c.umaRequestCache[requestUUID] = Vasp1InitialRequestData{ - umaLnurlpResponse: &lnurlpResponse, - receiverId: receiverId, - vasp2Domain: vasp2Domain, - } - return requestUUID -} - -func (c *Vasp1RequestCache) SaveNonUmaLnurlpResponseData(lnurlpResponse NonUmaLnurlpResponse, receiverId string, vasp2Domain string) string { - // Generate a UUID for this request - requestUUID := uuid.New().String() - c.umaRequestCache[requestUUID] = Vasp1InitialRequestData{ - nonUmaLnurlpResponse: &lnurlpResponse, - receiverId: receiverId, - vasp2Domain: vasp2Domain, + lnurlpResponse: lnurlpResponse, + receiverId: receiverId, + vasp2Domain: vasp2Domain, } return requestUUID } diff --git a/examples/uma-server/vasp2.go b/examples/uma-server/vasp2.go index 5cfe577..951618d 100644 --- a/examples/uma-server/vasp2.go +++ b/examples/uma-server/vasp2.go @@ -7,11 +7,9 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/lightsparkdev/go-sdk/services" - lsuma "github.com/lightsparkdev/go-sdk/uma" "github.com/uma-universal-money-address/uma-go-sdk/uma" "net/http" "os" - "strconv" "strings" "time" ) @@ -71,21 +69,40 @@ func (v *Vasp2) handleWellKnownLnurlp(context *gin.Context) { requestUrl := context.Request.URL requestUrl.Host = context.Request.Host + lnurlpRequest, err := uma.ParseLnurlpRequest(*requestUrl) + if err != nil { + var unsupportedVersionErr *uma.UnsupportedVersionError + if errors.As(err, &unsupportedVersionErr) { + context.JSON(http.StatusPreconditionFailed, gin.H{ + "status": "ERROR", + "reason": fmt.Sprintf("Unsupported version: %s", unsupportedVersionErr.UnsupportedVersion), + "supportedMajorVersions": unsupportedVersionErr.SupportedMajorVersions, + "unsupportedVersion": unsupportedVersionErr.UnsupportedVersion, + }) + return + } + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": err.Error(), + }) + return + } + + umaLnurlpRequest := lnurlpRequest.AsUmaRequest() // Fallback to regular LNURL if the request is not a UMA request. - if !uma.IsUmaLnurlpQuery(*requestUrl) { - v.handleNonUmaLnurlRequest(context) + if umaLnurlpRequest == nil { + v.handleNonUmaLnurlRequest(context, *lnurlpRequest) return } - responseJson, hadError := v.parseUmaQueryData(context) + responseJson, hadError := v.handleUmaQueryData(context, *umaLnurlpRequest) if hadError { return } - context.Data(http.StatusOK, "application/json", responseJson) - return + context.JSON(http.StatusOK, responseJson) } -func (v *Vasp2) handleNonUmaLnurlRequest(context *gin.Context) { +func (v *Vasp2) handleNonUmaLnurlRequest(context *gin.Context, lnurlpRequest uma.LnurlpRequest) { callback := v.getLnurlpCallback(context) metadata, err := v.getMetadata() @@ -96,39 +113,29 @@ func (v *Vasp2) handleNonUmaLnurlRequest(context *gin.Context) { }) return } + response, err := uma.GetLnurlpResponse( + lnurlpRequest, + callback, + metadata, + 1, + 100_000_000, + nil, + nil, + nil, + &[]uma.Currency{ + UsdCurrency, + SatsCurrency, + }, + nil, + nil, + nil, + ) - context.JSON(http.StatusOK, gin.H{ - "callback": callback, - "maxSendable": 10_000_000, - "minSendable": 1_000, - "metadata": metadata, - "tag": "payRequest", - }) + context.JSON(http.StatusOK, response) } -func (v *Vasp2) parseUmaQueryData(context *gin.Context) ([]byte, bool) { - requestUrl := context.Request.URL - requestUrl.Host = context.Request.Host - query, err := uma.ParseLnurlpRequest(*requestUrl) - if err != nil { - var unsupportedVersionErr *uma.UnsupportedVersionError - if errors.As(err, &unsupportedVersionErr) { - context.JSON(http.StatusPreconditionFailed, gin.H{ - "status": "ERROR", - "reason": fmt.Sprintf("Unsupported version: %s", unsupportedVersionErr.UnsupportedVersion), - "supportedMajorVersions": unsupportedVersionErr.SupportedMajorVersions, - "unsupportedVersion": unsupportedVersionErr.UnsupportedVersion, - }) - return nil, true - } - context.JSON(http.StatusBadRequest, gin.H{ - "status": "ERROR", - "reason": err.Error(), - }) - return nil, true - } - - vaspDomainValidationErr := ValidateDomain(query.VaspDomain) +func (v *Vasp2) handleUmaQueryData(context *gin.Context, lnurlpRequest uma.UmaLnurlpRequest) ([]byte, bool) { + vaspDomainValidationErr := ValidateDomain(lnurlpRequest.VaspDomain) if vaspDomainValidationErr != nil { context.JSON(http.StatusBadRequest, gin.H{ "status": "ERROR", @@ -136,7 +143,7 @@ func (v *Vasp2) parseUmaQueryData(context *gin.Context) ([]byte, bool) { }) return nil, true } - pubKeys, err := uma.FetchPublicKeyForVasp(query.VaspDomain, v.pubKeyCache) + pubKeys, err := uma.FetchPublicKeyForVasp(lnurlpRequest.VaspDomain, v.pubKeyCache) if err != nil || pubKeys == nil { context.JSON(http.StatusBadRequest, gin.H{ "status": "ERROR", @@ -153,7 +160,7 @@ func (v *Vasp2) parseUmaQueryData(context *gin.Context) ([]byte, bool) { }) return nil, true } - if err := uma.VerifyUmaLnurlpQuerySignature(query, sendingVaspSigningPubKey, v.nonceCache); err != nil { + if err := uma.VerifyUmaLnurlpQuerySignature(lnurlpRequest, sendingVaspSigningPubKey, v.nonceCache); err != nil { context.JSON(http.StatusBadRequest, gin.H{ "status": "ERROR", "reason": err.Error(), @@ -179,45 +186,29 @@ func (v *Vasp2) parseUmaQueryData(context *gin.Context) ([]byte, bool) { return nil, true } + isSubjectToTravelRule := true + kycStatus := uma.KycStatusVerified signedResponse, err := uma.GetLnurlpResponse( - query, - umaPrivateKey, - true, + lnurlpRequest.LnurlpRequest, v.getLnurlpCallback(context), metadata, 1, 100_000_000, - uma.CounterPartyDataOptions{ + &umaPrivateKey, + &isSubjectToTravelRule, + &uma.CounterPartyDataOptions{ uma.CounterPartyDataFieldIdentifier.String(): {Mandatory: true}, uma.CounterPartyDataFieldCompliance.String(): {Mandatory: true}, uma.CounterPartyDataFieldName.String(): {Mandatory: false}, uma.CounterPartyDataFieldEmail.String(): {Mandatory: false}, }, - []uma.Currency{ - { - Code: "USD", - Name: "US Dollars", - Symbol: "$", - MillisatoshiPerUnit: MillisatoshiPerUsd, - Convertible: uma.ConvertibleCurrency{ - MinSendable: 1, - MaxSendable: 1_000, - }, - Decimals: 2, - }, - { - Code: "SAT", - Name: "Satoshis", - Symbol: "SAT", - MillisatoshiPerUnit: 1000, - Convertible: uma.ConvertibleCurrency{ - MinSendable: 1, - MaxSendable: 100_000_000, - }, - Decimals: 0, - }, + &[]uma.Currency{ + UsdCurrency, + SatsCurrency, }, - uma.KycStatusVerified, + &kycStatus, + nil, + nil, ) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -250,12 +241,11 @@ func (v *Vasp2) handleLnurlPayreq(context *gin.Context) { return } - amountParam := context.Query("amount") - amountMsats, err := strconv.Atoi(amountParam) + payreq, err := uma.ParsePayRequestFromQueryParams(context.Request.URL.Query()) if err != nil { context.JSON(http.StatusBadRequest, gin.H{ "status": "ERROR", - "reason": fmt.Sprintf("Invalid amount %s: %v", amountParam, err), + "reason": fmt.Sprintf("Invalid request: %v", err), }) return } @@ -270,20 +260,40 @@ func (v *Vasp2) handleLnurlPayreq(context *gin.Context) { } lsClient := services.NewLightsparkClient(v.config.ApiClientID, v.config.ApiClientSecret, v.config.ClientBaseURL) - lsInvoice, err := lsClient.CreateLnurlInvoice(v.config.NodeUUID, int64(amountMsats), metadata, nil) + expirySecs := int32(600) // Expire in 10 minutes + invoiceCreator := LightsparkClientLnurlInvoiceCreator{ + LightsparkClient: *lsClient, + NodeId: v.config.NodeUUID, + ExpirySecs: &expirySecs, + } - if err != nil { - context.JSON(http.StatusInternalServerError, gin.H{ - "status": "ERROR", - "reason": err.Error(), - }) - return + conversionRate := 1000.0 + decimals := 0 + if *payreq.ReceivingCurrencyCode == "USD" { + conversionRate = MillisatoshiPerUsd + decimals = 2 } + exchangeFees := int64(0) - context.JSON(http.StatusOK, gin.H{ - "pr": lsInvoice.Data.EncodedPaymentRequest, - "routes": []string{}, - }) + payreqResponse, err := uma.GetPayReqResponse( + *payreq, + invoiceCreator, + metadata, + payreq.ReceivingCurrencyCode, + &decimals, + &conversionRate, + &exchangeFees, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + ) + + context.JSON(http.StatusOK, payreqResponse) } func (v *Vasp2) handleUmaPayreq(context *gin.Context) { @@ -313,6 +323,13 @@ func (v *Vasp2) handleUmaPayreq(context *gin.Context) { }) return } + if !request.IsUmaRequest() { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invalid request body: not a UMA request.", + }) + return + } sendingVaspDomain, err := uma.GetVaspDomainFromUmaAddress(*request.PayerData.Identifier()) if err != nil { @@ -367,14 +384,14 @@ func (v *Vasp2) handleUmaPayreq(context *gin.Context) { lsClient := services.NewLightsparkClient(v.config.ApiClientID, v.config.ApiClientSecret, v.config.ClientBaseURL) expirySecs := int32(600) // Expire in 10 minutes - invoiceCreator := lsuma.LightsparkClientUmaInvoiceCreator{ + invoiceCreator := LightsparkClientUmaInvoiceCreator{ LightsparkClient: *lsClient, NodeId: v.config.NodeUUID, ExpirySecs: &expirySecs, } conversionRate := 1000.0 - if request.ReceivingCurrencyCode == "USD" { + if *request.ReceivingCurrencyCode == "USD" { conversionRate = MillisatoshiPerUsd } exchangeFees := int64(100_000) @@ -398,7 +415,7 @@ func (v *Vasp2) handleUmaPayreq(context *gin.Context) { } decimals := 0 - if request.ReceivingCurrencyCode == "USD" { + if *request.ReceivingCurrencyCode == "USD" { decimals = 2 } receiverUma := "$" + v.config.Username + "@" + v.getVaspDomain(context) @@ -411,24 +428,27 @@ func (v *Vasp2) handleUmaPayreq(context *gin.Context) { return } payeeInfo := v.getPayeeInfo(request.RequestedPayeeData, context) + utxoCallback := v.getUtxoCallback(context, txID) response, err := uma.GetPayReqResponse( - request, + *request, invoiceCreator, metadata, request.ReceivingCurrencyCode, - decimals, - conversionRate, - exchangeFees, - receiverUtxos, + &decimals, + &conversionRate, + &exchangeFees, + &receiverUtxos, (*receiverNode).GetPublicKey(), - v.getUtxoCallback(context, txID), + &utxoCallback, &uma.PayeeData{ uma.CounterPartyDataFieldIdentifier.String(): payeeInfo.Identifier, uma.CounterPartyDataFieldName.String(): payeeInfo.Name, uma.CounterPartyDataFieldEmail.String(): payeeInfo.Email, }, - signingKey, - receiverUma, + &signingKey, + &receiverUma, + nil, + nil, ) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -507,3 +527,36 @@ func (v *Vasp2) getPayeeInfo(options *uma.CounterPartyDataOptions, context *gin. Identifier: "$" + v.config.Username + "@" + v.getVaspDomain(context), } } + +// TODO(Jeremy): Switch back to lightsparkdev/go-sdk version once the UMA changes are merged. +type LightsparkClientUmaInvoiceCreator struct { + LightsparkClient services.LightsparkClient + // NodeId: the node ID of the receiver. + NodeId string + // ExpirySecs: the number of seconds until the invoice expires. + ExpirySecs *int32 +} + +func (l LightsparkClientUmaInvoiceCreator) CreateInvoice(amountMsats int64, metadata string) (*string, error) { + invoice, err := l.LightsparkClient.CreateUmaInvoice(l.NodeId, amountMsats, metadata, l.ExpirySecs) + if err != nil { + return nil, err + } + return &invoice.Data.EncodedPaymentRequest, nil +} + +type LightsparkClientLnurlInvoiceCreator struct { + LightsparkClient services.LightsparkClient + // NodeId: the node ID of the receiver. + NodeId string + // ExpirySecs: the number of seconds until the invoice expires. + ExpirySecs *int32 +} + +func (l LightsparkClientLnurlInvoiceCreator) CreateInvoice(amountMsats int64, metadata string) (*string, error) { + invoice, err := l.LightsparkClient.CreateLnurlInvoice(l.NodeId, amountMsats, metadata, l.ExpirySecs) + if err != nil { + return nil, err + } + return &invoice.Data.EncodedPaymentRequest, nil +}