diff --git a/uma/protocol.go b/uma/protocol.go index 2615a06..8a526cb 100644 --- a/uma/protocol.go +++ b/uma/protocol.go @@ -1,202 +1,203 @@ package uma -import ( - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "net/url" - "strconv" - "strings" - "time" -) - -// LnurlpRequest is the first request in the UMA protocol. -// It is sent by the VASP that is sending the payment to find out information about the receiver. -type LnurlpRequest struct { - // ReceiverAddress is the address of the user at VASP2 that is receiving the payment. - ReceiverAddress string - // Nonce is a random string that is used to prevent replay attacks. - Nonce string - // Signature is the base64-encoded signature of sha256(ReceiverAddress|Nonce|Timestamp). - Signature string - // IsSubjectToTravelRule indicates VASP1 is a financial institution that requires travel rule information. - IsSubjectToTravelRule bool - // VaspDomain is the domain of the VASP that is sending the payment. It will be used by VASP2 to fetch the public keys of VASP1. - VaspDomain string - // Timestamp is the unix timestamp of when the request was sent. Used in the signature. - Timestamp time.Time - // UmaVersion is the version of the UMA protocol that VASP1 prefers to use for this transaction. For the version - // negotiation flow, see https://static.swimlanes.io/87f5d188e080cb8e0494e46f80f2ae74.png - UmaVersion string -} - -func (q *LnurlpRequest) EncodeToUrl() (*url.URL, error) { - receiverAddressParts := strings.Split(q.ReceiverAddress, "@") - if len(receiverAddressParts) != 2 { - return nil, errors.New("invalid receiver address") - } - scheme := "https" - if IsDomainLocalhost(receiverAddressParts[1]) { - scheme = "http" - } - lnurlpUrl := url.URL{ - Scheme: scheme, - Host: receiverAddressParts[1], - Path: fmt.Sprintf("/.well-known/lnurlp/%s", receiverAddressParts[0]), - } - queryParams := lnurlpUrl.Query() - queryParams.Add("signature", q.Signature) - queryParams.Add("vaspDomain", q.VaspDomain) - queryParams.Add("nonce", q.Nonce) - queryParams.Add("isSubjectToTravelRule", strconv.FormatBool(q.IsSubjectToTravelRule)) - queryParams.Add("timestamp", strconv.FormatInt(q.Timestamp.Unix(), 10)) - queryParams.Add("umaVersion", q.UmaVersion) - lnurlpUrl.RawQuery = queryParams.Encode() - return &lnurlpUrl, nil -} - -func (q *LnurlpRequest) signablePayload() []byte { - payloadString := strings.Join([]string{q.ReceiverAddress, q.Nonce, strconv.FormatInt(q.Timestamp.Unix(), 10)}, "|") - return []byte(payloadString) -} - -// LnurlpResponse is the response to the LnurlpRequest. -// It is sent by the VASP that is receiving the payment to provide information to the sender about the receiver. -type LnurlpResponse struct { - Tag string `json:"tag"` - Callback string `json:"callback"` - MinSendable int64 `json:"minSendable"` - MaxSendable int64 `json:"maxSendable"` - EncodedMetadata string `json:"metadata"` - Currencies []Currency `json:"currencies"` - RequiredPayerData PayerDataOptions `json:"payerData"` - Compliance LnurlComplianceResponse `json:"compliance"` - // UmaVersion is the version of the UMA protocol that VASP2 has chosen for this transaction based on its own support - // and VASP1's specified preference in the LnurlpRequest. For the version negotiation flow, see - // https://static.swimlanes.io/87f5d188e080cb8e0494e46f80f2ae74.png - UmaVersion string `json:"umaVersion"` -} - -// LnurlComplianceResponse is the `compliance` field of the LnurlpResponse. -type LnurlComplianceResponse struct { - // KycStatus indicates whether VASP2 has KYC information about the receiver. - KycStatus KycStatus `json:"kycStatus"` - // Signature is the base64-encoded signature of sha256(ReceiverAddress|Nonce|Timestamp). - Signature string `json:"signature"` - // Nonce is a random string that is used to prevent replay attacks. - Nonce string `json:"signatureNonce"` - // Timestamp is the unix timestamp of when the request was sent. Used in the signature. - Timestamp int64 `json:"signatureTimestamp"` - // IsSubjectToTravelRule indicates whether VASP2 is a financial institution that requires travel rule information. - IsSubjectToTravelRule bool `json:"isSubjectToTravelRule"` - // ReceiverIdentifier is the identifier of the receiver at VASP2. - ReceiverIdentifier string `json:"receiverIdentifier"` -} - -func (r *LnurlpResponse) signablePayload() []byte { - payloadString := strings.Join([]string{ - r.Compliance.ReceiverIdentifier, - r.Compliance.Nonce, - strconv.FormatInt(r.Compliance.Timestamp, 10), - }, "|") - return []byte(payloadString) -} - -// PayRequest is the request sent by the sender to the receiver to retrieve an invoice. -type PayRequest struct { - // CurrencyCode is the ISO 3-digit currency code that the receiver will receive for this payment. - CurrencyCode string `json:"currency"` - // Amount is the amount that the receiver will receive for this payment in the smallest unit of the specified currency (i.e. cents for USD). - Amount int64 `json:"amount"` - // PayerData is the data that the sender will send to the receiver to identify themselves. - PayerData PayerData `json:"payerData"` -} - -func (q *PayRequest) Encode() ([]byte, error) { - return json.Marshal(q) -} - -func (q *PayRequest) signablePayload() []byte { - senderAddress := q.PayerData.Identifier - signatureNonce := q.PayerData.Compliance.SignatureNonce - signatureTimestamp := q.PayerData.Compliance.SignatureTimestamp - payloadString := strings.Join([]string{senderAddress, signatureNonce, strconv.FormatInt(signatureTimestamp, 10)}, "|") - return []byte(payloadString) -} - -// 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. - EncodedInvoice string `json:"pr"` - // Routes is usually just an empty list from legacy LNURL, which was replaced by route hints in the BOLT11 invoice. - Routes []Route `json:"routes"` - Compliance PayReqResponseCompliance `json:"compliance"` - PaymentInfo PayReqResponsePaymentInfo `json:"paymentInfo"` -} - -type Route struct { - Pubkey string `json:"pubkey"` - Path []struct { - Pubkey string `json:"pubkey"` - Fee int64 `json:"fee"` - Msatoshi int64 `json:"msatoshi"` - Channel string `json:"channel"` - } `json:"path"` -} - -type PayReqResponseCompliance struct { - // NodePubKey is the public key of the receiver's node if known. - NodePubKey *string `json:"nodePubKey"` - // Utxos is a list of UTXOs of channels over which the receiver will likely receive the payment. - Utxos []string `json:"utxos"` - // 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"` -} - -type PayReqResponsePaymentInfo struct { - // 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 the specified currency. - // In this context, this is just for convenience. The conversion rate is also baked into the invoice amount itself. - // `invoice amount = amount * multiplier + exchangeFeesMillisatoshi` - Multiplier float64 `json:"multiplier"` - // Decimals is the number of digits after the decimal point for the receiving currency. For example, in USD, by - // convention, there are 2 digits for cents - $5.95. In this case, `Decimals` would be 2. This should align with the - // currency's `Decimals` field in the LNURLP response. It is included here for convenience. See - // [UMAD-04](/uma-04-local-currency.md) for details, edge cases, and examples. - Decimals int `json:"decimals"` - // ExchangeFeesMillisatoshi is the fees charged (in millisats) by the receiving VASP for this transaction. This is - // separate from the Multiplier. - ExchangeFeesMillisatoshi int64 `json:"exchangeFeesMillisatoshi"` -} - -// PubKeyResponse is sent from a VASP to another VASP to provide its public keys. -// It is the response to GET requests at `/.well-known/lnurlpubkey`. -type PubKeyResponse struct { - // SigningPubKeyHex is used to verify signatures from a VASP. Hex-encoded byte array. - SigningPubKeyHex string `json:"signingPubKey"` - // EncryptionPubKeyHex is used to encrypt TR info sent to a VASP. Hex-encoded byte array. - EncryptionPubKeyHex string `json:"encryptionPubKey"` - // ExpirationTimestamp [Optional] Seconds since epoch at which these pub keys must be refreshed. - // They can be safely cached until this expiration (or forever if null). - ExpirationTimestamp *int64 `json:"expirationTimestamp"` -} - -func (r *PubKeyResponse) SigningPubKey() ([]byte, error) { - return hex.DecodeString(r.SigningPubKeyHex) -} - -func (r *PubKeyResponse) EncryptionPubKey() ([]byte, error) { - return hex.DecodeString(r.EncryptionPubKeyHex) -} - -// UtxoWithAmount is a pair of utxo and amount transferred over that corresponding channel. -// It can be used to register payment for KYT. -type UtxoWithAmount struct { - // Utxo The utxo of the channel over which the payment went through in the format of :. - Utxo string `json:"utxo"` - - // Amount The amount of funds transferred in the payment in mSats. - Amount int64 `json:"amountMsats"` -} +// +//import ( +// "encoding/hex" +// "encoding/json" +// "errors" +// "fmt" +// "net/url" +// "strconv" +// "strings" +// "time" +//) +// +//// LnurlpRequest is the first request in the UMA protocol. +//// It is sent by the VASP that is sending the payment to find out information about the receiver. +//type LnurlpRequest struct { +// // ReceiverAddress is the address of the user at VASP2 that is receiving the payment. +// ReceiverAddress string +// // Nonce is a random string that is used to prevent replay attacks. +// Nonce string +// // Signature is the base64-encoded signature of sha256(ReceiverAddress|Nonce|Timestamp). +// Signature string +// // IsSubjectToTravelRule indicates VASP1 is a financial institution that requires travel rule information. +// IsSubjectToTravelRule bool +// // VaspDomain is the domain of the VASP that is sending the payment. It will be used by VASP2 to fetch the public keys of VASP1. +// VaspDomain string +// // Timestamp is the unix timestamp of when the request was sent. Used in the signature. +// Timestamp time.Time +// // UmaVersion is the version of the UMA protocol that VASP1 prefers to use for this transaction. For the version +// // negotiation flow, see https://static.swimlanes.io/87f5d188e080cb8e0494e46f80f2ae74.png +// UmaVersion string +//} +// +//func (q *LnurlpRequest) EncodeToUrl() (*url.URL, error) { +// receiverAddressParts := strings.Split(q.ReceiverAddress, "@") +// if len(receiverAddressParts) != 2 { +// return nil, errors.New("invalid receiver address") +// } +// scheme := "https" +// if IsDomainLocalhost(receiverAddressParts[1]) { +// scheme = "http" +// } +// lnurlpUrl := url.URL{ +// Scheme: scheme, +// Host: receiverAddressParts[1], +// Path: fmt.Sprintf("/.well-known/lnurlp/%s", receiverAddressParts[0]), +// } +// queryParams := lnurlpUrl.Query() +// queryParams.Add("signature", q.Signature) +// queryParams.Add("vaspDomain", q.VaspDomain) +// queryParams.Add("nonce", q.Nonce) +// queryParams.Add("isSubjectToTravelRule", strconv.FormatBool(q.IsSubjectToTravelRule)) +// queryParams.Add("timestamp", strconv.FormatInt(q.Timestamp.Unix(), 10)) +// queryParams.Add("umaVersion", q.UmaVersion) +// lnurlpUrl.RawQuery = queryParams.Encode() +// return &lnurlpUrl, nil +//} +// +//func (q *LnurlpRequest) signablePayload() []byte { +// payloadString := strings.Join([]string{q.ReceiverAddress, q.Nonce, strconv.FormatInt(q.Timestamp.Unix(), 10)}, "|") +// return []byte(payloadString) +//} +// +//// LnurlpResponse is the response to the LnurlpRequest. +//// It is sent by the VASP that is receiving the payment to provide information to the sender about the receiver. +//type LnurlpResponse struct { +// Tag string `json:"tag"` +// Callback string `json:"callback"` +// MinSendable int64 `json:"minSendable"` +// MaxSendable int64 `json:"maxSendable"` +// EncodedMetadata string `json:"metadata"` +// Currencies []Currency `json:"currencies"` +// RequiredPayerData PayerDataOptions `json:"payerData"` +// Compliance LnurlComplianceResponse `json:"compliance"` +// // UmaVersion is the version of the UMA protocol that VASP2 has chosen for this transaction based on its own support +// // and VASP1's specified preference in the LnurlpRequest. For the version negotiation flow, see +// // https://static.swimlanes.io/87f5d188e080cb8e0494e46f80f2ae74.png +// UmaVersion string `json:"umaVersion"` +//} +// +//// LnurlComplianceResponse is the `compliance` field of the LnurlpResponse. +//type LnurlComplianceResponse struct { +// // KycStatus indicates whether VASP2 has KYC information about the receiver. +// KycStatus KycStatus `json:"kycStatus"` +// // Signature is the base64-encoded signature of sha256(ReceiverAddress|Nonce|Timestamp). +// Signature string `json:"signature"` +// // Nonce is a random string that is used to prevent replay attacks. +// Nonce string `json:"signatureNonce"` +// // Timestamp is the unix timestamp of when the request was sent. Used in the signature. +// Timestamp int64 `json:"signatureTimestamp"` +// // IsSubjectToTravelRule indicates whether VASP2 is a financial institution that requires travel rule information. +// IsSubjectToTravelRule bool `json:"isSubjectToTravelRule"` +// // ReceiverIdentifier is the identifier of the receiver at VASP2. +// ReceiverIdentifier string `json:"receiverIdentifier"` +//} +// +//func (r *LnurlpResponse) signablePayload() []byte { +// payloadString := strings.Join([]string{ +// r.Compliance.ReceiverIdentifier, +// r.Compliance.Nonce, +// strconv.FormatInt(r.Compliance.Timestamp, 10), +// }, "|") +// return []byte(payloadString) +//} +// +//// PayRequest is the request sent by the sender to the receiver to retrieve an invoice. +//type PayRequest struct { +// // CurrencyCode is the ISO 3-digit currency code that the receiver will receive for this payment. +// CurrencyCode string `json:"currency"` +// // Amount is the amount that the receiver will receive for this payment in the smallest unit of the specified currency (i.e. cents for USD). +// Amount int64 `json:"amount"` +// // PayerData is the data that the sender will send to the receiver to identify themselves. +// PayerData PayerData `json:"payerData"` +//} +// +//func (q *PayRequest) Encode() ([]byte, error) { +// return json.Marshal(q) +//} +// +//func (q *PayRequest) signablePayload() []byte { +// senderAddress := q.PayerData.Identifier +// signatureNonce := q.PayerData.Compliance.SignatureNonce +// signatureTimestamp := q.PayerData.Compliance.SignatureTimestamp +// payloadString := strings.Join([]string{senderAddress, signatureNonce, strconv.FormatInt(signatureTimestamp, 10)}, "|") +// return []byte(payloadString) +//} +// +//// 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. +// EncodedInvoice string `json:"pr"` +// // Routes is usually just an empty list from legacy LNURL, which was replaced by route hints in the BOLT11 invoice. +// Routes []Route `json:"routes"` +// Compliance PayReqResponseCompliance `json:"compliance"` +// PaymentInfo PayReqResponsePaymentInfo `json:"paymentInfo"` +//} +// +//type Route struct { +// Pubkey string `json:"pubkey"` +// Path []struct { +// Pubkey string `json:"pubkey"` +// Fee int64 `json:"fee"` +// Msatoshi int64 `json:"msatoshi"` +// Channel string `json:"channel"` +// } `json:"path"` +//} +// +//type PayReqResponseCompliance struct { +// // NodePubKey is the public key of the receiver's node if known. +// NodePubKey *string `json:"nodePubKey"` +// // Utxos is a list of UTXOs of channels over which the receiver will likely receive the payment. +// Utxos []string `json:"utxos"` +// // 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"` +//} +// +//type PayReqResponsePaymentInfo struct { +// // 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 the specified currency. +// // In this context, this is just for convenience. The conversion rate is also baked into the invoice amount itself. +// // `invoice amount = amount * multiplier + exchangeFeesMillisatoshi` +// Multiplier float64 `json:"multiplier"` +// // Decimals is the number of digits after the decimal point for the receiving currency. For example, in USD, by +// // convention, there are 2 digits for cents - $5.95. In this case, `Decimals` would be 2. This should align with the +// // currency's `Decimals` field in the LNURLP response. It is included here for convenience. See +// // [UMAD-04](/uma-04-local-currency.md) for details, edge cases, and examples. +// Decimals int `json:"decimals"` +// // ExchangeFeesMillisatoshi is the fees charged (in millisats) by the receiving VASP for this transaction. This is +// // separate from the Multiplier. +// ExchangeFeesMillisatoshi int64 `json:"exchangeFeesMillisatoshi"` +//} +// +//// PubKeyResponse is sent from a VASP to another VASP to provide its public keys. +//// It is the response to GET requests at `/.well-known/lnurlpubkey`. +//type PubKeyResponse struct { +// // SigningPubKeyHex is used to verify signatures from a VASP. Hex-encoded byte array. +// SigningPubKeyHex string `json:"signingPubKey"` +// // EncryptionPubKeyHex is used to encrypt TR info sent to a VASP. Hex-encoded byte array. +// EncryptionPubKeyHex string `json:"encryptionPubKey"` +// // ExpirationTimestamp [Optional] Seconds since epoch at which these pub keys must be refreshed. +// // They can be safely cached until this expiration (or forever if null). +// ExpirationTimestamp *int64 `json:"expirationTimestamp"` +//} +// +//func (r *PubKeyResponse) SigningPubKey() ([]byte, error) { +// return hex.DecodeString(r.SigningPubKeyHex) +//} +// +//func (r *PubKeyResponse) EncryptionPubKey() ([]byte, error) { +// return hex.DecodeString(r.EncryptionPubKeyHex) +//} +// +//// UtxoWithAmount is a pair of utxo and amount transferred over that corresponding channel. +//// It can be used to register payment for KYT. +//type UtxoWithAmount struct { +// // Utxo The utxo of the channel over which the payment went through in the format of :. +// Utxo string `json:"utxo"` +// +// // Amount The amount of funds transferred in the payment in mSats. +// Amount int64 `json:"amountMsats"` +//} 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) }