From b05d03185f88d30ec223636cdfdbc24bee57a369 Mon Sep 17 00:00:00 2001 From: Rastislav Date: Thu, 4 Jul 2024 21:37:44 +0200 Subject: [PATCH] Initial --- .github/ISSUE_TEMPLATE/bug.yml | 63 +++++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature.yml | 43 ++++ .github/workflows/publish.yml | 35 +++ .github/workflows/test.yml | 24 ++ .gitignore | 29 +++ LICENSE | 19 ++ README.md | 217 +++++++++++++++- dist/index.d.ts | 66 +++++ dist/index.js | 368 +++++++++++++++++++++++++++ package.json | 55 ++++ src/index.ts | 388 +++++++++++++++++++++++++++++ test/paytoRl.test.ts | 80 ++++++ tsconfig.json | 14 ++ types/index.d.ts | 99 ++++++++ 15 files changed, 1503 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 test/paytoRl.test.ts create mode 100644 tsconfig.json create mode 100644 types/index.d.ts diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..1720b57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,63 @@ +name: Report Bug 🪲 +description: Create a bug report +labels: + - bug +body: + - type: markdown + attributes: + value: | + Please, provide as much details as possible to make proper evaluation and in the end resolve the bug report faster. + + - type: markdown + attributes: + value: | + Software specific details. + + - type: input + id: os + attributes: + label: OS and version + description: Name and version of OS used. + placeholder: OS + validations: + required: true + + - type: textarea + id: bug + attributes: + label: Bug + description: Describe the Bug. + placeholder: Bug description + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: Reproduce Bug + description: Steps to reproduce the behavior. + placeholder: 1. Step 1 + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: Describe the result you are expecting. + placeholder: Result + validations: + required: true + + - type: input + id: hash + attributes: + label: Commit hash + description: Commit hash if known. + placeholder: baf2aab0128f07262b564214ec70fc07befc6bb3 + + - type: textarea + id: additional + attributes: + label: Additional context + description: Please, place additional content or screenshots. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..1b4ea7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Readme + url: https://github.com/bchainhub/payto-url#readme + about: Read more about the Remark plug-in diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..24389b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,43 @@ +name: Feature request 🔮 +description: Suggest an idea for this project +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Suggest an idea for this project and developers will consider the integration. + + - type: textarea + id: problem + attributes: + label: Problem + description: Is your feature request related to a problem? Please describe. + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Solution + description: Describe the solution you'd like. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: Describe alternatives you've considered. + + - type: textarea + id: implementation + attributes: + label: Implementation + description: Describe implementation with example of the code. + + - type: textarea + id: additional + attributes: + label: Additional context + description: Please, place additional content or screenshots. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..1668d5e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,35 @@ +name: Publish to NPM + +on: + release: + types: [created] + +permissions: + id-token: write + contents: write + +jobs: + build-and-publish: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + + - name: Install Dependencies + run: npm i + + - name: Build Project + run: npm run build + + - name: Publish package on NPM 📦 + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9bc79e4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Testing + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '18.x' + registry-url: 'https://registry.npmjs.org' + - run: npm i + - run: npm run build --if-present + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7b2c84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +pids +logs +results + +npm-debug.log +node_modules +yarn.lock +yarn-error.log +package-lock.json + +/dist-test/ + +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6417b9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +CORE License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), +to to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +All distribution of the Covered Software in Source Code Form, including any +Modifications and/or Contributions must be disclosed and publicly available. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE TO ANY CLAIM, DAMAGES OR +OTHER LIABILITIES, WHETHER IN AN ACTION OF A CONTRACT, TORT, OR OTHERWISE, +ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE, OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index bd59c3b..03724d6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,215 @@ -# payto-url -Payto uniform resource locator + +# Payto-RL + +`payto-rl` is a TypeScript library for handling Payto resource locators (PRLs). This library is based on the [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) API and provides additional functionality for managing PRLs. + +## Installation + +You can install the `payto-rl` package using npm or yarn: + +### Using npm + +```sh +npm install payto-rl +``` + +### Using yarn + +```sh +yarn add payto-rl +``` + +## Usage + +Here is an example of how to use the `Payto-RL` package: + +### Example + +```typescript +import Payto from 'payto-rl'; + +const paytoString = 'payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.1&fiat=usd'; +const payto = new Payto(paytoString); + +console.log(payto.address); // Outputs: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa +console.log(payto.amount); // Outputs: 0.1 +console.log(payto.currency); // Outputs: ['btc', 'usd'] + +payto.amount = '0.2'; +console.log(payto.amount); // Outputs: 0.2 + +console.log(payto.toJSONObject()); + +/* +Outputs: +{ + address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + amount: '0.2', + currency: 'btc', + fiat: 'usd', + host: 'btc', + hostname: 'btc', + href: 'payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.2&fiat=usd', + network: 'btc', + pathname: '/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + protocol: 'payto:', + search: '?amount=0.2&fiat=usd' +} +*/ +``` + +## API + +### `Payto` + +#### Constructor + +```typescript +constructor(paytoString: string) +``` + +Creates a new `Payto` instance. + +#### Properties + +##### `address: string | null` + +Gets or sets the address component of the PRL. + +##### `amount: string | null` + +Gets or sets the amount component of the PRL. Amount consists of the number of units and the currency delimited by `:`. + +##### `bic: string | null` + +Gets or sets the BIC component of the PRL. + +##### `currency: [string | null, string | null]` + +Gets or sets the currency component of the PRL. + +##### `deadline: string | null` + +Gets or sets the deadline component of the PRL. + +##### `donate: string | null` + +Gets or sets the donate component of the PRL. + +##### `fiat: string | null` + +Gets or sets the fiat component of the PRL. + +##### `hash: string` + +Gets or sets the hash component of the PRL. + +##### `host: string` + +Gets or sets the host component of the PRL. + +##### `hostname: string` + +Gets or sets the hostname component of the PRL. + +##### `href: string` + +Gets or sets the href component of the PRL. + +##### `iban: string | null` + +Gets or sets the IBAN component of the PRL. + +##### `location: string | null` + +Gets or sets the location component of the PRL. + +##### `message: string | null` + +Gets or sets the message component of the PRL. + +##### `network: string` + +Gets or sets the network component of the PRL. + +##### `origin: string | null` + +Gets the origin component of the PRL. + +##### `password: string` + +Gets or sets the password component of the PRL. + +##### `pathname: string` + +Gets or sets the pathname component of the PRL. + +##### `port: string` + +Gets or sets the port component of the PRL. + +##### `protocol: string` + +Gets or sets the protocol component of the PRL. + +##### `receiverName: string | null` + +Gets or sets the receiver name component of the PRL. + +##### `recurring: string | null` + +Gets or sets the recurring component of the PRL. + +##### `route: string | null` + +Gets or sets the route component of the PRL. + +##### `routingNumber: string | null` + +Gets or sets the routing number component of the PRL. + +##### `search: string` + +Gets or sets the search component of the PRL. + +##### `searchParams: URLSearchParams` + +Gets the URLSearchParams object. + +##### `split: [string, string, boolean] | null` + +Gets or sets the split component of the PRL. + +##### `username: string` + +Gets or sets the username component of the PRL. + +##### `value: number | null` + +Gets or sets the amount component of the PRL. This contrasts with the `amount` working only with the currency string and not the currency number itself. + +#### Methods + +##### `toString(): string` + +Returns the string representation of the PRL. + +##### `toJSON(): string` + +Returns the JSON representation of the PRL. + +##### `toJSONObject(): object` + +Returns the PRL as a JSON object with all non-empty values. + +## License + +This project is licensed under the CORE License - see the [LICENSE](LICENSE) file for details. + +## Contributing + +Feel free to contribute to this project. You can open issues for feature requests or bug fixes. Pull requests are also welcome. + +## Acknowledgments + +This library is based on the [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) API from MDN. diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..e373928 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,66 @@ +declare class Payto { + private url; + constructor(paytoString: string); + private getHostnameParts; + private setHostnameParts; + get address(): string | null; + set address(value: string | null); + get amount(): string | null; + set amount(value: string | null); + get bic(): string | null; + set bic(value: string | null); + get currency(): [string | null, string | null]; + set currency(value: [number?, string?, string?]); + get deadline(): string | null; + set deadline(value: string | null); + get donate(): string | null; + set donate(value: string | null); + get fiat(): string | null; + set fiat(value: string | null); + get hash(): string; + set hash(value: string); + get host(): string; + set host(value: string); + get hostname(): string; + set hostname(value: string); + get href(): string; + set href(value: string); + get iban(): string | null; + set iban(value: string | null); + get location(): string | null; + set location(value: string | null); + get message(): string | null; + set message(value: string | null); + get network(): string; + set network(value: string); + get origin(): string | null; + get password(): string; + set password(value: string); + get pathname(): string; + set pathname(value: string); + get port(): string; + set port(value: string); + get protocol(): string; + set protocol(value: string); + get receiverName(): string | null; + set receiverName(value: string | null); + get recurring(): string | null; + set recurring(value: string | null); + get route(): string | null; + set route(value: string | null); + get routingNumber(): string | null; + set routingNumber(value: string | null); + get search(): string; + set search(value: string); + get searchParams(): URLSearchParams; + get split(): [string, string, boolean] | null; + set split(value: [string, string, boolean] | null); + get value(): number | null; + set value(value: number | null); + get username(): string; + set username(value: string); + toString(): string; + toJSON(): string; + toJSONObject(): object; +} +export default Payto; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..71f7503 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,368 @@ +class Payto { + constructor(paytoString) { + this.url = new URL(paytoString); + if (this.url.protocol !== 'payto:') { + throw new Error('Invalid protocol, must be payto:'); + } + } + getHostnameParts(array, type, position = 2) { + if (type === null || array[1]?.toLowerCase() === type) { + return array[position] || null; + } + return null; + } + setHostnameParts(value, position = 2) { + const addressArray = this.pathname.split('/'); + if (value) { + addressArray[position] = value; + } + else { + addressArray.splice(position, 1); + } + this.pathname = '/' + addressArray.filter(part => part).join('/'); + } + get address() { + return this.getHostnameParts(this.pathname.split('/'), null, 1); + } + set address(value) { + this.setHostnameParts(value, 1); + } + get amount() { + return this.searchParams.get('amount'); + } + set amount(value) { + if (value) { + this.searchParams.set('amount', value); + } + else { + this.searchParams.delete('amount'); + } + } + get bic() { + return this.getHostnameParts(this.pathname.split('/'), 'bic', 2); + } + set bic(value) { + this.setHostnameParts(value, 2); + } + get currency() { + const result = [null, null]; + const amountValue = this.searchParams.get('amount'); + const fiatValue = this.fiat; + if (amountValue) { + const amountArray = amountValue.split(':'); + if (amountArray[0] && isNaN(parseFloat(amountArray[0]))) { + result[0] = amountArray[0]; + } + } + if (fiatValue) { + result[1] = fiatValue; + } + return result; + } + set currency(value) { + const [amount, token, fiat] = value; + if (fiat) + this.fiat = fiat.toLowerCase(); + if (token) { + this.amount = `${token}:${amount ?? ''}`; + } + else if (amount) { + this.amount = `${amount}`; + } + } + get deadline() { + return this.searchParams.get('dl'); + } + set deadline(value) { + if (value) { + this.searchParams.set('dl', value); + } + else { + this.searchParams.delete('dl'); + } + } + get donate() { + if (this.searchParams.has('donate')) { + const donateValue = this.searchParams.get('donate'); + return donateValue !== null ? donateValue : '1'; + } + return null; + } + set donate(value) { + if (value === '0' || value === '1') { + this.searchParams.set('donate', value); + } + else if (value) { + this.searchParams.set('donate', '1'); + } + else { + this.searchParams.delete('donate'); + } + } + get fiat() { + return this.searchParams.get('fiat')?.toLowerCase() || null; + } + set fiat(value) { + if (value) { + this.searchParams.set('fiat', value); + } + else { + this.searchParams.delete('fiat'); + } + } + get hash() { + return this.url.hash; + } + set hash(value) { + this.url.hash = value; + } + get host() { + return this.url.host; + } + set host(value) { + this.url.host = value; + } + get hostname() { + return this.url.hostname.toLowerCase(); + } + set hostname(value) { + this.url.hostname = value.toLowerCase(); + } + get href() { + return this.url.href; + } + set href(value) { + this.url.href = value; + } + get iban() { + return this.getHostnameParts(this.pathname.split('/'), 'iban', 2); + } + set iban(value) { + this.setHostnameParts(value, 2); + } + get location() { + return this.searchParams.get('loc'); + } + set location(value) { + if (value) { + this.searchParams.set('loc', value); + } + else { + this.searchParams.delete('loc'); + } + } + get message() { + return this.searchParams.get('message'); + } + set message(value) { + if (value) { + this.searchParams.set('message', value); + } + else { + this.searchParams.delete('message'); + } + } + get network() { + return this.url.hostname.toLowerCase(); + } + set network(value) { + this.url.hostname = value.toLowerCase(); + } + get origin() { + const origin = this.url.origin; + return origin === 'null' || origin === '' ? null : origin; + } + get password() { + return this.url.password; + } + set password(value) { + this.url.password = value; + } + get pathname() { + return this.url.pathname; + } + set pathname(value) { + this.url.pathname = value; + } + get port() { + return this.url.port; + } + set port(value) { + this.url.port = value; + } + get protocol() { + return this.url.protocol; + } + set protocol(value) { + this.url.protocol = value; + } + get receiverName() { + return this.searchParams.get('receiver-name'); + } + set receiverName(value) { + if (value) { + this.searchParams.set('receiver-name', value); + } + else { + this.searchParams.delete('receiver-name'); + } + } + get recurring() { + return this.searchParams.get('rc')?.toLowerCase() || null; + } + set recurring(value) { + if (value) { + this.searchParams.set('rc', value); + } + else { + this.searchParams.delete('rc'); + } + } + get route() { + return this.getHostnameParts(this.pathname.split('/'), null, 3); + } + set route(value) { + this.setHostnameParts(value, 3); + } + get routingNumber() { + return this.getHostnameParts(this.pathname.split('/'), 'ach', 2); + } + set routingNumber(value) { + this.setHostnameParts(value, 2); + } + get search() { + return this.url.search; + } + set search(value) { + this.url.search = value; + } + get searchParams() { + return this.url.searchParams; + } + get split() { + const splitValue = this.searchParams.get('split'); + if (splitValue) { + const [amountPart, receiver] = splitValue.split('@'); + const [prefix, amount] = amountPart.split(':'); + return [receiver, amount, prefix === 'p']; + } + return null; + } + set split(value) { + if (value) { + const [receiver, amount, percentage] = value; + const prefix = percentage ? 'p:' : ''; + this.searchParams.set('split', `${prefix}${amount}@${receiver}`); + } + else { + this.searchParams.delete('split'); + } + } + get value() { + const amount = this.searchParams.get('amount'); + if (amount) { + const amountArray = amount.split(':'); + let amountParsed; + if (amountArray[1]) { + amountParsed = parseFloat(amountArray[1]); + } + else { + amountParsed = parseFloat(amountArray[0]); + } + if (!isNaN(amountParsed)) { + return amountParsed; + } + } + return null; + } + set value(value) { + if (value) { + const amount = this.searchParams.get('amount'); + if (amount) { + const amountArray = amount.split(':'); + if (amountArray[1]) { + this.searchParams.set('amount', amountArray[0] + ':' + value); + } + else { + this.searchParams.set('amount', value.toString()); + } + } + } + else { + this.searchParams.delete('amount'); + } + } + get username() { + return this.url.username; + } + set username(value) { + this.url.username = value; + } + toString() { + return this.url.toString(); + } + toJSON() { + return this.url.toJSON(); + } + toJSONObject() { + const obj = {}; + if (this.port) + obj.port = this.port; + if (this.currency[0]) + obj.currency = this.currency[0]; + if (this.currency[1]) + obj.fiat = this.currency[1]; + if (this.amount) + obj.amount = this.amount; + if (this.address) + obj.address = this.address; + if (this.bic) + obj.bic = this.bic; + if (this.deadline) + obj.deadline = this.deadline; + if (this.donate) + obj.donate = this.donate; + if (this.hash) + obj.hash = this.hash; + if (this.host) + obj.host = this.host; + if (this.hostname) + obj.hostname = this.hostname; + if (this.href) + obj.href = this.href; + if (this.iban) + obj.iban = this.iban; + if (this.location) + obj.location = this.location; + if (this.message) + obj.message = this.message; + if (this.network) + obj.network = this.network; + if (this.origin) + obj.origin = this.origin; + if (this.password) + obj.password = this.password; + if (this.pathname) + obj.pathname = this.pathname; + if (this.protocol) + obj.protocol = this.protocol; + if (this.receiverName) + obj.receiverName = this.receiverName; + if (this.recurring) + obj.recurring = this.recurring; + if (this.route) + obj.route = this.route; + if (this.routingNumber) + obj.routingNumber = this.routingNumber; + if (this.search) + obj.search = this.search; + if (this.split) + obj.split = this.split; + if (this.username) + obj.username = this.username; + if (this.value) + obj.value = this.value; + return obj; + } +} +export default Payto; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7b18ec1 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "payto-rl", + "version": "1.0.0", + "description": "Payto resource locator", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "test": "npm run build && node --loader ts-node/esm ./node_modules/uvu/bin.js test", + "build": "tsc", + "dev": "node --loader ts-node/esm --inspect src/index.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/bchainhub/payto-rl" + }, + "files": [ + "src/", + "dist/", + "LICENSE", + "README.md" + ], + "keywords": [ + "payto", + "protocol", + "prl", + "uniform", + "resource", + "locator", + "ican", + "iban", + "upi", + "ach", + "pix", + "cash" + ], + "author": "@rastislavcore", + "license": "CORE", + "bugs": { + "url": "https://github.com/bchainhub/payto-rl/issues" + }, + "homepage": "https://github.com/bchainhub/payto-rl#readme", + "engines": { + "node": ">=16" + }, + "exports": { + ".": "./dist/index.js" + }, + "devDependencies": { + "esm": "^3.2.25", + "ts-node": "^10.9.2", + "typescript": "^5.5.3", + "uvu": "^0.5.6" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..10e715c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,388 @@ +class Payto { + private url: URL; + + constructor(paytoString: string) { + this.url = new URL(paytoString); + if (this.url.protocol !== 'payto:') { + throw new Error('Invalid protocol, must be payto:'); + } + } + + private getHostnameParts(array: string[], type: string | null, position: number = 2): string | null { + if (type === null || array[1]?.toLowerCase() === type) { + return array[position] || null; + } + return null; + } + + private setHostnameParts(value: string | null, position: number = 2): void { + const addressArray = this.pathname.split('/'); + if (value) { + addressArray[position] = value; + } else { + addressArray.splice(position, 1); + } + this.pathname = '/' + addressArray.filter(part => part).join('/'); + } + + get address(): string | null { + return this.getHostnameParts(this.pathname.split('/'), null, 1); + } + + set address(value: string | null) { + this.setHostnameParts(value, 1); + } + + get amount(): string | null { + return this.searchParams.get('amount'); + } + + set amount(value: string | null) { + if (value) { + this.searchParams.set('amount', value); + } else { + this.searchParams.delete('amount'); + } + } + + get bic(): string | null { + return this.getHostnameParts(this.pathname.split('/'), 'bic', 2); + } + + set bic(value: string | null) { + this.setHostnameParts(value, 2); + } + + get currency(): [string | null, string | null] { + const result: [string | null, string | null] = [null, null]; + const amountValue = this.searchParams.get('amount'); + const fiatValue = this.fiat; + if (amountValue) { + const amountArray = amountValue.split(':'); + if (amountArray[0] && isNaN(parseFloat(amountArray[0]))) { + result[0] = amountArray[0]; + } + } + if (fiatValue) { + result[1] = fiatValue; + } + return result; + } + + set currency(value: [number?, string?, string?]) { + const [amount, token, fiat] = value; + if (fiat) this.fiat = fiat.toLowerCase(); + if (token) { + this.amount = `${token}:${amount ?? ''}`; + } else if (amount) { + this.amount = `${amount}`; + } + } + + get deadline(): string | null { + return this.searchParams.get('dl'); + } + + set deadline(value: string | null) { + if (value) { + this.searchParams.set('dl', value); + } else { + this.searchParams.delete('dl'); + } + } + + get donate(): string | null { + if (this.searchParams.has('donate')) { + const donateValue = this.searchParams.get('donate'); + return donateValue !== null ? donateValue : '1'; + } + return null; + } + + set donate(value: string | null) { + if (value === '0' || value === '1') { + this.searchParams.set('donate', value); + } else if (value) { + this.searchParams.set('donate', '1'); + } else { + this.searchParams.delete('donate'); + } + } + + get fiat(): string | null { + return this.searchParams.get('fiat')?.toLowerCase() || null; + } + + set fiat(value: string | null) { + if (value) { + this.searchParams.set('fiat', value); + } else { + this.searchParams.delete('fiat'); + } + } + + get hash(): string { + return this.url.hash; + } + + set hash(value: string) { + this.url.hash = value; + } + + get host(): string { + return this.url.host; + } + + set host(value: string) { + this.url.host = value; + } + + get hostname(): string { + return this.url.hostname.toLowerCase(); + } + + set hostname(value: string) { + this.url.hostname = value.toLowerCase(); + } + + get href(): string { + return this.url.href; + } + + set href(value: string) { + this.url.href = value; + } + + get iban(): string | null { + return this.getHostnameParts(this.pathname.split('/'), 'iban', 2); + } + + set iban(value: string | null) { + this.setHostnameParts(value, 2); + } + + get location(): string | null { + return this.searchParams.get('loc'); + } + + set location(value: string | null) { + if (value) { + this.searchParams.set('loc', value); + } else { + this.searchParams.delete('loc'); + } + } + + get message(): string | null { + return this.searchParams.get('message'); + } + + set message(value: string | null) { + if (value) { + this.searchParams.set('message', value); + } else { + this.searchParams.delete('message'); + } + } + + get network(): string { + return this.url.hostname.toLowerCase(); + } + + set network(value: string) { + this.url.hostname = value.toLowerCase(); + } + + get origin(): string | null { + const origin = this.url.origin; + return origin === 'null' || origin === '' ? null : origin; + } + + get password(): string { + return this.url.password; + } + + set password(value: string) { + this.url.password = value; + } + + get pathname(): string { + return this.url.pathname; + } + + set pathname(value: string) { + this.url.pathname = value; + } + + get port(): string { + return this.url.port; + } + + set port(value: string) { + this.url.port = value; + } + + get protocol(): string { + return this.url.protocol; + } + + set protocol(value: string) { + this.url.protocol = value; + } + + get receiverName(): string | null { + return this.searchParams.get('receiver-name'); + } + + set receiverName(value: string | null) { + if (value) { + this.searchParams.set('receiver-name', value); + } else { + this.searchParams.delete('receiver-name'); + } + } + + get recurring(): string | null { + return this.searchParams.get('rc')?.toLowerCase() || null; + } + + set recurring(value: string | null) { + if (value) { + this.searchParams.set('rc', value); + } else { + this.searchParams.delete('rc'); + } + } + + get route(): string | null { + return this.getHostnameParts(this.pathname.split('/'), null, 3); + } + + set route(value: string | null) { + this.setHostnameParts(value, 3); + } + + get routingNumber(): string | null { + return this.getHostnameParts(this.pathname.split('/'), 'ach', 2); + } + + set routingNumber(value: string | null) { + this.setHostnameParts(value, 2); + } + + get search(): string { + return this.url.search; + } + + set search(value: string) { + this.url.search = value; + } + + get searchParams(): URLSearchParams { + return this.url.searchParams; + } + + get split(): [string, string, boolean] | null { + const splitValue = this.searchParams.get('split'); + if (splitValue) { + const [amountPart, receiver] = splitValue.split('@'); + const [prefix, amount] = amountPart.split(':'); + return [receiver, amount, prefix === 'p']; + } + return null; + } + + set split(value: [string, string, boolean] | null) { + if (value) { + const [receiver, amount, percentage] = value; + const prefix = percentage ? 'p:' : ''; + this.searchParams.set('split', `${prefix}${amount}@${receiver}`); + } else { + this.searchParams.delete('split'); + } + } + + get value(): number | null { + const amount = this.searchParams.get('amount'); + if (amount) { + const amountArray = amount.split(':'); + let amountParsed; + if (amountArray[1]) { + amountParsed = parseFloat(amountArray[1]); + } else { + amountParsed = parseFloat(amountArray[0]); + } + if (!isNaN(amountParsed)) { + return amountParsed; + } + } + return null; + } + + set value(value: number | null) { + if (value) { + const amount = this.searchParams.get('amount'); + if (amount) { + const amountArray = amount.split(':'); + if (amountArray[1]) { + this.searchParams.set('amount', amountArray[0] + ':' + value); + } else { + this.searchParams.set('amount', value.toString()); + } + } + } else { + this.searchParams.delete('amount'); + } + } + + get username(): string { + return this.url.username; + } + + set username(value: string) { + this.url.username = value; + } + + toString(): string { + return this.url.toString(); + } + + toJSON(): string { + return this.url.toJSON(); + } + + toJSONObject(): object { + const obj: { [key: string]: any } = {}; + if (this.port) obj.port = this.port; + if (this.currency[0]) obj.currency = this.currency[0]; + if (this.currency[1]) obj.fiat = this.currency[1]; + if (this.amount) obj.amount = this.amount; + if (this.address) obj.address = this.address; + if (this.bic) obj.bic = this.bic; + if (this.deadline) obj.deadline = this.deadline; + if (this.donate) obj.donate = this.donate; + if (this.hash) obj.hash = this.hash; + if (this.host) obj.host = this.host; + if (this.hostname) obj.hostname = this.hostname; + if (this.href) obj.href = this.href; + if (this.iban) obj.iban = this.iban; + if (this.location) obj.location = this.location; + if (this.message) obj.message = this.message; + if (this.network) obj.network = this.network; + if (this.origin) obj.origin = this.origin; + if (this.password) obj.password = this.password; + if (this.pathname) obj.pathname = this.pathname; + if (this.protocol) obj.protocol = this.protocol; + if (this.receiverName) obj.receiverName = this.receiverName; + if (this.recurring) obj.recurring = this.recurring; + if (this.route) obj.route = this.route; + if (this.routingNumber) obj.routingNumber = this.routingNumber; + if (this.search) obj.search = this.search; + if (this.split) obj.split = this.split; + if (this.username) obj.username = this.username; + if (this.value) obj.value = this.value; + return obj; + } +} + +export default Payto; diff --git a/test/paytoRl.test.ts b/test/paytoRl.test.ts new file mode 100644 index 0000000..5afee30 --- /dev/null +++ b/test/paytoRl.test.ts @@ -0,0 +1,80 @@ +import { suite } from 'uvu'; +import * as assert from 'uvu/assert'; +import Payto from 'payto-rl'; + +const test = suite('Payto'); + +test('constructor with valid payto string', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.1'); + assert.is(payto.href, 'payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.1'); +}); + +test('constructor with invalid protocol', () => { + assert.throws(() => { + new Payto('http://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.1'); + }, /Invalid protocol, must be payto:/); +}); + +test('get and set address', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'); + assert.is(payto.address, '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'); + payto.address = '1BoatSLRHtKNngkdXEeobR76b53LETtpyT'; + assert.is(payto.address, '1BoatSLRHtKNngkdXEeobR76b53LETtpyT'); +}); + +test('get and set amount', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.1'); + assert.is(payto.amount, '0.1'); + payto.amount = '0.2'; + assert.is(payto.amount, '0.2'); +}); + +test('get and set currency', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=btc:0.1&fiat=usd'); + assert.equal(payto.currency, ['btc', 'usd']); + payto.currency = [0.2, 'eth', 'eur']; + assert.equal(payto.currency, ['eth', 'eur']); +}); + +test('get and set deadline', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?dl=1672531199'); + assert.is(payto.deadline, '1672531199'); + payto.deadline = '1672617599'; + assert.is(payto.deadline, '1672617599'); +}); + +test('get and set donate', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?donate=1'); + assert.is(payto.donate, '1'); + payto.donate = '0'; + assert.is(payto.donate, '0'); + payto.donate = null; + assert.is(payto.donate, null); +}); + +test('get and set fiat', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?fiat=usd'); + assert.is(payto.fiat, 'usd'); + payto.fiat = 'eur'; + assert.is(payto.fiat, 'eur'); +}); + +test('toJSONObject', () => { + const payto = new Payto('payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.1&fiat=usd'); + const jsonObject = payto.toJSONObject(); + assert.equal(jsonObject, { + address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + amount: '0.1', + fiat: 'usd', + host: 'btc', + hostname: 'btc', + href: 'payto://btc/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.1&fiat=usd', + network: 'btc', + pathname: '/1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + protocol: 'payto:', + search: '?amount=0.1&fiat=usd', + value: 0.1 + }); +}); + +test.run(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..75e440c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "outDir": "./dist", + "strict": true, + "forceConsistentCasingInFileNames": true, + "typeRoots": ["./node_modules/@types", "./types"], + "removeComments": true, + "declaration": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "test"] +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..f07c052 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,99 @@ +declare class Payto { + private url: URL; + + constructor(paytoString: string); + + private getHostnameParts(array: string[], type: string | null, position?: number): string | null; + private setHostnameParts(value: string | null, position?: number): void; + + get address(): string | null; + set address(value: string | null); + + get amount(): string | null; + set amount(value: string | null); + + get bic(): string | null; + set bic(value: string | null); + + get currency(): [string, string?, string?]; + set currency(value: [number?, string?, string?]); + + get deadline(): string | null; + set deadline(value: string | null); + + get donate(): boolean; + set donate(value: boolean); + + get fiat(): string | null; + set fiat(value: string | null); + + get hash(): string; + set hash(value: string); + + get host(): string; + set host(value: string); + + get hostname(): string; + set hostname(value: string); + + get href(): string; + set href(value: string); + + get iban(): string | null; + set iban(value: string | null); + + get location(): string | null; + set location(value: string | null); + + get message(): string | null; + set message(value: string | null); + + get network(): string; + set network(value: string); + + get origin(): string | null; + + get password(): string; + set password(value: string); + + get pathname(): string; + set pathname(value: string); + + get port(): string; + set port(value: string); + + get protocol(): string; + set protocol(value: string); + + get receiverName(): string | null; + set receiverName(value: string | null); + + get recurring(): string | null; + set recurring(value: string | null); + + get route(): string | null; + set route(value: string | null); + + get routingNumber(): string | null; + set routingNumber(value: string | null); + + get search(): string; + set search(value: string); + + get searchParams(): URLSearchParams; + + get split(): [string, string, boolean] | null; + set split(value: [string, string, boolean] | null); + + get username(): string; + set username(value: string); + + get value(): number | null; + set value(value: number | null); + + toString(): string; + toJSON(): string; + toJSONObject(): object; +} + +export default Payto;