diff --git a/packages/ui/styles/breakpoints.tsx b/packages/ui/styles/breakpoints.tsx index 19fb257ef..533f6c993 100644 --- a/packages/ui/styles/breakpoints.tsx +++ b/packages/ui/styles/breakpoints.tsx @@ -1,4 +1,5 @@ import { css } from "@emotion/react"; +import { debounce } from "lodash-es"; import React, { Fragment, useLayoutEffect, useState } from "react"; export enum Breakpoints { @@ -158,6 +159,25 @@ export function useBreakpoints() { return bp; } +export function useScreenWidth() { + const [screenWidth, setScreenWidth] = useState(window.innerWidth); + + useLayoutEffect(() => { + const handleResize = debounce(() => { + setScreenWidth(window.innerWidth); + }, 200); + handleResize(); + window.addEventListener("resize", handleResize); + window.addEventListener("deviceorientation", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + window.removeEventListener("deviceorientation", handleResize); + }; + }); + + return screenWidth; +} + type DisplayBreakpointProps = { css?: string; sm?: boolean; diff --git a/packages/uma/.eslintrc.cjs b/packages/uma/.eslintrc.cjs new file mode 100644 index 000000000..3e67495ec --- /dev/null +++ b/packages/uma/.eslintrc.cjs @@ -0,0 +1,17 @@ +module.exports = { + extends: ["@lightsparkdev/eslint-config/base"], + ignorePatterns: ["jest.config.ts"], + overrides: [ + { + files: ["**/*.ts?(x)"], + // parserOptions: { + // /* Allow linting for ts files outside src with tsconfig-eslint: */ + // project: ["./tsconfig.json", "./tsconfig-eslint.json"], + // }, + rules: { + /* Temporarily turn off no-explicit-any until these can be resolved LIG-3400: */ + "@typescript-eslint/no-explicit-any": "off", + }, + }, + ], +}; diff --git a/packages/uma/.fossa.yml b/packages/uma/.fossa.yml new file mode 100644 index 000000000..5877824ec --- /dev/null +++ b/packages/uma/.fossa.yml @@ -0,0 +1,6 @@ +version: 3 + +project: + id: lightspark/js-uma-sdk + name: js-uma-sdk + url: https://github.com/lightsparkdev/js-sdk diff --git a/packages/uma/.prettierrc b/packages/uma/.prettierrc new file mode 100644 index 000000000..55c1943ae --- /dev/null +++ b/packages/uma/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-organize-imports"] +} diff --git a/packages/uma/LICENSE b/packages/uma/LICENSE new file mode 100644 index 000000000..cbb91e498 --- /dev/null +++ b/packages/uma/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Lightspark Group, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/uma/README.md b/packages/uma/README.md new file mode 100644 index 000000000..ec9269eb2 --- /dev/null +++ b/packages/uma/README.md @@ -0,0 +1 @@ +# UMA SDK for Node.js diff --git a/packages/uma/jest.config.ts b/packages/uma/jest.config.ts new file mode 100644 index 000000000..b5d8b34e8 --- /dev/null +++ b/packages/uma/jest.config.ts @@ -0,0 +1,18 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest/presets/default-esm", + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + tsconfig: "tsconfig-test.json", + useESM: true, + }, + ], + }, +}; diff --git a/packages/uma/package.json b/packages/uma/package.json new file mode 100644 index 000000000..b40115bc0 --- /dev/null +++ b/packages/uma/package.json @@ -0,0 +1,105 @@ +{ + "name": "@lightsparkdev/uma", + "version": "0.0.0", + "description": "UMA SDK for JavaScript", + "author": "Lightspark Inc.", + "keywords": [ + "lightspark", + "bitcoin", + "lightning", + "payments", + "typescript" + ], + "homepage": "https://github.com/lightsparkdev/js-sdk", + "repository": { + "type": "git", + "url": "https://github.com/lightsparkdev/js-sdk.git" + }, + "bugs": { + "url": "https://github.com/lightsparkdev/js-sdk/issues" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } + }, + "./objects": { + "types": "./dist/objects/index.d.ts", + "import": { + "types": "./dist/objects/index.d.ts", + "default": "./dist/objects/index.js" + }, + "require": { + "types": "./dist/objects/index.d.ts", + "default": "./dist/objects/index.cjs" + } + } + }, + "type": "module", + "types": "./dist/index.d.ts", + "main": "./dist/index.js", + "module": "./dist/index.js", + "engines": { + "node": ">=18.17.0" + }, + "browser": { + "crypto": false + }, + "files": [ + "src/*", + "dist/*", + "CHANGELOG.md" + ], + "scripts": { + "build": "tsup --entry src/index.ts --entry src/objects/index.ts --format cjs,esm --dts", + "clean": "rm -rf .turbo && rm -rf dist", + "dev": "yarn build -- --watch", + "docs": "typedoc --media docs-media src", + "format:fix": "prettier src --write", + "format": "prettier src --check", + "lint:fix": "eslint --fix .", + "lint:fix:continue": "eslint --fix . || exit 0", + "lint:watch": "esw ./src -w --ext .ts,.tsx,.js --color", + "lint": "eslint .", + "postversion": "yarn build", + "test": "yarn jest --version && node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --bail", + "types": "tsc" + }, + "license": "Apache-2.0", + "dependencies": { + "@lightsparkdev/core": "0.3.11", + "@react-native-async-storage/async-storage": "^1.18.1", + "auto-bind": "^5.0.1", + "crypto": "^1.0.1", + "crypto-browserify": "^3.12.0", + "dayjs": "^1.11.7", + "graphql": "^16.6.0", + "graphql-ws": "^5.11.3", + "ws": "^8.12.1", + "zen-observable-ts": "^1.1.0" + }, + "devDependencies": { + "@lightsparkdev/eslint-config": "*", + "@types/crypto-js": "^4.1.1", + "@types/ws": "^8.5.4", + "dotenv": "^16.3.1", + "dotenv-cli": "^7.3.0", + "eslint": "^8.3.0", + "eslint-watch": "^8.0.0", + "jest": "^29.6.2", + "jsonwebtoken": "^9.0.1", + "prettier": "3.0.2", + "prettier-plugin-organize-imports": "^3.2.2", + "ts-jest": "^29.1.1", + "tsup": "^6.7.0", + "typedoc": "^0.24.7", + "typescript": "^4.9.5" + } +} diff --git a/packages/uma/src/Currency.ts b/packages/uma/src/Currency.ts new file mode 100644 index 000000000..e5dc18402 --- /dev/null +++ b/packages/uma/src/Currency.ts @@ -0,0 +1,8 @@ +export type Currency = { + code: string; + name: string; + symbol: string; + multiplier: number; + minSendable: number; + maxSendable: number; +}; diff --git a/packages/uma/src/KycStatus.ts b/packages/uma/src/KycStatus.ts new file mode 100644 index 000000000..0b054e4c6 --- /dev/null +++ b/packages/uma/src/KycStatus.ts @@ -0,0 +1,36 @@ +export enum KycStatus { + Unknown = "UNKNOWN", + NotVerified = "NOT_VERIFIED", + Pending = "PENDING", + Verified = "VERIFIED", +} + +export function kycStatusFromString(s: string): KycStatus { + switch (s) { + default: + return KycStatus.Unknown; + case "UNKNOWN": + return KycStatus.Unknown; + case "NOT_VERIFIED": + return KycStatus.NotVerified; + case "PENDING": + return KycStatus.Pending; + case "VERIFIED": + return KycStatus.Verified; + } +} + +export function kycStatusToString(k: KycStatus): string { + switch (k) { + default: + return "undefined"; + case KycStatus.Unknown: + return KycStatus.Unknown; + case KycStatus.NotVerified: + return KycStatus.NotVerified; + case KycStatus.Pending: + return KycStatus.Pending; + case KycStatus.Verified: + return KycStatus.Verified; + } +} diff --git a/packages/uma/src/PayerData.ts b/packages/uma/src/PayerData.ts new file mode 100644 index 000000000..a9d5a1607 --- /dev/null +++ b/packages/uma/src/PayerData.ts @@ -0,0 +1,40 @@ +import { type KycStatus } from "./KycStatus.js"; + +export type PayerDataOptions = { + nameRequired: boolean; + emailRequired: boolean; + complianceRequired: boolean; +}; + +export type PayerData = { + name?: string; + email?: string; + identifier: string; + compliance: CompliancePayerData; +}; + +export type CompliancePayerData = { + // Utxos is the list of UTXOs of the sender's channels that might be used to fund the payment. + utxos?: string[]; + // NodePubKey is the public key of the sender's node if known. + nodePubKey?: string; + // KycStatus indicates whether VASP1 has KYC information about the sender. + kycStatus: KycStatus; + // EncryptedTravelRuleInfo is the travel rule information of the sender. This is encrypted with the receiver's public encryption key. + encryptedTravelRuleInfo?: string; + // Signature is the base64-encoded signature of sha256(ReceiverAddress|Nonce|Timestamp). + signature: string; + signatureNonce: string; + signatureTimestamp: number; + // UtxoCallback is the URL that the receiver will call to send UTXOs of the channel that the receiver used to receive the payment once it completes. + utxoCallback: string; +}; + +export function payerDataOptionsToJSON(p: PayerDataOptions): string { + return JSON.stringify({ + identifier: { mandatory: true }, + name: { mandatory: p.nameRequired }, + email: { mandatory: p.emailRequired }, + compliance: { mandatory: p.complianceRequired }, + }); +} diff --git a/packages/uma/src/PublicKeyCache.ts b/packages/uma/src/PublicKeyCache.ts new file mode 100644 index 000000000..20b499860 --- /dev/null +++ b/packages/uma/src/PublicKeyCache.ts @@ -0,0 +1,33 @@ +import { type PubKeyResponse } from "./protocol.js"; + +export class PublicKeyCache { + cache: Map; + + constructor() { + this.cache = new Map(); + } + + fetchPublicKeyForVasp(vaspDomain: string) { + const entry = this.cache.get(vaspDomain); + if ( + entry === undefined || + (entry.expirationTimestamp !== undefined && + entry.expirationTimestamp < Date.now()) + ) { + return undefined; + } + return entry; + } + + addPublicKeyForVasp(vaspDomain: string, pubKey: PubKeyResponse) { + this.cache.set(vaspDomain, pubKey); + } + + removePublicKeyForVasp(vaspDomain: string) { + this.cache.delete(vaspDomain); + } + + clear() { + this.cache.clear(); + } +} diff --git a/packages/uma/src/index.ts b/packages/uma/src/index.ts new file mode 100644 index 000000000..235965f00 --- /dev/null +++ b/packages/uma/src/index.ts @@ -0,0 +1,8 @@ +// Copyright ©, 2023-present, Lightspark Group, Inc. - All Rights Reserved + +export * from "./Currency.js"; +export * from "./KycStatus.js"; +export * from "./PayerData.js"; +export * from "./protocol.js"; +export * from "./PublicKeyCache.js"; +export * from "./version.js"; diff --git a/packages/uma/src/protocol.ts b/packages/uma/src/protocol.ts new file mode 100644 index 000000000..10915630f --- /dev/null +++ b/packages/uma/src/protocol.ts @@ -0,0 +1,172 @@ +import { type Currency } from "./Currency.js"; +import { type KycStatus } from "./KycStatus.js"; +import { type PayerData, type PayerDataOptions } from "./PayerData.js"; + +// 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. +export type 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: boolean; + // 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: Date; + // 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; +}; + +// 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. +export type LnurlpResponse = { + tag: string; + callback: string; + minSendable: number; + maxSendable: number; + encodedMetadata: string; + currencies: Currency[]; + requiredPayerData: PayerDataOptions; + compliance: LnurlComplianceResponse; + // 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; +}; + +// LnurlComplianceResponse is the `compliance` field of the LnurlpResponse. +export type LnurlComplianceResponse = { + // KycStatus indicates whether VASP2 has KYC information about the receiver. + kycStatus: KycStatus; + // Signature is the base64-encoded signature of sha256(ReceiverAddress|Nonce|Timestamp). + signature: string; + // Nonce is a random string that is used to prevent replay attacks. + signatureNonce: string; + // Timestamp is the unix timestamp of when the request was sent. Used in the signature. + signatureTimestamp: number; + // IsSubjectToTravelRule indicates whether VASP2 is a financial institution that requires travel rule information. + isSubjectToTravelRule: boolean; + // ReceiverIdentifier is the identifier of the receiver at VASP2. + receiverIdentifier: string; +}; + +// PayRequest is the request sent by the sender to the receiver to retrieve an invoice. +export type PayRequest = { + // CurrencyCode is the ISO 3-digit currency code that the receiver will receive for this payment. + currencyCode: string; + // 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: number; + // PayerData is the data that the sender will send to the receiver to identify themselves. + payerData: PayerData; +}; + +// PayReqResponse is the response sent by the receiver to the sender to provide an invoice. +export type PayReqResponse = { + // EncodedInvoice is the BOLT11 invoice that the sender will pay. + encodedInvoice: string; + // Routes is usually just an empty list from legacy LNURL, which was replaced by route hints in the BOLT11 invoice. + routes: Route[]; + compliance: PayReqResponseCompliance; + paymentInfo: PayReqResponsePaymentInfo; +}; + +export type Route = { + pubkey: string; + path: { + pubkey: string; + fee: number; + msatoshi: number; + channel: string; + }[]; +}; + +export type PayReqResponseCompliance = { + // NodePubKey is the public key of the receiver's node if known. + nodePubKey?: string; + // Utxos is a list of UTXOs of channels over which the receiver will likely receive the payment. + utxos: string[]; + // 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; +}; + +export type PayReqResponsePaymentInfo = { + // CurrencyCode is the ISO 3-digit currency code that the receiver will receive for this payment. + currencyCode: string; + // Multiplier is the conversion rate. It is the number of millisatoshis that the receiver will receive for 1 unit of the specified currency. + multiplier: number; + // ExchangeFeesMillisatoshi is the fees charged (in millisats) by the receiving VASP for this transaction. This is + // separate from the Multiplier. + exchangeFeesMillisatoshi: number; +}; + +// 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`. +export type PubKeyResponse = { + // SigningPubKey is used to verify signatures from a VASP. + signingPubKey: string; + // EncryptionPubKey is used to encrypt TR info sent to a VASP. + encryptionPubKey: string; + // 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?: number; +}; + +// UtxoWithAmount is a pair of utxo and amount transferred over that corresponding channel. +// It can be used to register payment for KYT. +export type UtxoWithAmount = { + // Utxo The utxo of the channel over which the payment went through in the format of :. + utxo: string; + // Amount The amount of funds transferred in the payment in mSats. + amount: number; +}; + +export function encodeToUrl(q: LnurlpRequest): URL { + const receiverAddressParts = q.receiverAddress.split("@"); + if (receiverAddressParts.length !== 2) { + throw new Error("invalid receiver address"); + } + const scheme = receiverAddressParts[1].startsWith("localhost:") + ? "http" + : "https"; + const lnurlpUrl = new URL( + `${scheme}://${receiverAddressParts[1]}/.well-known/lnurlp/${receiverAddressParts[0]}`, + ); + const queryParams = lnurlpUrl.searchParams; + queryParams.set("signature", q.signature); + queryParams.set("vaspDomain", q.vaspDomain); + queryParams.set("nonce", q.nonce); + queryParams.set("isSubjectToTravelRule", q.isSubjectToTravelRule.toString()); + queryParams.set("timestamp", q.timestamp.getTime().toString()); + queryParams.set("umaVersion", q.umaVersion); + lnurlpUrl.search = queryParams.toString(); + return lnurlpUrl; +} + +export function encodePayRequest(q: PayRequest) { + return JSON.stringify(q); +} + +export function getSignableLnurlpRequestPayload(q: LnurlpRequest): string { + return [q.receiverAddress, q.nonce, q.timestamp.getTime().toString()].join( + "|", + ); +} + +export function getSignableLnurlpResponsePayload(r: LnurlpResponse): string { + return [ + r.compliance.receiverIdentifier, + r.compliance.signatureNonce, + r.compliance.signatureTimestamp.toString(), + ].join("|"); +} + +export function getSignablePayRequestPayload(q: PayRequest): string { + return `${q.payerData.identifier}|${ + q.payerData.compliance.signatureNonce + }|${q.payerData.compliance.signatureTimestamp.toString()}`; +} diff --git a/packages/uma/src/tests/uma.test.ts b/packages/uma/src/tests/uma.test.ts new file mode 100644 index 000000000..60cd2e400 --- /dev/null +++ b/packages/uma/src/tests/uma.test.ts @@ -0,0 +1,5 @@ +describe("uma", () => { + it("should do nothing", () => { + expect(true).toBe(true); + }); +}); diff --git a/packages/uma/src/version.ts b/packages/uma/src/version.ts new file mode 100644 index 000000000..b9f56efb5 --- /dev/null +++ b/packages/uma/src/version.ts @@ -0,0 +1,109 @@ +export const MAJOR_VERSION = 0; +export const MINOR_VERSION = 1; + +export const UmaProtocolVersion = `${MAJOR_VERSION}.${MINOR_VERSION}`; + +export class UnsupportedVersionError extends Error { + unsupportedVersion: string; + supportedMajorVersions: number[]; + + constructor(unsupportedVersion: string, supportedMajorVersions: number[]) { + super(`unsupported version: ${unsupportedVersion}`); + this.unsupportedVersion = unsupportedVersion; + this.supportedMajorVersions = supportedMajorVersions; + } +} + +export function getHighestSupportedVersionForMajorVersion( + majorVersion: number, +): string { + if (majorVersion !== MAJOR_VERSION) { + throw new Error("unsupported major version"); + } + return UmaProtocolVersion; +} + +export function selectHighestSupportedVersion( + otherVaspSupportedMajorVersions: number[], +): string { + let highestVersion: string | undefined; + const supportedMajorVersions = getSupportedMajorVersions(); + for (const otherVaspMajorVersion of otherVaspSupportedMajorVersions) { + if (!supportedMajorVersions.has(otherVaspMajorVersion)) { + continue; + } + + if (highestVersion === undefined) { + highestVersion = getHighestSupportedVersionForMajorVersion( + otherVaspMajorVersion, + ); + continue; + } + if (otherVaspMajorVersion > getMajorVersion(highestVersion)) { + highestVersion = getHighestSupportedVersionForMajorVersion( + otherVaspMajorVersion, + ); + } + } + if (highestVersion === undefined) { + throw new Error("no supported versions"); + } + return highestVersion; +} + +export function selectLowerVersion( + version1String: string, + version2String: string, +): string { + const version1 = parseVersion(version1String); + const version2 = parseVersion(version2String); + if ( + version1.major > version2.major || + (version1.major === version2.major && version1.minor > version2.minor) + ) { + return version2String; + } else { + return version1String; + } +} + +export function isVersionSupported(version: string): boolean { + const parsedVersion = parseVersion(version); + if (parsedVersion === undefined) { + return false; + } + return getSupportedMajorVersions().has(parsedVersion.major); +} + +export function getMajorVersion(version: string): number { + const parsedVersion = parseVersion(version); + if (parsedVersion === undefined) { + throw new Error("invalid version"); + } + return parsedVersion.major; +} + +export function getMinorVersion(version: string): number { + const parsedVersion = parseVersion(version); + if (parsedVersion === undefined) { + throw new Error("invalid version"); + } + return parsedVersion.minor; +} + +export function parseVersion(version: string): { + major: number; + minor: number; +} { + const [major, minor] = version.split("."); + if (major === undefined || minor === undefined) { + throw new Error("Invalid UMA version"); + } + return { major: parseInt(major), minor: parseInt(minor) }; +} + +export function getSupportedMajorVersions(): Set { + // NOTE: In the future, we may want to support multiple major versions in the same SDK, but for now, this keeps + // things simple. + return new Set([MAJOR_VERSION]); +} diff --git a/packages/uma/tsconfig-test.json b/packages/uma/tsconfig-test.json new file mode 100644 index 000000000..154337426 --- /dev/null +++ b/packages/uma/tsconfig-test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "resolveJsonModule": true, + "moduleResolution": "node", + "types": ["jest", "node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/packages/uma/tsconfig.json b/packages/uma/tsconfig.json new file mode 100644 index 000000000..4f6e9b75c --- /dev/null +++ b/packages/uma/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@lightsparkdev/tsconfig/base.json", + "compilerOptions": { + /* Allow implicit any due to generated files. TODO look into resolving + implicit any for generated files or move them out of the type checks */ + "noImplicitAny": false + }, + "include": ["src"], + "exclude": ["test", "node_modules", "dist"] +} diff --git a/yarn.lock b/yarn.lock index dc5938552..ba782f8ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4492,6 +4492,38 @@ __metadata: languageName: unknown linkType: soft +"@lightsparkdev/uma@workspace:packages/uma": + version: 0.0.0-use.local + resolution: "@lightsparkdev/uma@workspace:packages/uma" + dependencies: + "@lightsparkdev/core": 0.3.11 + "@lightsparkdev/eslint-config": "*" + "@react-native-async-storage/async-storage": ^1.18.1 + "@types/crypto-js": ^4.1.1 + "@types/ws": ^8.5.4 + auto-bind: ^5.0.1 + crypto: ^1.0.1 + crypto-browserify: ^3.12.0 + dayjs: ^1.11.7 + dotenv: ^16.3.1 + dotenv-cli: ^7.3.0 + eslint: ^8.3.0 + eslint-watch: ^8.0.0 + graphql: ^16.6.0 + graphql-ws: ^5.11.3 + jest: ^29.6.2 + jsonwebtoken: ^9.0.1 + prettier: 3.0.2 + prettier-plugin-organize-imports: ^3.2.2 + ts-jest: ^29.1.1 + tsup: ^6.7.0 + typedoc: ^0.24.7 + typescript: ^4.9.5 + ws: ^8.12.1 + zen-observable-ts: ^1.1.0 + languageName: unknown + linkType: soft + "@lightsparkdev/vite@0.0.0, @lightsparkdev/vite@workspace:packages/vite": version: 0.0.0-use.local resolution: "@lightsparkdev/vite@workspace:packages/vite"