diff --git a/examples/uma-server/server.go b/examples/uma-server/server.go index 5fc0632..08affb0 100644 --- a/examples/uma-server/server.go +++ b/examples/uma-server/server.go @@ -53,6 +53,14 @@ func main() { vasp1.handleClientPaymentConfirm(c) }) + engine.POST("/api/uma/pay_invoice", func(c *gin.Context) { + vasp1.handlePayInvoice(c) + }) + + engine.POST("/api/uma/request_pay_invoice", func(c *gin.Context) { + vasp1.handleRequestPayInvoice(c) + }) + // End VASP1 Routes // VASP2 Routes: @@ -67,9 +75,11 @@ func main() { engine.POST("/api/uma/payreq/:uuid", func(c *gin.Context) { vasp2.handleUmaPayreq(c) }) + engine.POST("/api/uma/create_invoice/:uuid", func(c *gin.Context) { vasp2.handleCreateInvoice(c) }) + engine.POST("/api/uma/create_and_send_invoice/:uuid", func(c *gin.Context) { vasp2.handleCreateAndSendInvoice(c) }) diff --git a/examples/uma-server/vasp1.go b/examples/uma-server/vasp1.go index 7c898bc..9f13272 100644 --- a/examples/uma-server/vasp1.go +++ b/examples/uma-server/vasp1.go @@ -15,6 +15,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/lightsparkdev/go-sdk/objects" "github.com/lightsparkdev/go-sdk/services" "github.com/lightsparkdev/go-sdk/utils" @@ -30,6 +31,7 @@ type Vasp1 struct { requestCache *Vasp1RequestCache nonceCache uma.NonceCache client *services.LightsparkClient + umaRequestStorage *Vasp1UmaRequestStorage } func NewVasp1(config *UmaConfig, pubKeyCache uma.PublicKeyCache) *Vasp1 { @@ -40,6 +42,7 @@ func NewVasp1(config *UmaConfig, pubKeyCache uma.PublicKeyCache) *Vasp1 { requestCache: NewVasp1RequestCache(), nonceCache: uma.NewInMemoryNonceCache(oneDayAgo), client: services.NewLightsparkClient(config.ApiClientID, config.ApiClientSecret, config.ClientBaseURL), + umaRequestStorage: &Vasp1UmaRequestStorage{}, } } @@ -237,6 +240,135 @@ func (v *Vasp1) getUtxoCallback(context *gin.Context, txId string) string { return fmt.Sprintf("%s%s/api/uma/utxocallback?txid=%s", scheme, context.Request.Host, txId) } +func (v *Vasp1) handlePayInvoice(context *gin.Context) { + invoiceString := context.Query("invoice") + invoice, err := uma.DecodeUmaInvoice(invoiceString) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invalid invoice", + }) + return + } + + vasp2Domain, err := uma.GetVaspDomainFromUmaAddress(invoice.ReceiverUma) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invalid receiver address", + }) + return + } + + vasp2PubKeys, err := uma.FetchPublicKeyForVasp(vasp2Domain, v.pubKeyCache) + if err != nil || vasp2PubKeys == nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to fetch public key for receiving VASP", + }) + return + } + + err = uma.VerifyUmaInvoiceSignature(*invoice, *vasp2PubKeys) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Failed to verify invoice signature", + }) + return + } + + if int64(invoice.Expiration) < time.Now().Unix() { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invoice has expired", + }) + return + } + + vasp2EncryptionPubKey, err := vasp2PubKeys.EncryptionPubKey() + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to get encryption pub key for receiving VASP", + }) + return + } + + umaSigningPrivateKey, err := v.config.UmaSigningPrivKeyBytes() + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": err.Error(), + }) + return + } + + var payerInfo *PayerInfo + if invoice.RequiredPayerData != nil { + payerInfoVal := v.getPayerInfo(*invoice.RequiredPayerData, context) + payerInfo = &payerInfoVal + } + + 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 { + log.Printf("Failed to get prescreening UTXOs: %v", err) + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to get prescreening UTXOs", + }) + return + } + + senderNode, err := GetNode(v.client, v.config.NodeUUID) + if err != nil || senderNode == nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to get sender node pub key", + }) + return + } + + txID := "1234" // In practice, you'd probably use some real transaction ID here. + var trFormat *umaprotocol.TravelRuleFormat + payreq, err := uma.GetUmaPayRequestWithInvoice( + int64(invoice.Amount), + vasp2EncryptionPubKey, + umaSigningPrivateKey, + invoice.ReceivingCurrency.Code, + true, + payerInfo.Identifier, + uma.MAJOR_VERSION, + payerInfo.Name, + payerInfo.Email, + &trInfo, + trFormat, + umaprotocol.KycStatusVerified, + &senderUtxos, + (*senderNode).GetPublicKey(), + v.getUtxoCallback(context, txID), + &umaprotocol.CounterPartyDataOptions{ + umaprotocol.CounterPartyDataFieldName.String(): {Mandatory: false}, + umaprotocol.CounterPartyDataFieldEmail.String(): {Mandatory: false}, + // Compliance and Identifier are mandatory fields added automatically. + }, + nil, + &invoice.InvoiceUUID, + ) + + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to generate payreq", + }) + return + } + + callbackUuid := uuid.New().String() + v.handlePayRequest(payreq, invoice.Callback, invoice.ReceiverUma, callbackUuid, context) +} + func (v *Vasp1) handleClientPayReq(context *gin.Context) { callbackUuid := context.Param("callbackUuid") initialRequestData, ok := v.requestCache.GetLnurlpResponseData(callbackUuid) @@ -392,6 +524,17 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { return } + payeeIdentifier := initialRequestData.receiverId + "@" + initialRequestData.vasp2Domain + v.handlePayRequest(payReq, initialRequestData.lnurlpResponse.Callback, payeeIdentifier, callbackUuid, context) +} + +func (v *Vasp1) handlePayRequest( + payReq *umaprotocol.PayRequest, + callback string, + payeeIdentifier string, + callbackUuid string, + context *gin.Context, +) { payReqBytes, err := json.Marshal(payReq) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -400,7 +543,7 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { }) return } - payreqResult, err := http.Post(initialRequestData.lnurlpResponse.Callback, "application/json", bytes.NewBuffer(payReqBytes)) + payreqResult, err := http.Post(callback, "application/json", bytes.NewBuffer(payReqBytes)) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ "status": "ERROR", @@ -446,13 +589,13 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { return } - payeeIdentifier := initialRequestData.receiverId + "@" + initialRequestData.vasp2Domain + umaMajorVersion := uma.MAJOR_VERSION if umaMajorVersion > 0 { if err := uma.VerifyPayReqResponseSignature( payreqResponse, *pubKeys, v.nonceCache, - payerInfo.Identifier, + "$" + v.config.Username + "@" + v.getVaspDomain(context), payeeIdentifier, ); err != nil { context.JSON(http.StatusBadRequest, gin.H{ @@ -476,6 +619,13 @@ func (v *Vasp1) handleClientPayReq(context *gin.Context) { } invoiceData := (*invoice).(objects.InvoiceData) compliance, err := payreqResponse.PayeeData.Compliance() + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to get compliance data", + }) + return + } var utxoCallback *string if compliance != nil && compliance.UtxoCallback != nil && *compliance.UtxoCallback != "" { utxoCallback = compliance.UtxoCallback @@ -581,7 +731,7 @@ func (v *Vasp1) handleClientPaymentConfirm(context *gin.Context) { if err != nil { log.Fatalf("Failed to marshal UTXOs: %v", err) } else if payReqData.utxoCallback != nil { - log.Printf("Sending UTXOs to %s: %s", *payReqData.utxoCallback, utxosWithAmounts) + log.Printf("Sending UTXOs to %s: %+v", *payReqData.utxoCallback, utxosWithAmounts) signingPrivateKey, err := v.config.UmaSigningPrivKeyBytes() if err != nil { context.JSON(http.StatusInternalServerError, gin.H{ @@ -648,21 +798,6 @@ func (v *Vasp1) waitForPaymentCompletion(payment *objects.OutgoingPayment) (*obj return payment, nil } -func (v *Vasp1) handlePubKeyRequest(context *gin.Context) { - twoWeeksFromNow := time.Now().AddDate(0, 0, 14) - twoWeeksFromNowSec := twoWeeksFromNow.Unix() - response, err := uma.GetPubKeyResponse(v.config.UmaSigningCertChain, v.config.UmaEncryptionCertChain, &twoWeeksFromNowSec) - if err != nil { - context.JSON(http.StatusInternalServerError, gin.H{ - "status": "ERROR", - "reason": err.Error(), - }) - return - } - - context.JSON(http.StatusOK, response) -} - func (v *Vasp1) handleNonUmaLnurlpResponse( lnurlpResponse umaprotocol.LnurlpResponse, receiverId string, receiverDomain string, context *gin.Context) { callbackUuid := v.requestCache.SaveLnurlpResponseData(lnurlpResponse, receiverId, receiverDomain) @@ -849,3 +984,61 @@ func (v *Vasp1) getPayerInfo(options umaprotocol.CounterPartyDataOptions, contex Identifier: "$" + v.config.Username + "@" + v.getVaspDomain(context), } } + +func (v *Vasp1) handleRequestPayInvoice(context *gin.Context) { + invoiceString := context.Query("invoice") + invoice, err := uma.DecodeUmaInvoice(invoiceString) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invalid invoice", + }) + return + } + + vasp2Domain, err := uma.GetVaspDomainFromUmaAddress(invoice.ReceiverUma) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invalid receiver address", + }) + return + } + + vasp2PubKeys, err := uma.FetchPublicKeyForVasp(vasp2Domain, v.pubKeyCache) + if err != nil || vasp2PubKeys == nil { + context.JSON(http.StatusInternalServerError, gin.H{ + "status": "ERROR", + "reason": "Failed to fetch public key for receiving VASP", + }) + return + } + + err = uma.VerifyUmaInvoiceSignature(*invoice, *vasp2PubKeys) + if err != nil { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Failed to verify invoice signature", + }) + return + } + + if int64(invoice.Expiration) < time.Now().Unix() { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invoice has expired", + }) + return + } + + if invoice.SenderUma == nil { + context.JSON(http.StatusBadRequest, gin.H{ + "status": "ERROR", + "reason": "Invoice missing sender address", + }) + return + } + + v.umaRequestStorage.AddUmaRequestToStorage(*invoice.SenderUma, invoiceString) + context.Status(http.StatusOK) +} \ No newline at end of file diff --git a/examples/uma-server/vasp1_request_cache.go b/examples/uma-server/vasp1_request_cache.go index 6e31c29..f6e8ee8 100644 --- a/examples/uma-server/vasp1_request_cache.go +++ b/examples/uma-server/vasp1_request_cache.go @@ -31,6 +31,17 @@ type Vasp1RequestCache struct { payReqCache map[string]Vasp1PayReqData } +type Vasp1UmaRequestStorage struct { + UmaRequests map[string][]string +} + +func (c *Vasp1UmaRequestStorage) AddUmaRequestToStorage(id string, invoice string) { + if c.UmaRequests[id] == nil { + c.UmaRequests[id] = make([]string, 0) + } + c.UmaRequests[id] = append(c.UmaRequests[id], invoice) +} + func NewVasp1RequestCache() *Vasp1RequestCache { return &Vasp1RequestCache{ umaRequestCache: make(map[string]Vasp1InitialRequestData),