diff --git a/uma/protocol/currency.go b/uma/protocol/currency.go index fb87fc9..c9bab40 100644 --- a/uma/protocol/currency.go +++ b/uma/protocol/currency.go @@ -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. @@ -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 { @@ -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 +} diff --git a/uma/protocol/pay_request.go b/uma/protocol/pay_request.go index 0087eed..db9ccbf 100644 --- a/uma/protocol/pay_request.go +++ b/uma/protocol/pay_request.go @@ -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. @@ -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 { @@ -107,18 +111,59 @@ 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 @@ -126,45 +171,25 @@ func (p *PayRequest) UnmarshalJSON(data []byte) error { 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 } @@ -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 +} diff --git a/uma/protocol/payee_data.go b/uma/protocol/payee_data.go index a50d6fd..33104b8 100644 --- a/uma/protocol/payee_data.go +++ b/uma/protocol/payee_data.go @@ -39,11 +39,14 @@ type CompliancePayeeData struct { // UtxoCallback is the URL that the sender VASP will call to send UTXOs of the channel that the sender used to send the payment once it completes. UtxoCallback *string `json:"utxoCallback"` // Signature is the base64-encoded signature of sha256(SenderAddress|ReceiverAddress|Nonce|Timestamp). - Signature string `json:"signature"` - // Nonce is a random string that is used to prevent replay attacks. - SignatureNonce string `json:"signatureNonce"` - // Timestamp is the unix timestamp (in seconds since epoch) of when the request was sent. Used in the signature. - SignatureTimestamp int64 `json:"signatureTimestamp"` + // Note: This field is optional for UMA v0.X backwards-compatibility. It is required for UMA v1.X. + Signature *string `json:"signature"` + // SignatureNonce is a random string that is used to prevent replay attacks. + // Note: This field is optional for UMA v0.X backwards-compatibility. It is required for UMA v1.X. + SignatureNonce *string `json:"signatureNonce"` + // SignatureTimestamp is the unix timestamp (in seconds since epoch) of when the request was sent. Used in the signature. + // Note: This field is optional for UMA v0.X backwards-compatibility. It is required for UMA v1.X. + SignatureTimestamp *int64 `json:"signatureTimestamp"` } func (c *CompliancePayeeData) AsMap() (map[string]interface{}, error) { @@ -63,11 +66,14 @@ func (c *CompliancePayeeData) SignablePayload(payerIdentifier string, payeeIdent if c == nil { return nil, errors.New("compliance data is missing") } + if c.SignatureNonce == nil || c.SignatureTimestamp == nil { + return nil, errors.New("compliance data is missing signature nonce or timestamp. Is this a v0.X response") + } payloadString := strings.Join([]string{ payerIdentifier, payeeIdentifier, - c.SignatureNonce, - strconv.FormatInt(c.SignatureTimestamp, 10), + *c.SignatureNonce, + strconv.FormatInt(*c.SignatureTimestamp, 10), }, "|") return []byte(payloadString), nil } diff --git a/uma/protocol/payreq_response.go b/uma/protocol/payreq_response.go index 4e5dcb5..9aa5e89 100644 --- a/uma/protocol/payreq_response.go +++ b/uma/protocol/payreq_response.go @@ -1,5 +1,10 @@ package protocol +import ( + "encoding/json" + "errors" +) + // PayReqResponse is the response sent by the receiver to the sender to provide an invoice. type PayReqResponse struct { // EncodedInvoice is the BOLT11 invoice that the sender will pay. @@ -8,7 +13,7 @@ type PayReqResponse struct { Routes []Route `json:"routes"` // PaymentInfo is information about the payment that the receiver will receive. Includes Final currency-related // information for the payment. Required for UMA. - PaymentInfo *PayReqResponsePaymentInfo `json:"paymentInfo"` + PaymentInfo *PayReqResponsePaymentInfo `json:"converted"` // PayeeData The data about the receiver that the sending VASP requested in the payreq request. // Required for UMA. PayeeData *PayeeData `json:"payeeData"` @@ -19,6 +24,9 @@ type PayReqResponse struct { Disposable *bool `json:"disposable"` // SuccessAction defines a struct which can be stored and shown to the user on payment success. See LUD-09. SuccessAction *map[string]string `json:"successAction"` + // UmaMajorVersion is the major version of the UMA protocol that the receiver is using. Only used + // for serialization and deserialization. Not included in the JSON response. + UmaMajorVersion int `json:"umaMajorVersion"` } func (p *PayReqResponse) IsUmaResponse() bool { @@ -45,7 +53,7 @@ type Route struct { type PayReqResponsePaymentInfo struct { // Amount is the amount that the receiver will receive in the receiving currency not including fees. The amount is // specified in the smallest unit of the currency (eg. cents for USD). - Amount int64 `json:"amount"` + Amount *int64 `json:"amount"` // CurrencyCode is the currency code that the receiver will receive for this payment. CurrencyCode string `json:"currencyCode"` // Multiplier is the conversion rate. It is the number of millisatoshis that the receiver will receive for 1 unit of @@ -62,3 +70,142 @@ type PayReqResponsePaymentInfo struct { // separate from the Multiplier. ExchangeFeesMillisatoshi int64 `json:"fee"` } + +type v0PayReqResponsePaymentInfo struct { + CurrencyCode string `json:"currencyCode"` + Multiplier float64 `json:"multiplier"` + Decimals int `json:"decimals"` + ExchangeFeesMillisatoshi int64 `json:"exchangeFeesMillisatoshi"` +} + +type v0PayReqResponse struct { + EncodedInvoice string `json:"pr"` + Routes []Route `json:"routes"` + PaymentInfo *v0PayReqResponsePaymentInfo `json:"paymentInfo"` + PayeeData *PayeeData `json:"payeeData"` + Disposable *bool `json:"disposable"` + SuccessAction *map[string]string `json:"successAction"` + Compliance *CompliancePayeeData `json:"compliance"` +} + +type v1PayReqResponse struct { + EncodedInvoice string `json:"pr"` + Routes []Route `json:"routes"` + PaymentInfo *PayReqResponsePaymentInfo `json:"converted"` + PayeeData *PayeeData `json:"payeeData"` + Disposable *bool `json:"disposable"` + SuccessAction *map[string]string `json:"successAction"` +} + +func (p *PayReqResponse) asV0() (*v0PayReqResponse, error) { + if p.UmaMajorVersion != 0 { + return nil, errors.New("not a v0 response") + } + compliance, err := p.PayeeData.Compliance() + if err != nil { + return nil, err + } + var v0PaymentInfo *v0PayReqResponsePaymentInfo + if p.PaymentInfo != nil { + v0PaymentInfo = &v0PayReqResponsePaymentInfo{ + CurrencyCode: p.PaymentInfo.CurrencyCode, + Multiplier: p.PaymentInfo.Multiplier, + Decimals: p.PaymentInfo.Decimals, + ExchangeFeesMillisatoshi: p.PaymentInfo.ExchangeFeesMillisatoshi, + } + } + return &v0PayReqResponse{ + EncodedInvoice: p.EncodedInvoice, + Routes: p.Routes, + PaymentInfo: v0PaymentInfo, + PayeeData: p.PayeeData, + Disposable: p.Disposable, + SuccessAction: p.SuccessAction, + Compliance: compliance, + }, nil +} + +func (p *PayReqResponse) asV1() *v1PayReqResponse { + if p.UmaMajorVersion != 1 { + return nil + } + return &v1PayReqResponse{ + EncodedInvoice: p.EncodedInvoice, + Routes: p.Routes, + PaymentInfo: p.PaymentInfo, + PayeeData: p.PayeeData, + Disposable: p.Disposable, + SuccessAction: p.SuccessAction, + } +} + +func (p *PayReqResponse) MarshalJSON() ([]byte, error) { + if p.UmaMajorVersion == 0 { + v0, err := p.asV0() + if err != nil { + return nil, err + } + return json.Marshal(v0) + } + return json.Marshal(p.asV1()) +} + +func (p *PayReqResponse) UnmarshalJSON(data []byte) error { + dataAsMap := make(map[string]interface{}) + err := json.Unmarshal(data, &dataAsMap) + if err != nil { + return err + } + umaVersion := 1 + if _, ok := dataAsMap["paymentInfo"]; ok { + umaVersion = 0 + } + if umaVersion == 0 { + var v0 v0PayReqResponse + err := json.Unmarshal(data, &v0) + if err != nil { + return err + } + var paymentInfo *PayReqResponsePaymentInfo + if v0.PaymentInfo != nil { + paymentInfo = &PayReqResponsePaymentInfo{ + CurrencyCode: v0.PaymentInfo.CurrencyCode, + Multiplier: v0.PaymentInfo.Multiplier, + Decimals: v0.PaymentInfo.Decimals, + ExchangeFeesMillisatoshi: v0.PaymentInfo.ExchangeFeesMillisatoshi, + } + } + if v0.Compliance != nil { + if v0.PayeeData == nil { + v0.PayeeData = &PayeeData{} + } + complianceMap, err := v0.Compliance.AsMap() + if err != nil { + return err + } + (*v0.PayeeData)["compliance"] = complianceMap + } + p.UmaMajorVersion = 0 + p.EncodedInvoice = v0.EncodedInvoice + p.Routes = v0.Routes + p.PaymentInfo = paymentInfo + p.PayeeData = v0.PayeeData + p.Disposable = v0.Disposable + p.SuccessAction = v0.SuccessAction + return nil + } + + var v1 v1PayReqResponse + err = json.Unmarshal(data, &v1) + if err != nil { + return err + } + p.UmaMajorVersion = 1 + p.EncodedInvoice = v1.EncodedInvoice + p.Routes = v1.Routes + p.PaymentInfo = v1.PaymentInfo + p.PayeeData = v1.PayeeData + p.Disposable = v1.Disposable + p.SuccessAction = v1.SuccessAction + return nil +} diff --git a/uma/protocol/post_transaction_callback.go b/uma/protocol/post_transaction_callback.go index f1b29d1..d538254 100644 --- a/uma/protocol/post_transaction_callback.go +++ b/uma/protocol/post_transaction_callback.go @@ -1,6 +1,7 @@ package protocol import ( + "errors" "strconv" "strings" ) @@ -11,13 +12,13 @@ type PostTransactionCallback struct { Utxos []UtxoWithAmount `json:"utxos"` // VaspDomain is the domain of the VASP that is sending the callback. // It will be used by the VASP to fetch the public keys of its counterparty. - VaspDomain string `json:"vaspDomain"` + VaspDomain *string `json:"vaspDomain"` // Signature is the base64-encoded signature of sha256(Nonce|Timestamp). - Signature string `json:"signature"` + Signature *string `json:"signature"` // Nonce is a random string that is used to prevent replay attacks. - Nonce string `json:"signatureNonce"` + Nonce *string `json:"signatureNonce"` // Timestamp is the unix timestamp of when the request was sent. Used in the signature. - Timestamp int64 `json:"signatureTimestamp"` + Timestamp *int64 `json:"signatureTimestamp"` } // UtxoWithAmount is a pair of utxo and amount transferred over that corresponding channel. @@ -30,10 +31,14 @@ type UtxoWithAmount struct { Amount int64 `json:"amountMsats"` } -func (c *PostTransactionCallback) SignablePayload() []byte { +func (c *PostTransactionCallback) SignablePayload() (*[]byte, error) { + if c.Nonce == nil || c.Timestamp == nil { + return nil, errors.New("nonce and timestamp must be set") + } payloadString := strings.Join([]string{ - c.Nonce, - strconv.FormatInt(c.Timestamp, 10), + *c.Nonce, + strconv.FormatInt(*c.Timestamp, 10), }, "|") - return []byte(payloadString) + payload := []byte(payloadString) + return &payload, nil } diff --git a/uma/test/protocol_test.go b/uma/test/protocol_test.go new file mode 100644 index 0000000..84ef378 --- /dev/null +++ b/uma/test/protocol_test.go @@ -0,0 +1,189 @@ +package uma_test + +import ( + "encoding/json" + "github.com/stretchr/testify/require" + umaprotocol "github.com/uma-universal-money-address/uma-go-sdk/uma/protocol" + "testing" +) + +func TestParseV0Currency(t *testing.T) { + currency := umaprotocol.Currency{ + Code: "USD", + Symbol: "$", + Name: "US Dollar", + MillisatoshiPerUnit: 12345, + Convertible: umaprotocol.ConvertibleCurrency{ + MinSendable: 100, + MaxSendable: 100000000, + }, + Decimals: 2, + UmaMajorVersion: 0, + } + + currencyJson, err := currency.MarshalJSON() + require.NoError(t, err) + currencyJsonMap := make(map[string]interface{}) + err = json.Unmarshal(currencyJson, ¤cyJsonMap) + require.NoError(t, err) + require.Equal(t, "USD", currencyJsonMap["code"]) + require.Equal(t, "$", currencyJsonMap["symbol"]) + require.Equal(t, "US Dollar", currencyJsonMap["name"]) + require.Equal(t, 12345.0, currencyJsonMap["multiplier"]) + require.Equal(t, 100.0, currencyJsonMap["minSendable"]) + require.Equal(t, 100000000.0, currencyJsonMap["maxSendable"]) + require.Equal(t, 2.0, currencyJsonMap["decimals"]) + + reserializedCurrency := umaprotocol.Currency{} + err = json.Unmarshal(currencyJson, &reserializedCurrency) + require.NoError(t, err) + require.Equal(t, currency, reserializedCurrency) +} + +func TestV0LnurlpResponse(t *testing.T) { + currencies := []umaprotocol.Currency{ + { + Code: "USD", + Symbol: "$", + Name: "US Dollar", + MillisatoshiPerUnit: 12345, + Convertible: umaprotocol.ConvertibleCurrency{ + MinSendable: 100, + MaxSendable: 100000000, + }, + Decimals: 2, + UmaMajorVersion: 0, + }, + } + umaVersion := "0.3" + lnurlpResponse := umaprotocol.LnurlpResponse{ + Callback: "https://example.com/lnurlp", + Tag: "withdrawRequest", + MinSendable: 1000, + MaxSendable: 1000000, + Currencies: ¤cies, + EncodedMetadata: "metadata", + UmaVersion: &umaVersion, + } + + lnurlpResponseJson, err := json.Marshal(lnurlpResponse) + require.NoError(t, err) + lnurlpResponseJsonMap := make(map[string]interface{}) + err = json.Unmarshal(lnurlpResponseJson, &lnurlpResponseJsonMap) + require.NoError(t, err) + require.Equal(t, "https://example.com/lnurlp", lnurlpResponseJsonMap["callback"]) + require.Equal(t, "withdrawRequest", lnurlpResponseJsonMap["tag"]) + require.Equal(t, 1000.0, lnurlpResponseJsonMap["minSendable"]) + require.Equal(t, 1000000.0, lnurlpResponseJsonMap["maxSendable"]) + require.Equal(t, "metadata", lnurlpResponseJsonMap["metadata"]) + require.Equal(t, "0.3", lnurlpResponseJsonMap["umaVersion"]) + require.Equal(t, 100.0, lnurlpResponseJsonMap["currencies"].([]interface{})[0].(map[string]interface{})["minSendable"]) + require.Equal(t, 100000000.0, lnurlpResponseJsonMap["currencies"].([]interface{})[0].(map[string]interface{})["maxSendable"]) + + reserializedLnurlpResponse := umaprotocol.LnurlpResponse{} + err = json.Unmarshal(lnurlpResponseJson, &reserializedLnurlpResponse) + require.NoError(t, err) + require.Equal(t, lnurlpResponse, reserializedLnurlpResponse) +} + +func TestV0PayRequest(t *testing.T) { + payerData := umaprotocol.PayerData{ + "identifier": "$foo@bar.com", + "name": "Foo Bar", + "email": "email@themail.com", + } + currencyCode := "USD" + comment := "comment" + payRequest := umaprotocol.PayRequest{ + ReceivingCurrencyCode: ¤cyCode, + Amount: 1000, + PayerData: &payerData, + UmaMajorVersion: 0, + Comment: &comment, + } + + payRequestJson, err := payRequest.MarshalJSON() + require.NoError(t, err) + payRequestJsonMap := make(map[string]interface{}) + err = json.Unmarshal(payRequestJson, &payRequestJsonMap) + require.NoError(t, err) + require.Equal(t, "USD", payRequestJsonMap["currency"]) + require.Equal(t, 1000.0, payRequestJsonMap["amount"]) + require.Equal(t, "$foo@bar.com", payRequestJsonMap["payerData"].(map[string]interface{})["identifier"]) + require.Equal(t, "Foo Bar", payRequestJsonMap["payerData"].(map[string]interface{})["name"]) + require.Equal(t, "email@themail.com", payRequestJsonMap["payerData"].(map[string]interface{})["email"]) + require.Equal(t, "comment", payRequestJsonMap["comment"]) + + reserializedPayRequest := umaprotocol.PayRequest{} + err = json.Unmarshal(payRequestJson, &reserializedPayRequest) + require.NoError(t, err) + require.Equal(t, payRequest, reserializedPayRequest) +} + +func TestParseV1PayReq(t *testing.T) { + payerData := umaprotocol.PayerData{ + "identifier": "$foo@bar.com", + "name": "Foo Bar", + "email": "email@themail.com", + } + currencyCode := "USD" + comment := "comment" + payRequest := umaprotocol.PayRequest{ + ReceivingCurrencyCode: ¤cyCode, + SendingAmountCurrencyCode: ¤cyCode, + Amount: 1000, + PayerData: &payerData, + UmaMajorVersion: 1, + Comment: &comment, + } + + payRequestJson, err := payRequest.MarshalJSON() + require.NoError(t, err) + payRequestJsonMap := make(map[string]interface{}) + err = json.Unmarshal(payRequestJson, &payRequestJsonMap) + require.NoError(t, err) + require.Equal(t, "USD", payRequestJsonMap["convert"]) + require.Equal(t, "1000.USD", payRequestJsonMap["amount"]) + require.Equal(t, "$foo@bar.com", payRequestJsonMap["payerData"].(map[string]interface{})["identifier"]) + require.Equal(t, "Foo Bar", payRequestJsonMap["payerData"].(map[string]interface{})["name"]) + require.Equal(t, "email@themail.com", payRequestJsonMap["payerData"].(map[string]interface{})["email"]) + require.Equal(t, "comment", payRequestJsonMap["comment"]) + + reserializedPayRequest := umaprotocol.PayRequest{} + err = json.Unmarshal(payRequestJson, &reserializedPayRequest) + require.NoError(t, err) + require.Equal(t, payRequest, reserializedPayRequest) +} + +func TestParseV0PayReqResponse(t *testing.T) { + payReqRespJson := `{ + "pr": "lnbc1000n1p0u3", + "routes": [], + "compliance": { + "nodePubKey": "02", + "utxos": ["txid"], + "utxoCallback": "https://example.com/utxo" + }, + "paymentInfo": { + "currencyCode": "USD", + "multiplier": 1000, + "decimals": 2, + "exchangeFeesMillisatoshi": 1000 + } + }` + payReqResp := umaprotocol.PayReqResponse{} + err := json.Unmarshal([]byte(payReqRespJson), &payReqResp) + require.NoError(t, err) + require.Equal(t, "lnbc1000n1p0u3", payReqResp.EncodedInvoice) + require.Equal(t, "USD", payReqResp.PaymentInfo.CurrencyCode) + require.Equal(t, 1000.0, payReqResp.PaymentInfo.Multiplier) + require.Equal(t, 2, payReqResp.PaymentInfo.Decimals) + require.Equal(t, int64(1000), payReqResp.PaymentInfo.ExchangeFeesMillisatoshi) + compliance, err := payReqResp.PayeeData.Compliance() + require.NoError(t, err) + require.NotNilf(t, compliance, "compliance is nil") + require.Equal(t, "02", *(compliance.NodePubKey)) + require.Equal(t, []string{"txid"}, compliance.Utxos) + require.Equal(t, "https://example.com/utxo", *(compliance.UtxoCallback)) + require.Equal(t, 0, payReqResp.UmaMajorVersion) +} diff --git a/uma/test/uma_test.go b/uma/test/uma_test.go index 61a6c61..a3a7501 100644 --- a/uma/test/uma_test.go +++ b/uma/test/uma_test.go @@ -45,52 +45,52 @@ func TestParse(t *testing.T) { func TestIsUmaQueryValid(t *testing.T) { urlString := "https://vasp2.com/.well-known/lnurlp/bob?signature=signature&nonce=12345&vaspDomain=vasp1.com&umaVersion=1.0&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ := url.Parse(urlString) - assert.True(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.True(t, uma.IsUmaLnurlpQuery(*urlObj)) } func TestIsUmaQueryMissingParams(t *testing.T) { urlString := "https://vasp2.com/.well-known/lnurlp/bob?nonce=12345&vaspDomain=vasp1.com&umaVersion=1.0&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ := url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/.well-known/lnurlp/bob?signature=signature&nonce=12345&vaspDomain=vasp1.com&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ = url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/.well-known/lnurlp/bob?signature=signature&vaspDomain=vasp1.com&umaVersion=1.0&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ = url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/.well-known/lnurlp/bob?signature=signature&umaVersion=1.0&nonce=12345&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ = url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/.well-known/lnurlp/bob?signature=signature&umaVersion=1.0&nonce=12345&vaspDomain=vasp1.com×tamp=12345678" urlObj, _ = url.Parse(urlString) // IsSubjectToTravelRule is optional - assert.True(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.True(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/.well-known/lnurlp/bob?signature=signature&nonce=12345&vaspDomain=vasp1.com&umaVersion=1.0&isSubjectToTravelRule=true" urlObj, _ = url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/.well-known/lnurlp/bob" urlObj, _ = url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) } func TestIsUmaQueryInvalidPath(t *testing.T) { urlString := "https://vasp2.com/.well-known/lnurla/bob?signature=signature&nonce=12345&vaspDomain=vasp1.com&umaVersion=1.0&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ := url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/bob?signature=signature&nonce=12345&vaspDomain=vasp1.com&umaVersion=1.0&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ = url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) urlString = "https://vasp2.com/?signature=signature&nonce=12345&vaspDomain=vasp1.com&umaVersion=1.0&isSubjectToTravelRule=true×tamp=12345678" urlObj, _ = url.Parse(urlString) - assert.False(t, uma.IsUmaLnurlpQuery(*urlObj)) + require.False(t, uma.IsUmaLnurlpQuery(*urlObj)) } func TestSignAndVerifyLnurlpRequest(t *testing.T) { @@ -100,7 +100,7 @@ func TestSignAndVerifyLnurlpRequest(t *testing.T) { require.NoError(t, err) query, err := uma.ParseLnurlpRequest(*queryUrl) require.NoError(t, err) - assert.Equal(t, *query.UmaVersion, uma.UmaProtocolVersion) + require.Equal(t, *query.UmaVersion, uma.UmaProtocolVersion) err = uma.VerifyUmaLnurlpQuerySignature(*query.AsUmaRequest(), privateKey.PubKey().SerializeUncompressed(), getNonceCache()) require.NoError(t, err) } @@ -212,6 +212,7 @@ func TestPayReqCreationAndParsing(t *testing.T) { "USD", true, "$alice@vasp1.com", + 1, nil, nil, &trInfo, @@ -253,7 +254,7 @@ func TestPayReqCreationAndParsing(t *testing.T) { eciesPrivKey := eciesgo.NewPrivateKeyFromBytes(receiverEncryptionPrivateKey.Serialize()) decryptedTrInfo, err := eciesgo.Decrypt(eciesPrivKey, encryptedTrInfoBytes) require.NoError(t, err) - assert.Equal(t, trInfo, string(decryptedTrInfo)) + require.Equal(t, trInfo, string(decryptedTrInfo)) } func TestMsatsPayReqCreationAndParsing(t *testing.T) { @@ -275,6 +276,7 @@ func TestMsatsPayReqCreationAndParsing(t *testing.T) { "USD", false, "$alice@vasp1.com", + 1, nil, nil, &trInfo, @@ -334,6 +336,7 @@ func TestPayReqResponseAndParsing(t *testing.T) { "USD", true, "$alice@vasp1.com", + 1, nil, nil, &trInfo, @@ -377,7 +380,7 @@ func TestPayReqResponseAndParsing(t *testing.T) { nil, ) require.NoError(t, err) - require.Equal(t, payreqResponse.PaymentInfo.Amount, payreq.Amount) + require.Equal(t, payreqResponse.PaymentInfo.Amount, &payreq.Amount) require.Equal(t, payreqResponse.PaymentInfo.CurrencyCode, *payreq.ReceivingCurrencyCode) payreqResponseJson, err := json.Marshal(payreqResponse) @@ -425,6 +428,7 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { "USD", false, "$alice@vasp1.com", + 1, nil, nil, &trInfo, @@ -469,7 +473,7 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { ) require.NoError(t, err) expectedAmount := int64(math.Round(float64(payreq.Amount-fee) / conversionRate)) - require.Equal(t, payreqResponse.PaymentInfo.Amount, expectedAmount) + require.Equal(t, payreqResponse.PaymentInfo.Amount, &expectedAmount) require.Equal(t, payreqResponse.PaymentInfo.CurrencyCode, *payreq.ReceivingCurrencyCode) payreqResponseJson, err := json.Marshal(payreqResponse) @@ -489,6 +493,90 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { require.NoError(t, err) } +func TestV0PayReqResponseAndParsing(t *testing.T) { + senderSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + receiverEncryptionPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + receiverSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + + trInfo := "some TR info for VASP2" + payeeOptions := umaprotocol.CounterPartyDataOptions{ + "identifier": umaprotocol.CounterPartyDataOption{ + Mandatory: true, + }, + "name": umaprotocol.CounterPartyDataOption{ + Mandatory: false, + }, + } + payreq, err := uma.GetUmaPayRequest( + 1_000_000, + receiverEncryptionPrivateKey.PubKey().SerializeUncompressed(), + senderSigningPrivateKey.Serialize(), + "USD", + false, + "$alice@vasp1.com", + 0, + nil, + nil, + &trInfo, + nil, + umaprotocol.KycStatusVerified, + nil, + nil, + "/api/lnurl/utxocallback?txid=1234", + &payeeOptions, + nil, + ) + require.NoError(t, err) + client := &FakeInvoiceCreator{} + metadata, err := createMetadataForBob() + require.NoError(t, err) + payeeData := umaprotocol.PayeeData{ + "identifier": "$bob@vasp2.com", + } + receivingCurrencyCode := "USD" + receivingCurrencyDecimals := 2 + fee := int64(100_000) + conversionRate := float64(24_150) + utxoCallback := "/api/lnurl/utxocallback?txid=1234" + payeeIdentifier := "$bob@vasp2.com" + serializedPrivateKey := receiverSigningPrivateKey.Serialize() + payreqResponse, err := uma.GetPayReqResponse( + *payreq, + client, + metadata, + &receivingCurrencyCode, + &receivingCurrencyDecimals, + &conversionRate, + &fee, + &[]string{"abcdef12345"}, + nil, + &utxoCallback, + &payeeData, + &serializedPrivateKey, + &payeeIdentifier, + nil, + nil, + ) + require.NoError(t, err) + require.Equal(t, payreqResponse.PaymentInfo.CurrencyCode, *payreq.ReceivingCurrencyCode) + require.Equal(t, payreqResponse.PaymentInfo.Multiplier, conversionRate) + require.Equal(t, payreqResponse.PaymentInfo.Decimals, receivingCurrencyDecimals) + require.Equal(t, payreqResponse.PaymentInfo.ExchangeFeesMillisatoshi, fee) + compliance, err := payreqResponse.PayeeData.Compliance() + require.NoError(t, err) + require.Equal(t, compliance.Utxos, []string{"abcdef12345"}) + + payreqResponseJson, err := json.Marshal(payreqResponse) + require.NoError(t, err) + + parsedResponse, err := uma.ParsePayReqResponse(payreqResponseJson) + require.NoError(t, err) + require.Equal(t, payreqResponse, parsedResponse) +} + func TestSignAndVerifyPostTransactionCallback(t *testing.T) { signingPrivateKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) @@ -511,7 +599,7 @@ func TestParsePayReqFromQueryParamsNoOptionalFields(t *testing.T) { params := url.Values{ "amount": {amount}, } - payreq, err := uma.ParsePayRequestFromQueryParams(params) + payreq, err := umaprotocol.ParsePayRequestFromQueryParams(params) require.NoError(t, err) require.Equal(t, payreq.Amount, int64(1000)) require.Nil(t, payreq.ReceivingCurrencyCode) @@ -541,7 +629,7 @@ func TestParsePayReqFromQueryParamsAllOptionalFields(t *testing.T) { "payeeData": {string(encodedPayeeData)}, "comment": {"This is a comment"}, } - payreq, err := uma.ParsePayRequestFromQueryParams(params) + payreq, err := umaprotocol.ParsePayRequestFromQueryParams(params) require.NoError(t, err) require.Equal(t, payreq.Amount, int64(1000)) require.Equal(t, *payreq.ReceivingCurrencyCode, "USD") @@ -575,12 +663,12 @@ func TestParseAndEncodePayReqToQueryParams(t *testing.T) { "payeeData": {string(encodedPayeeData)}, "comment": {"This is a comment"}, } - payreq, err := uma.ParsePayRequestFromQueryParams(params) + payreq, err := umaprotocol.ParsePayRequestFromQueryParams(params) require.NoError(t, err) encodedParams, err := payreq.EncodeAsUrlParams() require.NoError(t, err) require.Equal(t, params, *encodedParams) - payreqReparsed, err := uma.ParsePayRequestFromQueryParams(*encodedParams) + payreqReparsed, err := umaprotocol.ParsePayRequestFromQueryParams(*encodedParams) require.NoError(t, err) require.Equal(t, payreq, payreqReparsed) } diff --git a/uma/uma.go b/uma/uma.go index d8f80a0..d4bcd19 100644 --- a/uma/uma.go +++ b/uma/uma.go @@ -340,6 +340,16 @@ func GetLnurlpResponse( (*payerDataOptions)[protocol.CounterPartyDataFieldIdentifier.String()] = protocol.CounterPartyDataOption{Mandatory: true} } + // Ensure currencies are correctly serialized: + if umaVersion != nil { + umaVersionParsed, err := ParseVersion(*umaVersion) + if err != nil && umaVersionParsed != nil && umaVersionParsed.Major == 0 { + for i := range *currencyOptions { + (*currencyOptions)[i].UmaMajorVersion = 0 + } + } + } + var allowsNostr *bool = nil if nostrPubkey != nil { *allowsNostr = true @@ -423,28 +433,31 @@ func GetVaspDomainFromUmaAddress(umaAddress string) (string, error) { // // Args: // -// amount: the amount of the payment in the smallest unit of the specified currency (i.e. cents for USD). -// receiverEncryptionPubKey: the public key of the receiver that will be used to encrypt the travel rule information. -// sendingVaspPrivateKey: the private key of the VASP that is sending the payment. This will be used to sign the request. -// receivingCurrencyCode: the code of the currency that the receiver will receive for this payment. -// isAmountInReceivingCurrency: whether the amount field is specified in the smallest unit of the receiving -// currency or in msats (if false). -// payerIdentifier: the identifier of the sender. For example, $alice@vasp1.com -// payerName: the name of the sender (optional). -// payerEmail: the email of the sender (optional). -// trInfo: the travel rule information. This will be encrypted before sending to the receiver. -// trInfoFormat: the standardized format of the travel rule information (e.g. IVMS). Null indicates raw json or a -// custom format, or no travel rule information. -// payerKycStatus: whether the sender is a KYC'd customer of the sending VASP. -// payerUtxos: the list of UTXOs of the sender's channels that might be used to fund the payment. -// payerNodePubKey: If known, the public key of the sender's node. If supported by the receiving VASP's compliance provider, -// this will be used to pre-screen the sender's UTXOs for compliance purposes. -// utxoCallback: the URL that the receiver will call to send UTXOs of the channel that the receiver used to receive -// the payment once it completes. -// requestedPayeeData: the payer data options that the sender is requesting about the receiver. -// comment: a comment that the sender would like to include with the payment. This can only be included -// 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`. +// amount: the amount of the payment in the smallest unit of the specified currency (i.e. cents for USD). +// receiverEncryptionPubKey: the public key of the receiver that will be used to encrypt the travel rule information. +// sendingVaspPrivateKey: the private key of the VASP that is sending the payment. This will be used to sign the request. +// receivingCurrencyCode: the code of the currency that the receiver will receive for this payment. +// isAmountInReceivingCurrency: whether the amount field is specified in the smallest unit of the receiving +// currency or in msats (if false). +// payerIdentifier: the identifier of the sender. For example, $alice@vasp1.com +// umaMajorVersion: the major version of UMA used for this request. If non-UMA, this version is still relevant +// for which LUD-21 spec to follow. For the older LUD-21 spec, this should be 0. For the newer LUD-21 spec, +// this should be 1. +// payerName: the name of the sender (optional). +// payerEmail: the email of the sender (optional). +// trInfo: the travel rule information. This will be encrypted before sending to the receiver. +// trInfoFormat: the standardized format of the travel rule information (e.g. IVMS). Null indicates raw json or a +// custom format, or no travel rule information. +// payerKycStatus: whether the sender is a KYC'd customer of the sending VASP. +// payerUtxos: the list of UTXOs of the sender's channels that might be used to fund the payment. +// payerNodePubKey: If known, the public key of the sender's node. If supported by the receiving VASP's compliance provider, +// this will be used to pre-screen the sender's UTXOs for compliance purposes. +// utxoCallback: the URL that the receiver will call to send UTXOs of the channel that the receiver used to receive +// the payment once it completes. +// requestedPayeeData: the payer data options that the sender is requesting about the receiver. +// comment: a comment that the sender would like to include with the payment. This can only be included +// 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`. func GetUmaPayRequest( amount int64, receiverEncryptionPubKey []byte, @@ -452,6 +465,7 @@ func GetUmaPayRequest( receivingCurrencyCode string, isAmountInReceivingCurrency bool, payerIdentifier string, + umaMajorVersion int, payerName *string, payerEmail *string, trInfo *string, @@ -506,6 +520,7 @@ func GetUmaPayRequest( }, RequestedPayeeData: requestedPayeeData, Comment: comment, + UmaMajorVersion: umaMajorVersion, }, nil } @@ -575,63 +590,6 @@ func ParsePayRequest(bytes []byte) (*protocol.PayRequest, error) { return &response, 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) (*protocol.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] - } - receivingCurrencyCodeStr := query.Get("convert") - var receivingCurrencyCode *string - if receivingCurrencyCodeStr != "" { - receivingCurrencyCode = &receivingCurrencyCodeStr - } - - payerData := query.Get("payerData") - var payerDataObj *protocol.PayerData - if payerData != "" { - err = json.Unmarshal([]byte(payerData), &payerDataObj) - if err != nil { - return nil, err - } - } - requestedPayeeData := query.Get("payeeData") - var requestedPayeeDataObj *protocol.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 &protocol.PayRequest{ - SendingAmountCurrencyCode: sendingAmountCurrencyCode, - ReceivingCurrencyCode: receivingCurrencyCode, - Amount: amount, - PayerData: payerDataObj, - RequestedPayeeData: requestedPayeeDataObj, - Comment: comment, - }, nil -} - type InvoiceCreator interface { CreateInvoice(amountMsats int64, metadata string) (*string, error) } @@ -702,10 +660,6 @@ func GetPayReqResponse( if receivingCurrencyCode != nil && request.SendingAmountCurrencyCode != nil { msatsAmount = int64(math.Round(float64(request.Amount)*(conversionRateOrOne))) + feesOrZero } - receivingCurrencyAmount := request.Amount - if request.SendingAmountCurrencyCode == nil { - receivingCurrencyAmount = int64(math.Round(float64(msatsAmount-feesOrZero) / conversionRateOrOne)) - } payerDataStr := "" if request.PayerData != nil { @@ -765,6 +719,14 @@ func GetPayReqResponse( } } + receivingCurrencyAmount := &request.Amount + if request.SendingAmountCurrencyCode == nil { + receivingCurrencyAmountVal := int64(math.Round(float64(msatsAmount-feesOrZero) / conversionRateOrOne)) + receivingCurrencyAmount = &receivingCurrencyAmountVal + } + if request.UmaMajorVersion == 0 { + receivingCurrencyAmount = nil + } var paymentInfo *protocol.PayReqResponsePaymentInfo if receivingCurrencyCode != nil { paymentInfo = &protocol.PayReqResponsePaymentInfo{ @@ -776,12 +738,13 @@ func GetPayReqResponse( } } return &protocol.PayReqResponse{ - EncodedInvoice: *encodedInvoice, - Routes: []protocol.Route{}, - PaymentInfo: paymentInfo, - PayeeData: payeeData, - Disposable: disposable, - SuccessAction: successAction, + EncodedInvoice: *encodedInvoice, + Routes: []protocol.Route{}, + PaymentInfo: paymentInfo, + PayeeData: payeeData, + Disposable: disposable, + SuccessAction: successAction, + UmaMajorVersion: request.UmaMajorVersion, }, nil } @@ -855,9 +818,9 @@ func getSignedCompliancePayeeData( Utxos: receiverChannelUtxos, NodePubKey: receiverNodePubKey, UtxoCallback: utxoCallback, - Signature: "", - SignatureNonce: *nonce, - SignatureTimestamp: timestamp, + Signature: nil, + SignatureNonce: nonce, + SignatureTimestamp: ×tamp, } payloadString, err := complianceData.SignablePayload(payerIdentifier, payeeIdentifier) if err != nil { @@ -867,14 +830,14 @@ func getSignedCompliancePayeeData( if err != nil { return nil, err } - complianceData.Signature = *signature + complianceData.Signature = signature return &complianceData, nil } // ParsePayReqResponse Parses the uma pay request response from a raw response body. func ParsePayReqResponse(bytes []byte) (*protocol.PayReqResponse, error) { var response protocol.PayReqResponse - err := json.Unmarshal(bytes, &response) + err := response.UnmarshalJSON(bytes) if err != nil { return nil, err } @@ -905,9 +868,12 @@ func VerifyPayReqResponseSignature( if complianceData == nil { return errors.New("missing compliance data") } + if response.UmaMajorVersion == 0 { + return errors.New("signatures were added to payreq responses in UMA v1. This response is from an UMA v0 receiving VASP") + } err = nonceCache.CheckAndSaveNonce( - complianceData.SignatureNonce, - time.Unix(complianceData.SignatureTimestamp, 0), + *complianceData.SignatureNonce, + time.Unix(*complianceData.SignatureTimestamp, 0), ) if err != nil { return err @@ -916,7 +882,7 @@ func VerifyPayReqResponseSignature( if err != nil { return err } - return verifySignature(signablePayload, complianceData.Signature, otherVaspPubKey) + return verifySignature(signablePayload, *complianceData.Signature, otherVaspPubKey) } // GetPostTransactionCallback Creates a signed post transaction callback. @@ -935,17 +901,22 @@ func GetPostTransactionCallback( if err != nil { return nil, err } + timestamp := time.Now().Unix() unsignedCallback := protocol.PostTransactionCallback{ Utxos: utxos, - VaspDomain: vaspDomain, - Timestamp: time.Now().Unix(), - Nonce: *nonce, + VaspDomain: &vaspDomain, + Timestamp: ×tamp, + Nonce: nonce, } - signature, err := signPayload(unsignedCallback.SignablePayload(), signingPrivateKey) + signablePayload, err := unsignedCallback.SignablePayload() if err != nil { return nil, err } - unsignedCallback.Signature = *signature + signature, err := signPayload(*signablePayload, signingPrivateKey) + if err != nil { + return nil, err + } + unsignedCallback.Signature = signature return &unsignedCallback, nil } @@ -971,10 +942,16 @@ func VerifyPostTransactionCallbackSignature( otherVaspPubKey []byte, nonceCache NonceCache, ) error { - err := nonceCache.CheckAndSaveNonce(callback.Nonce, time.Unix(callback.Timestamp, 0)) + if callback.Signature == nil || callback.Nonce == nil || callback.Timestamp == nil { + return errors.New("missing signature. Is this a UMA v0 callback? UMA v0 does not require signatures.") + } + err := nonceCache.CheckAndSaveNonce(*callback.Nonce, time.Unix(*callback.Timestamp, 0)) + if err != nil { + return err + } + signablePayload, err := callback.SignablePayload() if err != nil { return err } - signablePayload := callback.SignablePayload() - return verifySignature(signablePayload, callback.Signature, otherVaspPubKey) + return verifySignature(*signablePayload, *callback.Signature, otherVaspPubKey) } diff --git a/uma/version.go b/uma/version.go index 993d699..7147381 100644 --- a/uma/version.go +++ b/uma/version.go @@ -10,6 +10,8 @@ import ( const MAJOR_VERSION = 1 const MINOR_VERSION = 0 +var backcompatVersions = []string{"0.3"} + var UmaProtocolVersion = fmt.Sprintf("%d.%d", MAJOR_VERSION, MINOR_VERSION) type UnsupportedVersionError struct { @@ -42,21 +44,36 @@ func GetSupportedMajorVersionsFromErrorResponseBody(errorResponseBody []byte) ([ } func GetSupportedMajorVersions() map[int]struct{} { - // NOTE: In the future, we may want to support multiple major versions in the same SDK, but for now, this keeps - // things simple. - return map[int]struct{}{ - MAJOR_VERSION: {}, + versions := make(map[int]struct{}) + versions[MAJOR_VERSION] = struct{}{} + for _, version := range backcompatVersions { + parsedVersion, err := ParseVersion(version) + if err != nil { + continue + } + versions[parsedVersion.Major] = struct{}{} } + + return versions } func GetHighestSupportedVersionForMajorVersion(majorVersion int) *ParsedVersion { // Note that this also only supports a single major version for now. If we support more than one major version in // the future, we'll need to change this. - if majorVersion != MAJOR_VERSION { - return nil + if majorVersion == MAJOR_VERSION { + parsedVersion, _ := ParseVersion(UmaProtocolVersion) + return parsedVersion + } + for _, version := range backcompatVersions { + parsedVersion, err := ParseVersion(version) + if err != nil { + continue + } + if parsedVersion.Major == majorVersion { + return parsedVersion + } } - parsedVersion, _ := ParseVersion(UmaProtocolVersion) - return parsedVersion + return nil } func SelectHighestSupportedVersion(otherVaspSupportedMajorVersions []int) *string {