[
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"root\": true,\n    \"parser\": \"@typescript-eslint/parser\",\n    \"plugins\": [\n      \"@typescript-eslint\"\n    ],\n    \"extends\": [\n      \"eslint:recommended\",\n      \"plugin:@typescript-eslint/eslint-recommended\",\n      \"plugin:@typescript-eslint/recommended\"\n    ],\n    \"env\": {\n        \"commonjs\": true,\n        \"es2020\": true,\n        \"node\": true\n    },\n    \"rules\": {\n      \"@typescript-eslint/explicit-module-boundary-types\": \"off\"\n    }\n  }"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\n\n# Local Netlify folder\n.netlify\n\n**/package-lock.json\n**/pnpm-lock.yaml\n**/node_modules\n**/.env\n**/local.settings.json\n\n**/tsconfig.package.tsbuildinfo\n**/build/\n**/dist/\n\n!config\nconfig/*\n!config/default.json"
  },
  {
    "path": ".nvmrc",
    "content": "16"
  },
  {
    "path": "LICENCE",
    "content": "Elastic License 2.0 (ELv2)\n\nURL: https://www.elastic.co/licensing/elastic-license\n\n## Acceptance\n\nBy using the software, you agree to all of the terms and conditions below.\n\n## Copyright License\n\nThe licensor grants you a non-exclusive, royalty-free, worldwide,\nnon-sublicensable, non-transferable license to use, copy, distribute, make\navailable, and prepare derivative works of the software, in each case subject to\nthe limitations and conditions below.\n\n## Limitations\n\nYou may not provide the software to third parties as a hosted or managed\nservice, where the service provides users with access to any substantial set of\nthe features or functionality of the software.\n\nYou may not move, change, disable, or circumvent the license key functionality\nin the software, and you may not remove or obscure any functionality in the\nsoftware that is protected by the license key.\n\nYou may not alter, remove, or obscure any licensing, copyright, or other notices\nof the licensor in the software. Any use of the licensor’s trademarks is subject\nto applicable law.\n\n## Patents\n\nThe licensor grants you a license, under any patent claims the licensor can\nlicense, or becomes able to license, to make, have made, use, sell, offer for\nsale, import and have imported the software, in each case subject to the\nlimitations and conditions in this license. This license does not cover any\npatent claims that you cause to be infringed by modifications or additions to\nthe software. If you or your company make any written claim that the software\ninfringes or contributes to infringement of any patent, your patent license for\nthe software granted under these terms ends immediately. If your company makes\nsuch a claim, your patent license ends immediately for work on behalf of your\ncompany.\n\n## Notices\n\nYou must ensure that anyone who gets a copy of any part of the software from you\nalso gets a copy of these terms.\n\nIf you modify the software, you must include in any modified copies of the\nsoftware prominent notices stating that you have modified the software.\n\n## No Other Rights\n\nThese terms do not imply any licenses other than those expressly granted in\nthese terms.\n\n## Termination\n\nIf you use the software in violation of these terms, such use is not licensed,\nand your licenses will automatically terminate. If the licensor provides you\nwith a notice of your violation, and you cease all violation of this license no\nlater than 30 days after you receive that notice, your licenses will be\nreinstated retroactively. However, if you violate these terms after such\nreinstatement, any additional violation of these terms will cause your licenses\nto terminate automatically and permanently.\n\n## No Liability\n\n*As far as the law allows, the software comes as is, without any warranty or\ncondition, and the licensor will not be liable to you for any damages arising\nout of these terms or the use or nature of the software, under any kind of\nlegal claim.*\n\n## Definitions\n\nThe **licensor** is the entity offering these terms, and the **software** is the\nsoftware the licensor makes available under these terms, including any portion\nof it.\n\n**you** refers to the individual or entity agreeing to these terms.\n\n**your company** is any legal entity, sole proprietorship, or other kind of\norganization that you work for, plus all organizations that have control over,\nare under the control of, or are under common control with that\norganization. **control** means ownership of substantially all the assets of an\nentity, or the power to direct its management and policies by vote, contract, or\notherwise. Control can be direct or indirect.\n\n**your licenses** are all the licenses granted to you for the software under\nthese terms.\n\n**use** means anything you do with the software requiring one of your licenses.\n\n**trademark** means trademarks, service marks, and similar rights."
  },
  {
    "path": "README.md",
    "content": "<!-- markdownlint-disable MD033 MD041 -->\n\n<br />\n\n<!-- http://ipa-reader.xyz/?text=ki%3Age%C9%AAt&voice=Joey --->\n\n<h2 align=\"center\">\n\nKeagate *(&#107;&#105;&colon;&#103;&#101;&#618;&#116;)* – A High-Performance Cryptocurrency Payment Gateway\n\n</h2>\n<!--\n<h4 align=\"center\">\n  <b>🚧 This project is actively in development 🚧</b>\n</h4>\n--->\n\n<h4 align=\"center\">\n    <img alt=\"Snyk vulnerabilities\" src=\"https://shields.io/snyk/vulnerabilities/github/dilan-dio4/keagate?style=flat-square\" />\n    <img alt=\"GitHub last commit\" src=\"https://img.shields.io/github/last-commit/dilan-dio4/Keagate?style=flat-square\">\n    <img alt=\"GitHub top language\" src=\"https://img.shields.io/github/languages/top/dilan-dio4/Keagate?style=flat-square\">\n    <a href=\"https://gitter.im/Keagate/community?utm_source=share-link&utm_medium=link&utm_campaign=share-link\"><img alt=\"Gitter\" src=\"https://img.shields.io/gitter/room/dilan-dio4/Keagate?style=flat-square\"></a>\n    <a href=\"https://dilan-dio4.github.io/keagate-example-swagger/\"><img alt=\"Swagger Validator\" src=\"https://img.shields.io/swagger/valid/3.0?specUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fdilan-dio4%2Fkeagate-example-swagger%2Fmain%2Fkeagate-openapi3.json&style=flat-square\"></a>\n</h4>\n\n<br />\n\n<p align=\"center\">\n  <img src=\"assets/icon-tiny.png\" width=\"150\" alt=\"Keagate Icon\">\n</p>\n\n<!-- TODO: Keagate Vector --->\n\n<!-- TABLE OF CONTENTS -->\n## Table of Contents\n\n* [About the Project](#about-the-project)\n  * [Purpose](#purpose)\n* [Installation](#installation)\n  * [One-liner](#one-liner)\n  * [Manual](#manual-installation)\n* [Configuration](#configuration)\n  * [CLI](#cli)\n  * [Custom](#custom)\n* [Payment Lifecycle](#payment-lifecycle)\n  * [API Method](#api-method)\n  * [Invoice Client Method](#invoice-client-method)\n* [Instant Payment Notifications](#instant-payment-notifications)\n* [Development](#development)\n  * [Adding an API Route](#adding-an-api-route)\n  * [Customizing the Invoice Interface](#customizing-the-invoice-interface)\n\n## About the Project\n\nKeagate is a self-hosted, high-performance cryptocurrency payment gateway. Payments can be administered [via API](https://dilan-dio4.github.io/keagate-example-swagger/) for flexibility or with the built-in invoicing client (*image below*).\n\n**Supported currencies: Bitcoin, Ethereum, Dogecoin, Solana, Litecoin, Polygon, Dash,** *Ripple (coming soon), Tron (coming soon)*.\n\n<p align=\"left\">\n  <img src=\"assets/invoice-frame.png\" width=\"600\" alt=\"Invoice Preview\">\n</p>\n\n### Purpose\n\n* No KYC\n* No fees, middleman, or escrow\n* Self-hosted/Private\n* Easily extensible\n* Lightweight and highly performant\n\nFunds go directly to your wallet via a one-time address that is generated for each payment.\n\n## Installation\n\n<p align=\"left\">\n  <a href=\"https://www.youtube.com/watch?v=dxMZIbeRJac\">\n    <img src=\"assets/yt-screenshot.png\" width=\"700\" alt=\"Keagate Youtube Preview\">\n  </a>\n</p>\n\n### One-liner\n\nThe purpose of this installation script is to get Keagate up-and-running quickly in a Linux environment. The CLI will guide you in configuring, managing, and securing the instance.\n\n```bash\nbash -c \"$(curl -sSL https://raw.githubusercontent.com/dilan-dio4/Keagate/main/packages/scripts/keagate.sh)\"\n```\n\n*Alternate*:\n\n```bash\ncurl -o keagate.sh https://raw.githubusercontent.com/dilan-dio4/Keagate/main/packages/scripts/keagate.sh\nchmod +x keagate.sh\n./keagate.sh\n```\n\nThis helper script has been tested on...\n\n* Ubuntu 18+\n* Debian 10+\n* Amazon Linux 4.14+\n* CentOS 7.9+\n\n...via AWS and Azure.\n\n<!-- No Docker quick install on Redhat RHEL -->\n\nThis script should run successfully on most flavors of Linux with some configuration. Otherwise, use the manual build, as it's fairly straightforward.\n\n### Manual Installation\n\n#### Prerequisites\n\n* MongoDB – [Install](https://www.mongodb.com/docs/manual/installation/)\n  * Running on your machine **OR** remotely via a connection string\n* Web server (like Nginx or Apache2) – [Install](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)\n  * Serving as a reverse proxy to `localhost:8081`\n  * `8081` is the default port that Keagate runs on, can be changed via the [*PORT* configuration option](#custom).\n* Node > 14 and NPM – [Install](https://github.com/nvm-sh/nvm#installing-and-updating)\n  * Use of `nvm` to manage Node and NPM is recommended\n\n```bash\n# +++ Don't have Node?\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash\nnvm install 16\nnvm use 16\n# ---\n\nnpm i -g pnpm\npnpm setup\npnpm -g pm2\n\ngit clone https://github.com/dilan-dio4/Keagate\ncd Keagate\npnpm i\npnpm run build\n\n# +++ Configure Keagate with:\nnode packages/scripts/build/configure.js\n# --- OR manually (see Configuration section)\n\npm2 start packages/backend/build/index.js --name Keagate --time\n```\n\n## Configuration\n\nKeagate requires some configuration. This is done via a file called `local.json` in `/config`, next to `default.json`. This file will automatically be used when you start Keagate. *Note that parameters in `local.json` will overwrite those in `default.json`*.\n\nThere are **two** methods to configure Keagate, and they can be used in conjunction with each other.\n\n### CLI\n\nKeagate has a built-in CLI to build configurations in [packages/scripts](packages/scripts/src/configure.ts). After you've cloned and built the package. Head to the root *Keagate* directory and execute the following:\n\n```bash\nnode packages/scripts/build/configure.js\n```\n\n*Note – this CLI is automatically launched in the one-liner installation script.*\n\nThe CLI will write the `config/local.json` file upon completion unless one already exists. In that case, it will write to `config/local2.json` and ask that you manually merge your new parameters, as needed.\n\n### Custom\n\nCreate or open the file `local.json` in `/config`. You can use the provided `default.json` file as a reference **(your `local.json` will override these)**.\n\n*The schema of the Keagate configuration can be seen (in TypeScript) at [packages/common/src/config.ts](packages/common/src/config.ts).*\n\n#### Currencies\n\nTo configure a single currency, add an object with the key of the currency's ticker with the following attributes:\n\nTicker can be one of `'LTC', 'BTC', 'ETH', 'DOGE', 'SOL', 'DASH', or 'MATIC'`. [See example](#example).\n\n| Key                              | Description                    | Required | Default |\n|----------------------------------|----------------------------|----------------------------------|--|\n| `ADMIN_PUBLIC_KEY`          | Public key (address) of your admin wallet    | **Yes** | *null* (string) |\n| `ADMIN_PRIVATE_KEY`         | Private key of admin wallet. Only needed if you plan on programmatically sending transactions  | No | *null* (string) |\n\n#### Protected options\n\nThis section details specific configuration parameters that should be handled with extra care. A malicious actor could manipulate the integrity of payments if they had access to these parameters.\n\n**There's a built-in script to securely generate and print these values at random:**\n\n```bash\nnode packages/scripts/build/setupSeeds.js\n# OR\nts-node packages/scripts/src/setupSeeds.ts\n\n# Prints\n{\n  \"INVOICE_ENC_KEY\": \"5036...9cc3\",\n  \"SEED\": \"eb08...3afc\",\n  \"KEAGATE_API_KEY\": \"9fac8f7d...c6568f97\",\n  \"IPN_HMAC_SECRET\": \"e50dd645...ea5baf54\"\n}\n```\n\n| Key                              | Description                    | Required | Default |\n|----------------------------------|----------------------------|----------------------------------|--|\n| `SEED`         | Seed for transactional wallet generator. Must be a 128-bit hex string. **Protect this value in production** | **Yes** | *null* (string) |\n| `KEAGATE_API_KEY`         | Api key that will be required in administrative request's `keagate-api-key` header. **Protect this value in production** | No | 'API-KEY' (string) |\n| `INVOICE_ENC_KEY`         | Key that will be used to encrypt payment IDs when distributed via invoice. **Protect this value in production** | **Yes** | *null* (string) |\n| `IPN_HMAC_SECRET`         | Key of the HMAC signature that is set in the `x-keagate-sig` header of each POST request when using [Instant Payment Notifications](#instant-payment-notifications). **Protect this value in production** | No | *null* (string) |\n\n#### Other options\n\n| Key                              | Description                    | Required | Default |\n|----------------------------------|----------------------------|----------------------------------|--|\n| `IP_WHITELIST`         | List of IP address [\"1.1.1.1\" , \"2.2.2.2\",...] to be whitelisted for administrative requests | No | [] (string[]) |\n| `TRANSACTION_TIMEOUT` | Milliseconds by which payments will be valid for. After that, the payment is expired | No | 1200000 [20 Minutes] (number) |\n| `TRANSACTION_MIN_REFRESH_TIME` | Minimum milliseconds by which transactions will idle between refreshes | No | 30000 [30 Seconds] (number) |\n| `TRANSACTION_SLIPPAGE_TOLERANCE` | Percentage of a total payment that is discounted as slippage.<br /><br />Example: a TRANSACTION_SLIPPAGE_TOLERANCE of 0.02 for a 100 SOL payment will be fulfilled at 98 SOL. | No | 0.02 (number) |\n| `BLOCKBOOK_RETRY_DELAY` | Milliseconds to wait before re-trying a failed Blockbook request. | No | 5000 (number) |\n| `MONGO_CONNECTION_STRING` | Connection string for MongoDB instance including any authentication. | No | 'mongodb://localhost:27017' (string) |\n| `MONGO_KEAGATE_DB` | Mongo database to use for storing/managing payments | No | 'keagate' (string) |\n| `IS_DEV` | **For development only**. Turn on testnets for given currencies and activate development features | No | false (boolean) |\n| `HOST` | Your domain or IP that Keagate is running on. **This is used for aesthetics and has no functional effect on Keagate** | No | *null* (string) |\n| `PORT` | The port that Keagate's backend API will run on | No | 8081 (number) |\n\n<!-- | `USE_SO_CHAIN` | [SoChain](https://sochain.com/api/#introduction) is a free blockchain infrastructure API for that allows for 300 requests/minute free-of-charge.<br /><br />Setting this to `true` will utilize SoChain for part of the btc, dash, and ltc payment process. **Recommended** | No | true (boolean) | -->\n\n#### Example\n\nYour `config/local.json` could look something like:\n\n```js\n{\n  \"LTC\": {\n    \"ADMIN_PUBLIC_KEY\": \"MY_WALLET_ADDRESS\",\n    \"ADMIN_PRIVATE_KEY\": \"MY_PRIVATE_KEY\"\n  },\n\n  \"KEAGATE_API_KEY\": \"abcd123\",\n  \"IP_WHITELIST\": [\"1.1.1.1\",\"2.2.2.2\"]\n  // ...\n}\n```\n\n## Payment Lifecycle\n\n### API Method\n\nThe workflow for creating & confirming payments in the API-driven method is as follows:\n\n1. Invoke the [`createPayment`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/post_createPayment) route. Pass the following parameters in the JSON body:\n\n    * *amount* – The total value of the payment\n    * *currency* – Shorthand name of the desired currency (e.g. `\"LTC\"`)\n    * [*ipnCallbackUrl* – URL to synchronously send payment status updates to](#instant-payment-notifications)\n    * **Optional** *extraId* – Some external identification string (useful for manually managing the identity of payment with [`getPaymentsByExtraId`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/get_getPaymentsByExtraId))\n\n2. Notify your customer to send the *amount* to the *publicKey* returned from [`createPayment`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/post_createPayment).\n  \n    * **Optional** Display [other metadata](https://dilan-dio4.github.io/keagate-example-swagger/#model-def-0) as well, for a streamlined user experience. Use the screenshot above as a reference.\n\n3. Wait for your customer to send the payment.\n\n    * **Optional** Invoke the [`getInvoiceStatus`](https://dilan-dio4.github.io/keagate-example-swagger/#/Invoice/get_getInvoiceStatus) route on a timer from the customer's device to provide real-time updates. *Note: this route doesn't return any sensitive information*.\n\n4. Confirm and process the payment in your [IPN](#instant-payment-notifications) route after receiving a **CONFIRMED** status of a payment of the same *id* or *extraId*.\n\n### Invoice Client Method\n\nThe workflow for creating & confirming payments with the built-in invoice client is as follows:\n\n1. Invoke the [`createPayment`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/post_createPayment) route. Pass the following parameters in the JSON body:\n\n    * *amount* – The total value of the payment\n    * *currency* – Shorthand name of the desired currency (e.g. `\"LTC\"`)\n    * Either or both of:\n      * [*invoiceCallbackUrl* – Route that your customers will be directed to after their payment finishes](#invoice-client-callback-url)\n      * [*ipnCallbackUrl* – URL to synchronously send payment status updates to](#instant-payment-notifications)\n    * **Optional** *extraId* – Some external identification string (useful for manually managing the identity of payment with [`getPaymentsByExtraId`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/get_getPaymentsByExtraId))\n\n2. Direct users to the URL route provided in the `invoiceUrl` attribute returned from [`createPayment`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/post_createPayment).\n\n    * *Note: this is not a full URL, just a path. It should be appended to the URL of your Keagate server*.\n\n3. Confirm and process the payment with either method:\n\n    * If you set *ipnCallbackUrl*, with your [IPN](#instant-payment-notifications) route after receiving a **CONFIRMED** status of a payment of the same *id* or *extraId*.\n    * If you set *invoiceCallbackUrl*, with the [Invoice Client Callback Url](#invoice-client-callback-url) page that customers are directed to after the invoice client observes a finished payment.\n\n### Invoice Client Callback Url\n\nTwo query parameters are appended to this URL when customers are directed to it from the invoice client:\n\n* *invoice_id* – The encrypted id of the payment (as seen in the `invoiceUrl` attribute of [`createPayment`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/post_createPayment)).\n* *status* – The [*assumed*] status of the payment: \"CONFIRMED\", \"FAILED\", \"EXPIRED\", etc. **This is just to help you visually notify the customer in your site, but cannot be trusted (see below)**.\n\n**You must still confirm the payment server-side**. This is because a malicious customer could just manually go to this route and set the status to **CONFIRMED**. On your backend, use [`getPaymentStatus`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/get_getPaymentStatus) to actually validate the status.\n\n*Note: you can send either a true database id or invoice_id [which are just encrypted database ids] to [`getPaymentStatus`](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/get_getPaymentStatus). Keagate can figure out which id it is based on the input length.*\n\nOnce you've validated the payment status via a server-side request from the *invoiceCallbackUrl* page, you can confirm and process the payment as normal.\n\n## Instant Payment Notifications\n\nTo be notified of payment updates in real-time, use instant payment notifications (IPN).\n\n### Use IPNs\n\n1. Make sure you have configured the *IPN_HMAC_SECRET* attribute in [Configuration](#protected-options). This will allow you to guarantee the origin and trust in the integrity of incoming messages.\n2. Have access to some API or serverless function that can be invoked publicly via URL.\n3. Pass this URL into the `ipnCallbackUrl` attribute of your [*createPayment*](https://dilan-dio4.github.io/keagate-example-swagger/#/Payment/post_createPayment) requests.\n\nJust like that, IPNs are all set up on Keagate. A POST request will be sent to the `ipnCallbackUrl` with a JSON object like that of [*TypeForRequest*](https://dilan-dio4.github.io/keagate-example-swagger/#model-def-0).\n\nBefore using these notifications, the last thing to do is validate all incoming messages via HMAC.\n\n### Validate IPN Messages\n\nThe previously configured *IPN_HMAC_SECRET* is used as a key in the sha-512 HMAC signature generated for the `x-keagate-sig` header of each notification.\n\n*Note: be sure to sort the request body alphabetically before generating your HMAC.*\n\nHere's a NodeJS example of validating this header in Express.\n\n```js\nvar crypto = require('crypto')\nvar express = require('express')\n\nconst app = express()\napp.use(express.json())\n\napp.post('/ipnCallback', (req, res) => {\n  // +++ Generate my signature\n  const hmac = crypto.createHmac('sha512', IPN_HMAC_SECRET)\n  hmac.update(JSON.stringify(req.body, Object.keys(req.body).sort()))\n  const signature = hmac.digest('hex')\n  // ---\n\n  if (signature === req.headers['x-keagate-sig']) {\n    // Good to go!\n    const id = req.body.id\n    // ...\n  } else {\n    // This notification may be spoofed...\n  }\n});\n```\n\n## Development\n\nDevelopment experience and extensibility are the utmost priority of this package.\n\nTo get started:\n\n1. Clone this repo.\n2. Install `pnpm` globally with `npm i -g pnpm`\n3. `cd Keagate && pnpm i`\n4. Add a MongoDB connection to the `MONGO_CONNECTION_STRING` attribute in `config/local.json`, along with some admin wallet credentials and the other [required configuration parameters](#custom). For development, the [Mongo Atlas free tier](https://www.mongodb.com/cloud/atlas/signup) works great.\n5. `pnpm run dev` to start the invoice client and backend.\n    * Any changes in `packages/invoice-client/src` will be automatically reflected on refresh.\n    * Any changes to the source of `packages/backend/src` will be reflected automatically via `ts-node-dev`.\n    * Any changes to `config/local.json` have to be manually refreshed.\n\nThe backend will run at `127.0.0.1:8081`. You can see your Swagger API docs at `http://127.0.0.1/docs`. Also, a test IPN callback server will run at `127.0.0.1:8082/ipnCallback` and a test invoice client redirect static site will be available at `http://127.0.0.1/dev-callback-site`.\n\n<details>\n\n<summary>\n\n### Adding an API Route\n\n</summary>\n\nKeagate follows the [Fastify plugin pattern](https://www.fastify.io/docs/latest/Reference/Plugins/). Place your route in [`packages/backend/src/routes`](packages/backend/src/routes). The `default export` of the file should be a function that takes a *Fastify* instance as a parameter. In that function, add your route to the provided *Fastify* instance. **Be sure to add a schema to your route via the `RouteShorthandOptions` type exported from Fastify. Schemas should be built with [TypeBox](https://github.com/sinclairzx81/typebox).\n\nThe schemas will appear in your Swagger docs for a unified developer experience.\n\nFinally, in [`packages/backend/src/index.ts`](packages/backend/src/index.ts), register your new route like so:\n\n```ts\nimport createPaymentStatusRoute from './routes/paymentStatus';\nimport createPaymentsByExtraIdRoute from './routes/paymentsByExtraId';\nimport create_YOUR_FUNCTIONALITY_Route from './routes/YOUR_FUNCTIONALITY'; // <--\n\n// ...\n\nserver.register(createPaymentStatusRoute);\nserver.register(createPaymentsByExtraIdRoute);\nserver.register(create_YOUR_FUNCTIONALITY_Route); // <--\n```\n\nUse [`packages/backend/src/routes/activePayments.ts`](packages/backend/src/routes/activePayments.ts) as a reference of an authenticated route.\n\nUse [`packages/backend/src/routes/invoiceStatus.ts`](packages/backend/src/routes/invoiceStatus.ts) as a reference of an unauthenticated route.\n\n</details>\n<details>\n\n<summary>\n\n### Customizing the Invoice Interface\n\n</summary>\n\nThe invoice client is a statically built React package (via Vite). This static build is served in `backend`. This functionality can be seen [here](packages/backend/src/routes/invoiceClient.ts).\n\nEditing the react package will automatically build to `packages/invoice-client/dist`, so just refresh the page to see any changes.\n\nThe source of `invoice-client`'s React project is pretty straightforward, so those familiar with React (& TailwindCSS) should have an easy time making their desired alterations.\n\n</details>\n\n<!--\n<details>\n\n<summary>\n\n### Adding a currency\n\n</summary>\n\nThere's four steps in adding a currency to this package.\n\n1. Add the ticker, along with some metadata, to the currencies type in [packages/common/src/currencies.ts](packages/common/src/currencies.ts).\n2. Create the admin wallet. This is where payments are finally sent to and presumably the real wallet of the client. Note that the admin wallet can also be used to programmatically send transactions as well.\n    * Start by taking a look at [Solana's admin wallet](packages/backend/src/adminWallets/Solana/index.ts) and note that you only need to implement two functions: `getBalance` and `sendTransaction`. The class that admin wallets inherit from, [GenericAdminWallet](packages/backend/src/adminWallets/GenericAdminWallet.ts), handles class inheritance.\n3. Create the transactional wallet. This class can be thought of a payment, since a new transactional wallet is created for every payment, along with a new Public Key and Private Key. Transactional wallets, and their associated payment data, are stored in Mongo.\n    1. Start by taking a look at [Solana's transactional wallet](packages/backend/src/transactionalWallets/Solana/index.ts) and note that you only need to implement three functions: `fromNew`, `getBalance` and `_cashOut`. The class that transactional wallets inherit from, [GenericTransactionalWallet](packages/backend/src/transactionalWallets/GenericTransactionalWallet.ts), handles the rest.  \n4. Add both the transactional and admin wallet classes to [packages/backend/src/currenciesToWallets.ts](packages/backend/src/currenciesToWallets.ts) so it can be referred to by ticker across the project\n\n**And that's it!** Start the dev environment (`pnpm run dev`) and create a new payment of any amount with your new currency.\n\n</details>\n\n<details>\n\n<summary>\n\n### Adding a blockchain API provider\n\n</summary>\n\nThe invoice client is a statically built React package (via Vite). This static build is served in `backend`. This functionality can be seen [here](packages/backend/src/routes/invoiceClient.ts).\n\nEditing the react package will automatically build to `dist`, so just refresh the page to see the changes.\n\nThe source code in invoice client is pretty straight-forward, so anyone familiar with React (& TailwindCSS) should have an easy time making their desired alterations.\n\n</details>\n\n<details>\n\n<summary>\n\n### API Providers\n\n</summary>\n\nIn order to check wallet balances and broadcast transactions, Keagate needs to interact with particular blockchain APIs. There's a variety of providers out there that support different sets of blockchains. This packages bundles up connectors to popular providers with a simple, unified API.\n\nExisting connectors can be seen in the [packages/api-providers](packages/api-providers/src/) folder. All of the one available in this package provide generous free tiers. Simply pass your API keys with the configuration below.\n\nCurrently available API providers:\n\n| Name  | Available chains |\n|-----------------|--------------|\n| NowNodes | dash, ltc, btc |\n| Tatum | ltc, btc, ada, and xrp |\n\nIt's very easy to add a provider, see [TatumProvider.ts](packages/api-providers/src/TatumProvider.ts) as an example.\n\nMake sure that one of the available API providers cover each currency you plan on using.\n\n</details>\n\n-->\n"
  },
  {
    "path": "TODO.md",
    "content": "# TODOs\n\n* ~~Flexible expire timer UI in invoice client~~\n  * ~~Formats like *DD-MM-YY hh:mm a* or *day-of-week hh:mm a* depending on expiration value.~~\n* Inline web payments via JS SDK\n  * Small pop-up UI (like Stripe)\n* Native api for request throttle values\n* Dynamic throttling\n  * Keagate public API for base times\n    * Blockbook and other providers have changing rate-limits\n  * Based on number of active payments(?)\n* Invoice client unconfirmed balance view\n  * Only needed for certain chains (DASH, LTC, BTC, DOGE)\n"
  },
  {
    "path": "assets/coins.json",
    "content": "{\n    \"bitcoin\": [\n        {\n            \"address_type\": 0,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": \"bc\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://btc1.trezor.io\",\n                \"https://btc2.trezor.io\",\n                \"https://btc3.trezor.io\",\n                \"https://btc4.trezor.io\",\n                \"https://btc5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Bitcoin\",\n            \"coin_name\": \"Bitcoin\",\n            \"coin_shortcut\": \"BTC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Bitcoin\",\n            \"segwit\": true,\n            \"shortcut\": \"BTC\",\n            \"signed_message_header\": \"Bitcoin Signed Message:\\n\",\n            \"slip44\": 0,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.5.2\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 111,\n            \"address_type_p2sh\": 196,\n            \"bech32_prefix\": \"tb\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://tbtc1.trezor.io\",\n                \"https://tbtc2.trezor.io\"\n            ],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Testnet\",\n            \"coin_name\": \"Testnet\",\n            \"coin_shortcut\": \"TEST\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 10000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Testnet\",\n            \"segwit\": true,\n            \"shortcut\": \"TEST\",\n            \"signed_message_header\": \"Bitcoin Signed Message:\\n\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.5.2\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 70615956,\n            \"xpub_magic\": 70617039,\n            \"xpub_magic_segwit_native\": 73342198,\n            \"xpub_magic_segwit_p2sh\": 71979618\n        },\n        {\n            \"address_type\": 53,\n            \"address_type_p2sh\": 55,\n            \"bech32_prefix\": \"acm\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Actinium\",\n            \"coin_name\": \"Actinium\",\n            \"coin_shortcut\": \"ACM\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"28d77872e23714562f49a1be792c276623c1bbe3fdcf21b6035cfde78b00b824\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100000,\n            \"name\": \"Actinium\",\n            \"segwit\": true,\n            \"shortcut\": \"ACM\",\n            \"signed_message_header\": \"Actinium Signed Message:\\n\",\n            \"slip44\": 228,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 55,\n            \"address_type_p2sh\": 16,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Axe\",\n            \"coin_name\": \"Axe\",\n            \"coin_shortcut\": \"AXE\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000c33631ca6f2f61368991ce2dc03306b5bb50bf7cede5cfbba6db38e52e6\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Axe\",\n            \"segwit\": false,\n            \"shortcut\": \"AXE\",\n            \"signed_message_header\": \"DarkCoin Signed Message:\\n\",\n            \"slip44\": 4242,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.3\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 50221816,\n            \"xpub_magic\": 50221772,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 230,\n            \"address_type_p2sh\": 235,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"BitCash\",\n            \"coin_name\": \"BitCash\",\n            \"coin_shortcut\": \"BITC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"7d57d87ff3c15a521530af60edee1887fba9c193eb518face926785c4cd8f4f1\",\n            \"max_address_length\": 53,\n            \"maxfee_kb\": 30000000,\n            \"min_address_length\": 52,\n            \"minfee_kb\": 1000,\n            \"name\": \"BitCash\",\n            \"segwit\": false,\n            \"shortcut\": \"BITC\",\n            \"signed_message_header\": \"Bitcash Signed Message:\\n\",\n            \"slip44\": 230,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 25,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 300,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Bitcloud\",\n            \"coin_name\": \"Bitcloud\",\n            \"coin_shortcut\": \"BTDX\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"000002d56463941c20eae5cb474cc805b646515d18bc7dc222a0885b206eadb0\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 10000,\n            \"name\": \"Bitcloud\",\n            \"segwit\": false,\n            \"shortcut\": \"BTDX\",\n            \"signed_message_header\": \"Diamond Signed Message:\\n\",\n            \"slip44\": 218,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 0,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://bch1.trezor.io\",\n                \"https://bch2.trezor.io\",\n                \"https://bch3.trezor.io\",\n                \"https://bch4.trezor.io\",\n                \"https://bch5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": \"bitcoincash\",\n            \"coin_label\": \"Bitcoin Cash\",\n            \"coin_name\": \"Bcash\",\n            \"coin_shortcut\": \"BCH\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": true,\n            \"fork_id\": 0,\n            \"hash_genesis_block\": \"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 500000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Bitcoin Cash\",\n            \"segwit\": false,\n            \"shortcut\": \"BCH\",\n            \"signed_message_header\": \"Bitcoin Signed Message:\\n\",\n            \"slip44\": 145,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 111,\n            \"address_type_p2sh\": 196,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": \"bchtest\",\n            \"coin_label\": \"Bitcoin Cash Testnet\",\n            \"coin_name\": \"Bcash Testnet\",\n            \"coin_shortcut\": \"TBCH\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": true,\n            \"fork_id\": 0,\n            \"hash_genesis_block\": \"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 10000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Bitcoin Cash Testnet\",\n            \"segwit\": false,\n            \"shortcut\": \"TBCH\",\n            \"signed_message_header\": \"Bitcoin Signed Message:\\n\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 70615956,\n            \"xpub_magic\": 70617039,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 38,\n            \"address_type_p2sh\": 23,\n            \"bech32_prefix\": \"btg\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://btg1.trezor.io\",\n                \"https://btg2.trezor.io\",\n                \"https://btg3.trezor.io\",\n                \"https://btg4.trezor.io\",\n                \"https://btg5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Bitcoin Gold\",\n            \"coin_name\": \"Bgold\",\n            \"coin_shortcut\": \"BTG\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": true,\n            \"fork_id\": 79,\n            \"hash_genesis_block\": \"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 500000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Bitcoin Gold\",\n            \"segwit\": true,\n            \"shortcut\": \"BTG\",\n            \"signed_message_header\": \"Bitcoin Gold Signed Message:\\n\",\n            \"slip44\": 156,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 111,\n            \"address_type_p2sh\": 196,\n            \"bech32_prefix\": \"tbtg\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Bitcoin Gold Testnet\",\n            \"coin_name\": \"Bgold Testnet\",\n            \"coin_shortcut\": \"TBTG\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": true,\n            \"fork_id\": 79,\n            \"hash_genesis_block\": \"00000000e0781ebe24b91eedc293adfea2f557b53ec379e78959de3853e6f9f6\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 500000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Bitcoin Gold Testnet\",\n            \"segwit\": true,\n            \"shortcut\": \"TBTG\",\n            \"signed_message_header\": \"Bitcoin Gold Signed Message:\\n\",\n            \"slip44\": 156,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 70615956,\n            \"xpub_magic\": 70617039,\n            \"xpub_magic_segwit_native\": 73342198,\n            \"xpub_magic_segwit_p2sh\": 71979618\n        },\n        {\n            \"address_type\": 4901,\n            \"address_type_p2sh\": 5039,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Bitcoin Private\",\n            \"coin_name\": \"Bprivate\",\n            \"coin_shortcut\": \"BTCP\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": 42,\n            \"hash_genesis_block\": \"0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602\",\n            \"max_address_length\": 95,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 35,\n            \"minfee_kb\": 1000,\n            \"name\": \"Bitcoin Private\",\n            \"segwit\": false,\n            \"shortcut\": \"BTCP\",\n            \"signed_message_header\": \"BitcoinPrivate Signed Message:\\n\",\n            \"slip44\": 183,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 3,\n            \"address_type_p2sh\": 125,\n            \"bech32_prefix\": \"btx\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Bitcore\",\n            \"coin_name\": \"Bitcore\",\n            \"coin_shortcut\": \"BTX\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Low\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"604148281e5c4b7f2487e5d03cd60d8e6f69411d613f6448034508cea52e9574\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Bitcore\",\n            \"segwit\": true,\n            \"shortcut\": \"BTX\",\n            \"signed_message_header\": \"BitCore Signed Message:\\n\",\n            \"slip44\": 160,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 102,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": \"bsd\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 200,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Bitsend\",\n            \"coin_name\": \"Bitsend\",\n            \"coin_shortcut\": \"BSD\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"0000012e1b8843ac9ce8c18603658eaf8895f99d3f5e7e1b7b1686f35e3c087a\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 10000,\n            \"name\": \"Bitsend\",\n            \"segwit\": true,\n            \"shortcut\": \"BSD\",\n            \"signed_message_header\": \"Bitsend Signed Message:\\n\",\n            \"slip44\": 91,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 28,\n            \"address_type_p2sh\": 35,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.capricoin.org\",\n                \"https://blockbook2.capricoin.org\",\n                \"https://blockbook3.capricoin.org\",\n                \"https://blockbook4.capricoin.org\"\n            ],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Capricoin\",\n            \"coin_name\": \"Capricoin\",\n            \"coin_shortcut\": \"CPC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 7,\n                \"High\": 20,\n                \"Low\": 1,\n                \"Normal\": 14\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000d23fa0fc52c90893adb1181c9ddffb6c797a3e41864b9a23aa2f2981fe3\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Capricoin\",\n            \"segwit\": false,\n            \"shortcut\": \"CPC\",\n            \"signed_message_header\": \"Capricoin Signed Message:\\n\",\n            \"slip44\": 289,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 76,\n            \"address_type_p2sh\": 16,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://dash1.trezor.io\",\n                \"https://dash2.trezor.io\",\n                \"https://dash3.trezor.io\",\n                \"https://dash4.trezor.io\",\n                \"https://dash5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Dash\",\n            \"coin_name\": \"Dash\",\n            \"coin_shortcut\": \"DASH\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000ffd590b1485b3caadc19b22e6379c733355108f107a430458cdf3407ab6\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Dash\",\n            \"segwit\": false,\n            \"shortcut\": \"DASH\",\n            \"signed_message_header\": \"DarkCoin Signed Message:\\n\",\n            \"slip44\": 5,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.5.2\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 50221816,\n            \"xpub_magic\": 50221772,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 140,\n            \"address_type_p2sh\": 19,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Dash Testnet\",\n            \"coin_name\": \"Dash Testnet\",\n            \"coin_shortcut\": \"tDASH\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000bafbc94add76cb75e2ec92894837288a481e5c005f6563d91623bf8bc2c\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 10000,\n            \"name\": \"Dash Testnet\",\n            \"segwit\": false,\n            \"shortcut\": \"tDASH\",\n            \"signed_message_header\": \"DarkCoin Signed Message:\\n\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 70615956,\n            \"xpub_magic\": 70617039,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 30,\n            \"address_type_p2sh\": 90,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 30,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Denarius\",\n            \"coin_name\": \"Denarius\",\n            \"coin_shortcut\": \"DNR\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000d5dbbda01621cfc16bbc1f9bf3264d641a5dbf0de89fd0182c2c4828fcd\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 10000,\n            \"name\": \"Denarius\",\n            \"segwit\": false,\n            \"shortcut\": \"DNR\",\n            \"signed_message_header\": \"Denarius Signed Message:\\n\",\n            \"slip44\": 116,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 30,\n            \"address_type_p2sh\": 63,\n            \"bech32_prefix\": \"dgb\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://dgb1.trezor.io\",\n                \"https://dgb2.trezor.io\"\n            ],\n            \"blocktime_seconds\": 15,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"DigiByte\",\n            \"coin_name\": \"DigiByte\",\n            \"coin_shortcut\": \"DGB\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"7497ea1b465eb39f1c8f507bc877078fe016d6fcb6dfad3a64c98dcc6e1e8496\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 500000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"DigiByte\",\n            \"segwit\": true,\n            \"shortcut\": \"DGB\",\n            \"signed_message_header\": \"DigiByte Signed Message:\\n\",\n            \"slip44\": 20,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.3\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 30,\n            \"address_type_p2sh\": 22,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://doge1.trezor.io\",\n                \"https://doge2.trezor.io\",\n                \"https://doge3.trezor.io\",\n                \"https://doge4.trezor.io\",\n                \"https://doge5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Dogecoin\",\n            \"coin_name\": \"Dogecoin\",\n            \"coin_shortcut\": \"DOGE\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 100000\n            },\n            \"dust_limit\": 10000000,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Dogecoin\",\n            \"segwit\": false,\n            \"shortcut\": \"DOGE\",\n            \"signed_message_header\": \"Dogecoin Signed Message:\\n\",\n            \"slip44\": 3,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.5.2\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 49988504,\n            \"xpub_magic\": 49990397,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 14,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": \"fc\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Feathercoin\",\n            \"coin_name\": \"Feathercoin\",\n            \"coin_shortcut\": \"FTC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Feathercoin\",\n            \"segwit\": true,\n            \"shortcut\": \"FTC\",\n            \"signed_message_header\": \"Feathercoin Signed Message:\\n\",\n            \"slip44\": 8,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76077806,\n            \"xpub_magic\": 76069926,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 35,\n            \"address_type_p2sh\": 94,\n            \"bech32_prefix\": \"flo\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 40,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Flo\",\n            \"coin_name\": \"Florincoin\",\n            \"coin_shortcut\": \"FLO\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"09c7781c9df90708e278c35d38ea5c9041d7ecfcdd1c56ba67274b7cff3e1cea\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100000,\n            \"name\": \"Flo\",\n            \"segwit\": true,\n            \"shortcut\": \"FLO\",\n            \"signed_message_header\": \"Florincoin Signed Message:\\n\",\n            \"slip44\": 216,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 15264107,\n            \"xpub_magic\": 1526049,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 28471030\n        },\n        {\n            \"address_type\": 36,\n            \"address_type_p2sh\": 16,\n            \"bech32_prefix\": \"fc\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://explorer.fujicoin.org\"\n            ],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Fujicoin\",\n            \"coin_name\": \"Fujicoin\",\n            \"coin_shortcut\": \"FJC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 20000,\n                \"High\": 100000,\n                \"Low\": 10000,\n                \"Normal\": 50000\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"adb6d9cfd74075e7f91608add4bd2a2ea636f70856183086842667a1597714a0\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 10000000,\n            \"name\": \"Fujicoin\",\n            \"segwit\": true,\n            \"shortcut\": \"FJC\",\n            \"signed_message_header\": \"FujiCoin Signed Message:\\n\",\n            \"slip44\": 75,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.1\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 38,\n            \"address_type_p2sh\": 10,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.gincoin.io\"\n            ],\n            \"blocktime_seconds\": 120,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"GIN\",\n            \"coin_name\": \"Gincoin\",\n            \"coin_shortcut\": \"GIN\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000cd6bde619b2c3b23ad2e384328a450a37fa28731debf748c3b17f91f97d\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"GIN\",\n            \"segwit\": false,\n            \"shortcut\": \"GIN\",\n            \"signed_message_header\": \"DarkCoin Signed Message:\\n\",\n            \"slip44\": 2000,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 50221816,\n            \"xpub_magic\": 50221772,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 38,\n            \"address_type_p2sh\": 62,\n            \"bech32_prefix\": \"game\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.gamecredits.network\"\n            ],\n            \"blocktime_seconds\": 90,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"GameCredits\",\n            \"coin_name\": \"GameCredits\",\n            \"coin_shortcut\": \"GAME\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"91ec5f25ee9a0ffa1af7d4da4db9a552228dd2dc77cdb15b738be4e1f55f30ee\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 5000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100000,\n            \"name\": \"GameCredits\",\n            \"segwit\": true,\n            \"shortcut\": \"GAME\",\n            \"signed_message_header\": \"GameCredits Signed Message:\\n\",\n            \"slip44\": 101,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 27108450,\n            \"xpub_magic\": 27106558,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 28471030\n        },\n        {\n            \"address_type\": 8329,\n            \"address_type_p2sh\": 8342,\n            \"bech32_prefix\": null,\n            \"bip115\": true,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Horizen\",\n            \"coin_name\": \"Horizen\",\n            \"coin_shortcut\": \"ZEN\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602\",\n            \"max_address_length\": 95,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 35,\n            \"minfee_kb\": 1000,\n            \"name\": \"Horizen\",\n            \"segwit\": false,\n            \"shortcut\": \"ZEN\",\n            \"signed_message_header\": \"Zcash Signed Message:\\n\",\n            \"slip44\": 121,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 60,\n            \"address_type_p2sh\": 85,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Komodo\",\n            \"coin_name\": \"Komodo\",\n            \"coin_shortcut\": \"KMD\",\n            \"consensus_branch_id\": {\n                \"1\": 0,\n                \"2\": 0,\n                \"3\": 1537743641,\n                \"4\": 1991772603\n            },\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"027e3758c3a65b12aa1046462b486d0a63bfa1beae327897f56c5cfb7daaae71\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Komodo\",\n            \"segwit\": false,\n            \"shortcut\": \"KMD\",\n            \"signed_message_header\": \"Komodo Signed Message:\\n\",\n            \"slip44\": 141,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.0\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 6198,\n            \"address_type_p2sh\": 6203,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Koto\",\n            \"coin_name\": \"Koto\",\n            \"coin_shortcut\": \"KOTO\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"6d424c350729ae633275d51dc3496e16cd1b1d195c164da00f39c499a2e9959e\",\n            \"max_address_length\": 95,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 35,\n            \"minfee_kb\": 1000,\n            \"name\": \"Koto\",\n            \"segwit\": false,\n            \"shortcut\": \"KOTO\",\n            \"signed_message_header\": \"Koto Signed Message:\\n\",\n            \"slip44\": 510,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 48,\n            \"address_type_p2sh\": 50,\n            \"bech32_prefix\": \"ltc\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://ltc1.trezor.io\",\n                \"https://ltc2.trezor.io\",\n                \"https://ltc3.trezor.io\",\n                \"https://ltc4.trezor.io\",\n                \"https://ltc5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Litecoin\",\n            \"coin_name\": \"Litecoin\",\n            \"coin_shortcut\": \"LTC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100000,\n            \"name\": \"Litecoin\",\n            \"segwit\": true,\n            \"shortcut\": \"LTC\",\n            \"signed_message_header\": \"Litecoin Signed Message:\\n\",\n            \"slip44\": 2,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.5.2\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 27106558,\n            \"xpub_magic\": 27108450,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 28471030\n        },\n        {\n            \"address_type\": 111,\n            \"address_type_p2sh\": 58,\n            \"bech32_prefix\": \"tltc\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Litecoin Testnet\",\n            \"coin_name\": \"Litecoin Testnet\",\n            \"coin_shortcut\": \"tLTC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Litecoin Testnet\",\n            \"segwit\": true,\n            \"shortcut\": \"tLTC\",\n            \"signed_message_header\": \"Litecoin Signed Message:\\n\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 70615956,\n            \"xpub_magic\": 70617039,\n            \"xpub_magic_segwit_native\": 73342198,\n            \"xpub_magic_segwit_p2sh\": 71979618\n        },\n        {\n            \"address_type\": 50,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": \"mec\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Megacoin\",\n            \"coin_name\": \"Megacoin\",\n            \"coin_shortcut\": \"MEC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Low\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"7520788e2d99eec7cf6cf7315577e1268e177fff94cb0a7caf6a458ceeea9ac2\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Megacoin\",\n            \"segwit\": true,\n            \"shortcut\": \"MEC\",\n            \"signed_message_header\": \"MegaCoin Signed Message:\\n\",\n            \"slip44\": 217,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 50,\n            \"address_type_p2sh\": 55,\n            \"bech32_prefix\": \"mona\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.electrum-mona.org\"\n            ],\n            \"blocktime_seconds\": 90,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Monacoin\",\n            \"coin_name\": \"Monacoin\",\n            \"coin_shortcut\": \"MONA\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"ff9f1c0116d19de7c9963845e129f9ed1bfc0b376eb54fd7afa42e0d418c8bb6\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 5000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100000,\n            \"name\": \"Monacoin\",\n            \"segwit\": true,\n            \"shortcut\": \"MONA\",\n            \"signed_message_header\": \"Monacoin Signed Message:\\n\",\n            \"slip44\": 22,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.0\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 16,\n            \"address_type_p2sh\": 76,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.monetaryunit.org\"\n            ],\n            \"blocktime_seconds\": 40,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"MonetaryUnit\",\n            \"coin_name\": \"MonetaryUnit\",\n            \"coin_shortcut\": \"MUE\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"0b58ed450b3819ca54ab0054c4d220ca4f887d21c9e55d2a333173adf76d987f\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"MonetaryUnit\",\n            \"segwit\": false,\n            \"shortcut\": \"MUE\",\n            \"signed_message_header\": \"MonetaryUnit Signed Message:\\n\",\n            \"slip44\": 31,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 50,\n            \"address_type_p2sh\": 9,\n            \"bech32_prefix\": \"my\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Myriad\",\n            \"coin_name\": \"Myriad\",\n            \"coin_shortcut\": \"XMY\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000ffde4c020b5938441a0ea3d314bf619eff0b38f32f78f7583cffa1ea485\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Myriad\",\n            \"segwit\": true,\n            \"shortcut\": \"XMY\",\n            \"signed_message_header\": \"Myriadcoin Signed Message:\\n\",\n            \"slip44\": 90,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 38,\n            \"address_type_p2sh\": 53,\n            \"bech32_prefix\": \"nix\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 120,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"NIX\",\n            \"coin_name\": \"NIX\",\n            \"coin_shortcut\": \"NIX\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"dd28ad86def767c3cfc34267a950d871fc7462bc57ea4a929fc3596d9b598e41\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 0,\n            \"name\": \"NIX\",\n            \"segwit\": true,\n            \"shortcut\": \"NIX\",\n            \"signed_message_header\": \"NIX Signed Message:\\n\",\n            \"slip44\": 400,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 52,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://nmc1.trezor.io\",\n                \"https://nmc2.trezor.io\"\n            ],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Namecoin\",\n            \"coin_name\": \"Namecoin\",\n            \"coin_shortcut\": \"NMC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 2940,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 10000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100000,\n            \"name\": \"Namecoin\",\n            \"segwit\": false,\n            \"shortcut\": \"NMC\",\n            \"signed_message_header\": \"Namecoin Signed Message:\\n\",\n            \"slip44\": 7,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.5.2\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 30,\n            \"address_type_p2sh\": 13,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.pivx.link\"\n            ],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"PIVX\",\n            \"coin_name\": \"PIVX\",\n            \"coin_shortcut\": \"PIVX\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"0000041e482b9b9691d98eefb48473405c0b8ec31b76df3797c74a78680ef818\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100,\n            \"name\": \"PIVX\",\n            \"segwit\": false,\n            \"shortcut\": \"PIVX\",\n            \"signed_message_header\": \"DarkNet Signed Message:\\n\",\n            \"slip44\": 119,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.0\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 35729707,\n            \"xpub_magic\": 36513075,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 139,\n            \"address_type_p2sh\": 19,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook-testnet.pivx.link\"\n            ],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"PIVX Testnet\",\n            \"coin_name\": \"PIVX Testnet\",\n            \"coin_shortcut\": \"tPIVX\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"0000041e482b9b9691d98eefb48473405c0b8ec31b76df3797c74a78680ef818\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100,\n            \"name\": \"PIVX Testnet\",\n            \"segwit\": false,\n            \"shortcut\": \"tPIVX\",\n            \"signed_message_header\": \"DarkNet Signed Message:\\n\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.0\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 981489719,\n            \"xpub_magic\": 981492128,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 47,\n            \"address_type_p2sh\": 22,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Pesetacoin\",\n            \"coin_name\": \"Pesetacoin\",\n            \"coin_shortcut\": \"PTC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 10000000,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"edfe5830b53251bfff733600b1cd5c192e761c011b055f07924634818c906438\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Pesetacoin\",\n            \"segwit\": false,\n            \"shortcut\": \"PTC\",\n            \"signed_message_header\": \"Pesetacoin Signed Message:\\n\",\n            \"slip44\": 109,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76079604,\n            \"xpub_magic\": 76071982,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 55,\n            \"address_type_p2sh\": 56,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.polispay.org\"\n            ],\n            \"blocktime_seconds\": 120,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Polis\",\n            \"coin_name\": \"Polis\",\n            \"coin_shortcut\": \"POLIS\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 5460,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"000009701eb781a8113b1af1d814e2f060f6408a2c990db291bc5108a1345c1e\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 100000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Polis\",\n            \"segwit\": false,\n            \"shortcut\": \"POLIS\",\n            \"signed_message_header\": \"Polis Signed Message:\\n\",\n            \"slip44\": 1997,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.2\",\n                \"trezor2\": \"2.1.1\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 65165637,\n            \"xpub_magic\": 65166718,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 23,\n            \"address_type_p2sh\": 83,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Primecoin\",\n            \"coin_name\": \"Primecoin\",\n            \"coin_shortcut\": \"XPM\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"963d17ba4dc753138078a2f56afb3af9674e2546822badff26837db9a0152106\",\n            \"max_address_length\": 35,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 26,\n            \"minfee_kb\": 1000,\n            \"name\": \"Primecoin\",\n            \"segwit\": false,\n            \"shortcut\": \"XPM\",\n            \"signed_message_header\": \"Primecoin Signed Message:\\n\",\n            \"slip44\": 24,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.0\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 60,\n            \"address_type_p2sh\": 122,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.ravencoin.org\"\n            ],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Ravencoin\",\n            \"coin_name\": \"Ravencoin\",\n            \"coin_shortcut\": \"RVN\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Low\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"0000006b444bc2f2ffe627be9d9e7e7a0730000870ef6eb6da46c8eae389df90\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Ravencoin\",\n            \"segwit\": false,\n            \"shortcut\": \"RVN\",\n            \"signed_message_header\": \"Raven Signed Message:\\n\",\n            \"slip44\": 175,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 76,\n            \"address_type_p2sh\": 16,\n            \"bech32_prefix\": \"xc\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 60,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Stakenet\",\n            \"coin_name\": \"Stakenet\",\n            \"coin_shortcut\": \"XSN\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 70,\n                \"High\": 200,\n                \"Low\": 10,\n                \"Normal\": 140\n            },\n            \"dust_limit\": 1000,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00000c822abdbb23e28f79a49d29b41429737c6c7e15df40d1b1f1b35907ae34\",\n            \"max_address_length\": 47,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Stakenet\",\n            \"segwit\": true,\n            \"shortcut\": \"XSN\",\n            \"signed_message_header\": \"DarkCoin Signed Message:\\n\",\n            \"slip44\": 199,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.0\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 130,\n            \"address_type_p2sh\": 30,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://blockbook.flurbo.xyz\",\n                \"https://blockbook.unobtanium.uno\"\n            ],\n            \"blocktime_seconds\": 30,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Unobtanium\",\n            \"coin_name\": \"Unobtanium\",\n            \"coin_shortcut\": \"UNO\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 50,\n                \"High\": 160,\n                \"Low\": 10,\n                \"Normal\": 100\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"000004c2fc5fffb810dccc197d603690099a68305232e552d96ccbe8e2c52b75\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 2000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Unobtanium\",\n            \"segwit\": false,\n            \"shortcut\": \"UNO\",\n            \"signed_message_header\": \"Unobtanium Signed Message:\\n\",\n            \"slip44\": 92,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.4\",\n                \"trezor2\": \"2.1.6\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 71,\n            \"address_type_p2sh\": 5,\n            \"bech32_prefix\": \"vtc\",\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://vtc1.trezor.io\",\n                \"https://vtc2.trezor.io\",\n                \"https://vtc3.trezor.io\",\n                \"https://vtc4.trezor.io\",\n                \"https://vtc5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Vertcoin\",\n            \"coin_name\": \"Vertcoin\",\n            \"coin_shortcut\": \"VTC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 1000\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"4d96a915f49d40b1e5c2844d1ee2dccb90013a990ccea12c492d22110489f0c4\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 100000,\n            \"name\": \"Vertcoin\",\n            \"segwit\": true,\n            \"shortcut\": \"VTC\",\n            \"signed_message_header\": \"Vertcoin Signed Message:\\n\",\n            \"slip44\": 28,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.1\",\n                \"trezor2\": \"2.0.5\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 71,\n            \"address_type_p2sh\": 33,\n            \"bech32_prefix\": \"via\",\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 24,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Viacoin\",\n            \"coin_name\": \"Viacoin\",\n            \"coin_shortcut\": \"VIA\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 7000,\n                \"High\": 20000,\n                \"Low\": 1000,\n                \"Normal\": 14000\n            },\n            \"dust_limit\": 54600,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"4e9b54001f9976049830128ec0331515eaabe35a70970d79971da1539a400ba1\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 40000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 1000,\n            \"name\": \"Viacoin\",\n            \"segwit\": true,\n            \"shortcut\": \"VIA\",\n            \"signed_message_header\": \"Viacoin Signed Message:\\n\",\n            \"slip44\": 14,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": 78792518,\n            \"xpub_magic_segwit_p2sh\": 77429938\n        },\n        {\n            \"address_type\": 7352,\n            \"address_type_p2sh\": 7357,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"ZClassic\",\n            \"coin_name\": \"ZClassic\",\n            \"coin_shortcut\": \"ZCL\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602\",\n            \"max_address_length\": 95,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 35,\n            \"minfee_kb\": 1000,\n            \"name\": \"ZClassic\",\n            \"segwit\": false,\n            \"shortcut\": \"ZCL\",\n            \"signed_message_header\": \"Zcash Signed Message:\\n\",\n            \"slip44\": 147,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.0\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 7352,\n            \"address_type_p2sh\": 7357,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [\n                \"https://zec1.trezor.io\",\n                \"https://zec2.trezor.io\",\n                \"https://zec3.trezor.io\",\n                \"https://zec4.trezor.io\",\n                \"https://zec5.trezor.io\"\n            ],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Zcash\",\n            \"coin_name\": \"Zcash\",\n            \"coin_shortcut\": \"ZEC\",\n            \"consensus_branch_id\": {\n                \"1\": 0,\n                \"2\": 0,\n                \"3\": 1537743641,\n                \"4\": 1991772603\n            },\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08\",\n            \"max_address_length\": 95,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 35,\n            \"minfee_kb\": 1000,\n            \"name\": \"Zcash\",\n            \"segwit\": false,\n            \"shortcut\": \"ZEC\",\n            \"signed_message_header\": \"Zcash Signed Message:\\n\",\n            \"slip44\": 133,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 7461,\n            \"address_type_p2sh\": 7354,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 150,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Zcash Testnet\",\n            \"coin_name\": \"Zcash Testnet\",\n            \"coin_shortcut\": \"TAZ\",\n            \"consensus_branch_id\": {\n                \"1\": 0,\n                \"2\": 0,\n                \"3\": 1537743641,\n                \"4\": 1991772603\n            },\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Normal\": 10\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38\",\n            \"max_address_length\": 95,\n            \"maxfee_kb\": 10000000,\n            \"min_address_length\": 35,\n            \"minfee_kb\": 1000,\n            \"name\": \"Zcash Testnet\",\n            \"segwit\": false,\n            \"shortcut\": \"TAZ\",\n            \"signed_message_header\": \"Zcash Signed Message:\\n\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 70615956,\n            \"xpub_magic\": 70617039,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 82,\n            \"address_type_p2sh\": 7,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Zcoin\",\n            \"coin_name\": \"Zcoin\",\n            \"coin_shortcut\": \"XZC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 10,\n                \"High\": 200,\n                \"Low\": 1,\n                \"Normal\": 100\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"4381deb85b1b2c9843c222944b616d997516dcbd6a964e1eaf0def0830695233\",\n            \"max_address_length\": 34,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 0,\n            \"name\": \"Zcoin\",\n            \"segwit\": false,\n            \"shortcut\": \"XZC\",\n            \"signed_message_header\": \"Zcoin Signed Message:\\n\",\n            \"slip44\": 136,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"xprv_magic\": 76066276,\n            \"xpub_magic\": 76067358,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        },\n        {\n            \"address_type\": 65,\n            \"address_type_p2sh\": 178,\n            \"bech32_prefix\": null,\n            \"bip115\": false,\n            \"blockbook\": [],\n            \"blocktime_seconds\": 600,\n            \"cashaddr_prefix\": null,\n            \"coin_label\": \"Zcoin Testnet\",\n            \"coin_name\": \"Zcoin Testnet\",\n            \"coin_shortcut\": \"tXZC\",\n            \"consensus_branch_id\": null,\n            \"curve_name\": \"secp256k1\",\n            \"decred\": false,\n            \"default_fee_b\": {\n                \"Economy\": 10,\n                \"High\": 200,\n                \"Low\": 1,\n                \"Normal\": 100\n            },\n            \"dust_limit\": 546,\n            \"force_bip143\": false,\n            \"fork_id\": null,\n            \"hash_genesis_block\": \"7ac038c193c2158c428c59f9ae0c02a07115141c6e9dc244ae96132e99b4e642\",\n            \"max_address_length\": 35,\n            \"maxfee_kb\": 1000000,\n            \"min_address_length\": 27,\n            \"minfee_kb\": 0,\n            \"name\": \"Zcoin Testnet\",\n            \"segwit\": false,\n            \"shortcut\": \"tXZC\",\n            \"signed_message_header\": \"Zcoin Signed Message:\\n\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"xprv_magic\": 70615956,\n            \"xpub_magic\": 70617039,\n            \"xpub_magic_segwit_native\": null,\n            \"xpub_magic_segwit_p2sh\": null\n        }\n    ],\n    \"erc20\": [],\n    \"eth\": [\n        {\n            \"blockbook\": [\n                \"https://eth1.trezor.io\",\n                \"https://eth2.trezor.io\"\n            ],\n            \"chain\": \"eth\",\n            \"chain_id\": 1,\n            \"name\": \"Ethereum\",\n            \"rskip60\": false,\n            \"shortcut\": \"ETH\",\n            \"slip44\": 60,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"url\": \"https://www.ethereum.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"exp\",\n            \"chain_id\": 2,\n            \"name\": \"Expanse\",\n            \"rskip60\": false,\n            \"shortcut\": \"EXP\",\n            \"slip44\": 40,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://expanse.tech\"\n        },\n        {\n            \"blockbook\": [\n                \"https://ropsten1.trezor.io\",\n                \"https://ropsten2.trezor.io\"\n            ],\n            \"chain\": \"rop\",\n            \"chain_id\": 3,\n            \"name\": \"Ethereum Testnet Ropsten\",\n            \"rskip60\": false,\n            \"shortcut\": \"tROP\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://www.ethereum.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"rin\",\n            \"chain_id\": 4,\n            \"name\": \"Ethereum Testnet Rinkeby\",\n            \"rskip60\": false,\n            \"shortcut\": \"tRIN\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://rinkeby.io\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"ubq\",\n            \"chain_id\": 8,\n            \"name\": \"Ubiq\",\n            \"rskip60\": false,\n            \"shortcut\": \"UBQ\",\n            \"slip44\": 108,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://ubiqsmart.com\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"etsc\",\n            \"chain_id\": 28,\n            \"name\": \"Ethereum Social\",\n            \"rskip60\": false,\n            \"shortcut\": \"ETSC\",\n            \"slip44\": 1128,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://ethereumsocial.kr\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"rsk\",\n            \"chain_id\": 30,\n            \"name\": \"RSK\",\n            \"rskip60\": true,\n            \"shortcut\": \"RBTC\",\n            \"slip44\": 137,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://www.rsk.co\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"trsk\",\n            \"chain_id\": 31,\n            \"name\": \"RSK Testnet\",\n            \"rskip60\": true,\n            \"shortcut\": \"tRBTC\",\n            \"slip44\": 37310,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://www.rsk.co\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"kov\",\n            \"chain_id\": 42,\n            \"name\": \"Ethereum Testnet Kovan\",\n            \"rskip60\": false,\n            \"shortcut\": \"tKOV\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://www.ethereum.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"go\",\n            \"chain_id\": 60,\n            \"name\": \"GoChain\",\n            \"rskip60\": false,\n            \"shortcut\": \"GO\",\n            \"slip44\": 6060,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://gochain.io\"\n        },\n        {\n            \"blockbook\": [\n                \"https://etc1.trezor.io\",\n                \"https://etc2.trezor.io\"\n            ],\n            \"chain\": \"etc\",\n            \"chain_id\": 61,\n            \"name\": \"Ethereum Classic\",\n            \"rskip60\": false,\n            \"shortcut\": \"ETC\",\n            \"slip44\": 61,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": true\n            },\n            \"url\": \"https://ethereumclassic.github.io\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"tetc\",\n            \"chain_id\": 62,\n            \"name\": \"Ethereum Classic Testnet\",\n            \"rskip60\": false,\n            \"shortcut\": \"tETC\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://ethereumclassic.github.io\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"ella\",\n            \"chain_id\": 64,\n            \"name\": \"Ellaism\",\n            \"rskip60\": false,\n            \"shortcut\": \"ELLA\",\n            \"slip44\": 163,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://ellaism.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"mix\",\n            \"chain_id\": 76,\n            \"name\": \"Mix\",\n            \"rskip60\": false,\n            \"shortcut\": \"MIX\",\n            \"slip44\": 76,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.10\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://www.mix-blockchain.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"clo\",\n            \"chain_id\": 820,\n            \"name\": \"Callisto\",\n            \"rskip60\": false,\n            \"shortcut\": \"CLO\",\n            \"slip44\": 820,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://callisto.network\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"ath\",\n            \"chain_id\": 1620,\n            \"name\": \"Atheios\",\n            \"rskip60\": false,\n            \"shortcut\": \"ATH\",\n            \"slip44\": 1620,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.3\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://atheios.com\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"egem\",\n            \"chain_id\": 1987,\n            \"name\": \"EtherGem\",\n            \"rskip60\": false,\n            \"shortcut\": \"EGEM\",\n            \"slip44\": 1987,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://egem.io\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"eosc\",\n            \"chain_id\": 2018,\n            \"name\": \"EOS Classic\",\n            \"rskip60\": false,\n            \"shortcut\": \"EOSC\",\n            \"slip44\": 2018,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://eos-classic.io\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"reosc\",\n            \"chain_id\": 2894,\n            \"name\": \"REOSC Ecosystem\",\n            \"rskip60\": false,\n            \"shortcut\": \"REOSC\",\n            \"slip44\": 2894,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.2\",\n                \"trezor2\": \"2.0.11\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://reosc.io\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"esn\",\n            \"chain_id\": 31102,\n            \"name\": \"Ethersocial Network\",\n            \"rskip60\": false,\n            \"shortcut\": \"ESN\",\n            \"slip44\": 31102,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.3\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://ethersocial.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"teo\",\n            \"chain_id\": 33416,\n            \"name\": \"Trust ETH reOrigin\",\n            \"rskip60\": false,\n            \"shortcut\": \"TEO\",\n            \"slip44\": 33416,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.8.2\",\n                \"trezor2\": \"2.1.1\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://tao.foundation\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"akroma\",\n            \"chain_id\": 200625,\n            \"name\": \"Akroma\",\n            \"rskip60\": false,\n            \"shortcut\": \"AKA\",\n            \"slip44\": 200625,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.3\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://akroma.io\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"etho\",\n            \"chain_id\": 1313114,\n            \"name\": \"Ether-1\",\n            \"rskip60\": false,\n            \"shortcut\": \"ETHO\",\n            \"slip44\": 1313114,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.3\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://ether1.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"music\",\n            \"chain_id\": 7762959,\n            \"name\": \"Musicoin\",\n            \"rskip60\": false,\n            \"shortcut\": \"MUSIC\",\n            \"slip44\": 184,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.3\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://musicoin.org\"\n        },\n        {\n            \"blockbook\": [],\n            \"chain\": \"pirl\",\n            \"chain_id\": 3125659152,\n            \"name\": \"Pirl\",\n            \"rskip60\": false,\n            \"shortcut\": \"PIRL\",\n            \"slip44\": 164,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.3\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            },\n            \"url\": \"https://pirl.io\"\n        }\n    ],\n    \"misc\": [\n        {\n            \"blockchain_link\": null,\n            \"curve\": \"ed25519\",\n            \"decimals\": 6,\n            \"name\": \"Cardano\",\n            \"shortcut\": \"ADA\",\n            \"slip44\": 1815,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            }\n        },\n        {\n            \"blockchain_link\": null,\n            \"curve\": \"secp256k1\",\n            \"decimals\": 8,\n            \"name\": \"Binance Chain\",\n            \"shortcut\": \"BNB\",\n            \"slip44\": 714,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.1.5\",\n                \"webwallet\": false\n            }\n        },\n        {\n            \"blockchain_link\": null,\n            \"curve\": \"secp256k1\",\n            \"decimals\": 4,\n            \"name\": \"EOS\",\n            \"shortcut\": \"EOS\",\n            \"slip44\": 194,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.1.1\",\n                \"webwallet\": false\n            }\n        },\n        {\n            \"blockchain_link\": null,\n            \"curve\": \"ed25519\",\n            \"decimals\": 8,\n            \"name\": \"Lisk\",\n            \"shortcut\": \"LSK\",\n            \"slip44\": 134,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            }\n        },\n        {\n            \"blockchain_link\": {\n                \"type\": \"ripple\",\n                \"url\": [\n                    \"wss://s.altnet.rippletest.net\"\n                ]\n            },\n            \"curve\": \"secp256k1\",\n            \"decimals\": 6,\n            \"name\": \"Ripple Testnet\",\n            \"shortcut\": \"tXRP\",\n            \"slip44\": 1,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            }\n        },\n        {\n            \"blockchain_link\": null,\n            \"curve\": \"ed25519\",\n            \"decimals\": 7,\n            \"name\": \"Stellar\",\n            \"shortcut\": \"XLM\",\n            \"slip44\": 148,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.7.1\",\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            }\n        },\n        {\n            \"blockchain_link\": {\n                \"type\": \"ripple\",\n                \"url\": [\n                    \"wss://s1.ripple.com\",\n                    \"wss://s-east.ripple.com\",\n                    \"wss://s-west.ripple.com\"\n                ]\n            },\n            \"curve\": \"secp256k1\",\n            \"decimals\": 6,\n            \"name\": \"Ripple\",\n            \"shortcut\": \"XRP\",\n            \"slip44\": 144,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            }\n        },\n        {\n            \"blockchain_link\": null,\n            \"curve\": \"ed25519\",\n            \"decimals\": 6,\n            \"name\": \"Tezos\",\n            \"shortcut\": \"XTZ\",\n            \"slip44\": 1729,\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": false,\n                \"trezor2\": \"2.0.8\",\n                \"webwallet\": false\n            }\n        }\n    ],\n    \"nem\": [\n        {\n            \"divisibility\": 6,\n            \"mosaic\": \"xem\",\n            \"name\": \"NEM\",\n            \"namespace\": \"nem\",\n            \"shortcut\": \"XEM\",\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"ticker\": \"XEM\"\n        },\n        {\n            \"divisibility\": 6,\n            \"fee\": 10,\n            \"levy\": \"MosaicLevy_Percentile\",\n            \"levy_mosaic\": \"coin\",\n            \"levy_namespace\": \"dim\",\n            \"mosaic\": \"coin\",\n            \"name\": \"DIMCOIN\",\n            \"namespace\": \"dim\",\n            \"networks\": [\n                104\n            ],\n            \"shortcut\": \"DIM\",\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"ticker\": \"DIM\"\n        },\n        {\n            \"divisibility\": 6,\n            \"mosaic\": \"token\",\n            \"name\": \"DIM TOKEN\",\n            \"namespace\": \"dim\",\n            \"networks\": [\n                104\n            ],\n            \"shortcut\": \"DIMTOK\",\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"ticker\": \"DIMTOK\"\n        },\n        {\n            \"divisibility\": 0,\n            \"mosaic\": \"breeze-token\",\n            \"name\": \"Breeze Token\",\n            \"namespace\": \"breeze\",\n            \"networks\": [\n                104\n            ],\n            \"shortcut\": \"BREEZE\",\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"ticker\": \"BREEZE\"\n        },\n        {\n            \"divisibility\": 0,\n            \"mosaic\": \"heart\",\n            \"name\": \"PacNEM Game Credits\",\n            \"namespace\": \"pacnem\",\n            \"networks\": [\n                104\n            ],\n            \"shortcut\": \"PAC:HRT\",\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"ticker\": \"PAC:HRT\"\n        },\n        {\n            \"divisibility\": 6,\n            \"fee\": 100,\n            \"levy\": \"MosaicLevy_Percentile\",\n            \"levy_mosaic\": \"xem\",\n            \"levy_namespace\": \"nem\",\n            \"mosaic\": \"cheese\",\n            \"name\": \"PacNEM Score Tokens\",\n            \"namespace\": \"pacnem\",\n            \"networks\": [\n                104\n            ],\n            \"shortcut\": \"PAC:CHS\",\n            \"support\": {\n                \"connect\": true,\n                \"trezor1\": \"1.6.2\",\n                \"trezor2\": \"2.0.7\",\n                \"webwallet\": false\n            },\n            \"ticker\": \"PAC:CHS\"\n        }\n    ]\n}"
  },
  {
    "path": "config/default.json",
    "content": "{\n    \"some_ticker (e.g. 'DASH')\": {\n        \"ADMIN_PUBLIC_KEY\": \"\",\n        \"ADMIN_PRIVATE_KEY\": \"\"\n    },\n    \n\n    \"KEAGATE_API_KEY\": \"API-KEY\",\n    \"IP_WHITELIST\": [],\n\n    \"TRANSACTION_TIMEOUT\": 1200000,\n    \"TRANSACTION_MIN_REFRESH_TIME\": 30000,\n    \"TRANSACTION_SLIPPAGE_TOLERANCE\": 0.02,\n    \"BLOCKBOOK_RETRY_DELAY\": 6000,\n    \n    \"MONGO_CONNECTION_STRING\": \"mongodb://localhost:27017\",\n    \"MONGO_KEAGATE_DB\": \"keagate\",\n\n    \"INVOICE_ENC_KEY\": \"D(G+KbPeShVmYq3s6v9y$B&E)H@McQfT\",\n\n    \"IS_DEV\": false,\n    \"USE_SO_CHAIN\": true,\n    \"PORT\": 8081\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"Keagate\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\\\"# Keagate\\\"\",\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"scripts\": {\n    \"build\": \"pnpm --filter \\\"*\\\" prebuild && tsc --build ./tsconfig.project.json\",\n    \"start:pm2-stop\": \"pm2 stop Keagate || true\",\n    \"start:pm2-del\": \"pm2 del Keagate || true\",\n    \"start:build-invoice-client\": \"pnpm --filter \\\"invoice-client\\\" build\",\n    \"start:pm2-start\": \"pm2 start packages/backend/build/index.js --name \\\"Keagate\\\" --time && pm2 save\",\n    \"start\": \"npm-run-all start:pm2-stop start:pm2-del start:build-invoice-client start:pm2-start\",\n    \"clean\": \"pnpm --filter \\\"*\\\" clean && shx rm -rf pnpm-lock.yaml node_modules\",\n    \"dev\": \"npm-run-all build --parallel dev:*\",\n    \"dev:backend\": \"pnpm --filter \\\"backend\\\" dev\",\n    \"dev:invoice-client\": \"pnpm --filter \\\"invoice-client\\\" dev\",\n    \"prettier-format\": \"prettier --config prettier.config.js 'packages/*/src/**/*{.ts,.tsx,.js,.jsx}' --write\",\n    \"find-circular-deps\": \"pnpx madge --circular --extensions ts,tsx packages/\"\n  },\n  \"author\": \"dilan-dio4\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"@types/big.js\": \"^6.1.3\",\n    \"@types/multicoin-address-validator\": \"^0.5.0\",\n    \"@types/node\": \"^18.0.1\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.14.2\",\n    \"@typescript-eslint/parser\": \"^4.14.2\",\n    \"eslint\": \"^7.19.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"prettier\": \"^2.7.1\",\n    \"shx\": \"^0.3.4\",\n    \"ts-node-dev\": \"^2.0.0\",\n    \"typescript\": \"^5.4.5\"\n  }\n}\n"
  },
  {
    "path": "packages/api-providers/package.json",
    "content": "{\n  \"name\": \"@keagate/api-providers\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\\\"# keagate\\\"\",\n  \"main\": \"build/index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"prebuild\": \"shx rm -f tsconfig.package.tsbuildinfo\",\n    \"build\": \"tsc --build ./tsconfig.package.json\",\n    \"clean\": \"shx rm -rf build pnpm-lock.yaml node_modules tsconfig.package.tsbuildinfo\"\n  },\n  \"dependencies\": {\n    \"@keagate/common\": \"^1.0.0\",\n    \"big.js\": \"^6.2.0\"\n  }\n}\n"
  },
  {
    "path": "packages/api-providers/src/GenericProvider.ts",
    "content": "import { AvailableCurrencies } from '@keagate/common';\n\nexport default abstract class GenericProvider {\n    public supportedCurrencies: AvailableCurrencies[];\n    constructor(..._: any[]) {\n        null;\n    }\n    abstract getBalance(currency: AvailableCurrencies, address: string): Promise<{ result: { confirmedBalance: number; unconfirmedBalance?: number } }>;\n    public sendTransaction?(currency: AvailableCurrencies, hexTransaction: string): Promise<{ result: string }>;\n}\n"
  },
  {
    "path": "packages/api-providers/src/NowNodesProvider.ts",
    "content": "import GenericProvider from './GenericProvider';\nimport { AvailableCurrencies, fGet, fPost } from '@keagate/common';\nimport units from './units';\nimport Big from 'big.js';\n\n// https://documenter.getpostman.com/view/13630829/TVmFkLwy#cebd6a63-13bc-4ba1-81f7-360c88871b90\n\nexport default class NowNodesProvider extends GenericProvider {\n    public supportedCurrencies: AvailableCurrencies[] = ['LTC', 'BTC'];\n\n    constructor(public apiKey: string, public currenciesToRpcUrls: Partial<Record<AvailableCurrencies, string>>) {\n        super();\n    }\n\n    async getBalance(currency: AvailableCurrencies, address: string): Promise<{ result: { confirmedBalance: number; unconfirmedBalance?: number } }> {\n        if (!this.supportedCurrencies.includes(currency)) {\n            throw new Error('Currency not supported');\n        }\n\n        const { balance } = await fGet(`https://${currency}book.nownodes.io/api/v2/address/${address}`, {\n            'api-key': this.apiKey,\n        });\n        const bigBalanceSatoshiLike = Big(balance);\n        let confirmedBalance: Big;\n        if ((currency as any) === 'DASH') {\n            confirmedBalance = bigBalanceSatoshiLike.times(Big(units.dash.duff));\n        } else if (currency === 'LTC') {\n            confirmedBalance = bigBalanceSatoshiLike.times(Big(units.ltc.litoshi));\n        } else if (currency === 'BTC') {\n            confirmedBalance = bigBalanceSatoshiLike.times(Big(units.btc.satoshi));\n        }\n        return {\n            result: {\n                confirmedBalance: confirmedBalance.toNumber(),\n                unconfirmedBalance: undefined,\n            },\n        };\n    }\n\n    async sendTransaction(currency: AvailableCurrencies, hexTransaction: string): Promise<{ result: string }> {\n        if (!this.supportedCurrencies.includes(currency)) {\n            throw new Error('Currency not supported');\n        }\n\n        // TODO: revert to getblocks\n        try {\n            const { result, error } = await fPost(\n                `https://${currency}.nownodes.io`,\n                {\n                    jsonrpc: '2.0',\n                    method: 'sendrawtransaction',\n                    params: [hexTransaction],\n                    API_key: this.apiKey,\n                    id: 'test',\n                },\n                {\n                    'Content-Type': 'application/json',\n                },\n            );\n\n            console.log(hexTransaction, result, error);\n\n            if (result === null) {\n                throw new Error(error);\n            }\n            return { result };\n        } catch (error) {\n            console.error(JSON.stringify(error));\n        }\n    }\n}\n"
  },
  {
    "path": "packages/api-providers/src/QuickNodeProvider.ts",
    "content": ""
  },
  {
    "path": "packages/api-providers/src/SoChainProvider.ts",
    "content": "import GenericProvider from './GenericProvider';\nimport { AvailableCurrencies, fGet } from '@keagate/common';\n\n// https://documenter.getpostman.com/view/13630829/TVmFkLwy#cebd6a63-13bc-4ba1-81f7-360c88871b90\n\nexport default class SoChainProvider extends GenericProvider {\n    public supportedCurrencies: AvailableCurrencies[] = ['LTC', 'BTC'];\n\n    async getBalance(currency: AvailableCurrencies, address: string): Promise<{ result: { confirmedBalance: number; unconfirmedBalance?: number } }> {\n        if (!this.supportedCurrencies.includes(currency)) {\n            throw new Error('Currency not supported');\n        }\n\n        const {\n            data: { confirmed_balance, unconfirmed_balance },\n        } = await fGet(`https://chain.so/api/v2/get_address_balance/${currency.toUpperCase()}/${address}`);\n        return {\n            result: {\n                confirmedBalance: +confirmed_balance,\n                unconfirmedBalance: +unconfirmed_balance,\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "packages/api-providers/src/TatumProvider.ts",
    "content": "import GenericProvider from './GenericProvider';\nimport { AvailableCurrencies, fGet, fPost, currencies } from '@keagate/common';\nimport units from './units';\nimport Big from 'big.js';\n\n// https://documenter.getpostman.com/view/13630829/TVmFkLwy#cebd6a63-13bc-4ba1-81f7-360c88871b90\n\nexport default class TatumProvider extends GenericProvider {\n    public supportedCurrencies: AvailableCurrencies[] = ['LTC', 'BTC'];\n\n    constructor(public apiKey: string, public location: 'eu1' | 'us-west1') {\n        super();\n    }\n\n    async getBalance(currency: AvailableCurrencies, address: string): Promise<{ result: { confirmedBalance: number; unconfirmedBalance?: number } }> {\n        if (!this.supportedCurrencies.includes(currency)) {\n            throw new Error('Currency not supported');\n        }\n\n        let confirmedBalance: Big;\n\n        if (currency === 'BTC' || currency === 'LTC') {\n            const { outgoing, incoming } = await fGet(\n                `https://api-${this.location}.tatum.io/v3/${currencies[currency].name.toLowerCase()}/address/balance/${address}`,\n                {\n                    'x-api-key': this.apiKey,\n                },\n            );\n            const bigBalanceSatoshiLike = Big(incoming).minus(Big(outgoing));\n            if (currency === 'BTC') {\n                confirmedBalance = bigBalanceSatoshiLike.times(Big(units.btc.satoshi));\n            } else if (currency === 'LTC') {\n                confirmedBalance = bigBalanceSatoshiLike.times(Big(units.ltc.litoshi));\n            }\n        }\n        // else if (currency === 'ADA') {\n        //     const {\n        //         summary: { assetBalances },\n        //     } = await fGet(`https://api-${this.location}.tatum.io/v3/${currency}/account/${address}`, {\n        //         'x-api-key': this.apiKey,\n        //     });\n\n        //     for (const currAsset of assetBalances) {\n        //         if (currAsset.asset.assetId === 'ada') {\n        //             confirmedBalance = Big(currAsset.quantity).times(Big(units.ada.lovelace));\n        //             break;\n        //         }\n        //     }\n        // } else if (currency === 'XRP') {\n        //     const { balance } = await fGet(`https://api-${this.location}.tatum.io/v3/${currency}/account/${address}/balance`, {\n        //         'x-api-key': this.apiKey,\n        //     });\n        //     confirmedBalance = Big(balance).times(Big(units.xrp.drop));\n        // }\n\n        return {\n            result: {\n                confirmedBalance: confirmedBalance.toNumber(),\n                unconfirmedBalance: undefined,\n            },\n        };\n    }\n\n    async sendTransaction(currency: AvailableCurrencies, hexTransaction: string): Promise<{ result: string }> {\n        if (!this.supportedCurrencies.includes(currency)) {\n            throw new Error('Currency not supported');\n        }\n\n        // const route = currency === 'ADA' || currency === 'XRP' ? currency : currencies[currency].name.toLowerCase();\n        const route = currencies[currency].name.toLowerCase();\n\n        const { txId } = await fPost(\n            `https://api-${this.location}.tatum.io/v3/${route}/broadcast`,\n            {\n                txData: hexTransaction,\n            },\n            {\n                'Content-Type': 'application/json',\n                'x-api-key': this.apiKey,\n            },\n        );\n        return { result: txId };\n    }\n}\n"
  },
  {
    "path": "packages/api-providers/src/index.ts",
    "content": "import NowNodesProvider from './NowNodesProvider';\nimport SoChainProvider from './SoChainProvider';\nimport TatumProvider from './TatumProvider';\nimport GenericProvider from './GenericProvider';\nimport { ConcreteConstructor } from '@keagate/common';\n\nexport type AvailableProviders = 'NowNodes' | 'Tatum';\n\nconst idsToProviders: Record<AvailableProviders, ConcreteConstructor<typeof GenericProvider>> = {\n    Tatum: TatumProvider,\n    NowNodes: NowNodesProvider,\n};\n\nexport default idsToProviders;\nexport { NowNodesProvider, SoChainProvider, TatumProvider, GenericProvider };\n"
  },
  {
    "path": "packages/api-providers/src/units.ts",
    "content": "// https://github.com/markpenovich/cryptocurrency-unit-convert/blob/master/Units.json\n\nconst units = {\n    btc: {\n        satoshi: '0.00000001',\n        bit: '0.000001',\n        ubtc: '0.000001',\n        mbtc: '0.001',\n        btc: '1',\n    },\n\n    bch: {\n        satoshi: '0.00000001',\n        bit: '0.000001',\n        ubch: '0.000001',\n        mbch: '0.001',\n        bch: '1',\n    },\n\n    eth: {\n        wei: '0.000000000000000001',\n        kwei: '0.000000000000001',\n        mwei: '0.000000000001',\n        gwei: '0.000000001',\n        finney: '0.001',\n        eth: '1',\n    },\n\n    xrp: {\n        drop: '0.000001',\n        xrp: '1',\n    },\n\n    ltc: {\n        litoshi: '0.00000001',\n        photon: '0.000001',\n        lite: '0.001',\n        ltc: '1',\n    },\n\n    dash: {\n        duff: '0.00000001',\n        dash: '1',\n    },\n\n    zec: {\n        zatoshi: '0.00000001',\n        zec: '1',\n    },\n    ada: {\n        lovelace: '0.000001',\n        ada: '1',\n    },\n};\n\nexport default units;\n"
  },
  {
    "path": "packages/api-providers/tsconfig.package.json",
    "content": "{\n\t\"extends\": \"../../tsconfig.base.json\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"./build\",\n\t\t\"rootDir\": \"./src\",\n\t\t\"composite\": true\n\t},\n\t\"include\": [\n\t\t\"src/**/*.ts\",\n\t\t\"src/**/*.js\",\n\t\t\"src/**/*.tsx\",\n\t\t\"src/**/*.jsx\"\n\t],\n\t\"exclude\": [\n\t\t\"test\",\n\t\t\"build\"\n\t],\n\t\"references\": []\n}"
  },
  {
    "path": "packages/backend/package.json",
    "content": "{\n  \"name\": \"@keagate/backend\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\\\"# keagate\\\"\",\n  \"main\": \"build/index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"prebuild\": \"shx rm -f tsconfig.package.tsbuildinfo\",\n    \"build\": \"tsc -p ./tsconfig.package.json\",\n    \"dev\": \"ts-node-dev --project ./tsconfig.package.json --respawn src/index.ts\",\n    \"prepack\": \"tsc --project tsconfig.build.json\",\n    \"clean\": \"shx rm -rf build pnpm-lock.yaml node_modules tsconfig.package.tsbuildinfo\"\n  },\n  \"dependencies\": {\n    \"@blockfrost/blockfrost-js\": \"^4.1.0\",\n    \"@fastify/static\": \"^7.0.1\",\n    \"@fastify/swagger\": \"^8.14.0\",\n    \"@fastify/swagger-ui\": \"^3.0.0\",\n    \"@keagate/api-providers\": \"workspace:^1.0.0\",\n    \"@keagate/common\": \"workspace:^1.0.0\",\n    \"@sinclair/typebox\": \"^0.24.7\",\n    \"@solana/web3.js\": \"^1.44.0\",\n    \"big.js\": \"^6.2.0\",\n    \"bip32\": \"^3.0.1\",\n    \"bip39\": \"^3.0.4\",\n    \"bitcore-lib-ltc\": \"^8.25.30\",\n    \"bottleneck\": \"^2.19.5\",\n    \"bs58\": \"^5.0.0\",\n    \"coinlib-port\": \"github:dilan-dio4/coinlib-port\",\n    \"config\": \"^3.3.7\",\n    \"cross-fetch\": \"^3.1.5\",\n    \"dayjs\": \"^1.11.3\",\n    \"ed25519-hd-key\": \"^1.2.0\",\n    \"ethers\": \"^5.6.9\",\n    \"fastify\": \"^4.26.0\",\n    \"fastify-plugin\": \"^4.5.1\",\n    \"find-up\": \"^6.3.0\",\n    \"lodash.sample\": \"^4.2.1\",\n    \"mongodb\": \"^4.7.0\",\n    \"multicoin-address-validator\": \"^0.5.10\",\n    \"tiny-secp256k1\": \"^2.2.1\",\n    \"tweetnacl\": \"^1.0.3\"\n  },\n  \"devDependencies\": {\n    \"@types/config\": \"^0.0.41\",\n    \"@types/lodash.sample\": \"^4.2.7\",\n    \"@types/node\": \"^18.0.1\",\n    \"typescript\": \"^5.4.2\"\n  }\n}\n"
  },
  {
    "path": "packages/backend/src/activityLoop.ts",
    "content": "import context from './context';\nimport GenericTransactionalWallet from './transactionalWallets/GenericTransactionalWallet';\nimport dayjs, { Dayjs } from 'dayjs';\nimport { PaymentStatusType, availableNativeCurrencies } from '@keagate/common';\nimport { delay } from './utils';\nimport config from './config';\nimport logger from './logger';\n\nclass ActivityLoop {\n    private needsToStop = false;\n    private lastBatchStart: Dayjs;\n\n    public start() {\n        this.startBatch();\n    }\n\n    public stop() {\n        this.needsToStop = true;\n    }\n\n    private async runTxsCohort(cohort: GenericTransactionalWallet[]) {\n        for (const aTrx of cohort) {\n            try {\n                await this.checkSingleTransaction(aTrx);\n            } catch (e) {\n                logger.log('Promise failed', e, aTrx);\n            }\n        }\n    }\n\n    private async startBatch() {\n        if (this.needsToStop) {\n            this.needsToStop = false;\n            return;\n        }\n\n        this.lastBatchStart = dayjs();\n\n        const txsByCohort: Record<typeof availableNativeCurrencies[number] | 'coinlib', GenericTransactionalWallet[]> = {\n            coinlib: [],\n            MATIC: [],\n            SOL: [],\n        };\n\n        for (const aTrx of Object.values(context.activePayments)) {\n            const { type, currency } = aTrx.getDetails();\n            if (type === 'coinlib') {\n                txsByCohort[type].push(aTrx);\n            } else {\n                txsByCohort[currency].push(aTrx);\n            }\n        }\n\n        await Promise.all(Object.values(txsByCohort).map((cohort) => this.runTxsCohort(cohort)));\n\n        const howLongTheBatchTook = dayjs().diff(this.lastBatchStart, 'millisecond');\n        if (howLongTheBatchTook < config.getTyped('TRANSACTION_MIN_REFRESH_TIME')) {\n            logger.log('Waiting for min refresh time');\n            await delay(config.getTyped('TRANSACTION_MIN_REFRESH_TIME') - howLongTheBatchTook);\n        }\n        this.startBatch();\n    }\n\n    private checkSingleTransaction(trx: GenericTransactionalWallet): Promise<PaymentStatusType> {\n        return new Promise((resolve, reject) => {\n            const timer = setTimeout(() => {\n                reject(new Error(`Promise timed out after 60s`));\n            }, 60000);\n            const runner = async () => {\n                let _status: PaymentStatusType;\n                try {\n                    await trx.checkTransaction((status) => (_status = status));\n                } catch (error) {\n                    _status = undefined;\n                }\n                if (_status) {\n                    clearTimeout(timer);\n                    resolve(_status);\n                } else {\n                    await delay(config.getTyped('BLOCKBOOK_RETRY_DELAY'));\n                    runner();\n                }\n            };\n            logger.log('Checking transaction: ', trx.getDetails().id);\n            runner();\n        });\n    }\n}\n\nexport default new ActivityLoop();\n"
  },
  {
    "path": "packages/backend/src/adminWallets/GenericAdminWallet.ts",
    "content": "import { availableCoinlibCurrencies, AvailableCurrencies } from '@keagate/common';\nimport WAValidator from 'multicoin-address-validator';\n\nexport default abstract class GenericAdminWallet {\n    public currency: AvailableCurrencies;\n    protected privateKey: string;\n\n    constructor(constructor: CoinlibAdminConstructor | NativeAdminConstructor) {\n        this.setFromObject(constructor);\n    }\n\n    public abstract getBalance(): Promise<{ result: { confirmedBalance: number; unconfirmedBalance?: number } }>;\n    public abstract sendTransaction(destination: string, amount: number): Promise<{ result: string }>;\n    // confirmTransaction\n    public isValidAddress(address: string): boolean {\n        return WAValidator.validate(address, this.currency);\n    }\n\n    protected setFromObject(update: CoinlibAdminConstructor | NativeAdminConstructor) {\n        for (const [key, val] of Object.entries(update)) {\n            this[key] = val;\n        }\n    }\n}\n\ninterface RootAdminConstructor {\n    privateKey: string;\n}\n\nexport interface CoinlibAdminConstructor extends RootAdminConstructor {\n    currency: typeof availableCoinlibCurrencies[number];\n}\n\nexport interface NativeAdminConstructor extends RootAdminConstructor {\n    publicKey: string; // TODO: don't need public\n}\n"
  },
  {
    "path": "packages/backend/src/adminWallets/coinlib/AdminCoinlibWrapper.ts",
    "content": "import GenericAdminWallet, { CoinlibAdminConstructor } from '../GenericAdminWallet';\nimport { AnyPayments, CoinPayments, NetworkType, BaseUnsignedTransaction, BaseSignedTransaction, BaseBroadcastResult, BalanceResult } from 'coinlib-port';\nimport config from '../../config';\nimport { delay, requestRetry, deadLogger } from '../../utils';\n\nexport default class AdminCoinlibWrapper extends GenericAdminWallet {\n    private coinlibMask: AnyPayments<any>;\n    private _initialized = false;\n\n    constructor(constructor: CoinlibAdminConstructor) {\n        super(constructor);\n        this.coinlibMask = CoinPayments.getFactory(this.currency as any).newPayments({\n            network: config.getTyped('IS_DEV') ? NetworkType.Testnet : NetworkType.Mainnet,\n            addressType: 'p2pkh',\n            keyPairs: [this.privateKey],\n            /** logger: deadLogger, */\n        } as any);\n\n        this.coinlibMask.init().then((_) => (this._initialized = true));\n    }\n\n    public async sendTransaction(destination: string, amount: number): Promise<{ result: string }> {\n        while (!this._initialized) {\n            await delay(1000);\n        }\n\n        const createTx = await requestRetry<BaseUnsignedTransaction>(() => this.coinlibMask.createTransaction(0, destination, '' + amount));\n        const signedTx = await requestRetry<BaseSignedTransaction>(() => this.coinlibMask.signTransaction(createTx));\n        const { id: txHash } = await requestRetry<BaseBroadcastResult>(() => this.coinlibMask.broadcastTransaction(signedTx));\n        return { result: txHash };\n    }\n\n    public async getBalance(): Promise<{ result: { confirmedBalance: number; unconfirmedBalance?: number } }> {\n        while (!this._initialized) {\n            await delay(1000);\n        }\n\n        const { confirmedBalance, unconfirmedBalance } = await requestRetry<BalanceResult>(() => this.coinlibMask.getBalance(0));\n        return {\n            result: {\n                confirmedBalance: +confirmedBalance,\n                unconfirmedBalance: +unconfirmedBalance,\n            },\n        };\n    }\n}\n"
  },
  {
    "path": "packages/backend/src/adminWallets/native/GenericNativeAdminWallet.ts",
    "content": "import GenericAdminWallet from '../GenericAdminWallet';\n\nexport default abstract class GenericNativeAdminWallet extends GenericAdminWallet {\n    public publicKey: string;\n}\n"
  },
  {
    "path": "packages/backend/src/adminWallets/native/Polygon/index.ts",
    "content": "import GenericNativeAdminWallet from '../GenericNativeAdminWallet';\nimport { ethers } from 'ethers';\nimport { NativeAdminConstructor } from '../../GenericAdminWallet';\nimport { availableNativeCurrencies } from '@keagate/common';\nimport config from '../../../config';\nimport Big from 'big.js';\nimport limiters from '../../../limiters';\n\nexport default class AdminPolygon extends GenericNativeAdminWallet {\n    private provider: ethers.providers.JsonRpcProvider;\n    private wallet: ethers.Wallet;\n    public currency: typeof availableNativeCurrencies[number] = 'MATIC';\n\n    constructor(constructor: NativeAdminConstructor) {\n        super(constructor);\n        this.provider = new ethers.providers.JsonRpcProvider(this.getRandomRPC());\n        this.wallet = new ethers.Wallet(this.privateKey, this.provider);\n    }\n\n    private getRandomRPC() {\n        if (config.getTyped('IS_DEV')) {\n            return 'https://rpc.ankr.com/polygon_mumbai';\n        } else {\n            return 'https://rpc.ankr.com/polygon';\n        }\n    }\n\n    async getBalance() {\n        const balance = await limiters[this.currency].schedule(() => this.wallet.getBalance());\n        // https://github.com/ethjs/ethjs-unit/blob/35d870eae1c32c652da88837a71e252a63a83ebb/src/index.js#L38\n        const confirmedBalance = Big(balance.toString()).div('1000000000000000000').toNumber();\n        return {\n            result: {\n                confirmedBalance,\n                unconfirmedBalance: null,\n            },\n        };\n    }\n\n    async sendTransaction(destination: string, amount: number) {\n        if (!this.isValidAddress(destination)) {\n            throw new Error('Invalid destination address');\n        }\n        const gasPrice = await limiters[this.currency].schedule(() => this.provider.getGasPrice());\n\n        // https://docs.ethers.io/v4/cookbook-accounts.html\n        const gasLimit = 21000;\n        const value = ethers.utils.parseEther('' + amount).sub(gasPrice.mul(gasLimit));\n        // https://docs.ethers.io/v5/api/providers/provider/#Provider-sendTransaction\n        const tx = {\n            to: destination,\n            value,\n            gasPrice,\n            gasLimit,\n        };\n        const txObj = await limiters[this.currency].schedule(() => this.wallet.sendTransaction(tx));\n        return { result: txObj.hash };\n    }\n}\n"
  },
  {
    "path": "packages/backend/src/adminWallets/native/Solana/HdWallet.ts",
    "content": "// https://github.com/p2p-org/p2p-wallet-web/blob/2d894ae5893b8566760c8a52050fde7e918d3139/packages/core/src/contexts/seed/utils/hd_wallet.ts\nimport * as ed25519 from 'ed25519-hd-key';\nimport nacl from 'tweetnacl';\nimport { bip32 } from '../../../utils';\nimport * as bip39 from 'bip39';\nimport { PublicKey } from '@solana/web3.js';\nimport config from '../../../config';\n\nconst DERIVATION_PATH = {\n    Deprecated: 'deprecated',\n    Bip44: 'bip44',\n    Bip44Change: 'bip44Change',\n};\n\ntype ValueOf<T> = T[keyof T];\n\nconst deriveSeed = (seed: string, walletIndex: number, derivationPath: ValueOf<typeof DERIVATION_PATH>): Buffer | undefined => {\n    switch (derivationPath) {\n        case DERIVATION_PATH.Deprecated: {\n            const path = `m/501'/${walletIndex}'/0/0`;\n            return bip32.fromSeed(Buffer.from(seed, 'hex')).derivePath(path).privateKey;\n        }\n        case DERIVATION_PATH.Bip44: {\n            const path = `m/44'/501'/${walletIndex}'`;\n            return ed25519.derivePath(path, seed).key;\n        }\n        case DERIVATION_PATH.Bip44Change: {\n            const path = `m/44'/501'/${walletIndex}'/0'`;\n            return ed25519.derivePath(path, seed).key;\n        }\n    }\n};\n\nexport function getKeyPairFromSeed(seed: string, walletIndex: number, derivationPath: ValueOf<typeof DERIVATION_PATH>) {\n    const derivedPrivateKey = deriveSeed(seed, walletIndex, derivationPath);\n    if (!derivedPrivateKey) throw new Error('Could not derive secretKey');\n    return nacl.sign.keyPair.fromSeed(derivedPrivateKey);\n}\n\n// const derivePublicKeyFromSeed = (\n//     seed: string,\n//     walletIndex: number,\n//     derivationPath: ValueOf<typeof DERIVATION_PATH>,\n// ) => {\n//     return getKeyPairFromSeed(seed, walletIndex, derivationPath).publicKey\n// }\n\n// const deriveSecretKeyFromSeed = (\n//     seed: string,\n//     walletIndex: number,\n//     derivationPath: ValueOf<typeof DERIVATION_PATH>,\n// ) => {\n//     return getKeyPairFromSeed(seed, walletIndex, derivationPath).secretKey\n// }\n\nexport const generateMnemonicAndSeedAsync = async () => {\n    const mnemonic = bip39.generateMnemonic(256);\n    const seed = await bip39.mnemonicToSeed(mnemonic);\n    return {\n        mnemonic,\n        seed: Buffer.from(seed).toString('hex'),\n    };\n};\n\n// const mnemonicToSeed = async (mnemonic: string) => {\n//     if (!bip39.validateMnemonic(mnemonic)) {\n//         throw new Error('Invalid seed words')\n//     }\n//     const seed = await bip39.mnemonicToSeed(mnemonic)\n//     return Buffer.from(seed).toString('hex')\n// }\n\nexport default function generateKeypair(walletIndex: number) {\n    const seed = config.getTyped('SEED');\n    const keypair = getKeyPairFromSeed(seed, walletIndex, 'bip44');\n    return {\n        publicKey: new PublicKey(keypair.publicKey).toString(),\n        secretKey: keypair.secretKey,\n    };\n}\n"
  },
  {
    "path": "packages/backend/src/adminWallets/native/Solana/index.ts",
    "content": "import { Connection, clusterApiUrl, PublicKey, Keypair, Transaction, SystemProgram, LAMPORTS_PER_SOL, sendAndConfirmTransaction } from '@solana/web3.js';\nimport GenericNativeAdminWallet from '../GenericNativeAdminWallet';\nimport base58 from 'bs58';\nimport { availableNativeCurrencies } from '@keagate/common';\nimport config from '../../../config';\nimport { NativeAdminConstructor } from '../../GenericAdminWallet';\nimport sample from 'lodash.sample';\nimport limiters from '../../../limiters';\n\nexport default class AdminSolana extends GenericNativeAdminWallet {\n    private connection: Connection;\n    public currency: typeof availableNativeCurrencies[number] = 'SOL';\n    static TRANSFER_FEE_LAMPORTS = 5000;\n\n    constructor(constructor: NativeAdminConstructor) {\n        super(constructor);\n        this.connection = new Connection(this.getRandomRPC(), 'confirmed');\n    }\n\n    private getRandomRPC() {\n        if (config.getTyped('IS_DEV')) {\n            return sample([clusterApiUrl('devnet'), 'https://rpc.ankr.com/solana_devnet']);\n        } else {\n            return sample([clusterApiUrl('mainnet-beta'), 'https://api.mainnet-beta.solana.com']);\n        }\n    }\n\n    async getBalance() {\n        const balance = await limiters[this.currency].schedule(() => this.connection.getBalance(new PublicKey(this.publicKey), 'confirmed'));\n\n        return {\n            result: {\n                confirmedBalance: balance / LAMPORTS_PER_SOL,\n                unconfirmedBalance: undefined,\n            },\n        };\n    }\n\n    async sendTransaction(destination: string, amount: number) {\n        if (!this.isValidAddress(destination)) {\n            throw new Error('Invalid destination address');\n        }\n\n        const latestBlockhash = await limiters[this.currency].schedule(() => this.connection.getLatestBlockhash('confirmed'));\n\n        const adminKeypair = Keypair.fromSecretKey(base58.decode(this.privateKey));\n\n        const transaction = new Transaction().add(\n            SystemProgram.transfer({\n                fromPubkey: adminKeypair.publicKey,\n                toPubkey: new PublicKey(destination),\n                lamports: Math.round(amount * LAMPORTS_PER_SOL) - AdminSolana.TRANSFER_FEE_LAMPORTS,\n            }),\n        );\n\n        transaction.recentBlockhash = latestBlockhash.blockhash;\n        transaction.feePayer = adminKeypair.publicKey;\n\n        try {\n            const signature = await limiters[this.currency].schedule(() => sendAndConfirmTransaction(this.connection, transaction, [adminKeypair]));\n            return { result: signature };\n        } catch (error) {\n            throw new Error(error);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/backend/src/config.ts",
    "content": "// https://itnext.io/node-config-made-type-safe-5be0a08ad5ba\nimport path from 'path';\n// TODO: Suppress NODE_APP_INSTANCE warning with PM2 + node-config\nprocess.env['NODE_CONFIG_DIR'] = path.join(__dirname, '..', '..', '..', 'config/'); // Must be in this order\nimport config from 'config';\nimport { MyConfig } from '@keagate/common';\n\nconst getTyped: <T extends keyof MyConfig>(key: T) => MyConfig[T] = <T extends keyof MyConfig>(key: T) => config.get(key);\nconst has: (setting: string) => boolean = (setting: string) => config.has(setting);\n\nconst obj = {\n    getTyped,\n    has,\n};\n\nexport default obj;\n"
  },
  {
    "path": "packages/backend/src/context.ts",
    "content": "import { availableCoinlibCurrencies, AvailableCurrencies, availableNativeCurrencies, ConcreteConstructor, currencies, arrayIncludes } from '@keagate/common';\nimport { AnyPayments } from 'coinlib-port';\nimport { WithId } from 'mongodb';\nimport GenericAdminWallet from './adminWallets/GenericAdminWallet';\nimport config from './config';\nimport { getExistingPayments } from './mongo';\nimport TransactionalCoinlibWrapper from './transactionalWallets/coinlib/TransactionalCoinlibWrapper';\nimport GenericTransactionalWallet from './transactionalWallets/GenericTransactionalWallet';\nimport GenericNativeTransactionalWallet from './transactionalWallets/native/GenericNativeTransactionalWallet';\nimport { CoinlibPayment, NativePayment } from './types';\nimport { getCoinlibCurrencyToClient, getNativeCurrencyToClient } from './currenciesToClients';\n\nclass KeagateContext {\n    public enabledNativeCurrencies: typeof availableNativeCurrencies[number][] = [];\n    public enabledCoinlibCurrencies: typeof availableCoinlibCurrencies[number][] = [];\n    public coinlibCurrencyToClient: Record<string, AnyPayments<any>> = {};\n    public nativeCurrencyToClient: Record<\n        typeof availableNativeCurrencies[number],\n        {\n            Admin: ConcreteConstructor<typeof GenericAdminWallet>;\n            Transactional: ConcreteConstructor<typeof GenericNativeTransactionalWallet>;\n        }\n    >;\n\n    public activePayments: Record<string, GenericTransactionalWallet> = {};\n\n    public async init() {\n        // Preserve order\n        this.initEnabledCurrencies();\n        this.nativeCurrencyToClient = getNativeCurrencyToClient();\n        this.coinlibCurrencyToClient = await getCoinlibCurrencyToClient();\n        await this.initActivePayments();\n    }\n\n    private initEnabledCurrencies() {\n        for (const currency of Object.keys(currencies)) {\n            const typedCurrency = currency as any;\n            if (!!config.has(currency) && !!config.getTyped(typedCurrency).ADMIN_PUBLIC_KEY) {\n                if (availableNativeCurrencies.includes(typedCurrency)) {\n                    this.enabledNativeCurrencies.push(typedCurrency);\n                } else if (availableCoinlibCurrencies.includes(typedCurrency)) {\n                    this.enabledCoinlibCurrencies.push(typedCurrency);\n                }\n            }\n        }\n    }\n\n    public async initActivePayments() {\n        // Collect all existing native payments in mongo and initalize them in the activePayments maps\n        const _activeNativePayments = (await getExistingPayments()) as WithId<NativePayment | CoinlibPayment>[];\n        this.activePayments = {};\n        for (const _currActivePayment of _activeNativePayments) {\n            const currTxCurrency = _currActivePayment.currency as AvailableCurrencies;\n\n            if (_currActivePayment.type === 'native') {\n                if (arrayIncludes(this.enabledNativeCurrencies, currTxCurrency)) {\n                    this.activePayments[_currActivePayment._id.toString()] = new this.nativeCurrencyToClient[currTxCurrency].Transactional().fromManual(\n                        {\n                            ..._currActivePayment,\n                            id: _currActivePayment._id.toString(),\n                        },\n                        {\n                            onDie: (id) => delete this.activePayments[id],\n                            adminWalletClass: this.nativeCurrencyToClient[currTxCurrency].Admin,\n                        },\n                    );\n                } else {\n                    console.error(`No transactional wallet found/enabled for currency ${_currActivePayment.currency}: ${_currActivePayment._id}`);\n                    continue;\n                }\n            } else if (_currActivePayment.type === 'coinlib') {\n                if (arrayIncludes(this.enabledCoinlibCurrencies, currTxCurrency)) {\n                    this.activePayments[_currActivePayment._id.toString()] = new TransactionalCoinlibWrapper().fromManual(\n                        {\n                            ..._currActivePayment,\n                            id: _currActivePayment._id.toString(),\n                        },\n                        {\n                            onDie: (id) => delete this.activePayments[id],\n                            walletIndex: _currActivePayment.walletIndex,\n                            currency: currTxCurrency,\n                            coinlibPayment: this.coinlibCurrencyToClient[currTxCurrency],\n                        },\n                    );\n                } else {\n                    console.error(`No transactional wallet found/enabled for currency ${_currActivePayment.currency}: ${_currActivePayment._id}`);\n                    continue;\n                }\n            }\n        }\n    }\n}\n\nexport default new KeagateContext();\n"
  },
  {
    "path": "packages/backend/src/currenciesToClients.ts",
    "content": "import { availableCoinlibCurrencies, availableNativeCurrencies, ConcreteConstructor } from '@keagate/common';\nimport { AnyPayments, CoinPayments, NetworkType, SUPPORTED_NETWORK_SYMBOLS } from 'coinlib-port';\nimport config from './config';\nimport GenericAdminWallet from './adminWallets/GenericAdminWallet';\nimport AdminPolygon from './adminWallets/native/Polygon';\nimport AdminSolana from './adminWallets/native/Solana';\nimport GenericNativeTransactionalWallet from './transactionalWallets/native/GenericNativeTransactionalWallet';\nimport TransactionalPolygon from './transactionalWallets/native/Polygon';\nimport TransactionalSolana from './transactionalWallets/native/Solana';\nimport { deadLogger } from './utils';\n\nexport const getNativeCurrencyToClient = (): Record<\n    typeof availableNativeCurrencies[number],\n    {\n        Admin: ConcreteConstructor<typeof GenericAdminWallet>;\n        Transactional: ConcreteConstructor<typeof GenericNativeTransactionalWallet>;\n    }\n> => ({\n    SOL: {\n        Admin: AdminSolana,\n        Transactional: TransactionalSolana,\n    },\n    MATIC: {\n        Admin: AdminPolygon,\n        Transactional: TransactionalPolygon,\n    },\n});\n\nexport async function getCoinlibCurrencyToClient(): Promise<Record<typeof availableCoinlibCurrencies[number], AnyPayments<any>>> {\n    const coinPayments = new CoinPayments({ seed: config.getTyped('SEED'), network: NetworkType.Mainnet /** logger: deadLogger */ });\n    const coinlibCurrencyToClient: Partial<Record<typeof availableCoinlibCurrencies[number], AnyPayments<any>>> = {};\n    for (const _currency of SUPPORTED_NETWORK_SYMBOLS) {\n        const currClient = coinPayments.forNetwork(_currency);\n        await currClient.init();\n        coinlibCurrencyToClient[_currency] = currClient;\n    }\n    return coinlibCurrencyToClient as Record<typeof availableCoinlibCurrencies[number], AnyPayments<any>>;\n}\n"
  },
  {
    "path": "packages/backend/src/devServer.ts",
    "content": "import fastify, { FastifyInstance } from 'fastify';\nimport crypto from 'crypto';\nimport config from './config';\n\ninterface IDevServer {\n    server: FastifyInstance;\n    name: string;\n    port: number;\n}\n\nfunction createIPNCallbackServer(): IDevServer {\n    /**\n     * For receiving IPN callbacks locally. Never use in production. Ideally, use\n     * something like lambda or Azure functions.\n     * More information here: https://github.com/dilan-dio4/coinlib-port#instant-payment-notifications\n     */\n    const ipnConsumer = fastify({\n        trustProxy: true,\n    });\n    ipnConsumer.post<{ Body: Record<string, any> }>('/ipnCallback', (request, reply) => {\n        const send = (status: string) => {\n            console.log(`[IPN CALLBACK DEV]: ${status}`);\n            reply.send(status);\n        };\n        if (request.headers['x-keagate-sig']) {\n            const hmac = crypto.createHmac('sha512', config.getTyped('IPN_HMAC_SECRET'));\n            hmac.update(JSON.stringify(request.body, Object.keys(request.body).sort()));\n            const signature = hmac.digest('hex');\n\n            if (signature === request.headers['x-keagate-sig']) {\n                send(\n                    'x-keagate-sig matches calculated signature. Can authenticate origin and validate message integrity.\\n\\n' +\n                        JSON.stringify(request.body, null, 2),\n                );\n            } else {\n                send(\n                    `x-keagate-sig header (${request.headers['x-keagate-sig']}) does not match calculated signature (${signature}). Cannot authenticate origin and validate message integrity.` +\n                        JSON.stringify(request.body, null, 2),\n                );\n            }\n        } else {\n            send('No x-keagate-sig found on POST request. Cannot authenticate origin and validate message integrity.');\n        }\n    });\n\n    return {\n        server: ipnConsumer,\n        name: 'IPN CALLBACK DEV',\n        port: 8082,\n    };\n}\n\nexport default async function devServer(server: FastifyInstance) {\n    const devServers: IDevServer[] = [createIPNCallbackServer()];\n    for (const currServer of devServers) {\n        await currServer.server.ready();\n        currServer.server.listen({ port: currServer.port }, (err, address) => {\n            if (err) {\n                console.error(err);\n                process.exit(1);\n            }\n            console.log(`[${currServer.name}]: dev server listening at ${address}`);\n        });\n    }\n\n    /**\n     * For testing redirects from the built-in invoice interface. This should basically\n     * be a website that utilizes the `status` and `invoice_id` query parameters\n     * that Keagate automatically appends to the passed in URL.\n     */\n    server.get('/dev-callback-site', (request, reply) => {\n        reply.header('Content-Type', 'text/html; charset=UTF-8');\n        reply.send(`\n        <!DOCTYPE html>\n<html>\n\n<body>\n    <h1 style=\"background-color: red; color: white;\">Keagate test site</h1>\n    <h2 style=\"font-weight: 400;\"><b>NEVER</b> actually call administrative routes or store your <em>keagate-api-key</em> directly with a user's browser!</h2>\n    <h2 style=\"font-weight: 400;\">Instead, call an API or Lambda from your site and invokes Keagate from there. &#128515; cheers!</h2>\n\n    <hr />\n\n    <p><b>Data read in URL params:</b></p>\n    <ol id=\"url-params-data\"></ol>\n\n    <p><b>Data read from Keagate based on <em>invoice_id</em>:</b></p>\n    <ol id=\"keagate-data\"></ol>\n\n    <p id=\"error-message\" style=\"color: red;\"></p>\n</body>\n<script type=\"text/javascript\">\n    window.addEventListener('load', function () {\n        const params = new URLSearchParams(document.location.search)\n\n        let newElements = '';\n        params.forEach(function (value, key) {\n            newElements += '<li><b>' + key + '</b>: ' + value + '</li>'\n        })\n        document.getElementById('url-params-data').innerHTML = newElements;\n\n        if (!params.get('invoice_id')) {\n            document.getElementById('error-message').innerText = \"No invoice_id parameter found\"\n        } else {\n            fetch('http://localhost:8081/getPaymentStatus?id=' + params.get('invoice_id'), {\n                headers: {\n                    \"keagate-api-key\": \"${config.getTyped('KEAGATE_API_KEY')}\"\n                }\n            })\n                .then(res => res.json())\n                .then(res => {\n                    let newKeagateElements = '';\n                    for (const [key, value] of Object.entries(res)) {\n                        newKeagateElements += '<li><b>' + key + '</b>: ' + value + '</li>'\n                    }\n                    document.getElementById('keagate-data').innerHTML = newKeagateElements;\n\n                })\n                .catch(err => document.getElementById('error-message').innerText = JSON.stringify(err))\n        }\n    })\n</script>\n\n</html>\n        `);\n    });\n}\n"
  },
  {
    "path": "packages/backend/src/index.ts",
    "content": "import config from './config';\nimport fastify from 'fastify';\nimport { currencies, availableCoinlibCurrencies, availableNativeCurrencies } from '@keagate/common';\nimport GenericAdminWallet from './adminWallets/GenericAdminWallet';\nimport auth from './middlewares/auth';\nimport createPaymentRoute from './routes/createPayment';\nimport createActivePaymentsRoute from './routes/activePayments';\nimport createPaymentStatusRoute from './routes/paymentStatus';\nimport createInvoiceClientRoute from './routes/invoiceClient';\nimport createSwaggerRoute from './routes/swagger';\nimport createInvoiceStatusRoute from './routes/invoiceStatus';\nimport createPaymentsByExtraIdRoute from './routes/paymentsByExtraId';\nimport context from './context';\nimport activityLoop from './activityLoop';\nimport AdminCoinlibWrapper from './adminWallets/coinlib/AdminCoinlibWrapper';\nimport devServer from './devServer';\nimport logger from './logger';\n\nconst server = fastify({\n    trustProxy: true,\n    ajv: {\n        customOptions: {\n            strict: 'log',\n            keywords: ['kind', 'modifier'],\n        },\n    },\n});\n\n/**\n * Native = currency processed by a wallet built into Keagate\n * Coinlib = currency processed by the port of coinlib\n */\n\nasync function main() {\n    await context.init();\n    server.register(createSwaggerRoute);\n    // Initialize the admin wallet routes for native currencies\n    for (const _currency of context.enabledNativeCurrencies) {\n        const coinName = currencies[_currency].name as typeof availableNativeCurrencies[number];\n        const publicKey: string = config.getTyped(_currency).ADMIN_PUBLIC_KEY;\n        const privateKey: string = config.getTyped(_currency).ADMIN_PRIVATE_KEY;\n\n        if (!publicKey || !privateKey) {\n            console.error(`No admin public key and private key found for currency ${_currency}`);\n            continue;\n        }\n\n        let currentClient: GenericAdminWallet;\n        if (context.nativeCurrencyToClient[_currency]) {\n            currentClient = new context.nativeCurrencyToClient[_currency].Admin({\n                publicKey,\n                privateKey,\n            });\n        } else {\n            console.error(`No admin wallet found for currency ${_currency}`);\n            continue;\n        }\n        // Get the balance of and send a transaction from the admin wallet\n        server.get(`/get${coinName}Balance`, { preHandler: auth }, (request, reply) => currentClient.getBalance());\n        server.post<{ Body: Record<string, any> }>(`/send${coinName}Transaction`, { preHandler: auth }, (request, reply) =>\n            currentClient.sendTransaction(request.body.destination, request.body.amount),\n        );\n    }\n\n    // Do the same for coinlib currencies\n    for (const _currency of context.enabledCoinlibCurrencies) {\n        const coinName = currencies[_currency].name as typeof availableCoinlibCurrencies[number];\n\n        const publicKey: string = config.getTyped(_currency).ADMIN_PUBLIC_KEY;\n        const privateKey: string = config.getTyped(_currency).ADMIN_PRIVATE_KEY;\n\n        if (!publicKey || !privateKey) {\n            console.error(`No admin public key and private key found for currency ${_currency}`);\n            continue;\n        }\n\n        const currentClient = new AdminCoinlibWrapper({\n            currency: _currency,\n            privateKey: privateKey,\n        });\n\n        server.get(`/get${coinName}Balance`, { preHandler: auth }, (request, reply) => currentClient.getBalance());\n        server.post<{ Body: Record<string, any> }>(`/send${coinName}Transaction`, { preHandler: auth }, (request, reply) =>\n            currentClient.sendTransaction(request.body.destination, request.body.amount),\n        );\n    }\n\n    // Create other routes for API and invoice client\n    server.register(createInvoiceClientRoute);\n    server.register(createInvoiceStatusRoute);\n    server.register(createPaymentRoute);\n    server.register(createActivePaymentsRoute);\n    server.register(createPaymentStatusRoute);\n    server.register(createPaymentsByExtraIdRoute);\n\n    // Start the processing intervals\n    activityLoop.start();\n\n    if (config.getTyped('IS_DEV') && config.has('IPN_HMAC_SECRET')) {\n        server.register(devServer);\n    }\n\n    await server.ready();\n    server.swagger();\n    server.listen({ port: config.getTyped('PORT') }, (err, address) => {\n        if (err) {\n            console.error(err);\n            process.exit(1);\n        }\n        logger.log(`Keagate backend server listening at ${address}`);\n    });\n}\n\nmain();\n"
  },
  {
    "path": "packages/backend/src/limiters.ts",
    "content": "import { availableNativeCurrencies } from '@keagate/common';\nimport Bottleneck from 'bottleneck';\n\ninterface ILimiterDetails {\n    rateLimit: number;\n    maxRetries: number;\n    maxConcurrent: number;\n}\n\nconst limiterDetails: Record<typeof availableNativeCurrencies[number], ILimiterDetails> = {\n    SOL: {\n        maxConcurrent: 1,\n        maxRetries: 5,\n        rateLimit: 500,\n    },\n    MATIC: {\n        maxConcurrent: 1,\n        maxRetries: 5,\n        rateLimit: 500,\n    },\n};\n\nfunction generateLimiter(ILimiterDetails: ILimiterDetails) {\n    const RATE_LIMIT = ILimiterDetails.rateLimit;\n\n    const limiter = new Bottleneck({\n        minTime: RATE_LIMIT,\n        maxConcurrent: ILimiterDetails.maxConcurrent,\n    });\n\n    limiter.on('failed', async (error, jobInfo) => {\n        if (jobInfo.retryCount < ILimiterDetails.maxRetries) {\n            return RATE_LIMIT + jobInfo.retryCount * 100;\n        }\n    });\n\n    return limiter;\n}\n\nconst limiters: Record<typeof availableNativeCurrencies[number], Bottleneck> = {\n    MATIC: generateLimiter(limiterDetails.MATIC),\n    SOL: generateLimiter(limiterDetails.SOL),\n};\n\nexport default limiters;\n"
  },
  {
    "path": "packages/backend/src/logger.ts",
    "content": "import config from './config';\n\nclass DelegateLogger {\n    log = (...args: any[]) => console.log('[KEAGATE LOG]: ', ...args);\n    debug = (...args: any[]) => config.has('IS_DEV') && config.getTyped('IS_DEV') && console.log('[KEAGATE DEBUG]: ', ...args);\n    success = (text = 'Complete') => console.log('\\x1b[1;32m \\u2714 ' + text + '\\x1b[0m');\n}\n\nconst logger = new DelegateLogger();\n\nexport default logger;\n"
  },
  {
    "path": "packages/backend/src/middlewares/auth.ts",
    "content": "import { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from 'fastify';\nimport config from '../config';\n\nlet IP_WHITELIST: Set<string> = undefined;\n\nif (config.getTyped('IP_WHITELIST').length > 0) {\n    IP_WHITELIST = new Set(config.getTyped('IP_WHITELIST'));\n    IP_WHITELIST.add('127.0.0.1');\n}\n\nconst auth = (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\n    if (request.headers['keagate-api-key'] === config.getTyped('KEAGATE_API_KEY')) {\n        if (IP_WHITELIST !== undefined) {\n            if (IP_WHITELIST.has(request.ip)) {\n                done();\n            }\n        } else {\n            done();\n        }\n    }\n};\n\nexport default auth;\n"
  },
  {
    "path": "packages/backend/src/mongo/generator.ts",
    "content": "import { MongoClient } from 'mongodb';\nimport config from '../config';\n\nlet clientInstance: MongoClient;\n\nexport default async function generator() {\n    if (!clientInstance) {\n        // Default options: https://github.com/mongodb/node-mongodb-native/blob/ee414476aa839e364bce6b26ab47859be1b99307/src/connection_string.ts#L825\n        // https://stackoverflow.com/a/56438581\n        const _client = new MongoClient(config.getTyped('MONGO_CONNECTION_STRING'), {\n            keepAlive: true,\n            socketTimeoutMS: 2000000,\n        });\n        await _client.connect();\n        _client.on('topologyClosed', () => (clientInstance = undefined));\n        clientInstance = _client;\n    }\n\n    return {\n        db: clientInstance.db(config.getTyped('MONGO_KEAGATE_DB')),\n        client: clientInstance,\n    };\n}\n"
  },
  {
    "path": "packages/backend/src/mongo/index.ts",
    "content": "import { Db } from 'mongodb';\nimport mongoGenerator from './generator';\n\nconst _load = (db: Db, collection: 'payments') =>\n    db\n        .collection(collection)\n        .find({ status: { $nin: ['FINISHED', 'EXPIRED', 'FAILED', 'CONFIRMED'] } })\n        .toArray();\n\nexport async function getExistingPayments() {\n    const { db } = await mongoGenerator();\n    return await _load(db, 'payments');\n}\n"
  },
  {
    "path": "packages/backend/src/routes/activePayments.ts",
    "content": "import { Static, Type } from '@sinclair/typebox';\nimport { FastifyInstance, RouteShorthandOptions } from 'fastify';\nimport { MongoPayment, ForRequest } from '../types';\nimport auth from '../middlewares/auth';\nimport context from '../context';\nimport { MongoTypeForRequest, cleanDetails, AdminRouteHeaders } from './types';\n\nconst ActivePaymentsResponse = Type.Array(MongoTypeForRequest, { description: 'Successful response of array of all active payments' });\n\nconst opts: RouteShorthandOptions = {\n    schema: {\n        response: {\n            200: ActivePaymentsResponse,\n        },\n        headers: AdminRouteHeaders,\n        tags: ['Payment'],\n        description: 'Fetch an array of all active payments. Payments that have been completed or expired will not appear here.',\n        summary: 'Fetch an array of all active payments',\n        security: [\n            {\n                ApiKey: [],\n            },\n        ],\n    },\n    preHandler: auth,\n};\n\nexport default async function createActivePaymentsRoute(server: FastifyInstance) {\n    server.get<{ Reply: Static<typeof ActivePaymentsResponse> }>('/activePayments', opts, async (request, reply) => {\n        await context.initActivePayments();\n        const cleanedTransactions: ForRequest<MongoPayment>[] = Object.values(context.activePayments).map((ele) => cleanDetails(ele.getDetails()));\n        reply.status(200).send(cleanedTransactions);\n    });\n}\n"
  },
  {
    "path": "packages/backend/src/routes/createPayment.ts",
    "content": "import { Static, Type } from '@sinclair/typebox';\nimport { FastifyInstance, RouteShorthandOptions } from 'fastify';\nimport auth from '../middlewares/auth';\nimport GenericTransactionalWallet from '../transactionalWallets/GenericTransactionalWallet';\nimport { AvailableCurrencies, arrayIncludes } from '@keagate/common';\nimport TransactionalCoinlibWrapper from '../transactionalWallets/coinlib/TransactionalCoinlibWrapper';\nimport { walletIndexGenerator } from '../transactionalWallets/coinlib/trxLimits';\nimport context from '../context';\nimport { currencyDusts } from '../transactionalWallets/coinlib/trxLimits';\nimport { cleanDetails, MongoTypeForRequest, AdminRouteHeaders, ErrorResponse } from './types';\nimport { IFromNew } from '../types';\n\nconst CreatePaymentBody = Type.Pick(MongoTypeForRequest, ['currency', 'amount', 'ipnCallbackUrl', 'invoiceCallbackUrl', 'extraId']);\n\nconst opts: RouteShorthandOptions = {\n    schema: {\n        body: CreatePaymentBody,\n        response: {\n            200: MongoTypeForRequest,\n            300: ErrorResponse,\n        },\n        headers: AdminRouteHeaders,\n        tags: ['Payment'],\n        description: 'Create a new payment.',\n        summary: 'Create a new payment',\n        security: [\n            {\n                ApiKey: [],\n            },\n        ],\n    },\n    preHandler: auth,\n};\n\nexport default async function createPaymentRoute(server: FastifyInstance) {\n    server.post<{ Body: Static<typeof CreatePaymentBody>; Reply: Static<typeof MongoTypeForRequest> | Static<typeof ErrorResponse> }>(\n        '/createPayment',\n        opts,\n        async (request, reply) => {\n            const { body } = request;\n\n            const createCurrency = body.currency.toUpperCase() as AvailableCurrencies;\n            let transactionalWallet: GenericTransactionalWallet;\n            const transactionalWalletNewObj: IFromNew = {\n                amount: body.amount,\n                invoiceCallbackUrl: body.invoiceCallbackUrl,\n                ipnCallbackUrl: body.ipnCallbackUrl,\n                extraId: body.extraId,\n            };\n            if (arrayIncludes(context.enabledNativeCurrencies, createCurrency)) {\n                transactionalWallet = await new context.nativeCurrencyToClient[createCurrency].Transactional().fromNew(transactionalWalletNewObj, {\n                    onDie: (id) => delete context.activePayments[id],\n                    adminWalletClass: context.nativeCurrencyToClient[createCurrency].Admin,\n                });\n            } else if (arrayIncludes(context.enabledCoinlibCurrencies, createCurrency)) {\n                if (createCurrency in currencyDusts && currencyDusts[createCurrency] >= body.amount) {\n                    reply\n                        .status(300)\n                        .send({ error: `Transaction amount is lower than the minimum for ${createCurrency}: ${currencyDusts[createCurrency]} dust` });\n                    return;\n                }\n                transactionalWallet = await new TransactionalCoinlibWrapper().fromNew(transactionalWalletNewObj, {\n                    onDie: (id) => delete context.activePayments[id],\n                    currency: createCurrency,\n                    walletIndex: walletIndexGenerator[createCurrency](),\n                    coinlibPayment: context.coinlibCurrencyToClient[createCurrency],\n                });\n            } else {\n                console.error(`No transactional wallet found/enabled for currency ${body.currency}`);\n                return;\n            }\n\n            context.activePayments[transactionalWallet.getDetails().id] = transactionalWallet;\n            reply.status(200).send(cleanDetails(transactionalWallet.getDetails()));\n        },\n    );\n}\n"
  },
  {
    "path": "packages/backend/src/routes/invoiceClient.ts",
    "content": "import { FastifyInstance, RouteShorthandOptions } from 'fastify';\nimport fastifyStatic from '@fastify/static';\nimport path from 'path';\nimport fastifyPlugin from 'fastify-plugin';\nimport { Type } from '@sinclair/typebox';\n\nexport default fastifyPlugin(async function createInvoiceClientRoute(server: FastifyInstance) {\n    server.register(fastifyStatic, {\n        root: path.join(__dirname, '..', '..', '..', 'invoice-client', 'dist'),\n        prefix: '/static-invoice',\n    });\n\n    const opts: RouteShorthandOptions = {\n        schema: {\n            tags: ['Invoice'],\n            description: `Route for Keagate's built-in invoice interface.`,\n            summary: `Route for Keagate's built-in invoice interface`,\n            params: Type.Object({\n                currency: Type.String({\n                    description: `Shorthand name of a payment's corresponding currency.`,\n                }),\n                invoiceId: Type.String({\n                    description: `Invoice identifier. This is generated automatically as the internal id of the payment encrypted with aes-256-cbc plus your config's INVOICE_ENC_KEY.`,\n                }),\n            }),\n        },\n    };\n\n    server.get('/invoice/:currency/:invoiceId', opts, (request, reply) => {\n        reply.sendFile('index.html');\n    });\n});\n"
  },
  {
    "path": "packages/backend/src/routes/invoiceStatus.ts",
    "content": "import { Static, Type } from '@sinclair/typebox';\nimport { FastifyInstance, RouteShorthandOptions } from 'fastify';\nimport { MongoPayment } from '../types';\nimport mongoGenerator from '../mongo/generator';\nimport { ObjectId, WithId } from 'mongodb';\nimport { decrypt } from '../utils';\nimport { ErrorResponse, MongoTypeForRequest } from './types';\n\nconst InvoiceStatusResponse = Type.Pick(\n    MongoTypeForRequest,\n    ['publicKey', 'amount', 'amountPaid', 'expiresAt', 'status', 'currency', 'invoiceCallbackUrl', 'memo'],\n    {\n        description: `Successful response of invoice status.`,\n    },\n);\n\nconst InvoiceStatusQueryString = Type.Object({\n    invoiceId: Type.String({\n        description: `The encrypted identifier given as the second part of the *invoiceUrl* path. This *invoiceUrl* is found in most of the administrative Keagate routes.`,\n    }),\n});\n\nconst opts: RouteShorthandOptions = {\n    schema: {\n        response: {\n            200: InvoiceStatusResponse,\n            300: ErrorResponse,\n        },\n        querystring: InvoiceStatusQueryString,\n        tags: ['Invoice'],\n        description: `Retrieve the status and associated data of an invoice. This is essentially the same as \\`/paymentStatus\\` except it doesn't \n        return any sensitive payment information and can be safely invoked from clients' machines to build custom payment interfaces`,\n        summary: 'Retrieve the status and associated data of an invoice',\n    },\n};\n\nexport default async function createInvoiceStatusRoute(server: FastifyInstance) {\n    server.get<{ Reply: Static<typeof InvoiceStatusResponse> | Static<typeof ErrorResponse>; Querystring: Static<typeof InvoiceStatusQueryString> }>(\n        '/getInvoiceStatus',\n        opts,\n        async (request, reply) => {\n            const invoiceId = request.query.invoiceId;\n            const mongoId = decrypt(invoiceId);\n            const { db } = await mongoGenerator();\n            const selectedPayment = (await db.collection('payments').findOne({ _id: new ObjectId(mongoId) })) as WithId<MongoPayment>;\n            if (!selectedPayment) {\n                reply.status(300).send({ error: `No transaction found with given id` });\n                return;\n            }\n\n            reply.status(200).send({\n                publicKey: selectedPayment.publicKey,\n                amount: selectedPayment.amount,\n                expiresAt: selectedPayment.expiresAt.toISOString(),\n                status: selectedPayment.status,\n                amountPaid: selectedPayment.amountPaid,\n                currency: selectedPayment.currency,\n                invoiceCallbackUrl: selectedPayment.invoiceCallbackUrl,\n                memo: 'memo' in selectedPayment ? selectedPayment.memo : undefined,\n            });\n        },\n    );\n}\n"
  },
  {
    "path": "packages/backend/src/routes/paymentStatus.ts",
    "content": "import { Static, Type } from '@sinclair/typebox';\nimport { FastifyInstance, RouteShorthandOptions } from 'fastify';\nimport auth from '../middlewares/auth';\nimport mongoGenerator from '../mongo/generator';\nimport { ObjectId, WithId } from 'mongodb';\nimport { MongoTypeForRequest, cleanDetails, AdminRouteHeaders, ErrorResponse } from './types';\nimport { decrypt } from '../utils';\n\nconst PaymentStatusQueryString = Type.Object({\n    id: Type.String({\n        description: `The database id or invoice id of an existing payment`,\n    }),\n});\n\nconst opts: RouteShorthandOptions = {\n    schema: {\n        response: {\n            300: ErrorResponse,\n            200: MongoTypeForRequest,\n        },\n        querystring: PaymentStatusQueryString,\n        headers: AdminRouteHeaders,\n        tags: ['Payment'],\n        description: 'Retrieve the status and associated data of a payment.',\n        summary: 'Retrieve the status and associated data of a payment',\n        security: [\n            {\n                ApiKey: [],\n            },\n        ],\n    },\n    preHandler: auth,\n};\n\nexport default async function createPaymentStatusRoute(server: FastifyInstance) {\n    server.get<{\n        Reply: Static<typeof MongoTypeForRequest> | Static<typeof ErrorResponse>;\n        Querystring: Static<typeof PaymentStatusQueryString>;\n    }>('/getPaymentStatus', opts, async (request, reply) => {\n        const providedId = request.query.id;\n        const databaseId = providedId.length === 64 ? decrypt(providedId) : providedId;\n        const { db } = await mongoGenerator();\n        const selectedPayment = (await db.collection('payments').findOne({ _id: new ObjectId(databaseId) })) as WithId<Record<string, any>> | null;\n        if (!selectedPayment) {\n            return reply.status(300).send({ error: 'No payment found with given database id or invoice id' });\n        }\n        reply.status(200).send(cleanDetails(selectedPayment as any));\n    });\n}\n"
  },
  {
    "path": "packages/backend/src/routes/paymentsByExtraId.ts",
    "content": "import { Static, Type } from '@sinclair/typebox';\nimport { FastifyInstance, RouteShorthandOptions } from 'fastify';\nimport auth from '../middlewares/auth';\nimport mongoGenerator from '../mongo/generator';\nimport { WithId } from 'mongodb';\nimport { MongoTypeForRequest, cleanDetails, AdminRouteHeaders } from './types';\nimport { ForRequest, MongoPayment } from '../types';\n\nconst PaymentsByExtraIdResponse = Type.Array(MongoTypeForRequest, {\n    description: `Successful response of payments`,\n});\n\nconst PaymentsByExtraIdQueryString = Type.Object({\n    extraId: Type.String({\n        description: `The extraId of an existing payment`,\n    }),\n});\n\nconst opts: RouteShorthandOptions = {\n    schema: {\n        response: {\n            200: PaymentsByExtraIdResponse,\n        },\n        querystring: PaymentsByExtraIdQueryString,\n        headers: AdminRouteHeaders,\n        tags: ['Payment'],\n        description: 'Fetch an array of all payments by *extraId*. All payments by this query will appear here.',\n        summary: 'Fetch an array of all payments by extraId',\n        security: [\n            {\n                ApiKey: [],\n            },\n        ],\n    },\n    preHandler: auth,\n};\n\nexport default async function createPaymentStatusRoute(server: FastifyInstance) {\n    server.get<{\n        Reply: Static<typeof PaymentsByExtraIdResponse> | string;\n        Querystring: Static<typeof PaymentsByExtraIdQueryString>;\n    }>('/getPaymentsByExtraId', opts, async (request, reply) => {\n        const extraId = request.query.extraId;\n        const { db } = await mongoGenerator();\n        const selectedPayments = (await db.collection('payments').find({ extraId }).toArray()) as WithId<MongoPayment>[];\n        const cleanedTransactions: ForRequest<MongoPayment>[] = selectedPayments.map((ele) => cleanDetails(ele));\n        reply.status(200).send(cleanedTransactions);\n    });\n}\n"
  },
  {
    "path": "packages/backend/src/routes/swagger.ts",
    "content": "import { FastifyInstance } from 'fastify';\nimport fastifySwagger from '@fastify/swagger';\nimport fastifySwaggerUi from '@fastify/swagger-ui';\nimport fastifyPlugin from 'fastify-plugin';\nimport config from '../config';\nimport { MongoTypeForRequest } from './types';\n\nexport default fastifyPlugin(async function createInvoiceClientRoute(server: FastifyInstance) {\n    server.register(fastifySwagger, {\n        openapi: {\n            info: {\n                title: 'Keagate – A high-performance crypto payment gateway',\n                version: '0.1.0',\n                description: '',\n            },\n            externalDocs: {\n                url: 'https://keagate.io',\n                description: 'Find more info here',\n            },\n            tags: [\n                {\n                    name: 'Payment',\n                    description:\n                        \"Payment lifecycle administrative routes. These routes require a valid `keagate-api-key` (set in local.json) and should never be directly invoked from your payees' machine.\",\n                },\n                {\n                    name: 'Invoice',\n                    description:\n                        \"Publicly available routes that can be safely called from your payees' devices. These do not return sensitive information. Internally, these routes are used in the built-in invoice UI.\",\n                },\n            ],\n            servers: [\n                {\n                    url: config.has('HOST') ? config.getTyped('HOST') : 'http://YOUR_SERVER',\n                },\n            ],\n            security: [],\n            components: {\n                securitySchemes: {\n                    ApiKey: {\n                        type: 'apiKey',\n                        name: 'keagate-api-key',\n                        in: 'header',\n                    },\n                },\n            },\n        }\n    });\n\n    server.register(fastifySwaggerUi, {\n        routePrefix: '/docs',\n        uiConfig: {\n            // docExpansion: 'full',\n            deepLinking: false,\n        },\n    });\n\n    server.addSchema({\n        $id: 'TypeForRequest',\n        ...MongoTypeForRequest,\n    });\n});\n"
  },
  {
    "path": "packages/backend/src/routes/types.ts",
    "content": "import { Type } from '@sinclair/typebox';\nimport { WithId } from 'mongodb';\nimport { ForRequest, MongoPayment } from '../types';\nimport { encrypt } from '../utils';\nimport { paymentStatuses } from '@keagate/common';\n\n// function StringEnum<T extends string[]>(values: [...T], options?: UnsafeOptions) {\n//     return Type.Unsafe<T[number]>({ enum: values, ...options });\n// }\n\nexport const MongoTypeForRequest = Type.Object(\n    {\n        publicKey: Type.String({\n            description: `The destination address of the payment wallet for the client to send their assets to. \n        This wallet is controlled programmatically and will automatically deposit to your admin wallet as defined in \\`/config/local.json\\`.\n        For most currencies, this address is generated uniquely upon newly created payments, except for some currencies like XRP \n        where a custom memo must be sent by the payee to be identified.`,\n        }),\n        amount: Type.Number({\n            description: `The total value of the payment as defined in \\`createPayment\\`. Note that this is not necessarily the exact amount required\n        before a payment is marked as complete. This depends on the value of *TRANSACTION_SLIPPAGE_TOLERANCE* in \\`/config/local.json\\` which dictates the\n        percentage of a payment that is discounted from the total payment to create a smoother transaction process with network fees.\n        `,\n        }),\n        amountPaid: Type.Number({\n            description: `The total value that has been confirmed as being paid by the payee.`,\n        }),\n        expiresAt: Type.String({\n            description: `An ISO 8601 timestamp detailing when a payment expires at. This value is calculated as [current date + *TRANSACTION_TIMEOUT* (a millisecond value in \\`/config/local.json\\`)].`,\n        }),\n        createdAt: Type.String({\n            description: `An ISO 8601 timestamp detailing when a payment was created.`,\n        }),\n        updatedAt: Type.String({\n            description: `An ISO 8601 timestamp detailing when a payment last received a status update.`,\n        }),\n        status: Type.String({\n            description: `The current status of a payment. Can be one of: ${JSON.stringify(paymentStatuses)}.`,\n        }),\n        id: Type.String({\n            description: `The internal id of a payment, also serves as Mongo's _id for the given record.`,\n        }),\n        extraId: Type.Optional(\n            Type.String({\n                description: `An optional extraId as defined in \\`createPayment\\`. This is useful for you manually managing the identity of payment.\n        Example: An e-commerce store assigns some identification parameter upon a user's checkout for internal use. They create a new payment (via \\`/createPayment\\`) with \n        this value in the *extraId* parameter. The e-commerce store now doesn't have to manage Keagate's assigned *id*, since this *extraId* will be passed in the ipnCallback\n        and can be used to find payments via \\`/getPaymentsByExtraId\\`.`,\n            }),\n        ),\n        ipnCallbackUrl: Type.Optional(\n            Type.String({\n                format: 'uri',\n                description: `An optional callback URL that will be invoked from Keagate when the status of a payment is updated. The request made by Keagate will\n        be a POST request with the body being a JSON object of this same schema.`,\n            }),\n        ),\n        invoiceCallbackUrl: Type.Optional(\n            Type.String({\n                format: 'uri',\n                description: `An optional URL that payees will be re-directed to when their payment finalizes within the invoice interface. A query string parameter will\n        be appended called *status* with the status of the payment. This re-direction will occur automatically.`,\n            }),\n        ),\n        payoutTransactionHash: Type.Optional(\n            Type.String({\n                description: `The transaction id of the successful payment from the generated wallet to your admin wallet of the same currency. This can be used in the corresponding block explorer of this payment's currency.`,\n            }),\n        ),\n        invoiceUrl: Type.String({\n            description: `The path of a Keagate invoice that can be optionally used by your payee's. This is easier to use than the API-driven workflow, but\n        is not required. Note that this is only a path and must be appended to your public domain that Keagate is running on.`,\n        }),\n        currency: Type.String({\n            description: `The shorthand name of the selected payment's corresponding currency.`,\n        }),\n        walletIndex: Type.Optional(\n            Type.Number({\n                description: `Can be ignored and is typically only used for debugging purposes.`,\n            }),\n        ),\n        memo: Type.Optional(\n            Type.String({\n                description: `If this payment currency doesn't support randomly generated wallets for each payment (such as XRP). The payee must pass this value\n        as the memo or description of their transaction.`,\n            }),\n        ),\n    },\n    {\n        description: `Successful response`,\n    },\n);\n\nexport function cleanDetails(details: MongoPayment | WithId<Omit<MongoPayment, 'id'>>): ForRequest<MongoPayment> {\n    const id = 'id' in details ? details.id : details._id.toString();\n\n    return {\n        publicKey: details.publicKey,\n        amount: details.amount,\n        expiresAt: typeof details.expiresAt === 'string' ? details.expiresAt : details.expiresAt.toISOString(),\n        createdAt: typeof details.createdAt === 'string' ? details.createdAt : details.createdAt.toISOString(),\n        updatedAt: typeof details.updatedAt === 'string' ? details.updatedAt : details.updatedAt.toISOString(),\n        status: details.status,\n        id,\n        extraId: details.extraId,\n        ipnCallbackUrl: details.ipnCallbackUrl,\n        invoiceCallbackUrl: details.invoiceCallbackUrl,\n        payoutTransactionHash: details.payoutTransactionHash,\n        invoiceUrl: `/invoice/${details.currency}/${encrypt(id)}`,\n        currency: details.currency,\n        amountPaid: details.amountPaid,\n        ...({\n            walletIndex: 'walletIndex' in details ? details.walletIndex : undefined,\n            memo: 'memo' in details ? details.memo : undefined,\n        } as any),\n    };\n}\n\nexport const AdminRouteHeaders = Type.Object({\n    'keagate-api-key': Type.String({\n        description: `The api key that you have set in \\`/config/local.json\\`. Default is \"API-KEY\", but must be changed for production use.`,\n    }),\n});\n\nexport const ErrorResponse = Type.Object(\n    {\n        error: Type.Optional(\n            Type.String({\n                description: `Error message string`,\n            }),\n        ),\n    },\n    {\n        description: `Error response with message.`,\n    },\n);\n"
  },
  {
    "path": "packages/backend/src/transactionalWallets/GenericTransactionalWallet.ts",
    "content": "import dayjs from 'dayjs';\nimport { ObjectId } from 'mongodb';\nimport { AvailableCurrencies, ConcreteConstructor, PaymentStatusType } from '@keagate/common';\nimport mongoGenerator from '../mongo/generator';\nimport { MongoPayment, IFromNew, INativeInitInDatabase, ICoinlibInitInDatabase } from '../types';\nimport config from '../config';\nimport GenericAdminWallet from '../adminWallets/GenericAdminWallet';\nimport { AnyPayments } from 'coinlib-port';\nimport crypto from 'crypto';\nimport fetch from 'cross-fetch';\nimport logger from '../logger';\n\nexport default abstract class GenericTransactionalWallet {\n    public currency: AvailableCurrencies;\n    protected type: 'coinlib' | 'native';\n    protected _initialized = false;\n\n    protected publicKey: string;\n    protected amount: number;\n    protected amountPaid: number;\n    protected expiresAt: Date;\n    protected createdAt: Date;\n    protected updatedAt: Date;\n    protected status: PaymentStatusType;\n    protected id: string;\n    protected extraId?: string;\n    protected ipnCallbackUrl?: string;\n    protected invoiceCallbackUrl?: string;\n    protected payoutTransactionHash?: string;\n\n    protected onDie: (id: string) => any;\n\n    // You implement these\n    // --\n    protected abstract _cashOut(balance?: number): Promise<string>;\n    protected abstract _getBalance(): Promise<number>;\n    // --\n\n    // fromManual always gets called from other `from` constructors\n    public abstract fromNew(obj: IFromNew, constructor: NativePaymentConstructor | CoinlibPaymentConstructor): Promise<this>;\n    public abstract getDetails(): MongoPayment;\n\n    public abstract fromManual(initObj: MongoPayment, constructor?: NativePaymentConstructor | CoinlibPaymentConstructor);\n    // This always gets called from the three `from` constructors\n\n    public abstract checkTransaction(statusCallback: (status: PaymentStatusType) => any): Promise<void>;\n\n    protected construct(constructor: CoinlibPaymentConstructor | NativePaymentConstructor): void {\n        this.setFromObject(constructor);\n    }\n\n    protected async cashOut(balance: number) {\n        try {\n            const [signature] = await Promise.all([this._cashOut(balance), this.updateStatus({ status: 'SENDING' })]);\n            this.updateStatus({ status: 'FINISHED', payoutTransactionHash: signature });\n            this.onDie(this.id);\n        } catch (error) {\n            this.updateStatus({ status: 'FAILED' }, JSON.stringify(error));\n            this.onDie(this.id);\n        }\n    }\n\n    protected async initInDatabase(obj: INativeInitInDatabase | ICoinlibInitInDatabase): Promise<MongoPayment> {\n        const now = dayjs().toDate();\n        const { db } = await mongoGenerator();\n        const insertObj: Omit<MongoPayment, 'id'> = {\n            amountPaid: 0,\n            expiresAt: dayjs().add(config.getTyped('TRANSACTION_TIMEOUT'), 'milliseconds').toDate(),\n            createdAt: now,\n            updatedAt: now,\n            status: 'WAITING',\n            type: this.type,\n            currency: this.currency,\n            ...obj,\n        };\n        const { insertedId } = await db.collection('payments').insertOne(insertObj);\n        return {\n            ...insertObj,\n            id: insertedId.toString(),\n        } as MongoPayment;\n    }\n\n    protected setFromObject(update: Partial<MongoPayment> | NativePaymentConstructor | CoinlibPaymentConstructor) {\n        for (const [key, val] of Object.entries(update)) {\n            this[key] = val;\n        }\n    }\n\n    protected async updateStatus(update: Partial<MongoPayment>, error?: string) {\n        const { db } = await mongoGenerator();\n        update.updatedAt = dayjs().toDate();\n        this.setFromObject(update);\n        if (error) {\n            logger.log(`Status updated on ${this.currency} payment ${this.id} error: `, error);\n            db.collection('payments').updateOne({ _id: new ObjectId(this.id) }, { $set: update });\n        } else {\n            logger.log(`Status updated on ${this.currency} payment ${this.id}: `, update.status);\n            db.collection('payments').updateOne({ _id: new ObjectId(this.id) }, { $set: update });\n        }\n        if (this.ipnCallbackUrl) {\n            const sendIPN = (body: Record<string, any>, sig?: string) => {\n                fetch(this.ipnCallbackUrl, {\n                    method: 'POST',\n                    body: JSON.stringify(body),\n                    headers: {\n                        'x-keagate-sig': sig,\n                        'Content-Type': 'application/json',\n                    },\n                }).catch((err) => logger.log('IPN Invoke failed with: ', JSON.stringify(err)));\n            };\n            if (config.has('IPN_HMAC_SECRET')) {\n                const details: MongoPayment = this.getDetails();\n                delete details['privateKey'];\n                if (error) {\n                    (details as any).error = error;\n                }\n                const hmac = crypto.createHmac('sha512', config.getTyped('IPN_HMAC_SECRET'));\n                hmac.update(JSON.stringify(details, Object.keys(details).sort()));\n                sendIPN(details, hmac.digest('hex'));\n            } else {\n                sendIPN(\n                    {\n                        error: 'No IPN_HMAC_SECRET configuration parameter set. Please set this up before using instant payment notifications. More information here: https://github.com/dilan-dio4/coinlib-port#instant-payment-notifications',\n                    },\n                    undefined,\n                );\n            }\n        }\n    }\n}\n\ninterface PaymentConstructorRoot {\n    onDie: (id: string) => any;\n}\n\nexport interface NativePaymentConstructor extends PaymentConstructorRoot {\n    adminWalletClass: ConcreteConstructor<typeof GenericAdminWallet>;\n}\n\nexport interface CoinlibPaymentConstructor extends PaymentConstructorRoot {\n    walletIndex: number;\n    currency: AvailableCurrencies;\n    coinlibPayment: AnyPayments<any>;\n}\n"
  },
  {
    "path": "packages/backend/src/transactionalWallets/coinlib/TransactionalCoinlibWrapper.ts",
    "content": "import { CoinlibPayment, IFromNew, MongoPayment } from '../../types';\nimport config from '../../config';\nimport { AnyPayments, BalanceResult, BaseUnsignedTransaction, BaseSignedTransaction, BaseBroadcastResult, UtxoInfo } from 'coinlib-port';\nimport GenericTransactionalWallet, { CoinlibPaymentConstructor } from '../GenericTransactionalWallet';\nimport { PaymentStatusType } from '@keagate/common';\nimport { requestRetry } from '../../utils';\nimport dayjs from 'dayjs';\nimport { minWalletBalances } from './trxLimits';\n\nexport default class TransactionalCoinlibWrapper extends GenericTransactionalWallet {\n    protected type = 'coinlib' as const;\n    private coinlibPayment: AnyPayments<any>;\n    private walletIndex: number;\n    private memo?: string;\n\n    public async fromNew(obj: IFromNew, constructor: CoinlibPaymentConstructor) {\n        // Instantiate --\n        this.construct(constructor);\n        // --\n\n        // NOT THE SAME AS `extraId` from transactional payments\n        const { address, extraId: memo } = await this.coinlibPayment.getPayport(this.walletIndex);\n        const mongoPayment = await this.initInDatabase({\n            ...obj,\n            publicKey: address,\n            walletIndex: this.walletIndex,\n            memo,\n        });\n        return this.fromManual(mongoPayment);\n    }\n\n    public fromManual(initObj: MongoPayment, constructor?: CoinlibPaymentConstructor) {\n        this.setFromObject(initObj);\n        if (constructor) {\n            // Direct instantiation --\n            this.construct(constructor);\n            // --\n        }\n        this._initialized = true;\n        return this;\n    }\n\n    public getDetails(): CoinlibPayment {\n        return {\n            amount: this.amount,\n            createdAt: this.createdAt,\n            expiresAt: this.expiresAt,\n            id: this.id,\n            publicKey: this.publicKey,\n            status: this.status,\n            updatedAt: this.updatedAt,\n            invoiceCallbackUrl: this.invoiceCallbackUrl,\n            ipnCallbackUrl: this.ipnCallbackUrl,\n            payoutTransactionHash: this.payoutTransactionHash,\n            currency: this.currency,\n            amountPaid: this.amountPaid,\n            type: this.type,\n            walletIndex: this.walletIndex,\n            extraId: this.extraId,\n            memo: this.memo,\n        };\n    }\n\n    public async checkTransaction(statusCallback: (status: PaymentStatusType) => any = (status: PaymentStatusType) => null) {\n        if (this.status === 'CONFIRMED' || this.status === 'SENDING') {\n            statusCallback(this.status);\n            return;\n        }\n\n        if (!this._initialized) {\n            statusCallback('WAITING');\n            return;\n        }\n\n        const { confirmedBalance: confirmedBalanceString, sweepable } = await requestRetry<BalanceResult>(() =>\n            this.coinlibPayment.getBalance(this.walletIndex),\n        );\n        const confirmedBalance = +confirmedBalanceString;\n        // Follow this flow...\n        if (confirmedBalance >= this.amount * (1 - config.getTyped('TRANSACTION_SLIPPAGE_TOLERANCE')) && sweepable) {\n            this.status = 'SENDING';\n            await this._cashOut(confirmedBalance);\n            statusCallback('CONFIRMED');\n            this.updateStatus({ status: 'CONFIRMED', amountPaid: confirmedBalance });\n        } else if (dayjs().isAfter(dayjs(this.expiresAt))) {\n            statusCallback('EXPIRED');\n            this.updateStatus({ status: 'EXPIRED' });\n            if (confirmedBalance > 0) {\n                await this._cashOut(confirmedBalance);\n            }\n            this.onDie(this.id);\n        } else if (confirmedBalance > 0 && this.amountPaid !== confirmedBalance) {\n            statusCallback('PARTIALLY_PAID');\n            this.updateStatus({ status: 'PARTIALLY_PAID', amountPaid: confirmedBalance });\n        } else {\n            statusCallback('WAITING');\n        }\n    }\n\n    protected async _cashOut(balance?: number) {\n        try {\n            let createTx: BaseUnsignedTransaction;\n\n            if (balance && this.currency in minWalletBalances) {\n                createTx = await requestRetry<BaseUnsignedTransaction>(() =>\n                    this.coinlibPayment.createTransaction(\n                        this.walletIndex,\n                        config.getTyped(this.currency).ADMIN_PUBLIC_KEY,\n                        '' + (balance - minWalletBalances[this.currency]),\n                    ),\n                );\n            } else {\n                const utxos = await requestRetry<UtxoInfo[]>(() => this.coinlibPayment.getUtxos(this.walletIndex));\n                createTx = await requestRetry<BaseUnsignedTransaction>(() =>\n                    this.coinlibPayment.createSweepTransaction(this.walletIndex, config.getTyped(this.currency).ADMIN_PUBLIC_KEY, {\n                        availableUtxos: utxos.length > 0 ? utxos : undefined,\n                    }),\n                );\n            }\n\n            const signedTx = await requestRetry<BaseSignedTransaction>(() => this.coinlibPayment.signTransaction(createTx));\n            const { id: txHash } = await requestRetry<BaseBroadcastResult>(() => this.coinlibPayment.broadcastTransaction(signedTx));\n            return txHash;\n        } catch (error) {\n            throw new Error(error);\n        }\n    }\n\n    protected async _getBalance(): Promise<number> {\n        const { confirmedBalance } = await requestRetry<BalanceResult>(() => this.coinlibPayment.getBalance(this.walletIndex));\n        return +confirmedBalance;\n    }\n}\n"
  },
  {
    "path": "packages/backend/src/transactionalWallets/coinlib/trxLimits.ts",
    "content": "import { availableCoinlibCurrencies } from '@keagate/common';\nimport crypto from 'crypto';\n\n// In pure unit\nexport const currencyDusts: Partial<Record<typeof availableCoinlibCurrencies[number], number>> = {\n    BTC: 0.00000546,\n    LTC: 0.00000546,\n    DOGE: 1,\n    ETH: 0.00000001,\n    DASH: 0.00000546,\n};\n\n// In pure unit\nexport const minWalletBalances: Partial<Record<typeof availableCoinlibCurrencies[number], number>> = {};\n\nfunction randU32Sync() {\n    return crypto.randomBytes(4).readUInt32BE(0);\n}\n\nconst safeRand = () => Math.round(randU32Sync() / 1000);\n\nexport const walletIndexGenerator: Record<typeof availableCoinlibCurrencies[number], () => number> = {\n    LTC: safeRand,\n    BTC: safeRand,\n    DOGE: safeRand,\n    ETH: safeRand,\n    DASH: safeRand,\n};\n"
  },
  {
    "path": "packages/backend/src/transactionalWallets/native/GenericNativeTransactionalWallet.ts",
    "content": "import { MongoPayment, NativePayment } from '../../types';\nimport config from '../../config';\nimport GenericAdminWallet from '../../adminWallets/GenericAdminWallet';\nimport GenericTransactionalWallet, { NativePaymentConstructor } from '../GenericTransactionalWallet';\nimport { PaymentStatusType } from '@keagate/common';\nimport dayjs from 'dayjs';\n\nexport default abstract class GenericNativeTransactionalWallet extends GenericTransactionalWallet {\n    protected privateKey: string;\n    protected adminWalletMask: GenericAdminWallet;\n    protected type = 'native' as const;\n\n    public fromManual(initObj: MongoPayment, constructor?: NativePaymentConstructor) {\n        this.setFromObject(initObj);\n        if (constructor) {\n            // Direct instantiation --\n            this.adminWalletMask = new constructor.adminWalletClass({\n                publicKey: this.publicKey,\n                privateKey: this.privateKey,\n            });\n            this.construct(constructor);\n            // --\n        }\n        this._initialized = true;\n        return this;\n    }\n\n    public getDetails(): NativePayment {\n        return {\n            amount: this.amount,\n            createdAt: this.createdAt,\n            expiresAt: this.expiresAt,\n            id: this.id,\n            publicKey: this.publicKey,\n            status: this.status,\n            updatedAt: this.updatedAt,\n            invoiceCallbackUrl: this.invoiceCallbackUrl,\n            ipnCallbackUrl: this.ipnCallbackUrl,\n            payoutTransactionHash: this.payoutTransactionHash,\n            currency: this.currency,\n            amountPaid: this.amountPaid,\n            type: this.type,\n            privateKey: this.privateKey,\n            extraId: this.extraId,\n        };\n    }\n\n    public async checkTransaction(statusCallback: (status: PaymentStatusType) => any = (status: PaymentStatusType) => null) {\n        if (this.status === 'CONFIRMED' || this.status === 'SENDING') {\n            statusCallback(this.status);\n            return;\n        }\n\n        if (!this._initialized) {\n            statusCallback('WAITING');\n            return;\n        }\n\n        const confirmedBalance = await this._getBalance();\n\n        // Follow this flow...\n        if (confirmedBalance >= this.amount * (1 - config.getTyped('TRANSACTION_SLIPPAGE_TOLERANCE'))) {\n            this.status = 'SENDING';\n            await this._cashOut(confirmedBalance);\n            statusCallback('CONFIRMED');\n            this.updateStatus({ status: 'CONFIRMED', amountPaid: confirmedBalance });\n        } else if (dayjs().isAfter(dayjs(this.expiresAt))) {\n            statusCallback('EXPIRED');\n            this.updateStatus({ status: 'EXPIRED' });\n            if (confirmedBalance > 0) {\n                await this._cashOut(confirmedBalance);\n            }\n            this.onDie(this.id);\n        } else if (confirmedBalance > 0 && this.amountPaid !== confirmedBalance) {\n            statusCallback('PARTIALLY_PAID');\n            this.updateStatus({ status: 'PARTIALLY_PAID', amountPaid: confirmedBalance });\n        } else {\n            statusCallback('WAITING');\n        }\n    }\n\n    protected async _getBalance(): Promise<number> {\n        const {\n            result: { confirmedBalance },\n        } = await this.adminWalletMask.getBalance();\n        return confirmedBalance;\n    }\n\n    protected async _cashOut(balance: number) {\n        try {\n            const { result } = await this.adminWalletMask.sendTransaction(config.getTyped(this.currency).ADMIN_PUBLIC_KEY, balance);\n            return result;\n        } catch (error) {\n            const details = this.getDetails();\n            throw new Error(`Error during cash out: ${JSON.stringify(error)} \\n\\n\\ton payment: ${details} \\n\\n, `);\n        }\n    }\n}\n"
  },
  {
    "path": "packages/backend/src/transactionalWallets/native/Polygon/index.ts",
    "content": "import { NativePaymentConstructor } from '../../GenericTransactionalWallet';\nimport GenericTransactionalWallet from '../GenericNativeTransactionalWallet';\nimport { AvailableCurrencies } from '@keagate/common';\nimport { IFromNew } from '../../../types';\nimport { ethers } from 'ethers';\n\nexport default class TransactionalPolygon extends GenericTransactionalWallet {\n    public currency: AvailableCurrencies = 'MATIC';\n\n    async fromNew(obj: IFromNew, constructor: NativePaymentConstructor) {\n        this.construct(constructor);\n\n        const newKeypair = ethers.Wallet.createRandom();\n        const publicKey = newKeypair.address;\n        const privateKey = newKeypair._signingKey().privateKey;\n\n        const mongoPayment = await this.initInDatabase({\n            ...obj,\n            publicKey,\n            privateKey,\n        });\n\n        this.adminWalletMask = new constructor.adminWalletClass({\n            publicKey,\n            privateKey,\n        });\n        return this.fromManual(mongoPayment);\n    }\n}\n"
  },
  {
    "path": "packages/backend/src/transactionalWallets/native/Solana/index.ts",
    "content": "import { NativePaymentConstructor } from '../../GenericTransactionalWallet';\nimport GenericTransactionalWallet from '../GenericNativeTransactionalWallet';\nimport { Keypair } from '@solana/web3.js';\nimport { AvailableCurrencies } from '@keagate/common';\nimport base58 from 'bs58';\nimport { IFromNew } from '../../../types';\n\nexport default class TransactionalSolana extends GenericTransactionalWallet {\n    public currency: AvailableCurrencies = 'SOL';\n\n    async fromNew(obj: IFromNew, constructor: NativePaymentConstructor) {\n        this.construct(constructor);\n\n        const newKeypair = Keypair.generate();\n        const privateKey = base58.encode(newKeypair.secretKey);\n        const publicKey = newKeypair.publicKey.toString();\n        const mongoPayment = await this.initInDatabase({\n            ...obj,\n            publicKey,\n            privateKey,\n        });\n\n        // This has to come after the above\n        this.adminWalletMask = new constructor.adminWalletClass({\n            publicKey,\n            privateKey,\n        });\n        return this.fromManual(mongoPayment);\n    }\n}\n"
  },
  {
    "path": "packages/backend/src/types.ts",
    "content": "import { AvailableCurrencies, PaymentStatusType } from '@keagate/common';\n\n// Inherited from all payment types\ninterface PaymentRoot {\n    amount: number;\n    amountPaid: number;\n    status: PaymentStatusType;\n    id: string;\n    extraId?: string;\n    ipnCallbackUrl?: string;\n    invoiceCallbackUrl?: string;\n    payoutTransactionHash?: string;\n    currency: AvailableCurrencies;\n    publicKey: string;\n    expiresAt: Date;\n    createdAt: Date;\n    updatedAt: Date;\n}\n\n// Native specific\nexport interface NativePayment extends PaymentRoot {\n    privateKey: string;\n    type: 'native';\n}\n\n// Coinlib specific\nexport interface CoinlibPayment extends PaymentRoot {\n    type: 'coinlib';\n    walletIndex: number;\n    memo?: string;\n}\n\n// Helpers\nexport type ForRequest<T> = Omit<T, 'expiresAt' | 'createdAt' | 'updatedAt'> & {\n    expiresAt: string;\n    createdAt: string;\n    updatedAt: string;\n    invoiceUrl: string;\n};\n\nexport type MongoPayment = CoinlibPayment | NativePayment;\n\nexport interface IFromNew {\n    amount: number;\n    ipnCallbackUrl?: string;\n    invoiceCallbackUrl: string;\n    extraId?: string;\n}\n\nexport interface INativeInitInDatabase extends IFromNew {\n    publicKey: string;\n    privateKey: string;\n}\n\nexport interface ICoinlibInitInDatabase extends IFromNew {\n    publicKey: string;\n    walletIndex: number;\n    memo?: string;\n}\n"
  },
  {
    "path": "packages/backend/src/utils.ts",
    "content": "import crypto from 'crypto';\nimport { BIP32Factory, BIP32API } from 'bip32';\nimport * as ecc from 'tiny-secp256k1';\nimport config from './config';\n\nexport const bip32: BIP32API = BIP32Factory(ecc);\n\nexport const isBase58 = (value: string): boolean => /^[A-HJ-NP-Za-km-z1-9]*$/.test(value);\n\n// https://gist.github.com/vlucas/2bd40f62d20c1d49237a109d491974eb?permalink_comment_id=3771967#gistcomment-3771967\n\nconst ENCRYPTION_KEY = config.getTyped('INVOICE_ENC_KEY').padEnd(32, 'a').slice(0, 32); // Must be 256 bits (32 characters)\nconst IV_LENGTH = 16; // For AES, this is always 16\nconst iv = 'dac6ff95b69d8a5b48f100269552d0b6'.slice(0, IV_LENGTH);\n\nexport function encrypt(text: string): string {\n    const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);\n    let encrypted = cipher.update(text);\n    encrypted = Buffer.concat([encrypted, cipher.final()]);\n    return encrypted.toString('hex');\n}\n\nexport function decrypt(text: string): string {\n    const encryptedText = Buffer.from(text, 'hex');\n    const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);\n    let decrypted = decipher.update(encryptedText);\n    decrypted = Buffer.concat([decrypted, decipher.final()]);\n    return decrypted.toString();\n}\n\nexport function randomSeedGenerator() {\n    return crypto.randomBytes(32).toString('hex');\n}\n\nexport const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport async function requestRetry<T>(request: (_?: any) => Promise<T>, delayMs = 2500, retryLimit = 15): Promise<T> {\n    let result: T;\n    let attempts = 0;\n    let lastError;\n    while (result === undefined) {\n        attempts++;\n\n        if (attempts >= retryLimit) {\n            throw new Error('Retry request exceeded maximum attempts: ' + JSON.stringify(lastError));\n        }\n\n        try {\n            result = await request();\n        } catch (error) {\n            console.error(error);\n            result = undefined;\n            lastError = error;\n        }\n\n        if (result === undefined) {\n            await delay(delayMs);\n        }\n    }\n    return result;\n}\n\nexport const deadLogger = {\n    error: (...args: any[]) => null,\n    warn: (...args: any[]) => null,\n    info: (...args: any[]) => null,\n    log: (...args: any[]) => null,\n    debug: (...args: any[]) => null,\n    trace: (...args: any[]) => null,\n};\n"
  },
  {
    "path": "packages/backend/tsconfig.package.json",
    "content": "{\n\t\"extends\": \"../../tsconfig.base.json\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"./build\",\n\t\t\"rootDir\": \"./src\",\n\t\t\"composite\": true\n\t},\n\t\"include\": [\n\t\t\"src/**/*.ts\",\n\t\t\"src/**/*.js\",\n\t\t\"src/**/*.tsx\",\n\t\t\"src/**/*.jsx\"\n\t],\n\t\"exclude\": [\n\t\t\"test\",\n\t\t\"build\"\n\t],\n\t\"references\": []\n}"
  },
  {
    "path": "packages/common/package.json",
    "content": "{\n  \"name\": \"@keagate/common\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\\\"# keagate\\\"\",\n  \"main\": \"build/index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"prebuild\": \"shx rm -f tsconfig.package.tsbuildinfo\",\n    \"build\": \"tsc --build ./tsconfig.package.json\",\n    \"clean\": \"shx rm -rf build pnpm-lock.yaml node_modules tsconfig.package.tsbuildinfo\"\n  },\n  \"dependencies\": {\n    \"cross-fetch\": \"^3.1.5\"\n  }\n}\n"
  },
  {
    "path": "packages/common/src/config.ts",
    "content": "import { AvailableCurrencies } from './currencies';\n\ntype MyCurrencyConfig = Partial<\n    Record<\n        AvailableCurrencies,\n        {\n            ADMIN_PUBLIC_KEY: string;\n            ADMIN_PRIVATE_KEY?: string;\n        }\n    >\n>;\n\nexport interface MyConfig extends MyCurrencyConfig {\n    KEAGATE_API_KEY: string;\n    IP_WHITELIST: string[];\n\n    SEED: string;\n\n    TRANSACTION_TIMEOUT: number;\n    TRANSACTION_MIN_REFRESH_TIME: number;\n    TRANSACTION_SLIPPAGE_TOLERANCE: number;\n\n    BLOCKBOOK_RETRY_DELAY: number;\n\n    MONGO_CONNECTION_STRING: string;\n    MONGO_KEAGATE_DB: string;\n\n    INVOICE_ENC_KEY: string;\n    IPN_HMAC_SECRET?: string;\n\n    IS_DEV: boolean;\n    USE_SO_CHAIN: boolean;\n\n    HOST?: string;\n    PORT: number;\n}\n"
  },
  {
    "path": "packages/common/src/currencies.ts",
    "content": "export const availableCoinlibCurrencies = ['LTC', 'BTC', 'ETH', 'DOGE', 'DASH'] as const;\nexport const availableNativeCurrencies = ['SOL', 'MATIC'] as const;\n\nexport type AvailableCurrencies = typeof availableCoinlibCurrencies[number] | typeof availableNativeCurrencies[number];\n\nexport const currencies: Record<AvailableCurrencies, { name: string; explorer: string; networkName?: string }> = {\n    LTC: {\n        name: 'Litecoin',\n        explorer: 'https://live.blockcypher.com/ltc/',\n    },\n    SOL: {\n        networkName: 'Solana Mainnet',\n        name: 'Solana',\n        explorer: 'https://explorer.solana.com/',\n    },\n    DASH: {\n        networkName: 'Dash Mainnet',\n        name: 'Dash',\n        explorer: 'https://chainz.cryptoid.info/dash/',\n    },\n    BTC: {\n        networkName: 'Bitcoin Network Mainnet',\n        name: 'Bitcoin',\n        explorer: 'https://live.blockcypher.com/btc/',\n    },\n    // ADA: {\n    //     name: 'Cardano',\n    //     explorer: 'https://cardanoscan.io/',\n    // },\n    // XRP: {\n    //     name: 'Ripple',\n    //     explorer: 'https://xrpscan.com/',\n    //     networkName: 'XRP Ledger',\n    // },\n    // TRX: {\n    //     name: 'Tron',\n    //     explorer: 'https://tronscan.org',\n    //     networkName: 'TRON network',\n    // },\n    MATIC: {\n        name: 'Polygon',\n        explorer: 'https://polygonscan.com/',\n        networkName: 'Polygon PoS Mainnet',\n    },\n    DOGE: {\n        name: 'Dogecoin',\n        explorer: 'https://dogechain.info/',\n    },\n    ETH: {\n        name: 'Ethereum',\n        explorer: 'https://etherscan.io/',\n        networkName: 'Ethereum Mainnet',\n    },\n    // XLM: {\n    //     name: 'Stellar',\n    //     explorer: 'https://stellarchain.io/'\n    // }\n};\n"
  },
  {
    "path": "packages/common/src/fetch.ts",
    "content": "import fetch from 'cross-fetch';\n\nexport async function fPost(route: string, body: Record<string, any>, headers?: Record<string, any>): Promise<Record<string, any>> {\n    const res = await fetch(route, {\n        method: 'POST',\n        body: JSON.stringify(body),\n        headers,\n    });\n    return await res.json();\n}\n\nexport async function fGet(route: string, headers?: Record<string, any>): Promise<Record<string, any>> {\n    const res = await fetch(route, {\n        headers,\n    });\n    return await res.json();\n}\n"
  },
  {
    "path": "packages/common/src/index.ts",
    "content": "export * from './fetch';\nexport * from './currencies';\nexport * from './utils';\nexport * from './types';\nexport * from './config';\n"
  },
  {
    "path": "packages/common/src/types.ts",
    "content": "// https://stackoverflow.com/a/66702014\nexport type ConcreteConstructor<T extends abstract new (...args: any) => any> = (T extends abstract new (...args: infer A) => infer R\n    ? new (...args: A) => R\n    : never) &\n    T;\n\nexport const paymentStatuses = ['WAITING', 'CONFIRMING', 'CONFIRMED', 'SENDING', 'FINISHED', 'PARTIALLY_PAID', 'FAILED', 'EXPIRED'] as const;\nexport type PaymentStatusType = typeof paymentStatuses[number];\n"
  },
  {
    "path": "packages/common/src/utils.ts",
    "content": "interface NativeUtxo {\n    txId: string;\n    outputIndex: number;\n    address: string;\n    script: string;\n    satoshis: number;\n}\n\nexport function convertChainsoToNativeUtxo(Utxos: Record<string, any>[], address: string, convertToSatoshis = false): NativeUtxo[] {\n    const out: NativeUtxo[] = [];\n    for (const currUtxo of Utxos) {\n        out.push({\n            address,\n            outputIndex: currUtxo.output_no,\n            script: currUtxo.script_hex,\n            txId: currUtxo.txid,\n            satoshis: Math.round(+currUtxo.value * (convertToSatoshis ? 1e8 : 1)),\n        });\n    }\n    return out;\n}\n\nexport function arrayIncludes<T>(arr: T[], ele: any): ele is T {\n    return arr.includes(ele);\n}\n"
  },
  {
    "path": "packages/common/tsconfig.package.json",
    "content": "{\n\t\"extends\": \"../../tsconfig.base.json\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"./build\",\n\t\t\"rootDir\": \"./src\",\n\t\t\"composite\": true\n\t},\n\t\"include\": [\n\t\t\"src/**/*.ts\",\n\t\t\"src/**/*.js\",\n\t\t\"src/**/*.tsx\",\n\t\t\"src/**/*.jsx\"\n\t],\n\t\"exclude\": [\n\t\t\"test\",\n\t\t\"build\"\n\t],\n\t\"references\": []\n}"
  },
  {
    "path": "packages/invoice-client/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/src/public/favicon.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Pending waiting</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossOrigin=\"true\" />\n    <link\n        href=\"https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap\"\n        rel=\"stylesheet\"\n    />\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/index.tsx\"></script>\n  </body>\n</html>"
  },
  {
    "path": "packages/invoice-client/package.json",
    "content": "{\n  \"name\": \"@keagate/invoice-client\",\n  \"scripts\": {\n    \"pureDev\": \"vite\",\n    \"dev\": \"tsc --project tsconfig.package.json && vite build --watch\",\n    \"prebuild\": \"shx rm -f tsconfig.package.tsbuildinfo\",\n    \"build\": \"vite build\",\n    \"clean\": \"shx rm -rf build dist pnpm-lock.yaml node_modules tsconfig.package.tsbuildinfo\"\n  },\n  \"dependencies\": {\n    \"@keagate/common\": \"workspace:^1.0.0\",\n    \"clsx\": \"^1.1.1\",\n    \"cross-fetch\": \"^3.1.5\",\n    \"cryptocurrency-icons\": \"^0.18.0\",\n    \"csstype\": \"^3.0.10\",\n    \"dayjs\": \"^1.11.3\",\n    \"flowbite\": \"^1.4.7\",\n    \"flowbite-react\": \"0.0.25\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-hot-toast\": \"^2.2.0\",\n    \"react-icons\": \"=4.3.1\",\n    \"use-async-effect\": \"^2.2.5\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/typography\": \"^0.5.2\",\n    \"@types/react\": \"^17.0.2\",\n    \"@types/react-dom\": \"^17.0.2\",\n    \"@vitejs/plugin-react\": \"^1.3.2\",\n    \"autoprefixer\": \"^10.4.7\",\n    \"postcss\": \"^8.4.14\",\n    \"tailwindcss\": \"^3.0.24\",\n    \"vite\": \"^2.9.14\",\n    \"vite-plugin-svgr\": \"^2.1.1\",\n    \"vite-tsconfig-paths\": \"^3.5.0\"\n  }\n}\n"
  },
  {
    "path": "packages/invoice-client/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "packages/invoice-client/src/components/App.tsx",
    "content": "import { Flowbite } from 'flowbite-react';\nimport { Toaster } from 'react-hot-toast';\nimport Invoice from './Invoice';\nimport '../styles/globals.css';\n\nfunction App() {\n    return (\n        <Flowbite>\n            <Invoice />\n            <Toaster\n                position='bottom-center'\n                toastOptions={{\n                    style: {\n                        fontFamily:\n                            'Rubik, -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif',\n                    },\n                    className: 'text-sm tracking-tight text-slate-700',\n                }}\n            />\n        </Flowbite>\n    );\n}\n\nexport default App;\n"
  },
  {
    "path": "packages/invoice-client/src/components/Invoice.tsx",
    "content": "import { Spinner, Alert } from 'flowbite-react';\nimport { ReactComponent as BtcIcon } from 'cryptocurrency-icons/svg/color/btc.svg';\nimport { ReactComponent as SolIcon } from 'cryptocurrency-icons/svg/color/sol.svg';\nimport { ReactComponent as LtcIcon } from 'cryptocurrency-icons/svg/color/ltc.svg';\nimport { ReactComponent as DashIcon } from 'cryptocurrency-icons/svg/color/dash.svg';\nimport { ReactComponent as MaticIcon } from 'cryptocurrency-icons/svg/color/matic.svg';\nimport { ReactComponent as EthIcon } from 'cryptocurrency-icons/svg/color/eth.svg';\nimport { ReactComponent as DogeIcon } from 'cryptocurrency-icons/svg/color/doge.svg';\n\nimport { BiTimer, BiCopy, BiErrorAlt } from 'react-icons/bi';\nimport { FiChevronUp, FiChevronDown } from 'react-icons/fi';\nimport { HiInformationCircle } from 'react-icons/hi';\nimport dayjs from 'dayjs';\nimport relativeTime from 'dayjs/plugin/relativeTime';\nimport clsx from 'clsx';\nimport React, { useState, useEffect } from 'react';\nimport { copyToClipboard } from '../utils/utils';\nimport { AvailableCurrencies, currencies, fGet, PaymentStatusType } from '@keagate/common';\nimport ThreeDotsOverlay from './ThreeDotsOverlay';\n\ndayjs.extend(relativeTime);\n\nexport default function Invoice() {\n    const currencyToIcon: Record<AvailableCurrencies, React.ReactChild> = {\n        LTC: <LtcIcon width={50} height={50} />,\n        SOL: <SolIcon width={50} height={50} />,\n        BTC: <BtcIcon width={50} height={50} />,\n        MATIC: <MaticIcon width={50} height={50} />,\n        DOGE: <DogeIcon width={50} height={50} />,\n        ETH: <EthIcon width={50} height={50} />,\n        DASH: <DashIcon width={50} height={50} />,\n    };\n\n    const [isBlockchainInfoOpen, setIsBlockchainInfoOpen] = useState<boolean>(false);\n    const [isTransactionDead, setIsTransactionDead] = useState<boolean>(false);\n    const [currency, setCurrency] = useState<AvailableCurrencies>();\n    const [invoiceId, setInvoiceId] = useState<string>('');\n    interface IInvoiceObject {\n        amount: number;\n        amountPaid: number;\n        currency: AvailableCurrencies;\n        expiresAt: string;\n        publicKey: string;\n        status: PaymentStatusType;\n        invoiceCallbackUrl?: string;\n        memo?: string;\n    }\n    const [invoiceObject, setInvoiceObject] = useState<IInvoiceObject>();\n    const [paymentMajorError, setPaymentMajorError] = useState<string>('');\n\n    useEffect(() => {\n        const params = window.location.pathname.split('/');\n        const _invoiceId = params.pop();\n        setInvoiceId(_invoiceId);\n        const _currency = params.pop().toUpperCase();\n        setCurrency(_currency as AvailableCurrencies);\n\n        // eslint-disable-next-line prefer-const\n        let interval: NodeJS.Timeout;\n        async function runner() {\n            const _invoiceObj = (await fGet(`/getInvoiceStatus?invoiceId=${_invoiceId}`)) as IInvoiceObject;\n            if ('error' in _invoiceObj) {\n                setPaymentMajorError(String(_invoiceObj['error']));\n                clearInterval(interval);\n                return;\n            }\n\n            setInvoiceObject(_invoiceObj);\n            document.title = `Payment ${_invoiceObj.status.toLowerCase().replace('_', ' ')}`;\n            if (\n                _invoiceObj.status === 'CONFIRMED' ||\n                _invoiceObj.status === 'FAILED' ||\n                _invoiceObj.status === 'FINISHED' ||\n                _invoiceObj.status === 'SENDING' ||\n                _invoiceObj.status === 'EXPIRED'\n            ) {\n                clearInterval(interval);\n                setIsTransactionDead(true);\n                if (_invoiceObj.invoiceCallbackUrl) {\n                    window.location.href = _invoiceObj.invoiceCallbackUrl + `?status=${_invoiceObj.status}&invoice_id=${_invoiceId}`;\n                }\n            }\n        }\n        runner();\n        interval = setInterval(runner, 6000);\n        return () => clearInterval(interval);\n    }, []);\n\n    if (paymentMajorError) {\n        return (\n            <div className='flex justify-center items-center flex-col h-[100vh] pb-10'>\n                <BiErrorAlt className='text-red-600' size={54} />\n                <p className='text-red-600 font-semibold text-lg mt-2 tracking-tight'>{paymentMajorError}</p>\n            </div>\n        );\n    }\n\n    if (!invoiceObject || !currency) {\n        return <ThreeDotsOverlay showDots flashDots />;\n    }\n\n    const blockchainDetails: { key: string; value: string; Component: React.FC }[] = [];\n    type SpanProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>;\n    type AProps = React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>;\n    currencies[currency]?.networkName &&\n        blockchainDetails.push({\n            key: 'Network Name',\n            value: currencies[currency].networkName,\n            Component: (props: SpanProps) => <span {...props} />,\n        });\n    blockchainDetails.push({\n        key: 'Full Name',\n        value: currencies[currency].name,\n        Component: (props: SpanProps) => <span {...props} />,\n    });\n    blockchainDetails.push({\n        key: 'Ticker',\n        value: currency,\n        Component: (props: SpanProps) => <span {...props} />,\n    });\n    blockchainDetails.push({\n        key: 'Chain Explorer',\n        value: currencies[currency].explorer,\n        Component: (props: AProps) => <a {...props} href={currencies[currency].explorer} target='_blank' />,\n    });\n\n    function getSpinnerText(): string {\n        switch (invoiceObject.status) {\n            case 'WAITING':\n                return 'Awaiting for payment confirmation';\n            case 'PARTIALLY_PAID':\n                return 'Payment partially fulfilled';\n            case 'EXPIRED':\n                return 'Invoice expired';\n            case 'CONFIRMING':\n                return 'Confirming transaction';\n            case 'CONFIRMED':\n            case 'SENDING':\n            case 'FINISHED':\n                return 'Successfully confirmed transaction';\n            case 'FAILED':\n                return 'Payment failed';\n        }\n    }\n\n    function isSpinnerBackgroundRed() {\n        if (invoiceObject.status === 'EXPIRED' || invoiceObject.status === 'FAILED') {\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    function formatExpiresAtTime(expiresAtString: string) {\n        const expiresObj = dayjs(expiresAtString);\n        if (expiresObj.isAfter(dayjs().add(10, 'm'))) {\n            return dayjs().to(expiresObj, true);\n        } else {\n            return expiresObj.format('h:mm A');\n        }\n\n    }\n\n    return (\n        <>\n            <div className='overflow-hidden max-w-full mx-auto sm:mt-14 sm:border sm:rounded-lg sm:max-w-[500px]'>\n                <div className={clsx('text-center py-2.5 text-white', isSpinnerBackgroundRed() ? 'bg-red-600' : 'bg-indigo-500')}>\n                    <span className='flex justify-center items-center'>\n                        {!isTransactionDead && <Spinner className='status-spinner' />}\n                        <p className='ml-3 text-sm font-medium sm:text-lg sm:font-semibold'>{getSpinnerText()}</p>\n                    </span>\n                </div>\n                <div className='border-b flex justify-between items-center py-4 px-5'>\n                    <div className='flex items-center'>\n                        {currencyToIcon[currency]}\n                        <div className='ml-2'>\n                            <p className='tracking-tight'>\n                                <b>Invoice</b>\n                            </p>\n                            <p\n                                className='text-slate-600 cursor-pointer hover:text-slate-800 tracking-tight'\n                                onClick={(_) => copyToClipboard(invoiceId, 'Copied invoice ID to clipboard')}\n                            >\n                                {invoiceId.slice(0, 3)}\n                                {String.fromCharCode(8230)}\n                                {invoiceId.slice(-3)}\n                            </p>\n                        </div>\n                    </div>\n                    <div>\n                        <p className='tracking-tight text-end'>\n                            <b>Expires</b>\n                        </p>\n                        <p className='flex items-center text-slate-600 tracking-tight'>\n                            <BiTimer size={20} className='mr-1' />\n                            {formatExpiresAtTime(invoiceObject.expiresAt)}\n                        </p>\n                    </div>\n                </div>\n                <div className='px-5 py-4'>\n                    <div\n                        className={clsx(\n                            'cursor-pointer flex justify-between items-center px-4 py-3',\n                            isBlockchainInfoOpen ? 'border-x border-t rounded-t-md' : 'border rounded-md',\n                        )}\n                        onClick={(_) => setIsBlockchainInfoOpen((prev) => !prev)}\n                    >\n                        <p className='font-bold'>Blockchain Details</p>\n                        {isBlockchainInfoOpen ? <FiChevronUp size={20} /> : <FiChevronDown size={20} />}\n                    </div>\n                    <div\n                        className={clsx(\n                            isBlockchainInfoOpen ? 'h-[140px] border-x border-b rounded-b-md' : 'h-0',\n                            'transition-all ease-in-out duration-200 overflow-hidden',\n                        )}\n                    >\n                        <div className='px-4 py-3'>\n                            {blockchainDetails.map(({ Component, ...ele }) => (\n                                <span className='text-sm flex pb-2' key={ele.key}>\n                                    <p className='w-[150px] font-bold'>{ele.key}:</p>\n                                    <Component>{ele.value}</Component>\n                                </span>\n                            ))}\n                        </div>\n                    </div>\n                </div>\n\n                <div className='bg-slate-100 py-8 px-5 h-full'>\n                    <div className='rounded-md bg-white py-5'>\n                        <div className='text-center px-4'>\n                            <p className='tracking-tight mb-0.5'>Amount:</p>\n                            <p\n                                className={clsx(\n                                    'text-lg font-bold transition-colors',\n                                    isTransactionDead ? 'text-gray-400' : 'text-black cursor-pointer hover:text-gray-500',\n                                )}\n                                onClick={(_) => !isTransactionDead && copyToClipboard('' + invoiceObject.amount, 'Copied value to clipboard')}\n                            >\n                                {invoiceObject.amount} {currency} {!isTransactionDead && <BiCopy className='inline-block mb-1' size={16} />}\n                            </p>\n                        </div>\n                        <div className='h-[1px] bg-slate-200 my-5 mx-5'></div>\n                        <div className='text-center px-4'>\n                            <p className='tracking-tight mb-1.5'>Payment Address:</p>\n                            <p\n                                className={clsx(\n                                    'transition-colors font-semibold mt-1.5 text-xs leading-tight tracking-wide break-all',\n                                    isTransactionDead ? 'text-gray-400' : 'cursor-pointer text-blue-500 hover:text-blue-600',\n                                )}\n                                onClick={(_) => !isTransactionDead && copyToClipboard(invoiceObject.publicKey, 'Copied address to clipboard')}\n                            >\n                                {invoiceObject.publicKey}\n                                {!isTransactionDead && <BiCopy className='ml-0.5 mb-[1px] inline-block' size={12} />}\n                            </p>\n                        </div>\n                        <div className='text-center px-4 mt-6 alert-root'>\n                            <Alert color='blue' icon={HiInformationCircle}>\n                                <span className='font-normal tracking-tight text-xs text-center w-full'>\n                                    Please verify the address and amount before sending the transaction.\n                                </span>\n                            </Alert>\n                        </div>\n                    </div>\n                    <div className='flex mt-10'>\n                        <div className='basis-5/12 text-center'>\n                            <p className='text-sm text-slate-600 tracking-tight'>Amount collected:</p>\n                            <p className='text-md font-bold'>\n                                {invoiceObject.amountPaid.toFixed(7)} {currency}\n                            </p>\n                        </div>\n                        <div className='basis-2/12 flex justify-center'>\n                            <div className='h-full bg-slate-300 w-[3px] rounded-md'></div>\n                        </div>\n                        <div className='basis-5/12 text-center'>\n                            <p className='text-sm text-slate-600 tracking-tight'>Amount due:</p>\n                            <p\n                                className={clsx(\n                                    'text-md font-bold transition-colors',\n                                    isTransactionDead ? 'text-gray-400' : 'text-black cursor-pointer hover:text-gray-500',\n                                )}\n                                onClick={(_) =>\n                                    !isTransactionDead && copyToClipboard('' + (invoiceObject.amount - invoiceObject.amountPaid), 'Copied value to clipboard')\n                                }\n                            >\n                                {(invoiceObject.amount - invoiceObject.amountPaid).toFixed(7)} {currency}{' '}\n                                {!isTransactionDead && <BiCopy className='inline-block mb-1' size={15} />}\n                            </p>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            <h1 className='text-xs font-bold text-center text-slate-600 my-7 tracking-tight'>\n                Powered by open-source software <a href='https://github.com/dilan-dio4/Keagate'>Keagate</a>\n            </h1>\n            <div className='bottom-0 fixed w-full -z-10 h-[40vh] bg-slate-100 sm:hidden sm:invisible'></div>\n        </>\n    );\n}\n"
  },
  {
    "path": "packages/invoice-client/src/components/ThreeDotsOverlay.tsx",
    "content": "import React, { useState, useEffect } from 'react';\nimport clsx from 'clsx';\n\ninterface IThreeDotsOverlay {\n    showDots: boolean;\n    flashDots?: boolean;\n    text?: string | JSX.Element;\n    className?: string;\n}\n\nexport default function ThreeDotsOverlay({ showDots, text, className, flashDots }: IThreeDotsOverlay) {\n    const [mounted, setMounted] = useState<boolean>(false);\n\n    useEffect(() => {\n        const timer = setTimeout((_) => setMounted(true), 100); // Dirty hack to force transition\n        return () => clearTimeout(timer);\n    }, []);\n\n    const overlayStyle: React.CSSProperties = {\n        zIndex: 1,\n        backgroundColor: '#ffffff',\n        position: 'fixed',\n        right: 0,\n        left: 0,\n        bottom: 0,\n        top: 0,\n        WebkitTapHighlightColor: 'transparent',\n    };\n\n    const textStyle: React.CSSProperties = {\n        color: '#a3a1a1',\n        whiteSpace: 'break-spaces',\n        margin: 'auto',\n        position: 'absolute',\n        paddingTop: 127,\n        paddingLeft: 10,\n        paddingRight: 10,\n        transition: 'opacity 550ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',\n        opacity: mounted ? 1 : 0,\n    };\n\n    return (\n        <div style={overlayStyle} className={clsx('flex', 'justify-center', 'items-center', className)}>\n            {showDots && (\n                <div className={clsx('dots-loader', flashDots && 'flash-please')}>\n                    <div></div>\n                    <div></div>\n                    <div></div>\n                </div>\n            )}\n            <p style={textStyle} className='text-center'>\n                {text || ''}\n            </p>\n        </div>\n    );\n}\n"
  },
  {
    "path": "packages/invoice-client/src/index.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport 'tailwindcss/tailwind.css';\nimport App from './components/App';\n\nReactDOM.render(\n    <React.StrictMode>\n        <App />\n    </React.StrictMode>,\n    document.getElementById('root'),\n);\n"
  },
  {
    "path": "packages/invoice-client/src/styles/globals.css",
    "content": "@import \"tailwindcss/base\";\n@import \"tailwindcss/components\";\n@import \"tailwindcss/utilities\";\n\nhtml, body {\n  padding: 0;\n  margin: 0;\n  font-family: Rubik, -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\na {\n  color: #1890ff;\n  text-decoration: none;\n  background-color: initial;\n  outline: none;\n}\n\n.status-spinner {\n  @apply !fill-white;\n  color: rgb(0 0 0 / 23%) !important;\n}\n\n.alert-root>div {\n  @apply  !py-3 !px-2;\n}\n\n.dots-loader {\n  display: flex;\n  justify-content: center;\n}\n\n.dots-loader>div {\n  width: 16px;\n  height: 16px;\n  margin: 3px 6px;\n  border-radius: 50%;\n  background-color: #a3a1a1;\n  opacity: 1;\n}\n\n.dots-loader.flash-please>div {\n  animation: flash-one 0.6s infinite alternate;\n}\n\n@keyframes flash-one {\n  to {\n    opacity: 0.1;\n  }\n}\n\n.dots-loader>div:nth-child(1) {\n  animation-delay: 0.5s;\n}\n\n.dots-loader>div:nth-child(2) {\n  animation-delay: 0.7s;\n}\n\n.dots-loader>div:nth-child(3) {\n  animation-delay: 0.9s;\n}"
  },
  {
    "path": "packages/invoice-client/src/utils/useDeviceSize.tsx",
    "content": "import { useState, useEffect } from 'react';\n\nconst useDeviceSize = () => {\n    const [width, setWidth] = useState<number>(0);\n    const [height, setHeight] = useState<number>(0);\n\n    const handleWindowResize = () => {\n        setWidth(window.innerWidth);\n        setHeight(window.innerHeight);\n    };\n\n    useEffect(() => {\n        // component is mounted and window is available\n        handleWindowResize();\n        window.addEventListener('resize', handleWindowResize);\n        // unsubscribe from the event on component unmount\n        return () => window.removeEventListener('resize', handleWindowResize);\n    }, []);\n\n    return {\n        width,\n        height,\n        isMobile: width < 760,\n    };\n};\n\nexport default useDeviceSize;\n"
  },
  {
    "path": "packages/invoice-client/src/utils/utils.ts",
    "content": "import toast from 'react-hot-toast';\n\nexport const copyToClipboard = (value: string, successText: string) => {\n    if (!navigator.clipboard) {\n        const myInput = document.createElement('input');\n        myInput.setAttribute('value', value);\n        myInput.setAttribute('hidden', 'true');\n        myInput.style.display = 'none';\n        myInput.style.visibility = 'hidden';\n        document.body.appendChild(myInput);\n        myInput.select();\n        try {\n            document.execCommand('copy');\n            toast.success(successText);\n        } catch (error) {\n            console.error(error);\n            toast.error('Error copying');\n        } finally {\n            document.body.removeChild(myInput);\n        }\n    } else {\n        navigator.clipboard.writeText(value).then(\n            () => {\n                toast.success(successText);\n            },\n            (error) => {\n                console.error(error);\n                toast.error('Error copying');\n            },\n        );\n    }\n};\n"
  },
  {
    "path": "packages/invoice-client/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n/// <reference types=\"vite-plugin-svgr/client\" />\n"
  },
  {
    "path": "packages/invoice-client/tailwind.config.js",
    "content": "/** @type {import('tailwindcss/tailwind-config').TailwindConfig} */\nconst config = {\n  content: [\n    \"./src/components/**/*.{js,ts,jsx,tsx}\",\n    \"./src/components/index.{js,ts,jsx,tsx}\",\n    \"./src/utils/**/*.{js,ts,jsx,tsx}\",\n    \"./node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}\",\n    \"./index.html\"\n  ],\n  theme: {\n    container: {\n      center: true,\n    },\n    animation: {\n      enter: 'enter 200ms ease-out',\n      'slide-in': 'slide-in 1.2s cubic-bezier(.41,.73,.51,1.02)',\n      leave: 'leave 150ms ease-in forwards',\n      spin: \"spin 1s linear infinite\"\n    },\n    keyframes: {\n      enter: {\n        '0%': { transform: 'scale(0.9)', opacity: 0 },\n        '100%': { transform: 'scale(1)', opacity: 1 },\n      },\n      leave: {\n        '0%': { transform: 'scale(1)', opacity: 1 },\n        '100%': { transform: 'scale(0.9)', opacity: 0 },\n      },\n      'slide-in': {\n        '0%': { transform: 'translateY(-100%)' },\n        '100%': { transform: 'translateY(0)' },\n      },\n      spin: {\n        '0%': { transform: 'rotate(0deg)' },\n        '100%': { transform: 'rotate(360deg)' }\n      }\n    },\n  },\n  plugins: [\n    require('@tailwindcss/typography'),\n    require('flowbite/plugin')\n  ],\n  darkMode: 'class'\n}\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/invoice-client/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": true,\n    \"esModuleInterop\": true\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"src/**/*.ts\",\n    \"src/**/*.tsx\"\n  ]\n}\n"
  },
  {
    "path": "packages/invoice-client/tsconfig.package.json",
    "content": "{\n\t\"extends\": \"../../tsconfig.base.json\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"./build\",\n\t\t\"rootDir\": \"./src\",\n\t\t\"composite\": true,\n\t\t\"lib\": [\n\t\t\t\"dom\",\n\t\t\t\"dom.iterable\",\n\t\t\t\"esnext\"\n\t\t],\n\t\t\"target\": \"ES5\",\n\t\t\"jsx\": \"react-jsx\"\n\t},\n\t\"include\": [\n\t\t\"src/**/*.ts\",\n\t\t\"src/**/*.js\",\n\t\t\"src/**/*.tsx\",\n\t\t\"src/**/*.jsx\"\n\t],\n\t\"exclude\": [\n\t\t\"test\",\n\t\t\"build\"\n\t],\n\t\"references\": []\n}"
  },
  {
    "path": "packages/invoice-client/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nimport tsconfigPaths from 'vite-tsconfig-paths'\nimport svgr from 'vite-plugin-svgr'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n    plugins: [react(), svgr(), tsconfigPaths()],\n    base: \"/static-invoice/\",\n\n    // // https://github.com/vitejs/vite/issues/5668#issuecomment-968125763\n    optimizeDeps: {\n        include: ['@keagate/common']\n    },\n    build: {\n        commonjsOptions: {\n            include: [/common/, /node_modules/]\n        }\n    }\n})"
  },
  {
    "path": "packages/scripts/assets/default.conf",
    "content": "server {\n\tlisten 80 default_server;\n\tlisten [::]:80 default_server;\n    server_name _;\n\n    location / {\n        proxy_pass http://localhost:8081;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection 'upgrade';\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_cache_bypass $http_upgrade;\n        proxy_connect_timeout 600;\n        proxy_send_timeout 600;\n        proxy_read_timeout 600;\n        send_timeout 600;\n    }\n\n    location /health/startup {\n        add_header Content-Type text/plain;\n        return 200 'healthy';\n    }\n}"
  },
  {
    "path": "packages/scripts/keagate.sh",
    "content": "#!/bin/bash\n\n{ # this ensures the entire script is downloaded #\n\n    # OS=\"$(uname -s)\"\n    if [ -n \"$SUDO_USER\" ]; then\n        HOME=\"$(getent passwd \"$SUDO_USER\" | cut -d: -f6)\"\n    fi\n\n    INSTALL_DIR=\"$HOME\"\n    FOLDER_NAME=\"Keagate\"\n    REPO_LOCATION=\"https://github.com/dilan-dio4/$FOLDER_NAME\"\n    NODE_ARGS=\"\"\n\n    # sudo chmod 777 -R .local\n\n    # Parse Flags\n    for i in \"$@\"; do\n        case $i in\n        -q | --quiet)\n            QUIET=\"true\"\n            NODE_ARGS+=\"$i \"\n            shift # past argument\n            # shift # past value\n            ;;\n        -v | --verbose)\n            VERBOSE=\"true\"\n            NODE_ARGS+=\"$i \"\n            shift # past argument\n            # shift # past value\n            ;;\n        *)\n            echo \"Unrecognized argument $i\"\n            exit 1\n            ;;\n        esac\n    done\n\n    keagate_has() {\n        command -v \"$1\" >/dev/null 2>&1\n    }\n\n    keagate_debug() {\n        if [ -n \"$VERBOSE\" ]; then\n            command printf %s\\\\n \"$*\" 2>/dev/null\n        fi\n    }\n\n    print_complete() {\n        echo -e \"\\033[1;32m \\xE2\\x9C\\x94 ${1:-\"Complete\"}\\033[0m\"\n    }\n\n    install_node() {\n        echo \"Installing Node and NPM via nvm...\"\n        curl -s -o nvm.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh\n        chmod +x ./nvm.sh\n        export NVM_DIR=\"$HOME/.nvm\"\n        ./nvm.sh >/dev/null 2>&1\n        rm ./nvm.sh\n        [ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\"                   # This loads nvm\n        [ -s \"$NVM_DIR/bash_completion\" ] && \\. \"$NVM_DIR/bash_completion\" # This loads nvm bash_completion\n        nvm install 16 >/dev/null 2>&1\n        nvm use 16 >/dev/null 2>&1\n        print_complete\n    }\n\n    clean_stdin() {\n        read -r -t 1 -n 10000 discard\n    }\n\n    # https://gist.github.com/kujiy/3a4d2dc368db93712f638a28462de62d\n    ask() {\n        # http://djm.me/ask\n        local prompt default REPLY\n        while true; do\n            if [ \"${2:-}\" = \"Y\" ]; then\n                prompt=\"Y/n\"\n                default=Y\n            elif [ \"${2:-}\" = \"N\" ]; then\n                prompt=\"y/N\"\n                default=N\n            else\n                prompt=\"y/n\"\n                default=\n            fi\n\n            # Ask the question (not using \"read -p\" as it uses stderr not stdout)\n            echo -n \"$1 [$prompt] \"\n            # Read the answer (use /dev/tty in case stdin is redirected from somewhere else)\n            read REPLY </dev/tty\n\n            # Default?\n            if [ -z \"$REPLY\" ]; then\n                REPLY=$default\n            fi\n\n            # Check if the reply is valid\n            case \"$REPLY\" in\n            Y* | y*) return 0 ;;\n            N* | n*) return 1 ;;\n            esac\n        done\n    }\n\n    if ! keagate_has \"git\"; then\n        echo \"git command not found. Installing...\"\n        if keagate_has \"apt\"; then\n            sudo apt -y install git\n        elif keagate_has \"dnf\"; then\n            sudo dnf -y install git\n        elif keagate_has \"yum\"; then\n            sudo yum -y install git\n        else\n            echo \"Could not install git from bash. Please install git, then run this script again. (For more information: https://git-scm.com/download/linux)\"\n            exit 1\n        fi\n        print_complete\n    else\n        print_complete \"git detected\"\n    fi\n\n    if ! keagate_has \"docker\"; then\n        echo \"Docker command not found. Installing...\"\n        # Install Docker - used for Mongo and Nginx\n        curl -fsSL https://get.docker.com -o get-docker.sh\n        sudo sh get-docker.sh >/dev/null 2>&1\n        rm get-docker.sh\n        if ! keagate_has \"docker\"; then\n            keagate_debug \"Falling back to native Docker install (yum only)...\"\n\n            if keagate_has \"yum\"; then\n                sudo yum update -y\n                sudo yum -y install docker\n                sudo service docker start\n            fi\n\n            if ! keagate_has \"docker\"; then\n                echo \"Could not install Docker from bash. Please install Docker, then run this script again. (For more information: https://docs.docker.com/desktop/linux/install/)\"\n                exit 1\n            fi\n        fi\n        print_complete\n    else\n        print_complete \"Docker detected\"\n    fi\n\n    # +++ Permissions, ports, and flavors\n    if [ -f \"/etc/debian_version\" ]; then\n        sudo apachectl stop >/dev/null 2>&1 || true\n    fi\n\n    # Fix permissions issue on certain ports from `docker run`\n    if [ ! -f \"/var/run/docker.sock\" ]; then\n        sudo service docker start\n    fi\n    sudo chmod 666 /var/run/docker.sock\n    # ---\n\n    if keagate_has \"node\" && keagate_has \"npm\"; then\n        print_complete \"Node and NPM detected\"\n        installed_node_version=$(node --version | cut -c 2-3)\n        if ((\"$installed_node_version\" < \"14\")); then\n            if ask \"Your existing Node version ($installed_node_version) is too low for Keagate. Would you like me to automatically upgrade Node and NPM? (You can revert back with \\`nvm install $installed_node_version && nvm use $installed_node_version\\`)\"; then\n                install_node\n            else\n                echo \"Please manually upgrade Node to at least version 14, then run this script again.\"\n                exit 1\n            fi\n        else\n            print_complete \"Node and NPM version $installed_node_version satisfactory\"\n        fi\n    else\n        install_node\n    fi\n\n    cd $INSTALL_DIR\n\n    if [ -d \"$FOLDER_NAME\" ]; then\n        keagate_debug \"Found an existing $FOLDER_NAME/. Asking for permission to override...\"\n        if ask \"Folder $FOLDER_NAME/ already exists in $INSTALL_DIR. Would you like me to overwrite this? (This will preserve \\`config/local.json\\`)\"; then\n            keagate_debug \"Caching existing local.json to a temporary file...\"\n            sudo chown -R \"$(whoami)\" $FOLDER_NAME/\n            cp -f $FOLDER_NAME/config/local.json ./local.json >/dev/null 2>&1 || true\n            rm -rf $FOLDER_NAME\n            echo \"Cloning Keagate repo...\"\n            git clone $REPO_LOCATION >/dev/null 2>&1\n            cp -f ./local.json $FOLDER_NAME/config/local.json >/dev/null 2>&1 || true\n            rm -f ./local.json || true\n        fi\n    else\n        echo \"Cloning Keagate repo...\"\n        git clone $REPO_LOCATION >/dev/null 2>&1\n    fi\n\n    sudo chown -R \"$(whoami)\" $FOLDER_NAME/\n    print_complete\n\n    cd $FOLDER_NAME\n\n    # >/dev/null 2>&1\n\n    echo \"Installing and configuring pnpm...\"\n    npm i -g pnpm >/dev/null 2>&1\n    export PNPM_HOME=\"$HOME/.local/share/pnpm\"\n    export PATH=\"$PNPM_HOME:$PATH\"\n    pnpm setup >/dev/null 2>&1\n    print_complete\n\n    echo \"Installing Keagate dependencies...\"\n    pnpm i --silent -g pm2\n    pnpm i --silent\n    print_complete\n\n    echo \"Building Keagate...\"\n    pnpm run build >/dev/null 2>&1\n    print_complete\n\n    node packages/scripts/build/configure.js \"$NODE_ARGS\"\n} # this ensures the entire script is downloaded #\n\n# https://www.shellcheck.net/\n"
  },
  {
    "path": "packages/scripts/package.json",
    "content": "{\n  \"name\": \"@keagate/scripts\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\\\"# keagate\\\"\",\n  \"main\": \"build/index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"prebuild\": \"shx rm -f tsconfig.package.tsbuildinfo\",\n    \"build\": \"tsc --build ./tsconfig.package.json\",\n    \"clean\": \"shx rm -rf build pnpm-lock.yaml node_modules tsconfig.package.tsbuildinfo\"\n  },\n  \"dependencies\": {\n    \"@expo/spawn-async\": \"^1.6.0\",\n    \"command-exists\": \"^1.2.9\",\n    \"commander\": \"^9.3.0\",\n    \"cross-fetch\": \"^3.1.5\",\n    \"kleur\": \"^4.1.5\",\n    \"mongodb\": \"^4.7.0\",\n    \"prompts\": \"^2.4.2\",\n    \"@keagate/common\": \"workspace:^1.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/command-exists\": \"^1.2.0\",\n    \"@types/node\": \"^18.0.1\",\n    \"@types/prompts\": \"^2.0.14\"\n  }\n}\n"
  },
  {
    "path": "packages/scripts/src/DelegateLogger.ts",
    "content": "export type LoggingLevels = 0 | 1 | 2;\n\nclass DelegateLogger {\n    logLevel: LoggingLevels = 1;\n\n    public setLogLevel(level: LoggingLevels) {\n        this.logLevel = level;\n    }\n\n    error = (...args: any[]) => this.logLevel >= 1 && console.error(...args);\n    log = (...args: any[]) => this.logLevel >= 1 && console.log(...args);\n    debug = (...args: any[]) => this.logLevel >= 2 && console.log(...args);\n    success = (text = 'Complete') => this.log('\\x1b[1;32m \\u2714 ' + text + '\\x1b[0m');\n}\n\nconst logger = new DelegateLogger();\n\nexport default logger;\n"
  },
  {
    "path": "packages/scripts/src/configure.ts",
    "content": "import { MyConfig } from '@keagate/common';\nimport { program } from 'commander';\nimport logger, { LoggingLevels } from './DelegateLogger';\nimport setupMongo from './setupMongo';\nimport setupNginx from './setupNginx';\nimport setupSeeds from './setupSeeds';\nimport setupWallets from './setupWallets';\nimport { existsSync, writeFileSync } from 'fs';\nimport path from 'path';\nimport opts, { setOpts } from './opts';\nimport spawnAsync from '@expo/spawn-async';\nimport kleur from 'kleur';\n\nprogram\n    .name('Keagate configure')\n    .description('CLI for configuring keagate')\n    .option('-q, --quiet', 'Install quietly without asking for configuration. Sensible defaults will be applied')\n    .option('-v, --verbose', 'Verbose logging')\n    .option('-d --dryrun', 'Dry run. No downloading of programs or editing of your file system will occur.');\n\nasync function main() {\n    program.parse();\n    setOpts(program.opts());\n\n    let logLevel: LoggingLevels = 1;\n    if (opts().quiet) {\n        logLevel = 0;\n    } else if (opts().verbose) {\n        logLevel = 2;\n    }\n    logger.setLogLevel(logLevel);\n\n    let config: Partial<MyConfig> = {};\n\n    const assignConfig = (newOptions: Partial<MyConfig>) => {\n        config = {\n            ...config,\n            ...newOptions,\n        };\n    };\n\n    assignConfig(await setupMongo());\n    assignConfig(await setupNginx());\n    assignConfig(await setupSeeds());\n    assignConfig(await setupWallets());\n\n    const prettyConfig = JSON.stringify(config, null, 2);\n\n    if (opts().dryrun) {\n        logger.log(prettyConfig);\n    } else {\n        if (existsSync(path.join(__dirname, '..', '..', '..', 'config/', 'local.json'))) {\n            // prettier-ignore\n            logger.log(\n                `\\n\\n ` +\n                `A ${kleur.italic(`config/local.json`)} already exists. To preserve the integrity ` +\n                `of your previous configuration. This new configuration will be ` +\n                `written to ${kleur.italic(`config/local2.json`)}. ${kleur.bold(`Manually merge`)} ` +\n                `the new configuration into ${kleur.italic(`config/local.json`)} as needed. `\n            );\n            writeFileSync(path.join(__dirname, '..', '..', '..', 'config/', 'local2.json'), prettyConfig);\n        } else {\n            writeFileSync(path.join(__dirname, '..', '..', '..', 'config/', 'local.json'), prettyConfig);\n        }\n\n        try {\n            await spawnAsync('pnpm', ['run', 'start'], {\n                cwd: path.join(__dirname, '..', '..', '..'),\n            });\n        } catch (error) {\n            // prettier-ignore\n            logger.error(\n                `\\n\\n ` +\n                `Error launching '${kleur.italic(`pm2`)}'. Make sure it's installed via` +\n                `'${kleur.italic(`pnpm i -g pm2`)}', then start the Keagate server with` +\n                `'${kleur.italic(`pnpm run start`)}'.`\n            )\n        }\n    }\n\n    // prettier-ignore\n    logger.log(\n        `\\n\\n ` +\n        `Keagate has successfully deployed on this machine. If you're using a cloud provider like ` +\n        `AWS or Azure, please be sure to ${kleur.bold(`enable public access via HTTP(S)`)}. ` +\n        `You can then locate your API documentation at ${kleur.underline(config.HOST + '/docs')} ` +\n        `and OpenAPI schema with '${kleur.italic(`curl localhost:8081/docs/yaml`)}'. ` +\n        `Most API routes require a ${kleur.bold(`KEAGATE_API_KEY`)} header. Yours has ` +\n        `been randomly generated as: \"${config.KEAGATE_API_KEY}\". More information on the payment ` +\n        `lifecycle is available at ${kleur.underline(`https://bit.ly/3ALFxYO`)}. ` +\n        `\\n\\n ` +\n        `Instant Payment Notifications should be verified via HMAC. Your ` +\n        `${kleur.bold(`IPN_HMAC_SECRET`)} has been randomly generated ` +\n        `as: \"${config.IPN_HMAC_SECRET}\". More information on IPNs is available ` +\n        `at ${kleur.underline(`https://bit.ly/3IuGZ3H`)}. ` +\n        `\\n\\n ` +\n        `You can always find and edit these configuration values ` +\n        `[and many others] in ${kleur.italic(`config/local.json`)}. ` +\n        `\\n\\n ` +\n        `The Keagate server is running via ${kleur.italic(`pm2`)}. To restart the server ` +\n        `execute '${kleur.italic(`pm2 restart Keagate`)}'. To monitor the server ` +\n        `execute '${kleur.italic(`pm2 monit Keagate`)}'. Read more about ${kleur.italic(`pm2`)} ` +\n        `at ${kleur.underline(`https://pm2.keymetrics.io/`)}. You may have to close and ` +\n        `re-open your terminal to start using the ${kleur.italic(`pm2`)} command.` +\n        `\\n\\n `\n    )\n\n    process.exit();\n}\n\nrequire.main && main();\n"
  },
  {
    "path": "packages/scripts/src/index.ts",
    "content": "export default null;\n// Just for entry point\n"
  },
  {
    "path": "packages/scripts/src/opts.ts",
    "content": "interface Opts {\n    quiet?: boolean;\n    verbose?: boolean;\n    dryrun?: boolean;\n}\n\nlet _opts: Opts;\n\nexport function setOpts(newOpts: Opts) {\n    _opts = newOpts;\n}\n\nexport default () => _opts;\n"
  },
  {
    "path": "packages/scripts/src/setupMongo.ts",
    "content": "import { MongoClient } from 'mongodb';\nimport prompts from 'prompts';\nimport spawnAsync from '@expo/spawn-async';\nimport logger from './DelegateLogger';\nimport kleur from 'kleur';\nimport { MyConfig } from '@keagate/common';\nimport opts from './opts';\n\nconst DEFAULT_MONGO_CONN_STR = 'mongodb://localhost:27017';\n\nasync function installMongo(): Promise<string> {\n    logger.log('Installing MongoDB via Docker...');\n    try {\n        await spawnAsync('docker', 'run -d -p 27017:27017 --name keagate-mongo mongo:4.4'.split(' '));\n    } catch (error) {\n        if (error.output && error.output[1].startsWith('docker: Error response from daemon: Conflict. The container name')) {\n            const { shouldOverwriteMongo } = await prompts({\n                type: 'toggle',\n                name: 'shouldOverwriteMongo',\n                message: `Detected an existing Keagate mongo instance. Would you like to overwrite this?`,\n                initial: false,\n                active: 'yes',\n                inactive: 'no',\n            });\n\n            if (shouldOverwriteMongo) {\n                await spawnAsync('docker', 'stop keagate-mongo'.split(' '));\n                await spawnAsync('docker', 'rm keagate-mongo'.split(' '));\n                return installMongo();\n            } else {\n                return DEFAULT_MONGO_CONN_STR;\n            }\n        } else {\n            logger.error(error);\n            throw new Error(error);\n        }\n    }\n    logger.success();\n    return DEFAULT_MONGO_CONN_STR;\n}\n\nasync function mongoFromConnectionString(mongoConnectionString: string): Promise<Partial<MyConfig>> {\n    // https://mongodb.github.io/node-mongodb-native/2.2/reference/faq/\n    logger.log('Testing MongoDB connection...');\n    let client: MongoClient;\n    try {\n        client = new MongoClient(mongoConnectionString, { connectTimeoutMS: 2000, serverSelectionTimeoutMS: 3000, noDelay: true });\n        await client.connect();\n    } catch (error) {\n        if (error.message.startsWith('connect ECONNREFUSED')) {\n            logger.log(kleur.bold().red(`Could not connect to mongo instance as provided with error \"ECONNREFUSED\".\\nPlease try again`));\n            return setupMongo();\n        } else {\n            logger.log(kleur.bold().red(`Could not connect to mongo instance as provided with error \"${error.message}\".\\nPlease try again`));\n            return setupMongo();\n        }\n    }\n\n    try {\n        await client.db('keagate').collection('test').find({}).limit(1).toArray();\n        logger.success();\n        return {\n            MONGO_CONNECTION_STRING: mongoConnectionString,\n        };\n    } catch (error) {\n        if (error.message.startsWith('user is not allowed to do action')) {\n            logger.log(kleur.underline(`Authentication required to invoke MongoDB.`));\n            const { mongoUsername, mongoPassword } = await prompts([\n                {\n                    type: 'text',\n                    name: 'mongoUsername',\n                    message: `Enter MongoDB username ` + kleur.italic(`(e.g. admin)`),\n                    validate: (val) => (val.length > 0 ? true : 'An input is required. Please try again.'),\n                },\n                {\n                    type: 'password',\n                    name: 'mongoPassword',\n                    message: (prev) => `Enter ${prev}'s password`,\n                },\n            ]);\n\n            const mongoConnectionWithoutAuth = mongoConnectionString.replace(/\\/\\/.*@/, '//');\n            const connectionStringParts = mongoConnectionWithoutAuth.split('//');\n            const newConnectionString = connectionStringParts.shift() + '//' + `${mongoUsername}:${mongoPassword}@` + connectionStringParts.join('//');\n            logger.debug(`Trying new auth connection string as ${newConnectionString}`);\n            return mongoFromConnectionString(newConnectionString);\n        } else {\n            logger.log(kleur.bold().red(`Could invoke mongo instance with error \"${error.message}\".\\nPlease try again`));\n            return setupMongo();\n        }\n    }\n}\n\nexport default async function setupMongo(): Promise<Partial<MyConfig>> {\n    const { mongoConfig } = await prompts({\n        type: 'select',\n        name: 'mongoConfig',\n        message: 'Choose MongoDB Configuration',\n        choices: [\n            { title: `Install MongoDB on this machine`, value: 'INSTALL' },\n            { title: `Use an existing local MongoDB connection URL ` + kleur.italic(`(e.g mongodb://localhost:27017)`), value: 'CONNECTION-STRING' },\n            { title: `Use an existing remote MongoDB connection URL ` + kleur.italic(`(e.g. mongodb://clusterN.MY-LOCATION.net)`), value: 'CONNECTION-STRING' },\n        ],\n        initial: 0,\n    });\n\n    if (mongoConfig === 'CONNECTION-STRING') {\n        const { mongoConnectionString } = await prompts({\n            type: 'text',\n            name: 'mongoConnectionString',\n            message: `Enter a MongoDB connection string ` + kleur.italic('(e.g mongodb://admin:password@localhost:27017)'),\n            validate: (val) => (val.length > 0 ? true : 'An input is required. Please try again.'),\n        });\n        return mongoFromConnectionString(mongoConnectionString);\n    } else if (mongoConfig === 'INSTALL') {\n        if (opts().dryrun) {\n            return { MONGO_CONNECTION_STRING: DEFAULT_MONGO_CONN_STR };\n        } else {\n            const mongoUrl = await installMongo();\n            return mongoFromConnectionString(mongoUrl);\n        }\n    } else {\n        return setupMongo();\n    }\n}\n"
  },
  {
    "path": "packages/scripts/src/setupNginx.ts",
    "content": "import spawnAsync from '@expo/spawn-async';\nimport { MyConfig, fGet } from '@keagate/common';\nimport kleur from 'kleur';\nimport prompts from 'prompts';\nimport opts from './opts';\nimport logger from './DelegateLogger';\n\nasync function _installNginx(dockerString: string, host: string): Promise<Partial<MyConfig>> {\n    logger.log('Installing Nginx via Docker...');\n    if (!opts().dryrun) {\n        try {\n            await spawnAsync('docker', dockerString.split(' '), { shell: true /** Fixes 'docker-entrypoint.sh: 38: exec: : Permission denied' error */ });\n        } catch (error) {\n            if (error.output && error.output[1].startsWith('docker: Error response from daemon: Conflict. The container name')) {\n                logger.debug('Docker container already exists. Trying to remove and re-install...');\n                await spawnAsync('docker', 'stop keagate-nginx'.split(' '));\n                await spawnAsync('docker', 'rm keagate-nginx'.split(' '));\n                return _installNginx(dockerString, host);\n            } else {\n                logger.error(error);\n                throw new Error(error);\n            }\n        }\n    }\n    logger.success();\n    return { HOST: host };\n}\n\nasync function installNginxNoSSL(): Promise<Partial<MyConfig>> {\n    const { ip } = await fGet('https://api.ipify.org?format=json');\n    // prettier-ignore\n    return await _installNginx(\n        `run ` +\n        `-d ` +\n        `--network host ` +\n        `--name keagate-nginx ` +\n        `--restart on-failure ` +\n        `-v ${process.env.HOME || '~'}/Keagate/packages/scripts/assets/default.conf:/etc/nginx/conf.d/default.conf ` + \n        `nginx:mainline `,\n        \"http://\" + ip\n    )\n}\n\nasync function installNginxWithSSL(SSLDomains: string): Promise<Partial<MyConfig>> {\n    // https://github.com/Valian/docker-nginx-auto-ssl\n    // https://github.com/Valian/docker-nginx-auto-ssl/blob/master/snippets/server-proxy.conf\n    // prettier-ignore\n    return await _installNginx(\n        `run ` +\n        `-d ` +\n        `--network host ` +\n        `--name keagate-nginx ` +\n        `--restart on-failure ` +\n        `-e ALLOWED_DOMAINS=\"${SSLDomains}\" ` + \n        `-e SITES=\"${SSLDomains}=localhost:8081\" ` + \n        `valian/docker-nginx-auto-ssl `,\n        \"https://\" + SSLDomains\n    )\n}\n\nexport default async function setupNginx(): Promise<Partial<MyConfig>> {\n    const { nginxConfig } = await prompts({\n        type: 'select',\n        name: 'nginxConfig',\n        message: 'Are you using a manual Nginx setup or would you like us to configure it for you?',\n        choices: [\n            { title: `Configure it for me ` + kleur.white().bold('(Recommended)'), value: 'INSTALL' },\n            { title: `I'm using a manual setup for Keagate ` + kleur.white().bold('(Not recommended)'), value: 'MANUAL' },\n        ],\n        initial: 0,\n    });\n\n    if (nginxConfig === 'INSTALL') {\n        const { SSLWanted, SSLDomains } = await prompts([\n            {\n                type: 'toggle',\n                name: 'SSLWanted',\n                message: 'Do you want to enable SSL (a valid domain name must be pointing to this machine)?',\n                initial: false,\n                active: 'yes',\n                inactive: 'no',\n            },\n            {\n                type: (prev) => (prev ? 'text' : null),\n                name: 'SSLDomains',\n                message: 'What is your domain name (e.g. (www|api).example.com or example.com)?',\n                validate: (val) => (val.length > 0 ? true : 'An input is required. Please try again.'),\n            },\n        ]);\n        if (SSLWanted) {\n            return installNginxWithSSL(SSLDomains);\n        } else {\n            return installNginxNoSSL();\n        }\n    } else if (nginxConfig === 'MANUAL') {\n        return {};\n    } else {\n        return setupNginx();\n    }\n}\n"
  },
  {
    "path": "packages/scripts/src/setupSeeds.ts",
    "content": "import { MyConfig } from '@keagate/common';\nimport crypto from 'crypto';\n\nfunction randomSeedGenerator(length: number) {\n    return crypto.randomBytes(length).toString('hex');\n}\n\nexport default async function setupSeeds(): Promise<Partial<MyConfig>> {\n    // TODO IP whitelist?\n    return {\n        INVOICE_ENC_KEY: randomSeedGenerator(16),\n        SEED: randomSeedGenerator(16),\n        KEAGATE_API_KEY: randomSeedGenerator(32),\n        IPN_HMAC_SECRET: randomSeedGenerator(32),\n    };\n}\n\nrequire.main === module && setupSeeds().then((res) => console.log(JSON.stringify(res, null, 2)));\n"
  },
  {
    "path": "packages/scripts/src/setupWallets.ts",
    "content": "import { MyConfig, availableCoinlibCurrencies, availableNativeCurrencies, AvailableCurrencies, currencies } from '@keagate/common';\nimport prompts from 'prompts';\n\nexport default async function setupWallets(): Promise<Partial<MyConfig>> {\n    // eslint-disable-next-line no-constant-condition\n    const { selectedCurrencies } = await prompts({\n        type: 'multiselect',\n        name: 'selectedCurrencies',\n        message: 'Which currencies would you like to configure. (You can add more later)',\n        choices: [...availableCoinlibCurrencies, ...availableNativeCurrencies].map((ele) => ({ title: currencies[ele].name, value: ele })),\n        hint: '- Space to select. Return to submit',\n        min: 1,\n    });\n\n    const keagateConfig: Partial<MyConfig> = {};\n\n    for (const currency of selectedCurrencies as AvailableCurrencies[]) {\n        const currencyName = currencies[currency].name;\n\n        const { publicKey, privateKey, shouldPrivateKey } = await prompts([\n            {\n                type: 'text',\n                name: 'publicKey',\n                message: `What's your ${currencyName} public key (where Keagate will send to)?`,\n                validate: (val) => (val.length > 0 ? true : 'An input is required. Please try again.'),\n            },\n            {\n                type: 'toggle',\n                name: 'shouldPrivateKey',\n                message: `Would you like to be able to send ${currencyName} from your admin wallet via Keagate's API (requires private key)?`,\n                initial: false,\n                active: 'yes',\n                inactive: 'no',\n            },\n            {\n                type: (prev) => (prev ? 'password' : null),\n                name: 'privateKey',\n                message: `What's your ${currencyName} private key?`,\n                validate: (val) => (val.length > 0 ? true : 'An input is required. Please try again.'),\n            },\n        ]);\n        keagateConfig[currency] = {\n            ADMIN_PUBLIC_KEY: publicKey,\n            ADMIN_PRIVATE_KEY: shouldPrivateKey ? privateKey : undefined,\n        };\n    }\n\n    return keagateConfig;\n}\n"
  },
  {
    "path": "packages/scripts/tsconfig.package.json",
    "content": "{\n\t\"extends\": \"../../tsconfig.base.json\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"./build\",\n\t\t\"rootDir\": \"./src\",\n\t\t\"composite\": true\n\t},\n\t\"include\": [\n\t\t\"src/**/*.ts\",\n\t\t\"src/**/*.js\",\n\t\t\"src/**/*.tsx\",\n\t\t\"src/**/*.jsx\"\n\t],\n\t\"exclude\": [\n\t\t\"test\",\n\t\t\"build\"\n\t],\n\t\"references\": []\n}"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'packages/*'"
  },
  {
    "path": "prettier.config.js",
    "content": "module.exports = {\n    printWidth: 160,\n    singleQuote: true,\n    trailingComma: 'all',\n    semi: true,\n    jsxSingleQuote: true,\n    bracketSpacing: true,\n    tabWidth: 4,\n    useTabs: false,\n}"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n    \"compilerOptions\": {\n        \"declaration\": true,\n        \"resolveJsonModule\": true,\n        \"sourceMap\": true,\n        \"allowJs\": true,\n        \"esModuleInterop\": true,\n        \"skipLibCheck\": true,\n        \"allowSyntheticDefaultImports\": true,\n        \"module\": \"CommonJS\",\n        \"target\": \"ES2018\"\n    }\n}"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"extends\": \"./tsconfig.base.json\",\n    \"compilerOptions\": {\n        \"baseUrl\": \".\",\n        \"noEmit\": true,\n        \"paths\": {\n            \"@keagate/*\": [\n                \"./packages/*/src\"\n            ]\n        }\n    }\n}"
  },
  {
    "path": "tsconfig.project.json",
    "content": "{\n    \"files\": [],\n    \"references\": [\n        {\n            \"path\": \"./packages/common/tsconfig.package.json\",\n        },\n        {\n            \"path\": \"./packages/backend/tsconfig.package.json\",\n        },\n        {\n            \"path\": \"./packages/invoice-client/tsconfig.package.json\",\n        },\n        {\n            \"path\": \"./packages/api-providers/tsconfig.package.json\",\n        },\n        {\n            \"path\": \"./packages/scripts/tsconfig.package.json\",\n        }\n    ]\n}"
  }
]