From c40bf378ceb01dd0acfc7ac29ea57d7b0604afc3 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Thu, 14 Mar 2024 00:38:27 -0700 Subject: [PATCH 1/3] Break up the protocol file into individual files. --- uma/protocol.go | 541 ---------------------- uma/{ => protocol}/counter_party_data.go | 2 +- uma/{ => protocol}/currency.go | 2 +- uma/{ => protocol}/kyc_status.go | 2 +- uma/protocol/lnurl_request.go | 111 +++++ uma/protocol/lnurl_response.go | 101 ++++ uma/protocol/pay_request.go | 215 +++++++++ uma/{ => protocol}/payee_data.go | 22 +- uma/{ => protocol}/payer_data.go | 2 +- uma/protocol/payreq_response.go | 64 +++ uma/protocol/post_transaction_callback.go | 39 ++ uma/protocol/pub_key_response.go | 23 + uma/public_key_cache.go | 19 +- uma/test/uma_test.go | 59 +-- uma/uma.go | 164 +++---- uma/{ => utils}/url_utils.go | 2 +- 16 files changed, 702 insertions(+), 666 deletions(-) delete mode 100644 uma/protocol.go rename uma/{ => protocol}/counter_party_data.go (97%) rename uma/{ => protocol}/currency.go (99%) rename uma/{ => protocol}/kyc_status.go (98%) create mode 100644 uma/protocol/lnurl_request.go create mode 100644 uma/protocol/lnurl_response.go create mode 100644 uma/protocol/pay_request.go rename uma/{ => protocol}/payee_data.go (80%) rename uma/{ => protocol}/payer_data.go (99%) create mode 100644 uma/protocol/payreq_response.go create mode 100644 uma/protocol/post_transaction_callback.go create mode 100644 uma/protocol/pub_key_response.go rename uma/{ => utils}/url_utils.go (95%) diff --git a/uma/protocol.go b/uma/protocol.go deleted file mode 100644 index 0ae48a3..0000000 --- a/uma/protocol.go +++ /dev/null @@ -1,541 +0,0 @@ -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 -} - -// AsUmaRequest returns the request as an UmaLnurlpRequest if it is a valid UMA request, otherwise it returns nil. -// This is useful for validation and avoiding nil pointer dereferences. -func (q *LnurlpRequest) AsUmaRequest() *UmaLnurlpRequest { - if !q.IsUmaRequest() { - return nil - } - return &UmaLnurlpRequest{ - LnurlpRequest: *q, - ReceiverAddress: q.ReceiverAddress, - Nonce: *q.Nonce, - Signature: *q.Signature, - IsSubjectToTravelRule: q.IsSubjectToTravelRule != nil && *q.IsSubjectToTravelRule, - VaspDomain: *q.VaspDomain, - Timestamp: *q.Timestamp, - UmaVersion: *q.UmaVersion, - } -} - -// IsUmaRequest returns true if the request is a valid UMA request, otherwise, if any fields are missing, it returns false. -func (q *LnurlpRequest) IsUmaRequest() bool { - return q.VaspDomain != nil && q.Nonce != nil && q.Signature != nil && q.Timestamp != nil && q.UmaVersion != nil -} - -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() - if q.IsUmaRequest() { - queryParams.Add("signature", *q.Signature) - queryParams.Add("vaspDomain", *q.VaspDomain) - queryParams.Add("nonce", *q.Nonce) - isSubjectToTravelRule := *q.IsSubjectToTravelRule - queryParams.Add("isSubjectToTravelRule", strconv.FormatBool(isSubjectToTravelRule)) - queryParams.Add("timestamp", strconv.FormatInt(q.Timestamp.Unix(), 10)) - queryParams.Add("umaVersion", *q.UmaVersion) - } - lnurlpUrl.RawQuery = queryParams.Encode() - return &lnurlpUrl, nil -} - -// UmaLnurlpRequest 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 UmaLnurlpRequest struct { - LnurlpRequest - // 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) signablePayload() ([]byte, error) { - if q.Timestamp == nil || q.Nonce == nil { - return nil, errors.New("timestamp and nonce are required for signing") - } - payloadString := strings.Join([]string{q.ReceiverAddress, *q.Nonce, strconv.FormatInt(q.Timestamp.Unix(), 10)}, "|") - return []byte(payloadString), nil -} - -// 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 is the list of currencies that the receiver can quote. See LUD-21. Required for UMA. - Currencies *[]Currency `json:"currencies"` - // RequiredPayerData the data about the payer that the sending VASP must provide in order to send a payment. - RequiredPayerData *CounterPartyDataOptions `json:"payerData"` - // Compliance is compliance-related data from the receiving VASP for UMA. - 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"` - // CommentCharsAllowed is the number of characters that the sender can include in the comment field of the pay request. - CommentCharsAllowed *int `json:"commentAllowed"` - // NostrPubkey is an optional nostr pubkey used for nostr zaps (NIP-57). If set, it should be a valid BIP-340 public - // key in hex format. - NostrPubkey *string `json:"nostrPubkey"` - // AllowsNostr should be set to true if the receiving VASP allows nostr zaps (NIP-57). - AllowsNostr *bool `json:"allowsNostr"` -} - -// 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) IsUmaResponse() bool { - return r.Compliance != nil && r.UmaVersion != nil && r.Currencies != nil && r.RequiredPayerData != nil -} - -func (r *LnurlpResponse) AsUmaResponse() *UmaLnurlpResponse { - if !r.IsUmaResponse() { - return nil - } - return &UmaLnurlpResponse{ - LnurlpResponse: *r, - Currencies: *r.Currencies, - RequiredPayerData: *r.RequiredPayerData, - Compliance: *r.Compliance, - UmaVersion: *r.UmaVersion, - CommentCharsAllowed: r.CommentCharsAllowed, - NostrPubkey: r.NostrPubkey, - AllowsNostr: r.AllowsNostr, - } -} - -// UmaLnurlpResponse is the UMA 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 UmaLnurlpResponse struct { - LnurlpResponse - // Currencies is the list of currencies that the receiver can quote. See LUD-21. Required for UMA. - Currencies []Currency `json:"currencies"` - // RequiredPayerData the data about the payer that the sending VASP must provide in order to send a payment. - RequiredPayerData CounterPartyDataOptions `json:"payerData"` - // Compliance is compliance-related data from the receiving VASP for UMA. - 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"` - // CommentCharsAllowed is the number of characters that the sender can include in the comment field of the pay request. - CommentCharsAllowed *int `json:"commentAllowed"` - // NostrPubkey is an optional nostr pubkey used for nostr zaps (NIP-57). If set, it should be a valid BIP-340 public - // key in hex format. - NostrPubkey *string `json:"nostrPubkey"` - // AllowsNostr should be set to true if the receiving VASP allows nostr zaps (NIP-57). - AllowsNostr *bool `json:"allowsNostr"` -} - -func (r *UmaLnurlpResponse) 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 { - // SendingAmountCurrencyCode is the currency code of the `amount` field. `nil` indicates that `amount` is in - // millisatoshis as in LNURL without LUD-21. If this is not `nil`, then `amount` is in the smallest unit of the - // specified currency (e.g. cents for USD). This currency code can be any currency which the receiver can quote. - // However, there are two most common scenarios for UMA: - // - // 1. If the sender wants the receiver wants to receive a specific amount in their receiving - // currency, then this field should be the same as `receiving_currency_code`. This is useful - // for cases where the sender wants to ensure that the receiver receives a specific amount - // in that destination currency, regardless of the exchange rate, for example, when paying - // for some goods or services in a foreign currency. - // - // 2. If the sender has a specific amount in their own currency that they would like to send, - // then this field should be left as `None` to indicate that the amount is in millisatoshis. - // This will lock the sent amount on the sender side, and the receiver will receive the - // equivalent amount in their receiving currency. NOTE: In this scenario, the sending VASP - // *should not* pass the sending currency code here, as it is not relevant to the receiver. - // Rather, by specifying an invoice amount in msats, the sending VASP can ensure that their - // user will be sending a fixed amount, regardless of the exchange rate on the receiving side. - SendingAmountCurrencyCode *string `json:"sendingAmountCurrencyCode"` - // ReceivingCurrencyCode is the ISO 3-digit currency code that the receiver will receive for this payment. Defaults - // to amount being specified in msats if this is not provided. - ReceivingCurrencyCode *string `json:"convert"` - // 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) if `SendingAmountCurrencyCode` is not `nil`. Otherwise, it is the amount in - // millisatoshis. - Amount int64 `json:"amount"` - // PayerData is the data that the sender will send to the receiver to identify themselves. Required for UMA, as is - // the `compliance` field in the `payerData` object. - PayerData *PayerData `json:"payerData"` - // RequestedPayeeData is the data that the sender is requesting about the payee. - RequestedPayeeData *CounterPartyDataOptions `json:"payeeData"` - // Comment is 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`. - Comment *string `json:"comment"` -} - -// IsUmaRequest returns true if the request is a valid UMA request, otherwise, if any fields are missing, it returns false. -func (p *PayRequest) IsUmaRequest() bool { - if p.PayerData == nil { - return false - } - - compliance, err := p.PayerData.Compliance() - if err != nil { - return false - } - - return compliance != nil && p.PayerData.Identifier() != nil -} - -func (p *PayRequest) MarshalJSON() ([]byte, error) { - 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 -} - -func (p *PayRequest) UnmarshalJSON(data []byte) error { - var rawReq map[string]interface{} - err := json.Unmarshal(data, &rawReq) - if err != nil { - return err - } - convert, ok := rawReq["convert"].(string) - if ok { - p.ReceivingCurrencyCode = &convert - } - amount, ok := rawReq["amount"].(string) - if !ok { - return errors.New("missing or invalid amount field") - } - amountParts := strings.Split(amount, ".") - if len(amountParts) > 2 { - return errors.New("invalid amount field") - } - p.Amount, err = strconv.ParseInt(amountParts[0], 10, 64) - if err != nil { - return err - } - if len(amountParts) == 2 && len(amountParts[1]) > 0 { - p.SendingAmountCurrencyCode = &amountParts[1] - } - payerDataJson, ok := rawReq["payerData"].(map[string]interface{}) - if ok { - payerDataJsonBytes, err := json.Marshal(payerDataJson) - if err != nil { - return err - } - var payerData PayerData - err = json.Unmarshal(payerDataJsonBytes, &payerData) - if err != nil { - return err - } - p.PayerData = &payerData - } - payeeDataJson, ok := rawReq["payeeData"].(map[string]interface{}) - if ok { - payeeDataJsonBytes, err := json.Marshal(payeeDataJson) - if err != nil { - return err - } - var payeeData CounterPartyDataOptions - err = json.Unmarshal(payeeDataJsonBytes, &payeeData) - if err != nil { - return err - } - p.RequestedPayeeData = &payeeData - } - comment, ok := rawReq["comment"].(string) - if ok { - p.Comment = &comment - } - return nil -} - -func (p *PayRequest) Encode() ([]byte, error) { - return json.Marshal(p) -} - -func (p *PayRequest) EncodeAsUrlParams() (*url.Values, error) { - jsonBytes, err := json.Marshal(p) - if err != nil { - return nil, err - } - jsonMap := make(map[string]interface{}) - err = json.Unmarshal(jsonBytes, &jsonMap) - if err != nil { - return nil, err - } - payReqParams := url.Values{} - for key, value := range jsonMap { - valueString, ok := value.(string) - if ok { - payReqParams.Add(key, valueString) - } else { - valueBytes, err := json.Marshal(value) - if err != nil { - return nil, err - } - payReqParams.Add(key, string(valueBytes)) - } - } - return &payReqParams, nil -} - -func (p *PayRequest) signablePayload() ([]byte, error) { - if p.PayerData == nil { - return nil, errors.New("payer data is missing") - } - senderAddress := p.PayerData.Identifier() - if senderAddress == nil || *senderAddress == "" { - return nil, errors.New("payer data identifier is missing") - } - complianceData, err := p.PayerData.Compliance() - if err != nil { - return nil, err - } - if complianceData == nil { - return nil, errors.New("compliance payer data is missing") - } - signatureNonce := complianceData.SignatureNonce - signatureTimestamp := complianceData.SignatureTimestamp - payloadString := strings.Join([]string{ - *senderAddress, - signatureNonce, - strconv.FormatInt(signatureTimestamp, 10), - }, "|") - return []byte(payloadString), nil -} - -// 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"` - // 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"` - // PayeeData The data about the receiver that the sending VASP requested in the payreq request. - // Required for UMA. - PayeeData *PayeeData `json:"payeeData"` - // Disposable This field may be used by a WALLET to decide whether the initial LNURL link will be stored locally - // for later reuse or erased. If disposable is null, it should be interpreted as true, so if SERVICE intends its - // LNURL links to be stored it must return `disposable: false`. UMA should never return `disposable: false` due to - // signature nonce checks, etc. See LUD-11. - 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"` -} - -func (p *PayReqResponse) IsUmaResponse() bool { - if p.PaymentInfo == nil || p.PayeeData == nil { - return false - } - compliance, err := p.PayeeData.Compliance() - if err != nil { - return false - } - return compliance != nil -} - -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 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"` - // 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:"fee"` -} - -func (c *CompliancePayeeData) signablePayload(payerIdentifier string, payeeIdentifier string) ([]byte, error) { - if c == nil { - return nil, errors.New("compliance data is missing") - } - payloadString := strings.Join([]string{ - payerIdentifier, - payeeIdentifier, - c.SignatureNonce, - strconv.FormatInt(c.SignatureTimestamp, 10), - }, "|") - return []byte(payloadString), nil -} - -// 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"` -} - -// PostTransactionCallback is sent between VASPs after the payment is complete. -type PostTransactionCallback struct { - // Utxos is a list of utxo/amounts corresponding to the VASPs channels. - 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"` - // Signature is the base64-encoded signature of sha256(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"` -} - -func (c *PostTransactionCallback) signablePayload() []byte { - payloadString := strings.Join([]string{ - c.Nonce, - strconv.FormatInt(c.Timestamp, 10), - }, "|") - return []byte(payloadString) -} diff --git a/uma/counter_party_data.go b/uma/protocol/counter_party_data.go similarity index 97% rename from uma/counter_party_data.go rename to uma/protocol/counter_party_data.go index 44a2f40..6ec0cfb 100644 --- a/uma/counter_party_data.go +++ b/uma/protocol/counter_party_data.go @@ -1,4 +1,4 @@ -package uma +package protocol type CounterPartyDataOption struct { Mandatory bool `json:"mandatory"` diff --git a/uma/currency.go b/uma/protocol/currency.go similarity index 99% rename from uma/currency.go rename to uma/protocol/currency.go index f4f3c4d..fb87fc9 100644 --- a/uma/currency.go +++ b/uma/protocol/currency.go @@ -1,4 +1,4 @@ -package uma +package protocol type Currency struct { // Code is the ISO 4217 (if applicable) currency code (eg. "USD"). For cryptocurrencies, this will be a ticker diff --git a/uma/kyc_status.go b/uma/protocol/kyc_status.go similarity index 98% rename from uma/kyc_status.go rename to uma/protocol/kyc_status.go index 0df3702..5dfc941 100644 --- a/uma/kyc_status.go +++ b/uma/protocol/kyc_status.go @@ -1,4 +1,4 @@ -package uma +package protocol import "encoding/json" diff --git a/uma/protocol/lnurl_request.go b/uma/protocol/lnurl_request.go new file mode 100644 index 0000000..9758b87 --- /dev/null +++ b/uma/protocol/lnurl_request.go @@ -0,0 +1,111 @@ +package protocol + +import ( + "errors" + "fmt" + "github.com/uma-universal-money-address/uma-go-sdk/uma/utils" + "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 +} + +// AsUmaRequest returns the request as an UmaLnurlpRequest if it is a valid UMA request, otherwise it returns nil. +// This is useful for validation and avoiding nil pointer dereferences. +func (q *LnurlpRequest) AsUmaRequest() *UmaLnurlpRequest { + if !q.IsUmaRequest() { + return nil + } + return &UmaLnurlpRequest{ + LnurlpRequest: *q, + ReceiverAddress: q.ReceiverAddress, + Nonce: *q.Nonce, + Signature: *q.Signature, + IsSubjectToTravelRule: q.IsSubjectToTravelRule != nil && *q.IsSubjectToTravelRule, + VaspDomain: *q.VaspDomain, + Timestamp: *q.Timestamp, + UmaVersion: *q.UmaVersion, + } +} + +// IsUmaRequest returns true if the request is a valid UMA request, otherwise, if any fields are missing, it returns false. +func (q *LnurlpRequest) IsUmaRequest() bool { + return q.VaspDomain != nil && q.Nonce != nil && q.Signature != nil && q.Timestamp != nil && q.UmaVersion != nil +} + +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 utils.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() + if q.IsUmaRequest() { + queryParams.Add("signature", *q.Signature) + queryParams.Add("vaspDomain", *q.VaspDomain) + queryParams.Add("nonce", *q.Nonce) + isSubjectToTravelRule := *q.IsSubjectToTravelRule + queryParams.Add("isSubjectToTravelRule", strconv.FormatBool(isSubjectToTravelRule)) + queryParams.Add("timestamp", strconv.FormatInt(q.Timestamp.Unix(), 10)) + queryParams.Add("umaVersion", *q.UmaVersion) + } + lnurlpUrl.RawQuery = queryParams.Encode() + return &lnurlpUrl, nil +} + +// UmaLnurlpRequest 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 UmaLnurlpRequest struct { + LnurlpRequest + // 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) SignablePayload() ([]byte, error) { + if q.Timestamp == nil || q.Nonce == nil { + return nil, errors.New("timestamp and nonce are required for signing") + } + payloadString := strings.Join([]string{q.ReceiverAddress, *q.Nonce, strconv.FormatInt(q.Timestamp.Unix(), 10)}, "|") + return []byte(payloadString), nil +} diff --git a/uma/protocol/lnurl_response.go b/uma/protocol/lnurl_response.go new file mode 100644 index 0000000..a7a3626 --- /dev/null +++ b/uma/protocol/lnurl_response.go @@ -0,0 +1,101 @@ +package protocol + +import ( + "strconv" + "strings" +) + +// 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 is the list of currencies that the receiver can quote. See LUD-21. Required for UMA. + Currencies *[]Currency `json:"currencies"` + // RequiredPayerData the data about the payer that the sending VASP must provide in order to send a payment. + RequiredPayerData *CounterPartyDataOptions `json:"payerData"` + // Compliance is compliance-related data from the receiving VASP for UMA. + 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"` + // CommentCharsAllowed is the number of characters that the sender can include in the comment field of the pay request. + CommentCharsAllowed *int `json:"commentAllowed"` + // NostrPubkey is an optional nostr pubkey used for nostr zaps (NIP-57). If set, it should be a valid BIP-340 public + // key in hex format. + NostrPubkey *string `json:"nostrPubkey"` + // AllowsNostr should be set to true if the receiving VASP allows nostr zaps (NIP-57). + AllowsNostr *bool `json:"allowsNostr"` +} + +// 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) IsUmaResponse() bool { + return r.Compliance != nil && r.UmaVersion != nil && r.Currencies != nil && r.RequiredPayerData != nil +} + +func (r *LnurlpResponse) AsUmaResponse() *UmaLnurlpResponse { + if !r.IsUmaResponse() { + return nil + } + return &UmaLnurlpResponse{ + LnurlpResponse: *r, + Currencies: *r.Currencies, + RequiredPayerData: *r.RequiredPayerData, + Compliance: *r.Compliance, + UmaVersion: *r.UmaVersion, + CommentCharsAllowed: r.CommentCharsAllowed, + NostrPubkey: r.NostrPubkey, + AllowsNostr: r.AllowsNostr, + } +} + +// UmaLnurlpResponse is the UMA 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 UmaLnurlpResponse struct { + LnurlpResponse + // Currencies is the list of currencies that the receiver can quote. See LUD-21. Required for UMA. + Currencies []Currency `json:"currencies"` + // RequiredPayerData the data about the payer that the sending VASP must provide in order to send a payment. + RequiredPayerData CounterPartyDataOptions `json:"payerData"` + // Compliance is compliance-related data from the receiving VASP for UMA. + 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"` + // CommentCharsAllowed is the number of characters that the sender can include in the comment field of the pay request. + CommentCharsAllowed *int `json:"commentAllowed"` + // NostrPubkey is an optional nostr pubkey used for nostr zaps (NIP-57). If set, it should be a valid BIP-340 public + // key in hex format. + NostrPubkey *string `json:"nostrPubkey"` + // AllowsNostr should be set to true if the receiving VASP allows nostr zaps (NIP-57). + AllowsNostr *bool `json:"allowsNostr"` +} + +func (r *UmaLnurlpResponse) SignablePayload() []byte { + payloadString := strings.Join([]string{ + r.Compliance.ReceiverIdentifier, + r.Compliance.Nonce, + strconv.FormatInt(r.Compliance.Timestamp, 10), + }, "|") + return []byte(payloadString) +} diff --git a/uma/protocol/pay_request.go b/uma/protocol/pay_request.go new file mode 100644 index 0000000..0087eed --- /dev/null +++ b/uma/protocol/pay_request.go @@ -0,0 +1,215 @@ +package protocol + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strconv" + "strings" +) + +// PayRequest is the request sent by the sender to the receiver to retrieve an invoice. +type PayRequest struct { + // SendingAmountCurrencyCode is the currency code of the `amount` field. `nil` indicates that `amount` is in + // millisatoshis as in LNURL without LUD-21. If this is not `nil`, then `amount` is in the smallest unit of the + // specified currency (e.g. cents for USD). This currency code can be any currency which the receiver can quote. + // However, there are two most common scenarios for UMA: + // + // 1. If the sender wants the receiver wants to receive a specific amount in their receiving + // currency, then this field should be the same as `receiving_currency_code`. This is useful + // for cases where the sender wants to ensure that the receiver receives a specific amount + // in that destination currency, regardless of the exchange rate, for example, when paying + // for some goods or services in a foreign currency. + // + // 2. If the sender has a specific amount in their own currency that they would like to send, + // then this field should be left as `None` to indicate that the amount is in millisatoshis. + // This will lock the sent amount on the sender side, and the receiver will receive the + // equivalent amount in their receiving currency. NOTE: In this scenario, the sending VASP + // *should not* pass the sending currency code here, as it is not relevant to the receiver. + // Rather, by specifying an invoice amount in msats, the sending VASP can ensure that their + // user will be sending a fixed amount, regardless of the exchange rate on the receiving side. + SendingAmountCurrencyCode *string `json:"sendingAmountCurrencyCode"` + // ReceivingCurrencyCode is the ISO 3-digit currency code that the receiver will receive for this payment. Defaults + // to amount being specified in msats if this is not provided. + ReceivingCurrencyCode *string `json:"convert"` + // 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) if `SendingAmountCurrencyCode` is not `nil`. Otherwise, it is the amount in + // millisatoshis. + Amount int64 `json:"amount"` + // PayerData is the data that the sender will send to the receiver to identify themselves. Required for UMA, as is + // the `compliance` field in the `payerData` object. + PayerData *PayerData `json:"payerData"` + // RequestedPayeeData is the data that the sender is requesting about the payee. + RequestedPayeeData *CounterPartyDataOptions `json:"payeeData"` + // Comment is 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`. + Comment *string `json:"comment"` +} + +// IsUmaRequest returns true if the request is a valid UMA request, otherwise, if any fields are missing, it returns false. +func (p *PayRequest) IsUmaRequest() bool { + if p.PayerData == nil { + return false + } + + compliance, err := p.PayerData.Compliance() + if err != nil { + return false + } + + return compliance != nil && p.PayerData.Identifier() != nil +} + +func (p *PayRequest) MarshalJSON() ([]byte, error) { + 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 +} + +func (p *PayRequest) UnmarshalJSON(data []byte) error { + var rawReq map[string]interface{} + err := json.Unmarshal(data, &rawReq) + if err != nil { + return err + } + convert, ok := rawReq["convert"].(string) + if ok { + p.ReceivingCurrencyCode = &convert + } + amount, ok := rawReq["amount"].(string) + if !ok { + return errors.New("missing or invalid amount field") + } + amountParts := strings.Split(amount, ".") + if len(amountParts) > 2 { + return errors.New("invalid amount field") + } + p.Amount, err = strconv.ParseInt(amountParts[0], 10, 64) + if err != nil { + return err + } + if len(amountParts) == 2 && len(amountParts[1]) > 0 { + p.SendingAmountCurrencyCode = &amountParts[1] + } + payerDataJson, ok := rawReq["payerData"].(map[string]interface{}) + if ok { + payerDataJsonBytes, err := json.Marshal(payerDataJson) + if err != nil { + return err + } + var payerData PayerData + err = json.Unmarshal(payerDataJsonBytes, &payerData) + if err != nil { + return err + } + p.PayerData = &payerData + } + payeeDataJson, ok := rawReq["payeeData"].(map[string]interface{}) + if ok { + payeeDataJsonBytes, err := json.Marshal(payeeDataJson) + if err != nil { + return err + } + var payeeData CounterPartyDataOptions + err = json.Unmarshal(payeeDataJsonBytes, &payeeData) + if err != nil { + return err + } + p.RequestedPayeeData = &payeeData + } + comment, ok := rawReq["comment"].(string) + if ok { + p.Comment = &comment + } + return nil +} + +func (p *PayRequest) Encode() ([]byte, error) { + return json.Marshal(p) +} + +func (p *PayRequest) EncodeAsUrlParams() (*url.Values, error) { + jsonBytes, err := json.Marshal(p) + if err != nil { + return nil, err + } + jsonMap := make(map[string]interface{}) + err = json.Unmarshal(jsonBytes, &jsonMap) + if err != nil { + return nil, err + } + payReqParams := url.Values{} + for key, value := range jsonMap { + valueString, ok := value.(string) + if ok { + payReqParams.Add(key, valueString) + } else { + valueBytes, err := json.Marshal(value) + if err != nil { + return nil, err + } + payReqParams.Add(key, string(valueBytes)) + } + } + return &payReqParams, nil +} + +func (p *PayRequest) SignablePayload() ([]byte, error) { + if p.PayerData == nil { + return nil, errors.New("payer data is missing") + } + senderAddress := p.PayerData.Identifier() + if senderAddress == nil || *senderAddress == "" { + return nil, errors.New("payer data identifier is missing") + } + complianceData, err := p.PayerData.Compliance() + if err != nil { + return nil, err + } + if complianceData == nil { + return nil, errors.New("compliance payer data is missing") + } + signatureNonce := complianceData.SignatureNonce + signatureTimestamp := complianceData.SignatureTimestamp + payloadString := strings.Join([]string{ + *senderAddress, + signatureNonce, + strconv.FormatInt(signatureTimestamp, 10), + }, "|") + return []byte(payloadString), nil +} diff --git a/uma/payee_data.go b/uma/protocol/payee_data.go similarity index 80% rename from uma/payee_data.go rename to uma/protocol/payee_data.go index 80175a0..a50d6fd 100644 --- a/uma/payee_data.go +++ b/uma/protocol/payee_data.go @@ -1,6 +1,11 @@ -package uma +package protocol -import "encoding/json" +import ( + "encoding/json" + "errors" + "strconv" + "strings" +) // PayeeData is the data that the payer wants to know about the payee. It can be any json data. type PayeeData map[string]interface{} @@ -53,3 +58,16 @@ func (c *CompliancePayeeData) AsMap() (map[string]interface{}, error) { } return complianceMap, nil } + +func (c *CompliancePayeeData) SignablePayload(payerIdentifier string, payeeIdentifier string) ([]byte, error) { + if c == nil { + return nil, errors.New("compliance data is missing") + } + payloadString := strings.Join([]string{ + payerIdentifier, + payeeIdentifier, + c.SignatureNonce, + strconv.FormatInt(c.SignatureTimestamp, 10), + }, "|") + return []byte(payloadString), nil +} diff --git a/uma/payer_data.go b/uma/protocol/payer_data.go similarity index 99% rename from uma/payer_data.go rename to uma/protocol/payer_data.go index 3102c84..5129ddc 100644 --- a/uma/payer_data.go +++ b/uma/protocol/payer_data.go @@ -1,4 +1,4 @@ -package uma +package protocol import ( "encoding/json" diff --git a/uma/protocol/payreq_response.go b/uma/protocol/payreq_response.go new file mode 100644 index 0000000..4e5dcb5 --- /dev/null +++ b/uma/protocol/payreq_response.go @@ -0,0 +1,64 @@ +package protocol + +// 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"` + // 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"` + // PayeeData The data about the receiver that the sending VASP requested in the payreq request. + // Required for UMA. + PayeeData *PayeeData `json:"payeeData"` + // Disposable This field may be used by a WALLET to decide whether the initial LNURL link will be stored locally + // for later reuse or erased. If disposable is null, it should be interpreted as true, so if SERVICE intends its + // LNURL links to be stored it must return `disposable: false`. UMA should never return `disposable: false` due to + // signature nonce checks, etc. See LUD-11. + 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"` +} + +func (p *PayReqResponse) IsUmaResponse() bool { + if p.PaymentInfo == nil || p.PayeeData == nil { + return false + } + compliance, err := p.PayeeData.Compliance() + if err != nil { + return false + } + return compliance != nil +} + +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 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"` + // 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:"fee"` +} diff --git a/uma/protocol/post_transaction_callback.go b/uma/protocol/post_transaction_callback.go new file mode 100644 index 0000000..f1b29d1 --- /dev/null +++ b/uma/protocol/post_transaction_callback.go @@ -0,0 +1,39 @@ +package protocol + +import ( + "strconv" + "strings" +) + +// PostTransactionCallback is sent between VASPs after the payment is complete. +type PostTransactionCallback struct { + // Utxos is a list of utxo/amounts corresponding to the VASPs channels. + 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"` + // Signature is the base64-encoded signature of sha256(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"` +} + +// 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"` +} + +func (c *PostTransactionCallback) SignablePayload() []byte { + payloadString := strings.Join([]string{ + c.Nonce, + strconv.FormatInt(c.Timestamp, 10), + }, "|") + return []byte(payloadString) +} diff --git a/uma/protocol/pub_key_response.go b/uma/protocol/pub_key_response.go new file mode 100644 index 0000000..ba02f55 --- /dev/null +++ b/uma/protocol/pub_key_response.go @@ -0,0 +1,23 @@ +package protocol + +import "encoding/hex" + +// 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) +} diff --git a/uma/public_key_cache.go b/uma/public_key_cache.go index 2dd46a7..6dbec2b 100644 --- a/uma/public_key_cache.go +++ b/uma/public_key_cache.go @@ -1,16 +1,19 @@ package uma -import "time" +import ( + "github.com/uma-universal-money-address/uma-go-sdk/uma/protocol" + "time" +) // PublicKeyCache is an interface for a cache of public keys for other VASPs. // // Implementations of this interface should be thread-safe. type PublicKeyCache interface { // FetchPublicKeyForVasp fetches the public key entry for a VASP if in the cache, otherwise returns nil. - FetchPublicKeyForVasp(vaspDomain string) *PubKeyResponse + FetchPublicKeyForVasp(vaspDomain string) *protocol.PubKeyResponse // AddPublicKeyForVasp adds a public key entry for a VASP to the cache. - AddPublicKeyForVasp(vaspDomain string, pubKey *PubKeyResponse) + AddPublicKeyForVasp(vaspDomain string, pubKey *protocol.PubKeyResponse) // RemovePublicKeyForVasp removes a public key for a VASP from the cache. RemovePublicKeyForVasp(vaspDomain string) @@ -20,16 +23,16 @@ type PublicKeyCache interface { } type InMemoryPublicKeyCache struct { - cache map[string]*PubKeyResponse + cache map[string]*protocol.PubKeyResponse } func NewInMemoryPublicKeyCache() *InMemoryPublicKeyCache { return &InMemoryPublicKeyCache{ - cache: make(map[string]*PubKeyResponse), + cache: make(map[string]*protocol.PubKeyResponse), } } -func (c *InMemoryPublicKeyCache) FetchPublicKeyForVasp(vaspDomain string) *PubKeyResponse { +func (c *InMemoryPublicKeyCache) FetchPublicKeyForVasp(vaspDomain string) *protocol.PubKeyResponse { entry := c.cache[vaspDomain] if entry == nil || (entry.ExpirationTimestamp != nil && time.Unix(*entry.ExpirationTimestamp, 0).Before(time.Now())) { return nil @@ -37,7 +40,7 @@ func (c *InMemoryPublicKeyCache) FetchPublicKeyForVasp(vaspDomain string) *PubKe return entry } -func (c *InMemoryPublicKeyCache) AddPublicKeyForVasp(vaspDomain string, pubKey *PubKeyResponse) { +func (c *InMemoryPublicKeyCache) AddPublicKeyForVasp(vaspDomain string, pubKey *protocol.PubKeyResponse) { c.cache[vaspDomain] = pubKey } @@ -46,5 +49,5 @@ func (c *InMemoryPublicKeyCache) RemovePublicKeyForVasp(vaspDomain string) { } func (c *InMemoryPublicKeyCache) Clear() { - c.cache = make(map[string]*PubKeyResponse) + c.cache = make(map[string]*protocol.PubKeyResponse) } diff --git a/uma/test/uma_test.go b/uma/test/uma_test.go index 8b67206..c7878f1 100644 --- a/uma/test/uma_test.go +++ b/uma/test/uma_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uma-universal-money-address/uma-go-sdk/uma" + "github.com/uma-universal-money-address/uma-go-sdk/uma/protocol" "math" "net/url" "strconv" @@ -151,7 +152,7 @@ func TestSignAndVerifyLnurlpResponse(t *testing.T) { metadata, err := createMetadataForBob() require.NoError(t, err) isSubjectToTravelRule := true - kycStatus := uma.KycStatusVerified + kycStatus := protocol.KycStatusVerified response, err := uma.GetLnurlpResponse( request, "https://vasp2.com/api/lnurl/payreq/$bob", @@ -160,18 +161,18 @@ func TestSignAndVerifyLnurlpResponse(t *testing.T) { 10_000_000, &serializedPrivateKey, &isSubjectToTravelRule, - &uma.CounterPartyDataOptions{ - "name": uma.CounterPartyDataOption{Mandatory: false}, - "email": uma.CounterPartyDataOption{Mandatory: false}, - "compliance": uma.CounterPartyDataOption{Mandatory: true}, + &protocol.CounterPartyDataOptions{ + "name": protocol.CounterPartyDataOption{Mandatory: false}, + "email": protocol.CounterPartyDataOption{Mandatory: false}, + "compliance": protocol.CounterPartyDataOption{Mandatory: true}, }, - &[]uma.Currency{ + &[]protocol.Currency{ { Code: "USD", Name: "US Dollar", Symbol: "$", MillisatoshiPerUnit: 34_150, - Convertible: uma.ConvertibleCurrency{ + Convertible: protocol.ConvertibleCurrency{ MinSendable: 1, MaxSendable: 10_000_000, }, @@ -200,7 +201,7 @@ func TestPayReqCreationAndParsing(t *testing.T) { trInfo := "some TR info for VASP2" ivmsVersion := "101.1" - trFormat := uma.TravelRuleFormat{ + trFormat := protocol.TravelRuleFormat{ Type: "IVMS", Version: &ivmsVersion, } @@ -215,7 +216,7 @@ func TestPayReqCreationAndParsing(t *testing.T) { nil, &trInfo, &trFormat, - uma.KycStatusVerified, + protocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -263,7 +264,7 @@ func TestMsatsPayReqCreationAndParsing(t *testing.T) { trInfo := "some TR info for VASP2" ivmsVersion := "101.1" - trFormat := uma.TravelRuleFormat{ + trFormat := protocol.TravelRuleFormat{ Type: "IVMS", Version: &ivmsVersion, } @@ -278,7 +279,7 @@ func TestMsatsPayReqCreationAndParsing(t *testing.T) { nil, &trInfo, &trFormat, - uma.KycStatusVerified, + protocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -318,11 +319,11 @@ func TestPayReqResponseAndParsing(t *testing.T) { require.NoError(t, err) trInfo := "some TR info for VASP2" - payeeOptions := uma.CounterPartyDataOptions{ - "identifier": uma.CounterPartyDataOption{ + payeeOptions := protocol.CounterPartyDataOptions{ + "identifier": protocol.CounterPartyDataOption{ Mandatory: true, }, - "name": uma.CounterPartyDataOption{ + "name": protocol.CounterPartyDataOption{ Mandatory: false, }, } @@ -337,7 +338,7 @@ func TestPayReqResponseAndParsing(t *testing.T) { nil, &trInfo, nil, - uma.KycStatusVerified, + protocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -348,7 +349,7 @@ func TestPayReqResponseAndParsing(t *testing.T) { client := &FakeInvoiceCreator{} metadata, err := createMetadataForBob() require.NoError(t, err) - payeeData := uma.PayeeData{ + payeeData := protocol.PayeeData{ "identifier": "$bob@vasp2.com", } receivingCurrencyCode := "USD" @@ -409,11 +410,11 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { require.NoError(t, err) trInfo := "some TR info for VASP2" - payeeOptions := uma.CounterPartyDataOptions{ - "identifier": uma.CounterPartyDataOption{ + payeeOptions := protocol.CounterPartyDataOptions{ + "identifier": protocol.CounterPartyDataOption{ Mandatory: true, }, - "name": uma.CounterPartyDataOption{ + "name": protocol.CounterPartyDataOption{ Mandatory: false, }, } @@ -428,7 +429,7 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { nil, &trInfo, nil, - uma.KycStatusVerified, + protocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -439,7 +440,7 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { client := &FakeInvoiceCreator{} metadata, err := createMetadataForBob() require.NoError(t, err) - payeeData := uma.PayeeData{ + payeeData := protocol.PayeeData{ "identifier": "$bob@vasp2.com", } receivingCurrencyCode := "USD" @@ -518,16 +519,16 @@ func TestParsePayReqFromQueryParamsNoOptionalFields(t *testing.T) { func TestParsePayReqFromQueryParamsAllOptionalFields(t *testing.T) { amount := "1000.USD" - payerData := uma.PayerData{ + payerData := protocol.PayerData{ "identifier": "$bob@vasp.com", } encodedPayerData, err := json.Marshal(payerData) require.NoError(t, err) - payeeData := uma.CounterPartyDataOptions{ - "identifier": uma.CounterPartyDataOption{ + payeeData := protocol.CounterPartyDataOptions{ + "identifier": protocol.CounterPartyDataOption{ Mandatory: true, }, - "name": uma.CounterPartyDataOption{ + "name": protocol.CounterPartyDataOption{ Mandatory: false, }, } @@ -552,16 +553,16 @@ func TestParsePayReqFromQueryParamsAllOptionalFields(t *testing.T) { func TestParseAndEncodePayReqToQueryParams(t *testing.T) { amount := "1000.USD" - payerData := uma.PayerData{ + payerData := protocol.PayerData{ "identifier": "$bob@vasp.com", } encodedPayerData, err := json.Marshal(payerData) require.NoError(t, err) - payeeData := uma.CounterPartyDataOptions{ - "identifier": uma.CounterPartyDataOption{ + payeeData := protocol.CounterPartyDataOptions{ + "identifier": protocol.CounterPartyDataOption{ Mandatory: true, }, - "name": uma.CounterPartyDataOption{ + "name": protocol.CounterPartyDataOption{ Mandatory: false, }, } diff --git a/uma/uma.go b/uma/uma.go index 0b4f75f..d8f80a0 100644 --- a/uma/uma.go +++ b/uma/uma.go @@ -9,6 +9,8 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" eciesgo "github.com/ecies/go/v2" + "github.com/uma-universal-money-address/uma-go-sdk/uma/protocol" + "github.com/uma-universal-money-address/uma-go-sdk/uma/utils" "io" "math" "math/big" @@ -31,14 +33,14 @@ import ( // // vaspDomain: the domain of the VASP. // cache: the PublicKeyCache cache to use. You can use the InMemoryPublicKeyCache struct, or implement your own persistent cache with any storage type. -func FetchPublicKeyForVasp(vaspDomain string, cache PublicKeyCache) (*PubKeyResponse, error) { +func FetchPublicKeyForVasp(vaspDomain string, cache PublicKeyCache) (*protocol.PubKeyResponse, error) { publicKey := cache.FetchPublicKeyForVasp(vaspDomain) if publicKey != nil { return publicKey, nil } scheme := "https://" - if IsDomainLocalhost(vaspDomain) { + if utils.IsDomainLocalhost(vaspDomain) { scheme = "http://" } resp, err := http.Get(scheme + vaspDomain + "/.well-known/lnurlpubkey") @@ -62,7 +64,7 @@ func FetchPublicKeyForVasp(vaspDomain string, cache PublicKeyCache) (*PubKeyResp return nil, err } - var pubKeyResponse PubKeyResponse + var pubKeyResponse protocol.PubKeyResponse err = json.Unmarshal(responseBodyBytes, &pubKeyResponse) if err != nil { return nil, err @@ -105,7 +107,7 @@ func signPayload(payload []byte, privateKeyBytes []byte) (*string, error) { // query: the signed query to verify. // otherVaspPubKey: the bytes of the signing public key of the VASP making this request. // nonceCache: the NonceCache cache to use to prevent replay attacks. -func VerifyPayReqSignature(query *PayRequest, otherVaspPubKey []byte, nonceCache NonceCache) error { +func VerifyPayReqSignature(query *protocol.PayRequest, otherVaspPubKey []byte, nonceCache NonceCache) error { complianceData, err := query.PayerData.Compliance() if err != nil { return err @@ -120,7 +122,7 @@ func VerifyPayReqSignature(query *PayRequest, otherVaspPubKey []byte, nonceCache if err != nil { return err } - signablePayload, err := query.signablePayload() + signablePayload, err := query.SignablePayload() if err != nil { return err } @@ -187,7 +189,7 @@ func GetSignedLnurlpRequestUrl( umaVersion = *umaVersionOverride } now := time.Now() - unsignedRequest := LnurlpRequest{ + unsignedRequest := protocol.LnurlpRequest{ ReceiverAddress: receiverAddress, IsSubjectToTravelRule: &isSubjectToTravelRule, VaspDomain: &senderVaspDomain, @@ -195,7 +197,7 @@ func GetSignedLnurlpRequestUrl( Nonce: nonce, UmaVersion: &umaVersion, } - signablePayload, err := unsignedRequest.signablePayload() + signablePayload, err := unsignedRequest.SignablePayload() if err != nil { return nil, err } @@ -219,7 +221,7 @@ func IsUmaLnurlpQuery(url url.URL) bool { // Args: // // url: the full URL of the uma request. -func ParseLnurlpRequest(url url.URL) (*LnurlpRequest, error) { +func ParseLnurlpRequest(url url.URL) (*protocol.LnurlpRequest, error) { query := url.Query() signature := query.Get("signature") vaspDomain := query.Get("vaspDomain") @@ -258,7 +260,7 @@ func ParseLnurlpRequest(url url.URL) (*LnurlpRequest, error) { return &s } - return &LnurlpRequest{ + return &protocol.LnurlpRequest{ ReceiverAddress: receiverAddress, VaspDomain: nilIfEmpty(vaspDomain), UmaVersion: nilIfEmpty(umaVersion), @@ -276,12 +278,12 @@ func ParseLnurlpRequest(url url.URL) (*LnurlpRequest, error) { // query: the signed query to verify. // otherVaspSigningPubKey: the public key of the VASP making this request in bytes. // nonceCache: the NonceCache cache to use to prevent replay attacks. -func VerifyUmaLnurlpQuerySignature(query UmaLnurlpRequest, otherVaspSigningPubKey []byte, nonceCache NonceCache) error { +func VerifyUmaLnurlpQuerySignature(query protocol.UmaLnurlpRequest, otherVaspSigningPubKey []byte, nonceCache NonceCache) error { err := nonceCache.CheckAndSaveNonce(query.Nonce, query.Timestamp) if err != nil { return err } - signablePayload, err := query.signablePayload() + signablePayload, err := query.SignablePayload() if err != nil { return err } @@ -289,21 +291,21 @@ func VerifyUmaLnurlpQuerySignature(query UmaLnurlpRequest, otherVaspSigningPubKe } func GetLnurlpResponse( - request LnurlpRequest, + request protocol.LnurlpRequest, callback string, encodedMetadata string, minSendableSats int64, maxSendableSats int64, privateKeyBytes *[]byte, requiresTravelRuleInfo *bool, - payerDataOptions *CounterPartyDataOptions, - currencyOptions *[]Currency, - receiverKycStatus *KycStatus, + payerDataOptions *protocol.CounterPartyDataOptions, + currencyOptions *[]protocol.Currency, + receiverKycStatus *protocol.KycStatus, commentCharsAllowed *int, nostrPubkey *string, -) (*LnurlpResponse, error) { +) (*protocol.LnurlpResponse, error) { isUmaRequest := request.IsUmaRequest() - var complianceResponse *LnurlComplianceResponse + var complianceResponse *protocol.LnurlComplianceResponse var umaVersion *string if isUmaRequest { @@ -332,18 +334,18 @@ func GetLnurlpResponse( // UMA always requires compliance and identifier fields: if payerDataOptions == nil { - payerDataOptions = &CounterPartyDataOptions{} + payerDataOptions = &protocol.CounterPartyDataOptions{} } - (*payerDataOptions)[CounterPartyDataFieldCompliance.String()] = CounterPartyDataOption{Mandatory: true} - (*payerDataOptions)[CounterPartyDataFieldIdentifier.String()] = CounterPartyDataOption{Mandatory: true} + (*payerDataOptions)[protocol.CounterPartyDataFieldCompliance.String()] = protocol.CounterPartyDataOption{Mandatory: true} + (*payerDataOptions)[protocol.CounterPartyDataFieldIdentifier.String()] = protocol.CounterPartyDataOption{Mandatory: true} } var allowsNostr *bool = nil if nostrPubkey != nil { *allowsNostr = true } - return &LnurlpResponse{ - Tag: "payRequest", + return &protocol.LnurlpResponse{ + Tag: "protocol.PayRequest", Callback: callback, MinSendable: minSendableSats * 1000, MaxSendable: maxSendableSats * 1000, @@ -359,11 +361,11 @@ func GetLnurlpResponse( } func getSignedLnurlpComplianceResponse( - query LnurlpRequest, + query protocol.LnurlpRequest, privateKeyBytes []byte, isSubjectToTravelRule bool, - receiverKycStatus KycStatus, -) (*LnurlComplianceResponse, error) { + receiverKycStatus protocol.KycStatus, +) (*protocol.LnurlComplianceResponse, error) { timestamp := time.Now().Unix() nonce, err := GenerateNonce() if err != nil { @@ -374,7 +376,7 @@ func getSignedLnurlpComplianceResponse( if err != nil { return nil, err } - return &LnurlComplianceResponse{ + return &protocol.LnurlComplianceResponse{ KycStatus: receiverKycStatus, Signature: *signature, Nonce: *nonce, @@ -391,16 +393,16 @@ func getSignedLnurlpComplianceResponse( // response: the signed response to verify. // otherVaspSigningPubKey: the public key of the VASP making this request in bytes. // nonceCache: the NonceCache cache to use to prevent replay attacks. -func VerifyUmaLnurlpResponseSignature(response UmaLnurlpResponse, otherVaspSigningPubKey []byte, nonceCache NonceCache) error { +func VerifyUmaLnurlpResponseSignature(response protocol.UmaLnurlpResponse, otherVaspSigningPubKey []byte, nonceCache NonceCache) error { err := nonceCache.CheckAndSaveNonce(response.Compliance.Nonce, time.Unix(response.Compliance.Timestamp, 0)) if err != nil { return err } - return verifySignature(response.signablePayload(), response.Compliance.Signature, otherVaspSigningPubKey) + return verifySignature(response.SignablePayload(), response.Compliance.Signature, otherVaspSigningPubKey) } -func ParseLnurlpResponse(bytes []byte) (*LnurlpResponse, error) { - var response LnurlpResponse +func ParseLnurlpResponse(bytes []byte) (*protocol.LnurlpResponse, error) { + var response protocol.LnurlpResponse err := json.Unmarshal(bytes, &response) if err != nil { return nil, err @@ -417,7 +419,7 @@ func GetVaspDomainFromUmaAddress(umaAddress string) (string, error) { return addressParts[1], nil } -// GetUmaPayRequest Creates a signed UMA pay request. For non-UMA LNURL requests, just construct a PayRequest directly. +// GetUmaPayRequest Creates a signed UMA pay request. For non-UMA LNURL requests, just construct a protocol.PayRequest directly. // // Args: // @@ -453,14 +455,14 @@ func GetUmaPayRequest( payerName *string, payerEmail *string, trInfo *string, - trInfoFormat *TravelRuleFormat, - payerKycStatus KycStatus, + trInfoFormat *protocol.TravelRuleFormat, + payerKycStatus protocol.KycStatus, payerUtxos *[]string, payerNodePubKey *string, utxoCallback string, - requestedPayeeData *CounterPartyDataOptions, + requestedPayeeData *protocol.CounterPartyDataOptions, comment *string, -) (*PayRequest, error) { +) (*protocol.PayRequest, error) { complianceData, err := getSignedCompliancePayerData( receiverEncryptionPubKey, sendingVaspPrivateKey, @@ -476,11 +478,11 @@ func GetUmaPayRequest( return nil, err } if requestedPayeeData == nil { - requestedPayeeData = &CounterPartyDataOptions{} + requestedPayeeData = &protocol.CounterPartyDataOptions{} } // UMA always requires compliance and identifier fields: - (*requestedPayeeData)[CounterPartyDataFieldCompliance.String()] = CounterPartyDataOption{Mandatory: true} - (*requestedPayeeData)[CounterPartyDataFieldIdentifier.String()] = CounterPartyDataOption{Mandatory: true} + (*requestedPayeeData)[protocol.CounterPartyDataFieldCompliance.String()] = protocol.CounterPartyDataOption{Mandatory: true} + (*requestedPayeeData)[protocol.CounterPartyDataFieldIdentifier.String()] = protocol.CounterPartyDataOption{Mandatory: true} sendingAmountCurrencyCode := &receivingCurrencyCode if !isAmountInReceivingCurrency { @@ -492,15 +494,15 @@ func GetUmaPayRequest( return nil, err } - return &PayRequest{ + return &protocol.PayRequest{ SendingAmountCurrencyCode: sendingAmountCurrencyCode, ReceivingCurrencyCode: &receivingCurrencyCode, Amount: amount, - PayerData: &PayerData{ - CounterPartyDataFieldName.String(): payerName, - CounterPartyDataFieldEmail.String(): payerEmail, - CounterPartyDataFieldIdentifier.String(): payerIdentifier, - CounterPartyDataFieldCompliance.String(): complianceDataMap, + PayerData: &protocol.PayerData{ + protocol.CounterPartyDataFieldName.String(): payerName, + protocol.CounterPartyDataFieldEmail.String(): payerEmail, + protocol.CounterPartyDataFieldIdentifier.String(): payerIdentifier, + protocol.CounterPartyDataFieldCompliance.String(): complianceDataMap, }, RequestedPayeeData: requestedPayeeData, Comment: comment, @@ -512,12 +514,12 @@ func getSignedCompliancePayerData( sendingVaspPrivateKeyBytes []byte, payerIdentifier string, trInfo *string, - trInfoFormat *TravelRuleFormat, - payerKycStatus KycStatus, + trInfoFormat *protocol.TravelRuleFormat, + payerKycStatus protocol.KycStatus, payerUtxos *[]string, payerNodePubKey *string, utxoCallback string, -) (*CompliancePayerData, error) { +) (*protocol.CompliancePayerData, error) { timestamp := time.Now().Unix() nonce, err := GenerateNonce() if err != nil { @@ -536,7 +538,7 @@ func getSignedCompliancePayerData( return nil, err } - return &CompliancePayerData{ + return &protocol.CompliancePayerData{ EncryptedTravelRuleInfo: encryptedTrInfo, TravelRuleFormat: trInfoFormat, KycStatus: payerKycStatus, @@ -564,8 +566,8 @@ func encryptTrInfo(trInfo string, receiverEncryptionPubKey []byte) (*string, err return &encryptedTrInfoHex, nil } -func ParsePayRequest(bytes []byte) (*PayRequest, error) { - var response PayRequest +func ParsePayRequest(bytes []byte) (*protocol.PayRequest, error) { + var response protocol.PayRequest err := json.Unmarshal(bytes, &response) if err != nil { return nil, err @@ -576,7 +578,7 @@ func ParsePayRequest(bytes []byte) (*PayRequest, error) { // 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) { +func ParsePayRequestFromQueryParams(query url.Values) (*protocol.PayRequest, error) { amountStr := query.Get("amount") if amountStr == "" { return nil, errors.New("missing amount") @@ -600,7 +602,7 @@ func ParsePayRequestFromQueryParams(query url.Values) (*PayRequest, error) { } payerData := query.Get("payerData") - var payerDataObj *PayerData + var payerDataObj *protocol.PayerData if payerData != "" { err = json.Unmarshal([]byte(payerData), &payerDataObj) if err != nil { @@ -608,7 +610,7 @@ func ParsePayRequestFromQueryParams(query url.Values) (*PayRequest, error) { } } requestedPayeeData := query.Get("payeeData") - var requestedPayeeDataObj *CounterPartyDataOptions + var requestedPayeeDataObj *protocol.CounterPartyDataOptions if requestedPayeeData != "" { err = json.Unmarshal([]byte(requestedPayeeData), &requestedPayeeDataObj) if err != nil { @@ -620,7 +622,7 @@ func ParsePayRequestFromQueryParams(query url.Values) (*PayRequest, error) { if commentParam != "" { comment = &commentParam } - return &PayRequest{ + return &protocol.PayRequest{ SendingAmountCurrencyCode: sendingAmountCurrencyCode, ReceivingCurrencyCode: receivingCurrencyCode, Amount: amount, @@ -665,7 +667,7 @@ type InvoiceCreator interface { // `disposable: false`. See LUD-11. // successAction: an optional action that the wallet should take once the payment is complete. See LUD-09. func GetPayReqResponse( - request PayRequest, + request protocol.PayRequest, invoiceCreator InvoiceCreator, metadata string, receivingCurrencyCode *string, @@ -675,12 +677,12 @@ func GetPayReqResponse( receiverChannelUtxos *[]string, receiverNodePubKey *string, utxoCallback *string, - payeeData *PayeeData, + payeeData *protocol.PayeeData, receivingVaspPrivateKey *[]byte, payeeIdentifier *string, disposable *bool, successAction *map[string]string, -) (*PayReqResponse, error) { +) (*protocol.PayReqResponse, error) { if request.SendingAmountCurrencyCode != nil && *request.SendingAmountCurrencyCode != *receivingCurrencyCode { return nil, errors.New("the sdk only supports sending in either SAT or the receiving currency") } @@ -717,7 +719,7 @@ func GetPayReqResponse( if err != nil { return nil, err } - var complianceData *CompliancePayeeData + var complianceData *protocol.CompliancePayeeData if request.IsUmaRequest() { err = validateUmaPayReqFields( receivingCurrencyCode, @@ -750,22 +752,22 @@ func GetPayReqResponse( return nil, err } if payeeData == nil { - payeeData = &PayeeData{ - CounterPartyDataFieldIdentifier.String(): *payeeIdentifier, + payeeData = &protocol.PayeeData{ + protocol.CounterPartyDataFieldIdentifier.String(): *payeeIdentifier, } } - if existingCompliance := (*payeeData)[CounterPartyDataFieldCompliance.String()]; existingCompliance == nil { + if existingCompliance := (*payeeData)[protocol.CounterPartyDataFieldCompliance.String()]; existingCompliance == nil { complianceDataAsMap, err := complianceData.AsMap() if err != nil { return nil, err } - (*payeeData)[CounterPartyDataFieldCompliance.String()] = complianceDataAsMap + (*payeeData)[protocol.CounterPartyDataFieldCompliance.String()] = complianceDataAsMap } } - var paymentInfo *PayReqResponsePaymentInfo + var paymentInfo *protocol.PayReqResponsePaymentInfo if receivingCurrencyCode != nil { - paymentInfo = &PayReqResponsePaymentInfo{ + paymentInfo = &protocol.PayReqResponsePaymentInfo{ Amount: receivingCurrencyAmount, CurrencyCode: *receivingCurrencyCode, Multiplier: *conversionRate, @@ -773,9 +775,9 @@ func GetPayReqResponse( ExchangeFeesMillisatoshi: *receiverFeesMillisats, } } - return &PayReqResponse{ + return &protocol.PayReqResponse{ EncodedInvoice: *encodedInvoice, - Routes: []Route{}, + Routes: []protocol.Route{}, PaymentInfo: paymentInfo, PayeeData: payeeData, Disposable: disposable, @@ -843,13 +845,13 @@ func getSignedCompliancePayeeData( receiverChannelUtxos []string, receiverNodePubKey *string, utxoCallback *string, -) (*CompliancePayeeData, error) { +) (*protocol.CompliancePayeeData, error) { timestamp := time.Now().Unix() nonce, err := GenerateNonce() if err != nil { return nil, err } - complianceData := CompliancePayeeData{ + complianceData := protocol.CompliancePayeeData{ Utxos: receiverChannelUtxos, NodePubKey: receiverNodePubKey, UtxoCallback: utxoCallback, @@ -857,7 +859,7 @@ func getSignedCompliancePayeeData( SignatureNonce: *nonce, SignatureTimestamp: timestamp, } - payloadString, err := complianceData.signablePayload(payerIdentifier, payeeIdentifier) + payloadString, err := complianceData.SignablePayload(payerIdentifier, payeeIdentifier) if err != nil { return nil, err } @@ -870,8 +872,8 @@ func getSignedCompliancePayeeData( } // ParsePayReqResponse Parses the uma pay request response from a raw response body. -func ParsePayReqResponse(bytes []byte) (*PayReqResponse, error) { - var response PayReqResponse +func ParsePayReqResponse(bytes []byte) (*protocol.PayReqResponse, error) { + var response protocol.PayReqResponse err := json.Unmarshal(bytes, &response) if err != nil { return nil, err @@ -890,7 +892,7 @@ func ParsePayReqResponse(bytes []byte) (*PayReqResponse, error) { // payerIdentifier: the identifier of the sender. For example, $alice@vasp1.com // payeeIdentifier: the identifier of the receiver. For example, $bob@vasp2.com func VerifyPayReqResponseSignature( - response *PayReqResponse, + response *protocol.PayReqResponse, otherVaspPubKey []byte, nonceCache NonceCache, payerIdentifier string, @@ -910,7 +912,7 @@ func VerifyPayReqResponseSignature( if err != nil { return err } - signablePayload, err := complianceData.signablePayload(payerIdentifier, payeeIdentifier) + signablePayload, err := complianceData.SignablePayload(payerIdentifier, payeeIdentifier) if err != nil { return err } @@ -925,21 +927,21 @@ func VerifyPayReqResponseSignature( // vaspDomain: the domain of the VASP initiating the callback. // signingPrivateKey: the private key of the VASP initiating the callback. This will be used to sign the request. func GetPostTransactionCallback( - utxos []UtxoWithAmount, + utxos []protocol.UtxoWithAmount, vaspDomain string, signingPrivateKey []byte, -) (*PostTransactionCallback, error) { +) (*protocol.PostTransactionCallback, error) { nonce, err := GenerateNonce() if err != nil { return nil, err } - unsignedCallback := PostTransactionCallback{ + unsignedCallback := protocol.PostTransactionCallback{ Utxos: utxos, VaspDomain: vaspDomain, Timestamp: time.Now().Unix(), Nonce: *nonce, } - signature, err := signPayload(unsignedCallback.signablePayload(), signingPrivateKey) + signature, err := signPayload(unsignedCallback.SignablePayload(), signingPrivateKey) if err != nil { return nil, err } @@ -947,8 +949,8 @@ func GetPostTransactionCallback( return &unsignedCallback, nil } -func ParsePostTransactionCallback(bytes []byte) (*PostTransactionCallback, error) { - var callback PostTransactionCallback +func ParsePostTransactionCallback(bytes []byte) (*protocol.PostTransactionCallback, error) { + var callback protocol.PostTransactionCallback err := json.Unmarshal(bytes, &callback) if err != nil { return nil, err @@ -965,7 +967,7 @@ func ParsePostTransactionCallback(bytes []byte) (*PostTransactionCallback, error // otherVaspPubKey: the bytes of the signing public key of the VASP making this request. // nonceCache: the NonceCache cache to use to prevent replay attacks. func VerifyPostTransactionCallbackSignature( - callback *PostTransactionCallback, + callback *protocol.PostTransactionCallback, otherVaspPubKey []byte, nonceCache NonceCache, ) error { @@ -973,6 +975,6 @@ func VerifyPostTransactionCallbackSignature( if err != nil { return err } - signablePayload := callback.signablePayload() + signablePayload := callback.SignablePayload() return verifySignature(signablePayload, callback.Signature, otherVaspPubKey) } diff --git a/uma/url_utils.go b/uma/utils/url_utils.go similarity index 95% rename from uma/url_utils.go rename to uma/utils/url_utils.go index 3f91b0e..3c10f03 100644 --- a/uma/url_utils.go +++ b/uma/utils/url_utils.go @@ -1,4 +1,4 @@ -package uma +package utils import "strings" From bcde4d223cbf39cf1bcc082e8542d7e95ef69c94 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Thu, 14 Mar 2024 08:49:02 -0700 Subject: [PATCH 2/3] fixtest --- uma/test/uma_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uma/test/uma_test.go b/uma/test/uma_test.go index c7878f1..2ac6065 100644 --- a/uma/test/uma_test.go +++ b/uma/test/uma_test.go @@ -24,7 +24,7 @@ func TestParse(t *testing.T) { nonce := "12345" vaspDomain := "vasp1.com" umaVersion := "1.0" - expectedQuery := uma.LnurlpRequest{ + expectedQuery := protocol.LnurlpRequest{ ReceiverAddress: "bob@vasp2.com", Signature: &signature, IsSubjectToTravelRule: &isSubjectToTravelRule, @@ -493,7 +493,7 @@ func TestSignAndVerifyPostTransactionCallback(t *testing.T) { signingPrivateKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) callback, err := uma.GetPostTransactionCallback( - []uma.UtxoWithAmount{{Utxo: "abcdef12345", Amount: 1000}}, + []protocol.UtxoWithAmount{{Utxo: "abcdef12345", Amount: 1000}}, "my-vasp.com", signingPrivateKey.Serialize(), ) @@ -585,7 +585,7 @@ func TestParseAndEncodePayReqToQueryParams(t *testing.T) { require.Equal(t, payreq, payreqReparsed) } -func createLnurlpRequest(t *testing.T, signingPrivateKey []byte) uma.LnurlpRequest { +func createLnurlpRequest(t *testing.T, signingPrivateKey []byte) protocol.LnurlpRequest { queryUrl, err := uma.GetSignedLnurlpRequestUrl(signingPrivateKey, "$bob@vasp2.com", "vasp1.com", true, nil) require.NoError(t, err) query, err := uma.ParseLnurlpRequest(*queryUrl) From 1e39ed2fc5da75f1e363a5ca1434d16235aa5528 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Thu, 14 Mar 2024 10:35:55 -0700 Subject: [PATCH 3/3] alias --- uma/test/uma_test.go | 66 ++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/uma/test/uma_test.go b/uma/test/uma_test.go index 2ac6065..61a6c61 100644 --- a/uma/test/uma_test.go +++ b/uma/test/uma_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uma-universal-money-address/uma-go-sdk/uma" - "github.com/uma-universal-money-address/uma-go-sdk/uma/protocol" + umaprotocol "github.com/uma-universal-money-address/uma-go-sdk/uma/protocol" "math" "net/url" "strconv" @@ -24,7 +24,7 @@ func TestParse(t *testing.T) { nonce := "12345" vaspDomain := "vasp1.com" umaVersion := "1.0" - expectedQuery := protocol.LnurlpRequest{ + expectedQuery := umaprotocol.LnurlpRequest{ ReceiverAddress: "bob@vasp2.com", Signature: &signature, IsSubjectToTravelRule: &isSubjectToTravelRule, @@ -152,7 +152,7 @@ func TestSignAndVerifyLnurlpResponse(t *testing.T) { metadata, err := createMetadataForBob() require.NoError(t, err) isSubjectToTravelRule := true - kycStatus := protocol.KycStatusVerified + kycStatus := umaprotocol.KycStatusVerified response, err := uma.GetLnurlpResponse( request, "https://vasp2.com/api/lnurl/payreq/$bob", @@ -161,18 +161,18 @@ func TestSignAndVerifyLnurlpResponse(t *testing.T) { 10_000_000, &serializedPrivateKey, &isSubjectToTravelRule, - &protocol.CounterPartyDataOptions{ - "name": protocol.CounterPartyDataOption{Mandatory: false}, - "email": protocol.CounterPartyDataOption{Mandatory: false}, - "compliance": protocol.CounterPartyDataOption{Mandatory: true}, + &umaprotocol.CounterPartyDataOptions{ + "name": umaprotocol.CounterPartyDataOption{Mandatory: false}, + "email": umaprotocol.CounterPartyDataOption{Mandatory: false}, + "compliance": umaprotocol.CounterPartyDataOption{Mandatory: true}, }, - &[]protocol.Currency{ + &[]umaprotocol.Currency{ { Code: "USD", Name: "US Dollar", Symbol: "$", MillisatoshiPerUnit: 34_150, - Convertible: protocol.ConvertibleCurrency{ + Convertible: umaprotocol.ConvertibleCurrency{ MinSendable: 1, MaxSendable: 10_000_000, }, @@ -201,7 +201,7 @@ func TestPayReqCreationAndParsing(t *testing.T) { trInfo := "some TR info for VASP2" ivmsVersion := "101.1" - trFormat := protocol.TravelRuleFormat{ + trFormat := umaprotocol.TravelRuleFormat{ Type: "IVMS", Version: &ivmsVersion, } @@ -216,7 +216,7 @@ func TestPayReqCreationAndParsing(t *testing.T) { nil, &trInfo, &trFormat, - protocol.KycStatusVerified, + umaprotocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -264,7 +264,7 @@ func TestMsatsPayReqCreationAndParsing(t *testing.T) { trInfo := "some TR info for VASP2" ivmsVersion := "101.1" - trFormat := protocol.TravelRuleFormat{ + trFormat := umaprotocol.TravelRuleFormat{ Type: "IVMS", Version: &ivmsVersion, } @@ -279,7 +279,7 @@ func TestMsatsPayReqCreationAndParsing(t *testing.T) { nil, &trInfo, &trFormat, - protocol.KycStatusVerified, + umaprotocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -319,11 +319,11 @@ func TestPayReqResponseAndParsing(t *testing.T) { require.NoError(t, err) trInfo := "some TR info for VASP2" - payeeOptions := protocol.CounterPartyDataOptions{ - "identifier": protocol.CounterPartyDataOption{ + payeeOptions := umaprotocol.CounterPartyDataOptions{ + "identifier": umaprotocol.CounterPartyDataOption{ Mandatory: true, }, - "name": protocol.CounterPartyDataOption{ + "name": umaprotocol.CounterPartyDataOption{ Mandatory: false, }, } @@ -338,7 +338,7 @@ func TestPayReqResponseAndParsing(t *testing.T) { nil, &trInfo, nil, - protocol.KycStatusVerified, + umaprotocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -349,7 +349,7 @@ func TestPayReqResponseAndParsing(t *testing.T) { client := &FakeInvoiceCreator{} metadata, err := createMetadataForBob() require.NoError(t, err) - payeeData := protocol.PayeeData{ + payeeData := umaprotocol.PayeeData{ "identifier": "$bob@vasp2.com", } receivingCurrencyCode := "USD" @@ -410,11 +410,11 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { require.NoError(t, err) trInfo := "some TR info for VASP2" - payeeOptions := protocol.CounterPartyDataOptions{ - "identifier": protocol.CounterPartyDataOption{ + payeeOptions := umaprotocol.CounterPartyDataOptions{ + "identifier": umaprotocol.CounterPartyDataOption{ Mandatory: true, }, - "name": protocol.CounterPartyDataOption{ + "name": umaprotocol.CounterPartyDataOption{ Mandatory: false, }, } @@ -429,7 +429,7 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { nil, &trInfo, nil, - protocol.KycStatusVerified, + umaprotocol.KycStatusVerified, nil, nil, "/api/lnurl/utxocallback?txid=1234", @@ -440,7 +440,7 @@ func TestMsatsPayReqResponseAndParsing(t *testing.T) { client := &FakeInvoiceCreator{} metadata, err := createMetadataForBob() require.NoError(t, err) - payeeData := protocol.PayeeData{ + payeeData := umaprotocol.PayeeData{ "identifier": "$bob@vasp2.com", } receivingCurrencyCode := "USD" @@ -493,7 +493,7 @@ func TestSignAndVerifyPostTransactionCallback(t *testing.T) { signingPrivateKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) callback, err := uma.GetPostTransactionCallback( - []protocol.UtxoWithAmount{{Utxo: "abcdef12345", Amount: 1000}}, + []umaprotocol.UtxoWithAmount{{Utxo: "abcdef12345", Amount: 1000}}, "my-vasp.com", signingPrivateKey.Serialize(), ) @@ -519,16 +519,16 @@ func TestParsePayReqFromQueryParamsNoOptionalFields(t *testing.T) { func TestParsePayReqFromQueryParamsAllOptionalFields(t *testing.T) { amount := "1000.USD" - payerData := protocol.PayerData{ + payerData := umaprotocol.PayerData{ "identifier": "$bob@vasp.com", } encodedPayerData, err := json.Marshal(payerData) require.NoError(t, err) - payeeData := protocol.CounterPartyDataOptions{ - "identifier": protocol.CounterPartyDataOption{ + payeeData := umaprotocol.CounterPartyDataOptions{ + "identifier": umaprotocol.CounterPartyDataOption{ Mandatory: true, }, - "name": protocol.CounterPartyDataOption{ + "name": umaprotocol.CounterPartyDataOption{ Mandatory: false, }, } @@ -553,16 +553,16 @@ func TestParsePayReqFromQueryParamsAllOptionalFields(t *testing.T) { func TestParseAndEncodePayReqToQueryParams(t *testing.T) { amount := "1000.USD" - payerData := protocol.PayerData{ + payerData := umaprotocol.PayerData{ "identifier": "$bob@vasp.com", } encodedPayerData, err := json.Marshal(payerData) require.NoError(t, err) - payeeData := protocol.CounterPartyDataOptions{ - "identifier": protocol.CounterPartyDataOption{ + payeeData := umaprotocol.CounterPartyDataOptions{ + "identifier": umaprotocol.CounterPartyDataOption{ Mandatory: true, }, - "name": protocol.CounterPartyDataOption{ + "name": umaprotocol.CounterPartyDataOption{ Mandatory: false, }, } @@ -585,7 +585,7 @@ func TestParseAndEncodePayReqToQueryParams(t *testing.T) { require.Equal(t, payreq, payreqReparsed) } -func createLnurlpRequest(t *testing.T, signingPrivateKey []byte) protocol.LnurlpRequest { +func createLnurlpRequest(t *testing.T, signingPrivateKey []byte) umaprotocol.LnurlpRequest { queryUrl, err := uma.GetSignedLnurlpRequestUrl(signingPrivateKey, "$bob@vasp2.com", "vasp1.com", true, nil) require.NoError(t, err) query, err := uma.ParseLnurlpRequest(*queryUrl)