Skip to content

Commit

Permalink
Merge pull request #81 from lightsparkdev/feat/lnurlcompat
Browse files Browse the repository at this point in the history
Add improved non-UMA LNURL compatibility
  • Loading branch information
jklein24 authored Mar 9, 2024
2 parents 691f29e + 0b56135 commit 3b59fa1
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 177 deletions.
27 changes: 27 additions & 0 deletions examples/uma-server/currencies.go
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion examples/uma-server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
4 changes: 4 additions & 0 deletions examples/uma-server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
14 changes: 0 additions & 14 deletions examples/uma-server/raw_lnurl.go

This file was deleted.

172 changes: 121 additions & 51 deletions examples/uma-server/vasp1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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",
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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{
Expand All @@ -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{
Expand All @@ -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:
// "[email protected]".
var trFormat *uma.TravelRuleFormat
payReq, err := uma.GetPayRequest(
payReq, err := uma.GetUmaPayRequest(
amountInt64,
vasp2EncryptionPubKey,
umaSigningPrivateKey,
currencyCode,
!isAmountInMsats,
amountInt64,
payerInfo.Identifier,
payerInfo.Name,
payerInfo.Email,
Expand All @@ -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{
Expand All @@ -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",
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -564,38 +583,83 @@ 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",
"reason": "Failed to parse callback URL",
})
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: &currencyCode,
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())
Expand Down Expand Up @@ -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{
Expand All @@ -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 {
Expand Down
24 changes: 6 additions & 18 deletions examples/uma-server/vasp1_request_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 3b59fa1

Please sign in to comment.