Skip to content

Commit

Permalink
Sender side
Browse files Browse the repository at this point in the history
  • Loading branch information
zhenlu committed Sep 18, 2024
1 parent d7e0856 commit 67e116c
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 19 deletions.
10 changes: 10 additions & 0 deletions examples/uma-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
})
Expand Down
231 changes: 212 additions & 19 deletions examples/uma-server/vasp1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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{},
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand All @@ -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",
Expand Down Expand Up @@ -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{
Expand All @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
11 changes: 11 additions & 0 deletions examples/uma-server/vasp1_request_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 67e116c

Please sign in to comment.