Skip to content

Commit

Permalink
Add backwards-compatibility for UMA v0 from v1 SDK
Browse files Browse the repository at this point in the history
  • Loading branch information
jklein24 committed Mar 17, 2024
1 parent 6f89e8f commit 2b7356b
Show file tree
Hide file tree
Showing 9 changed files with 993 additions and 410 deletions.
401 changes: 201 additions & 200 deletions uma/protocol.go

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions uma/protocol/currency.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package protocol

import "encoding/json"

type Currency struct {
// Code is the ISO 4217 (if applicable) currency code (eg. "USD"). For cryptocurrencies, this will be a ticker
// symbol, such as BTC for Bitcoin.
Expand All @@ -25,6 +27,10 @@ type Currency struct {
// `decimals` would be 8.
// For details on edge cases and examples, see https://github.com/uma-universal-money-address/protocol/blob/main/umad-04-lnurlp-response.md.
Decimals int `json:"decimals"`

// UmaMajorVersion is the major version of the UMA protocol that the VASP supports for this currency. This is used
// for serialization, but is not serialized itself.
UmaMajorVersion int `json:"-"`
}

type ConvertibleCurrency struct {
Expand All @@ -36,3 +42,78 @@ type ConvertibleCurrency struct {
// smallest unit of the currency (eg. cents for USD).
MaxSendable int64 `json:"max"`
}

type v0Currency struct {
Code string `json:"code"`
Name string `json:"name"`
Symbol string `json:"symbol"`
MillisatoshiPerUnit float64 `json:"multiplier"`
MinSendable int64 `json:"minSendable"`
MaxSendable int64 `json:"maxSendable"`
Decimals int `json:"decimals"`
}

type v1Currency struct {
Code string `json:"code"`
Name string `json:"name"`
Symbol string `json:"symbol"`
MillisatoshiPerUnit float64 `json:"multiplier"`
Convertible ConvertibleCurrency `json:"convertible"`
Decimals int `json:"decimals"`
}

func (c *Currency) MarshalJSON() ([]byte, error) {
if c.UmaMajorVersion == 0 {
return json.Marshal(&v0Currency{
Code: c.Code,
Name: c.Name,
Symbol: c.Symbol,
MillisatoshiPerUnit: c.MillisatoshiPerUnit,
MinSendable: c.Convertible.MinSendable,
MaxSendable: c.Convertible.MaxSendable,
Decimals: c.Decimals,
})
}
return json.Marshal(&v1Currency{
Code: c.Code,
Name: c.Name,
Symbol: c.Symbol,
MillisatoshiPerUnit: c.MillisatoshiPerUnit,
Convertible: c.Convertible,
Decimals: c.Decimals,
})
}

func (c *Currency) UnmarshalJSON(data []byte) error {
jsonData := make(map[string]interface{})
if err := json.Unmarshal(data, &jsonData); err != nil {
return err
}
if _, ok := jsonData["minSendable"]; ok {
v0 := &v0Currency{}
if err := json.Unmarshal(data, v0); err != nil {
return err
}
c.Code = v0.Code
c.Name = v0.Name
c.Symbol = v0.Symbol
c.MillisatoshiPerUnit = v0.MillisatoshiPerUnit
c.Convertible.MinSendable = v0.MinSendable
c.Convertible.MaxSendable = v0.MaxSendable
c.Decimals = v0.Decimals
c.UmaMajorVersion = 0
return nil
}
v1 := &v1Currency{}
if err := json.Unmarshal(data, v1); err != nil {
return err
}
c.Code = v1.Code
c.Name = v1.Name
c.Symbol = v1.Symbol
c.MillisatoshiPerUnit = v1.MillisatoshiPerUnit
c.Convertible = v1.Convertible
c.Decimals = v1.Decimals
c.UmaMajorVersion = 1
return nil
}
227 changes: 158 additions & 69 deletions uma/protocol/pay_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ type PayRequest struct {
// if the receiver included the `commentAllowed` field in the lnurlp response. The length of
// the comment must be less than or equal to the value of `commentAllowed`.
Comment *string `json:"comment"`
// UmaMajorVersion is the major version of the UMA protocol that the VASP supports for this currency. This is used
// for serialization, but is not serialized itself.
UmaMajorVersion int `json:"-"`
}

type v0PayRequest struct {
ReceivingCurrencyCode *string `json:"currency"`
Amount int64 `json:"amount"`
PayerData *PayerData `json:"payerData"`
RequestedPayeeData *CounterPartyDataOptions `json:"payeeData"`
Comment *string `json:"comment"`
}

type v1PayRequest struct {
ReceivingCurrencyCode *string `json:"convert"`
Amount string `json:"amount"`
PayerData *PayerData `json:"payerData"`
RequestedPayeeData *CounterPartyDataOptions `json:"payeeData"`
Comment *string `json:"comment"`
}

// IsUmaRequest returns true if the request is a valid UMA request, otherwise, if any fields are missing, it returns false.
Expand All @@ -63,42 +82,27 @@ func (p *PayRequest) IsUmaRequest() bool {
}

func (p *PayRequest) MarshalJSON() ([]byte, error) {
if p.UmaMajorVersion == 0 {
return json.Marshal(&v0PayRequest{
ReceivingCurrencyCode: p.ReceivingCurrencyCode,
Amount: p.Amount,
PayerData: p.PayerData,
RequestedPayeeData: p.RequestedPayeeData,
Comment: p.Comment,
})
}

amount := strconv.FormatInt(p.Amount, 10)
if p.SendingAmountCurrencyCode != nil {
amount = fmt.Sprintf("%s.%s", amount, *p.SendingAmountCurrencyCode)
}
var payerDataJson []byte
if p.PayerData != nil {
var err error
payerDataJson, err = json.Marshal(p.PayerData)
if err != nil {
return nil, err
}
}
reqStr := fmt.Sprintf(`{
"amount": "%s"`, amount)
if p.ReceivingCurrencyCode != nil {
reqStr += fmt.Sprintf(`,
"convert": "%s"`, *p.ReceivingCurrencyCode)
}
if p.PayerData != nil {
reqStr += fmt.Sprintf(`,
"payerData": %s`, payerDataJson)
}
if p.RequestedPayeeData != nil {
payeeDataJson, err := json.Marshal(p.RequestedPayeeData)
if err != nil {
return nil, err
}
reqStr += fmt.Sprintf(`,
"payeeData": %s`, payeeDataJson)
}
if p.Comment != nil {
reqStr += fmt.Sprintf(`,
"comment": "%s"`, *p.Comment)
}
reqStr += "}"
return []byte(reqStr), nil
return json.Marshal(&v1PayRequest{
ReceivingCurrencyCode: p.ReceivingCurrencyCode,
Amount: amount,
PayerData: p.PayerData,
RequestedPayeeData: p.RequestedPayeeData,
Comment: p.Comment,
})
}

func (p *PayRequest) UnmarshalJSON(data []byte) error {
Expand All @@ -107,64 +111,85 @@ func (p *PayRequest) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
convert, ok := rawReq["convert"].(string)
isAmountString := false
if _, ok := rawReq["amount"].(string); ok {
isAmountString = true
} else {
_, ok = rawReq["amount"].(float64)
if !ok {
return errors.New("missing or invalid amount field")
}
}
isUma := false
payerData, ok := rawReq["payerData"].(map[string]interface{})
if ok {
p.ReceivingCurrencyCode = &convert
_, ok = payerData["compliance"].(map[string]interface{})
if ok {
isUma = true
}
}
isV1 := false
if _, ok := rawReq["convert"].(string); ok {
isV1 = isUma
}
if isV1 || isAmountString {
var v1Req v1PayRequest
err = json.Unmarshal(data, &v1Req)
if err != nil {
return err
}
return p.UnmarshalFromV1(v1Req)
}
amount, ok := rawReq["amount"].(string)
if !ok {
return errors.New("missing or invalid amount field")
var v0Req v0PayRequest
err = json.Unmarshal(data, &v0Req)
if err != nil {
return err
}
err = p.UnmarshalFromV0(v0Req)
if err != nil {
return err
}
return nil
}

func (p *PayRequest) UnmarshalFromV1(request v1PayRequest) error {
p.UmaMajorVersion = 1
p.ReceivingCurrencyCode = request.ReceivingCurrencyCode
p.PayerData = request.PayerData
p.RequestedPayeeData = request.RequestedPayeeData
p.Comment = request.Comment
amount := request.Amount
amountParts := strings.Split(amount, ".")
if len(amountParts) > 2 {
return errors.New("invalid amount field")
}
var err error
p.Amount, err = strconv.ParseInt(amountParts[0], 10, 64)
if err != nil {
return err
}
if len(amountParts) == 2 && len(amountParts[1]) > 0 {
p.SendingAmountCurrencyCode = &amountParts[1]
}
payerDataJson, ok := rawReq["payerData"].(map[string]interface{})
if ok {
payerDataJsonBytes, err := json.Marshal(payerDataJson)
if err != nil {
return err
}
var payerData PayerData
err = json.Unmarshal(payerDataJsonBytes, &payerData)
if err != nil {
return err
}
p.PayerData = &payerData
}
payeeDataJson, ok := rawReq["payeeData"].(map[string]interface{})
if ok {
payeeDataJsonBytes, err := json.Marshal(payeeDataJson)
if err != nil {
return err
}
var payeeData CounterPartyDataOptions
err = json.Unmarshal(payeeDataJsonBytes, &payeeData)
if err != nil {
return err
}
p.RequestedPayeeData = &payeeData
}
comment, ok := rawReq["comment"].(string)
if ok {
p.Comment = &comment
}
return nil
}

func (p *PayRequest) UnmarshalFromV0(request v0PayRequest) error {
p.UmaMajorVersion = 0
p.ReceivingCurrencyCode = request.ReceivingCurrencyCode
p.PayerData = request.PayerData
p.RequestedPayeeData = request.RequestedPayeeData
p.Comment = request.Comment
p.Amount = request.Amount
return nil
}

func (p *PayRequest) Encode() ([]byte, error) {
return json.Marshal(p)
return p.MarshalJSON()
}

func (p *PayRequest) EncodeAsUrlParams() (*url.Values, error) {
jsonBytes, err := json.Marshal(p)
jsonBytes, err := p.MarshalJSON()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -213,3 +238,67 @@ func (p *PayRequest) SignablePayload() ([]byte, error) {
}, "|")
return []byte(payloadString), nil
}

// ParsePayRequestFromQueryParams Parses a pay request from query parameters.
// This is useful for parsing a non-UMA pay request from a URL query since raw LNURL uses a GET request for the payreq,
// whereas UMA uses a POST request.
func ParsePayRequestFromQueryParams(query url.Values) (*PayRequest, error) {
amountStr := query.Get("amount")
if amountStr == "" {
return nil, errors.New("missing amount")
}
amountParts := strings.Split(amountStr, ".")
if len(amountParts) > 2 {
return nil, errors.New("invalid amount")
}
amount, err := strconv.ParseInt(amountParts[0], 10, 64)
if err != nil {
return nil, err
}
var sendingAmountCurrencyCode *string
if len(amountParts) == 2 {
sendingAmountCurrencyCode = &amountParts[1]
}
v1ReceivingCurrencyCodeStr := query.Get("convert")
v0ReceivingCurrencyCodeStr := query.Get("currency")
umaMajorVersion := 1
var receivingCurrencyCode *string
if v1ReceivingCurrencyCodeStr != "" {
receivingCurrencyCode = &v1ReceivingCurrencyCodeStr
} else if v0ReceivingCurrencyCodeStr != "" {
receivingCurrencyCode = &v0ReceivingCurrencyCodeStr
umaMajorVersion = 0
}

payerData := query.Get("payerData")
var payerDataObj *PayerData
if payerData != "" {
err = json.Unmarshal([]byte(payerData), &payerDataObj)
if err != nil {
return nil, err
}
}
requestedPayeeData := query.Get("payeeData")
var requestedPayeeDataObj *CounterPartyDataOptions
if requestedPayeeData != "" {
err = json.Unmarshal([]byte(requestedPayeeData), &requestedPayeeDataObj)
if err != nil {
return nil, err
}
}
commentParam := query.Get("comment")
var comment *string
if commentParam != "" {
comment = &commentParam
}

return &PayRequest{
SendingAmountCurrencyCode: sendingAmountCurrencyCode,
ReceivingCurrencyCode: receivingCurrencyCode,
Amount: amount,
PayerData: payerDataObj,
RequestedPayeeData: requestedPayeeDataObj,
Comment: comment,
UmaMajorVersion: umaMajorVersion,
}, nil
}
Loading

0 comments on commit 2b7356b

Please sign in to comment.