├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile-project ├── LICENSE ├── README.md ├── bin └── cli.js ├── blockchains ├── lisk │ ├── adapter.js │ ├── config.dev.json │ └── config.prod.json ├── package-lock.json ├── package.json └── rise │ ├── adapter.js │ ├── config.dev.json │ └── config.prod.json ├── kubernetes ├── agc-broker-deployment.yaml ├── agc-broker-service.yaml ├── agc-ingress.yaml ├── agc-state-deployment.yaml ├── agc-state-service.yaml ├── crypticle-worker-deployment.yaml ├── crypticle-worker-service.yaml ├── gke │ ├── ingress-nginx-cloud-generic.yaml │ ├── ingress-nginx-mandatory.yaml │ └── rethinkdb-statefulset.yaml ├── local-path-storage.yaml ├── rethinkdb-admin-deployment.yaml ├── rethinkdb-admin-service.yaml ├── rethinkdb-proxy-deployment.yaml ├── rethinkdb-proxy-service.yaml ├── rethinkdb-rbac.yaml ├── rethinkdb-service.yaml └── rethinkdb-statefulset.yaml ├── nodemon.json ├── package-lock.json ├── package.json ├── public ├── app-main.js ├── components │ ├── component-account.js │ ├── component-deposits.js │ ├── component-make-transfer.js │ ├── component-make-withdrawal.js │ ├── component-transfers.js │ └── component-withdrawals.js ├── favicon.png ├── img │ └── crypticle-logo-small.png ├── index.html ├── package-lock.json ├── package.json ├── pages │ ├── page-dashboard.js │ ├── page-home.js │ ├── page-impersonate.js │ ├── page-login.js │ ├── page-signup-admin.js │ └── page-signup.js └── stylesheets │ └── main.css ├── schemas ├── data-schema.js └── rpc-schema.js ├── server.js ├── services └── account-service.js └── utils └── sharding.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | blockchains/node_modules/ 3 | package-lock.json 4 | crypticle-k8s.json 5 | state.json 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | state.json 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11.13.0-slim 2 | 3 | LABEL maintainer="Jonathan Gros-Dubois" 4 | LABEL version="2.0.3" 5 | LABEL description="Docker file for Crypticle." 6 | 7 | RUN mkdir -p /usr/src/ 8 | WORKDIR /usr/src/ 9 | COPY . /usr/src/ 10 | 11 | WORKDIR /usr/src/blockchains/ 12 | RUN npm install . 13 | 14 | WORKDIR /usr/src/public/ 15 | RUN npm install . 16 | 17 | WORKDIR /usr/src/ 18 | RUN npm install . 19 | 20 | EXPOSE 8000 21 | 22 | CMD ["npm", "run", "start:docker"] 23 | -------------------------------------------------------------------------------- /Dockerfile-project: -------------------------------------------------------------------------------- 1 | FROM node:11.13.0-slim 2 | 3 | LABEL maintainer="Jonathan Gros-Dubois" 4 | LABEL version="1.0.0" 5 | LABEL description="Volume container which holds source code for a Crypticle service." 6 | 7 | RUN mkdir -p /usr/src/ 8 | WORKDIR /usr/src/ 9 | COPY . /usr/src/ 10 | 11 | WORKDIR /usr/src/blockchains/ 12 | RUN npm install . 13 | 14 | WORKDIR /usr/src/ 15 | 16 | # Since this is just a volume container, we don't need to run any init commands. 17 | CMD ["sleep", "infinity"] 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypticle 2 | 3 | [![Join the chat at https://gitter.im/crypticle-io/community](https://badges.gitter.im/crypticle-io/community.svg)](https://gitter.im/crypticle-io/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ## This project is in alpha and has not yet been battle-tested. 6 | 7 | A multi-tenant off-chain payment microservice with support for transaction sharding. Crypticle lets users convert value between blockchain tokens and pegged credit which can be efficiently spent and/or transferred between accounts within the context of a centralized service/platform. The credit is entrusted to the service provider and is pegged to the value of the underlying blockchain tokens - This credit can be used to support an unlimited rate of off-chain transactions. Any unused credit can be converted back into trustless blockchain tokens at any time. 8 | 9 | The goal of this project is to provide a fast, simple and scalable off-chain mechanism for spending and receiving blockchain tokens. 10 | By reducing the friction involved in converting back and forth between trustless blockchain tokens and entrusted pegged credit, users can choose their level of risk exposure when it comes to their spending money. 11 | 12 | Some potential use cases: 13 | 14 | - Exchange 15 | - Marketplace 16 | - SaaS platform 17 | - Game currency 18 | - Centralized currency which is pegged to a cryptocurrency 19 | 20 | After a Crypticle node has been attached to a specific Blockchain and has started accepting deposits from users, it becomes difficult for a Crypticle service provider to move away from that blockchain without violating the implicit agreement that they have with their users. Attaching services to a specific blockchain using Crypticle should therefore help to create sustainable demand for the underlying blockchain token. 21 | 22 | ## Setup 23 | 24 | ### Software requirements 25 | 26 | - Node.js v11.13 or higher: https://nodejs.org/en/ 27 | - RethinkDB v2.3 or higher: https://rethinkdb.com/ 28 | 29 | ### Run from source 30 | 31 | - Launch the RethinkDB server in a separate terminal (e.g. using the `rethinkdb` command) 32 | - `git clone git@github.com:SocketCluster/crypticle.git` 33 | - `cd crypticle && npm install` 34 | - `cd public && npm install ; cd ..` 35 | - `cd blockchains && npm install ; cd ..` 36 | - `npm start` 37 | 38 | ## Deploy and scale on Kubernetes from the command line 39 | 40 | The service is designed to be deployed and scaled on a Kubernetes cluster. 41 | 42 | ### Software requirements 43 | 44 | You will need the following software installed in order to deploy to a K8s cluster: 45 | - Node.js version 11 or higher. [Download Node.js](https://nodejs.org/en/). 46 | - `docker` CLI. [Install Docker](https://docs.docker.com/install/). 47 | - `kubectl` CLI. [Install Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/). 48 | 49 | - If using Rancher, you will need to install the Rancher server on a remote machine of your choice; you will then be able to create your cluster from the Rancher control panel. [Install Rancher](https://rancher.com/quick-start/). 50 | - If using GKE, you will need the `gcloud` command from the Google Cloud SDK. [Install Google Cloud SDK](https://cloud.google.com/sdk/docs/quickstarts). 51 | 52 | ### Deployment steps 53 | 54 | Once you have the required software, follow these steps to deploy Crypticle to a K8s cluster (this approach works best with [Rancher](https://rancher.com/)): 55 | 56 | - Setup your Kubernetes cluster with multiple nodes on your provider (3 is ideal for testing). 57 | - Get the `Kubeconfig` from your K8s control panel (or cloud provider) and paste it into the `~/.kube/config` file on your local machine (replace the whole file content). 58 | - Open a new terminal window/tab to make sure that `kubectl` has the latest environment. 59 | - Install the `crypticle` CLI tool with `npm install -g crypticle`. 60 | - Create your project directory with `crypticle create myproject`. 61 | - Navigate to your project directory with `cd myproject`. 62 | - Make sure that your production config file (e.g. `blockchains/rise/config.prod.json` for a Rise project) contains the correct values; this config is the one which will be used by default in the K8s cluster. 63 | - Upload configs to your K8s cluster using `kubectl create configmap crypticle-config --from-file=blockchains/rise/config.prod.json --from-file=blockchains/rise/config.dev.json` (replace `/rise/` with your blockchain name). 64 | - Upload secrets to your K8s cluster with `kubectl create secret generic crypticle-secret --from-literal=SECRET_SIGNUP_KEY=313e7cc1-ad75-4030-a927-6a09f39c1603 --from-literal=AUTH_KEY=15d16361-6402-41a5-8840-d2a330b8ea40 --from-literal=STORAGE_ENCRYPTION_KEY=0111394e-3b3e-4eb3-9759-21741cf055c7 --from-literal=BLOCKCHAIN_WALLET_PASSPHRASE="drastic spot aerobic web wave tourist library first scout fatal inherit arrange"` (replace the values with your own). 65 | - If your custom `adapter.js` file has any dependencies, make sure that they are all inside the `blockchains/node_modules/` directory (to allow them to build correctly). 66 | - Use `crypticle deploy` to build your Docker image containing your custom adapter logic and your config files and then deploy it to your K8s cluster. 67 | - To access the Crypticle app (after deployment has completed), use `kubectl describe ingress agc-ingress` to get ingress IP addresses; you can copy and paste any of them directly into your browser's address bar. 68 | 69 | ### GKE differences 70 | 71 | - Before you execute any of the commands above, make sure that you have the `gcloud` command installed (see [quickstart guides](https://cloud.google.com/sdk/docs/quickstarts)). Check that `gcloud` is installed using the `gcloud -v` command (it should show you a list of version numbers). 72 | - Create a K8s cluster from your GKE control panel. 73 | - Once your cluster is ready, go to the `Clusters` section and click on the `Connect` button next to your cluster; then run the provided `gcloud container clusters ...` command in your terminal. 74 | - Follow all the deployment steps from the section above with the following differences: 75 | - Skip the step where you need to set the `~/.kube/config` file content; the `gcloud` command above from GKE should take care of this automatically. 76 | - Instead of `crypticle deploy`, use `crypticle deploy --gke` (this will cause `.yaml` files from the `kubernetes/gke/` directory to override those in the main `kubernetes/` directory). 77 | - To access the Crypticle app (after deployment has completed), go to the `Services & Ingress` section and click on the link from the `ingress-nginx` service. 78 | 79 | ## Scaling on K8s 80 | 81 | You can scale any `Deployment` or the RethinkDB `StatefulSet` using standard `kubectl scale ... --replicas=...` commands. 82 | Be very careful when scaling down the RethinkDB `StatefulSet` as this may cause data loss if the underlying persistent volumes are removed. 83 | 84 | ## Sharding transaction processing in the database 85 | 86 | The RethinkDB admin control panel lets you shard any table at the click of a button. 87 | After you've scaled your `rethinkdb` service to multiple hosts, you will be able to increase the number of shards for the `Transaction` table. 88 | 89 | ## Contributions 90 | 91 | This software is distributed under the `AGPL-3.0` license. You are free to use and distribute the code so long as the code which uses or is derived from this project is made public under the same license. If you want to make a contribution to Crypticle then you must grant the Crytpicle project owners the right to use and redistribute your contributed code, content or media under any license. In addition to the main `AGPL-3.0` license, the Crypticle project owners reserve the right to distribute Crypticle (along with contributions made by any third parties) under alternate licenses for commercial purposes. 92 | 93 | ## Enterprise licenses 94 | 95 | If the terms of the `AGPL-3.0` license are not suitable for your use case, please contact a Crypticle project owner to discuss alternative options. 96 | -------------------------------------------------------------------------------- /blockchains/lisk/adapter.js: -------------------------------------------------------------------------------- 1 | const LiskPassphrase = require('@liskhq/lisk-passphrase'); 2 | const LiskCryptography = require('@liskhq/lisk-cryptography'); 3 | const { APIClient: LiskApiClient } = require('@liskhq/lisk-api-client'); 4 | const LiskTransactions = require('@liskhq/lisk-transactions'); 5 | 6 | class LiskAdapter { 7 | constructor(options) { 8 | this.client = new LiskApiClient([options.nodeAddress]); 9 | } 10 | 11 | generateWallet() { 12 | const passphrase = LiskPassphrase.Mnemonic.generateMnemonic(); 13 | 14 | const { publicKey, privateKey } = LiskCryptography.getPrivateAndPublicKeyFromPassphrase(passphrase); 15 | const address = LiskCryptography.getAddressFromPublicKey(publicKey); 16 | 17 | return { 18 | passphrase, 19 | privateKey: Buffer.from(privateKey).toString('hex'), 20 | publicKey: Buffer.from(publicKey).toString('hex'), 21 | address 22 | }; 23 | } 24 | 25 | async fetchHeight() { 26 | return (await this.client.node.getStatus()).data.height; 27 | } 28 | 29 | async fetchBlocks({ offset = 0, limit = 10 } = {}) { 30 | let queryParams = { 31 | sort: 'height:asc', 32 | offset, 33 | limit 34 | }; 35 | 36 | return Promise.all( 37 | (await this.client.blocks.get(queryParams)).data.map(async (block) => { 38 | return { 39 | ...block, 40 | transactions: (await this.client.transactions.get({blockId: block.id})).data 41 | }; 42 | }) 43 | ); 44 | } 45 | 46 | async fetchTransaction(transactionId) { 47 | return (await this.client.transactions.get({id: transactionId})).data[0]; 48 | } 49 | 50 | async fetchWalletBalance(walletAddress) { 51 | return (await this.client.accounts.get({address: walletAddress})).data[0].balance; 52 | } 53 | 54 | async signTransaction(transaction, passphrase) { 55 | let liskTransaction = { 56 | type: 0, 57 | amount: transaction.amount, 58 | recipientId: transaction.recipient, 59 | fee: await this.fetchFees(), 60 | asset: {}, 61 | senderPublicKey: LiskCryptography.getAddressAndPublicKeyFromPassphrase(passphrase).publicKey 62 | }; 63 | return LiskTransactions.utils.prepareTransaction(liskTransaction, passphrase); 64 | } 65 | 66 | async sendTransaction(signedTransaction) { 67 | return this.client.transactions.broadcast(signedTransaction); 68 | } 69 | 70 | async fetchFees() { 71 | return LiskTransactions.constants.TRANSFER_FEE.toString(); 72 | } 73 | } 74 | 75 | module.exports = LiskAdapter; 76 | -------------------------------------------------------------------------------- /blockchains/lisk/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "databaseName": "crypticle_lisk_dev", 3 | "databaseHost": "127.0.0.1", 4 | "databasePort": 28015, 5 | "authTokenExpiry": 3600000, 6 | "authTokenRenewalInterval": 20000, 7 | "maxSocketBackpressure": 1000, 8 | "transactionSettlementInterval": 5000, 9 | "withdrawalProcessingInterval": 8000, 10 | "maxTransactionSettlementsPerAccount": 10, 11 | "maxConcurrentWithdrawalsPerAccount": 5, 12 | "maxConcurrentDebitsPerAccount": 50, 13 | "blockchainSync": true, 14 | "blockchainNodeAddress": "https://testnet.lisk.io", 15 | "blockPollInterval": 4000, 16 | "blockFetchLimit": 100, 17 | "blockchainWithdrawalMaxAttempts": 20, 18 | "bcryptPasswordRounds": 10, 19 | "publicInfo": { 20 | "cryptocurrency": { 21 | "name": "Lisk", 22 | "symbol": "LSK", 23 | "unit": "100000000" 24 | }, 25 | "mainWalletAddress": "5572024383106046591L", 26 | "requiredDepositBlockConfirmations": 3, 27 | "requiredWithdrawalBlockConfirmations": 3, 28 | "paginationShowTotalCounts": false, 29 | "maxPageSize": 100, 30 | "alwaysRequireSecretSignupKey": false, 31 | "enableAdminAccountSignup": true, 32 | "withdrawalFees": "10000000", 33 | "depositFees": "20000000", 34 | "termsOfServiceURL": null, 35 | "privacyPolicyURL": null 36 | }, 37 | "secretSignupKey": "313e7cc1-ad75-4030-a927-6a09f39c1603", 38 | "storageEncryptionKey": "0111394e-3b3e-4eb3-9759-21741cf055c7", 39 | "blockchainWalletPassphrase": "seven help fluid old donate nation duck romance thumb verb trumpet flower", 40 | "authKey": "15d16361-6402-41a5-8840-d2a330b8ea40" 41 | } 42 | -------------------------------------------------------------------------------- /blockchains/lisk/config.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "databaseName": "crypticle_lisk_prod", 3 | "databaseHost": "rethinkdb-proxy", 4 | "databasePort": 28015, 5 | "authTokenExpiry": 600000, 6 | "authTokenRenewalInterval": 20000, 7 | "maxSocketBackpressure": 1000, 8 | "transactionSettlementInterval": 5000, 9 | "withdrawalProcessingInterval": 8000, 10 | "maxTransactionSettlementsPerAccount": 10, 11 | "maxConcurrentWithdrawalsPerAccount": 10, 12 | "maxConcurrentDebitsPerAccount": 50, 13 | "blockchainSync": true, 14 | "blockchainNodeAddress": "https://mainnet.lisk.io", 15 | "blockPollInterval": 4000, 16 | "blockFetchLimit": 100, 17 | "blockchainWithdrawalMaxAttempts": 20, 18 | "bcryptPasswordRounds": 10, 19 | "publicInfo": { 20 | "cryptocurrency": { 21 | "name": "Lisk", 22 | "symbol": "LSK", 23 | "unit": "100000000" 24 | }, 25 | "mainWalletAddress": "606085525141988367L", 26 | "requiredDepositBlockConfirmations": 102, 27 | "requiredWithdrawalBlockConfirmations": 102, 28 | "paginationShowTotalCounts": false, 29 | "maxPageSize": 100, 30 | "alwaysRequireSecretSignupKey": false, 31 | "enableAdminAccountSignup": true, 32 | "withdrawalFees": "10000000", 33 | "depositFees": "20000000", 34 | "termsOfServiceURL": null, 35 | "privacyPolicyURL": null 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /blockchains/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypticle-blockchains", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@liskhq/bignum": { 8 | "version": "1.3.1", 9 | "resolved": "https://registry.npmjs.org/@liskhq/bignum/-/bignum-1.3.1.tgz", 10 | "integrity": "sha512-q9+NvqbpmXOqpPmV8Y+XSEIUJFMZDGyfW6rkN9Ej3nzPb/qurY/Ic2UPTeTTaj8+q/bcw5JUwTb86hi7PIziDg==", 11 | "requires": { 12 | "@types/node": "11.11.2" 13 | }, 14 | "dependencies": { 15 | "@types/node": { 16 | "version": "11.11.2", 17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.2.tgz", 18 | "integrity": "sha512-iEaHiDNkHv4Jrm9O5T37OYEUwjJesiyt6ZlhLFK0sbo4CLD0jyCOB4Pc2F9iD3MbW2397SLNxZKdDGntGaBjQQ==" 19 | } 20 | } 21 | }, 22 | "@liskhq/lisk-api-client": { 23 | "version": "2.0.2", 24 | "resolved": "https://registry.npmjs.org/@liskhq/lisk-api-client/-/lisk-api-client-2.0.2.tgz", 25 | "integrity": "sha512-PrpCli1IvIxEUE+F28y6dGlg9OgoyJRxbadGGNJEiy0mT/wXaNLUG5F3hifGjfaeryHIjv/20mjbUArlt5jRSQ==", 26 | "requires": { 27 | "@types/node": "10.12.21", 28 | "axios": "0.19.0" 29 | } 30 | }, 31 | "@liskhq/lisk-cryptography": { 32 | "version": "2.1.1", 33 | "resolved": "https://registry.npmjs.org/@liskhq/lisk-cryptography/-/lisk-cryptography-2.1.1.tgz", 34 | "integrity": "sha512-1ZkpYPZ8UtbRaTtnwQalKyUtcCebhxfTgBmgx+68mmcUAiENEj/VPeTb/3LUQzJHLaKs9gCKY/N3KSD06nR3zA==", 35 | "requires": { 36 | "@liskhq/bignum": "1.3.1", 37 | "@types/ed2curve": "0.2.2", 38 | "@types/node": "10.12.21", 39 | "buffer-reverse": "1.0.1", 40 | "ed2curve": "0.2.1", 41 | "sodium-native": "2.2.1", 42 | "tweetnacl": "1.0.1", 43 | "varuint-bitcoin": "1.1.0" 44 | } 45 | }, 46 | "@liskhq/lisk-passphrase": { 47 | "version": "2.0.2", 48 | "resolved": "https://registry.npmjs.org/@liskhq/lisk-passphrase/-/lisk-passphrase-2.0.2.tgz", 49 | "integrity": "sha512-LW9upxW9ZRqJ+0Z9/Js6rUgVRGKfgGo3dB1exVchNOZkgMz9sC14wEUY6yCrmhmtgQwn9K2LWer1ayr5swnx1g==", 50 | "requires": { 51 | "@types/bip39": "2.4.1", 52 | "@types/node": "10.12.21", 53 | "bip39": "2.5.0" 54 | }, 55 | "dependencies": { 56 | "bip39": { 57 | "version": "2.5.0", 58 | "resolved": "https://registry.npmjs.org/bip39/-/bip39-2.5.0.tgz", 59 | "integrity": "sha512-xwIx/8JKoT2+IPJpFEfXoWdYwP7UVAoUxxLNfGCfVowaJE7yg1Y5B1BVPqlUNsBq5/nGwmFkwRJ8xDW4sX8OdA==", 60 | "requires": { 61 | "create-hash": "^1.1.0", 62 | "pbkdf2": "^3.0.9", 63 | "randombytes": "^2.0.1", 64 | "safe-buffer": "^5.0.1", 65 | "unorm": "^1.3.3" 66 | } 67 | } 68 | } 69 | }, 70 | "@liskhq/lisk-transactions": { 71 | "version": "2.2.0", 72 | "resolved": "https://registry.npmjs.org/@liskhq/lisk-transactions/-/lisk-transactions-2.2.0.tgz", 73 | "integrity": "sha512-/iDrAvgxxF1wJQ9Urtcl8n1sHANkPZGMYoA/gOAmfBqts3GYbWIefSkq934TGcupntD8FZ8JNmUipIKfYsIM6A==", 74 | "requires": { 75 | "@liskhq/bignum": "1.3.1", 76 | "@liskhq/lisk-cryptography": "2.1.1", 77 | "@types/node": "10.12.21", 78 | "ajv": "6.8.1", 79 | "ajv-merge-patch": "4.1.0" 80 | } 81 | }, 82 | "@types/bip39": { 83 | "version": "2.4.1", 84 | "resolved": "https://registry.npmjs.org/@types/bip39/-/bip39-2.4.1.tgz", 85 | "integrity": "sha512-QHx0qI6JaTIW/S3zxE/bXrwOWu6Boos+LZ4438xmFAHY5k+qHkExMdAnb/DENEt2RBnOdZ6c5J+SHrnLEhUohQ==", 86 | "requires": { 87 | "@types/node": "*" 88 | } 89 | }, 90 | "@types/ed2curve": { 91 | "version": "0.2.2", 92 | "resolved": "https://registry.npmjs.org/@types/ed2curve/-/ed2curve-0.2.2.tgz", 93 | "integrity": "sha512-G1sTX5xo91ydevQPINbL2nfgVAj/s1ZiqZxC8OCWduwu+edoNGUm5JXtTkg9F3LsBZbRI46/0HES4CPUE2wc9g==", 94 | "requires": { 95 | "tweetnacl": "^1.0.0" 96 | } 97 | }, 98 | "@types/node": { 99 | "version": "10.12.21", 100 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz", 101 | "integrity": "sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ==" 102 | }, 103 | "ajv": { 104 | "version": "6.8.1", 105 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.8.1.tgz", 106 | "integrity": "sha512-eqxCp82P+JfqL683wwsL73XmFs1eG6qjw+RD3YHx+Jll1r0jNd4dh8QG9NYAeNGA/hnZjeEDgtTskgJULbxpWQ==", 107 | "requires": { 108 | "fast-deep-equal": "^2.0.1", 109 | "fast-json-stable-stringify": "^2.0.0", 110 | "json-schema-traverse": "^0.4.1", 111 | "uri-js": "^4.2.2" 112 | } 113 | }, 114 | "ajv-merge-patch": { 115 | "version": "4.1.0", 116 | "resolved": "https://registry.npmjs.org/ajv-merge-patch/-/ajv-merge-patch-4.1.0.tgz", 117 | "integrity": "sha512-0mAYXMSauA8RZ7r+B4+EAOYcZEcO9OK5EiQCR7W7Cv4E44pJj56ZnkKLJ9/PAcOc0dT+LlV9fdDcq2TxVJfOYw==", 118 | "requires": { 119 | "fast-json-patch": "^2.0.6", 120 | "json-merge-patch": "^0.2.3" 121 | } 122 | }, 123 | "axios": { 124 | "version": "0.19.0", 125 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", 126 | "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", 127 | "requires": { 128 | "follow-redirects": "1.5.10", 129 | "is-buffer": "^2.0.2" 130 | } 131 | }, 132 | "bech32-buffer": { 133 | "version": "0.1.2", 134 | "resolved": "https://registry.npmjs.org/bech32-buffer/-/bech32-buffer-0.1.2.tgz", 135 | "integrity": "sha512-WPSZN/HiNxlPsjDLu02arCdC6ZF0IMU/0aG5QJiV/JCaeDAcP1royrltQCpQeYtcYRlWvhFA8SZcV6RlUuAkjQ==" 136 | }, 137 | "bip39": { 138 | "version": "3.0.2", 139 | "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", 140 | "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", 141 | "requires": { 142 | "@types/node": "11.11.6", 143 | "create-hash": "^1.1.0", 144 | "pbkdf2": "^3.0.9", 145 | "randombytes": "^2.0.1" 146 | }, 147 | "dependencies": { 148 | "@types/node": { 149 | "version": "11.11.6", 150 | "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", 151 | "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" 152 | } 153 | } 154 | }, 155 | "buffer-reverse": { 156 | "version": "1.0.1", 157 | "resolved": "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz", 158 | "integrity": "sha1-SSg8jvpvkBvAH6MwTQYCeXGuL2A=" 159 | }, 160 | "bytebuffer": { 161 | "version": "5.0.1", 162 | "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", 163 | "integrity": "sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=", 164 | "requires": { 165 | "long": "~3" 166 | }, 167 | "dependencies": { 168 | "long": { 169 | "version": "3.2.0", 170 | "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", 171 | "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" 172 | } 173 | } 174 | }, 175 | "cipher-base": { 176 | "version": "1.0.4", 177 | "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", 178 | "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", 179 | "requires": { 180 | "inherits": "^2.0.1", 181 | "safe-buffer": "^5.0.1" 182 | } 183 | }, 184 | "create-hash": { 185 | "version": "1.2.0", 186 | "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", 187 | "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", 188 | "requires": { 189 | "cipher-base": "^1.0.1", 190 | "inherits": "^2.0.1", 191 | "md5.js": "^1.3.4", 192 | "ripemd160": "^2.0.1", 193 | "sha.js": "^2.4.0" 194 | } 195 | }, 196 | "create-hmac": { 197 | "version": "1.1.7", 198 | "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", 199 | "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", 200 | "requires": { 201 | "cipher-base": "^1.0.3", 202 | "create-hash": "^1.1.0", 203 | "inherits": "^2.0.1", 204 | "ripemd160": "^2.0.0", 205 | "safe-buffer": "^5.0.1", 206 | "sha.js": "^2.4.8" 207 | } 208 | }, 209 | "debug": { 210 | "version": "3.1.0", 211 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 212 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 213 | "requires": { 214 | "ms": "2.0.0" 215 | } 216 | }, 217 | "deep-equal": { 218 | "version": "1.0.1", 219 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 220 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 221 | }, 222 | "dpos-api-wrapper": { 223 | "version": "1.4.0", 224 | "resolved": "https://registry.npmjs.org/dpos-api-wrapper/-/dpos-api-wrapper-1.4.0.tgz", 225 | "integrity": "sha512-q67OHs/Y4tym96Xrg2m5rxn37/+l2CJpp0KQJQlJxUnRT+1MEFsf0ByvAdnkFloMHjt4tT7Brp779UhuXM4lkg==", 226 | "requires": { 227 | "axios": "^0.19.0" 228 | } 229 | }, 230 | "dpos-offline": { 231 | "version": "2.0.7", 232 | "resolved": "https://registry.npmjs.org/dpos-offline/-/dpos-offline-2.0.7.tgz", 233 | "integrity": "sha512-Mr0JMxJFENV5spoWBvsygW6IL3esH9Ty0t5XCP7Gwj2Q2nAPDwuketUYkyzxrRTqB9thM8LjipPJuCqLGx9J+w==", 234 | "requires": { 235 | "bech32-buffer": "^0.1.2", 236 | "bytebuffer": "^5.0.1", 237 | "is-empty": "^1.2.0", 238 | "long": "^4.0.0", 239 | "tweetnacl": "^1.0.0", 240 | "tweetnacl-util": "^0.15.0", 241 | "type-tagger": "^1.0.0", 242 | "utility-types": "^2.1.0", 243 | "varuint-bitcoin": "^1.1.0" 244 | } 245 | }, 246 | "ed2curve": { 247 | "version": "0.2.1", 248 | "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.2.1.tgz", 249 | "integrity": "sha1-Iuaqo1aePE2/Tu+ilhLsMp5YGQw=", 250 | "requires": { 251 | "tweetnacl": "0.x.x" 252 | }, 253 | "dependencies": { 254 | "tweetnacl": { 255 | "version": "0.14.5", 256 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 257 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 258 | } 259 | } 260 | }, 261 | "fast-deep-equal": { 262 | "version": "2.0.1", 263 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 264 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 265 | }, 266 | "fast-json-patch": { 267 | "version": "2.1.0", 268 | "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-2.1.0.tgz", 269 | "integrity": "sha512-PipOsAKamRw7+CXtKiieehyjUeDVPJ5J7b2kdJYerEf6TSUQoD2ijpVyZ88KQm5YXziff4h762bz3+vzf56khg==", 270 | "requires": { 271 | "deep-equal": "^1.0.1" 272 | } 273 | }, 274 | "fast-json-stable-stringify": { 275 | "version": "2.0.0", 276 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 277 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 278 | }, 279 | "follow-redirects": { 280 | "version": "1.5.10", 281 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 282 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 283 | "requires": { 284 | "debug": "=3.1.0" 285 | } 286 | }, 287 | "hash-base": { 288 | "version": "3.0.4", 289 | "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", 290 | "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", 291 | "requires": { 292 | "inherits": "^2.0.1", 293 | "safe-buffer": "^5.0.1" 294 | } 295 | }, 296 | "inherits": { 297 | "version": "2.0.4", 298 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 299 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 300 | }, 301 | "ini": { 302 | "version": "1.3.5", 303 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 304 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", 305 | "optional": true 306 | }, 307 | "is-buffer": { 308 | "version": "2.0.3", 309 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", 310 | "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" 311 | }, 312 | "is-empty": { 313 | "version": "1.2.0", 314 | "resolved": "https://registry.npmjs.org/is-empty/-/is-empty-1.2.0.tgz", 315 | "integrity": "sha1-3pu1snhzigWgsJpX4ftNSjQan2s=" 316 | }, 317 | "json-merge-patch": { 318 | "version": "0.2.3", 319 | "resolved": "https://registry.npmjs.org/json-merge-patch/-/json-merge-patch-0.2.3.tgz", 320 | "integrity": "sha1-+ixrWvh9p3uuKWalidUuI+2B/kA=", 321 | "requires": { 322 | "deep-equal": "^1.0.0" 323 | } 324 | }, 325 | "json-schema-traverse": { 326 | "version": "0.4.1", 327 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 328 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 329 | }, 330 | "long": { 331 | "version": "4.0.0", 332 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 333 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 334 | }, 335 | "md5.js": { 336 | "version": "1.3.5", 337 | "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", 338 | "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", 339 | "requires": { 340 | "hash-base": "^3.0.0", 341 | "inherits": "^2.0.1", 342 | "safe-buffer": "^5.1.2" 343 | } 344 | }, 345 | "ms": { 346 | "version": "2.0.0", 347 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 348 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 349 | }, 350 | "nan": { 351 | "version": "2.14.0", 352 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 353 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", 354 | "optional": true 355 | }, 356 | "node-gyp-build": { 357 | "version": "3.9.0", 358 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz", 359 | "integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==", 360 | "optional": true 361 | }, 362 | "pbkdf2": { 363 | "version": "3.0.17", 364 | "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", 365 | "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", 366 | "requires": { 367 | "create-hash": "^1.1.2", 368 | "create-hmac": "^1.1.4", 369 | "ripemd160": "^2.0.1", 370 | "safe-buffer": "^5.0.1", 371 | "sha.js": "^2.4.8" 372 | } 373 | }, 374 | "punycode": { 375 | "version": "2.1.1", 376 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 377 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 378 | }, 379 | "randombytes": { 380 | "version": "2.1.0", 381 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 382 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 383 | "requires": { 384 | "safe-buffer": "^5.1.0" 385 | } 386 | }, 387 | "ripemd160": { 388 | "version": "2.0.2", 389 | "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", 390 | "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", 391 | "requires": { 392 | "hash-base": "^3.0.0", 393 | "inherits": "^2.0.1" 394 | } 395 | }, 396 | "risejs": { 397 | "version": "1.5.1", 398 | "resolved": "https://registry.npmjs.org/risejs/-/risejs-1.5.1.tgz", 399 | "integrity": "sha512-uxwOR9Axih7cL0UA2eKrRexBXLLHHIiA2WYhGBpVVemaQtN9nZjmuH2bV/MX60ouU1uStR8N0f8dWa4wqlwTTw==", 400 | "requires": { 401 | "dpos-api-wrapper": "^1.4.0" 402 | } 403 | }, 404 | "safe-buffer": { 405 | "version": "5.2.0", 406 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 407 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 408 | }, 409 | "sha.js": { 410 | "version": "2.4.11", 411 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 412 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 413 | "requires": { 414 | "inherits": "^2.0.1", 415 | "safe-buffer": "^5.0.1" 416 | } 417 | }, 418 | "sodium-native": { 419 | "version": "2.2.1", 420 | "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-2.2.1.tgz", 421 | "integrity": "sha512-3CfftYV2ATXQFMIkLOvcNUk/Ma+lran0855j5Z/HEjUkSTzjLZi16CK362udOoNVrwn/TwGV8bKEt5OylsFrQA==", 422 | "optional": true, 423 | "requires": { 424 | "ini": "^1.3.5", 425 | "nan": "^2.4.0", 426 | "node-gyp-build": "^3.0.0" 427 | } 428 | }, 429 | "tweetnacl": { 430 | "version": "1.0.1", 431 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", 432 | "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==" 433 | }, 434 | "tweetnacl-util": { 435 | "version": "0.15.0", 436 | "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.0.tgz", 437 | "integrity": "sha1-RXbBzuXi1j0gf+5S8boCgZSAvHU=" 438 | }, 439 | "type-tagger": { 440 | "version": "1.0.0", 441 | "resolved": "https://registry.npmjs.org/type-tagger/-/type-tagger-1.0.0.tgz", 442 | "integrity": "sha512-FIPqqpmDgdaulCnRoKv1/d3U4xVBUrYn42QXWNP3XYmgfPUDuBUsgFOb9ntT0aIe0UsUP+lknpQ5d9Kn36RssA==" 443 | }, 444 | "unorm": { 445 | "version": "1.6.0", 446 | "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", 447 | "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" 448 | }, 449 | "uri-js": { 450 | "version": "4.2.2", 451 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 452 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 453 | "requires": { 454 | "punycode": "^2.1.0" 455 | } 456 | }, 457 | "utility-types": { 458 | "version": "2.1.0", 459 | "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-2.1.0.tgz", 460 | "integrity": "sha512-/nP2gqavggo6l38rtQI/CdeV+2fmBGXVvHgj9kV2MAnms3TIi77Mz9BtapPFI0+GZQCqqom0vACQ+VlTTaCovw==" 461 | }, 462 | "varuint-bitcoin": { 463 | "version": "1.1.0", 464 | "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.0.tgz", 465 | "integrity": "sha512-jCEPG+COU/1Rp84neKTyDJQr478/hAfVp5xxYn09QEH0yBjbmPeMfuuQIrp+BUD83hybtYZKhr5elV3bvdV1bA==", 466 | "requires": { 467 | "safe-buffer": "^5.1.1" 468 | } 469 | } 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /blockchains/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypticle-blockchains", 3 | "version": "1.0.0", 4 | "description": "Crypticle blockchain packages.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "@liskhq/lisk-api-client": "^2.0.0", 11 | "@liskhq/lisk-cryptography": "^2.0.0", 12 | "@liskhq/lisk-passphrase": "^2.0.0", 13 | "@liskhq/lisk-transactions": "^2.0.0", 14 | "bip39": "^3.0.1", 15 | "dpos-offline": "^2.0.7", 16 | "risejs": "^1.5.1", 17 | "tweetnacl": "^1.0.1" 18 | }, 19 | "keywords": [ 20 | "crypticle", 21 | "blockchain" 22 | ], 23 | "author": "Jonathan Gros-Dubois", 24 | "license": "AGPL-3.0" 25 | } 26 | -------------------------------------------------------------------------------- /blockchains/rise/adapter.js: -------------------------------------------------------------------------------- 1 | const rise = require('risejs').rise; 2 | const {dposOffline} = require('dpos-offline'); 3 | const {Rise} = dposOffline; 4 | const bip39 = require('bip39'); 5 | const tweetnacl = require('tweetnacl'); 6 | const crypto = require('crypto'); 7 | 8 | function hashSha256(data) { 9 | const dataHash = crypto.createHash('sha256'); 10 | dataHash.update(data, 'utf8'); 11 | return dataHash.digest(); 12 | } 13 | 14 | class RiseAdapter { 15 | constructor(options) { 16 | rise.nodeAddress = options.nodeAddress; 17 | } 18 | 19 | async generateWallet() { 20 | const passphrase = bip39.generateMnemonic(); 21 | const hashedSeed = hashSha256(passphrase); 22 | 23 | let {publicKey, secretKey} = tweetnacl.sign.keyPair.fromSeed(hashedSeed); 24 | return { 25 | passphrase, 26 | privateKey: Buffer.from(secretKey).toString('hex'), 27 | publicKey: Buffer.from(publicKey).toString('hex'), 28 | address: Rise.calcAddress(publicKey) 29 | }; 30 | } 31 | 32 | async fetchHeight() { 33 | return (await rise.blocks.getHeight()).height; 34 | } 35 | 36 | // offset is the block height from which to start fetching. 37 | // limit is the number of blocks to fetch. 38 | async fetchBlocks(options) { 39 | let queryParams = { 40 | orderBy: 'height:asc', 41 | offset: options.offset, 42 | limit: options.limit 43 | }; 44 | return (await rise.blocks.getBlocks(queryParams)).blocks; 45 | } 46 | 47 | async fetchTransaction(transactionId) { 48 | return (await rise.transactions.get(transactionId)).transaction; 49 | } 50 | 51 | async fetchWalletBalance(walletAddress) { 52 | return (await rise.accounts.getBalance(walletAddress)).balance; 53 | } 54 | 55 | signTransaction(transaction, passphrase) { 56 | return Rise.txs.createAndSign(transaction, passphrase); 57 | } 58 | 59 | async sendTransaction(signedTransaction) { 60 | return rise.transactions.put(signedTransaction); 61 | } 62 | 63 | async fetchFees(transaction) { 64 | return Rise.txs.baseFees.send.toString(); 65 | } 66 | } 67 | 68 | module.exports = RiseAdapter; 69 | -------------------------------------------------------------------------------- /blockchains/rise/config.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "databaseName": "crypticle_rise_dev", 3 | "databaseHost": "127.0.0.1", 4 | "databasePort": 28015, 5 | "authTokenExpiry": 3600000, 6 | "authTokenRenewalInterval": 20000, 7 | "maxSocketBackpressure": 1000, 8 | "transactionSettlementInterval": 5000, 9 | "withdrawalProcessingInterval": 20000, 10 | "maxTransactionSettlementsPerAccount": 10, 11 | "maxConcurrentWithdrawalsPerAccount": 5, 12 | "maxConcurrentDebitsPerAccount": 50, 13 | "blockchainSync": true, 14 | "blockchainNodeAddress": "https://wallet.rise.vision", 15 | "blockPollInterval": 5000, 16 | "blockFetchLimit": 100, 17 | "blockchainWithdrawalMaxAttempts": 20, 18 | "bcryptPasswordRounds": 10, 19 | "publicInfo": { 20 | "cryptocurrency": { 21 | "name": "Rise", 22 | "symbol": "RISE", 23 | "unit": "100000000" 24 | }, 25 | "mainWalletAddress": "16460447528999404929R", 26 | "requiredDepositBlockConfirmations": 3, 27 | "requiredWithdrawalBlockConfirmations": 3, 28 | "paginationShowTotalCounts": false, 29 | "maxPageSize": 100, 30 | "alwaysRequireSecretSignupKey": false, 31 | "enableAdminAccountSignup": true, 32 | "withdrawalFees": "10000000", 33 | "depositFees": "20000000", 34 | "termsOfServiceURL": null, 35 | "privacyPolicyURL": null 36 | }, 37 | "secretSignupKey": "313e7cc1-ad75-4030-a927-6a09f39c1603", 38 | "storageEncryptionKey": "0111394e-3b3e-4eb3-9759-21741cf055c7", 39 | "blockchainWalletPassphrase": "drastic spot aerobic web wave tourist library first scout fatal inherit arrange", 40 | "authKey": "15d16361-6402-41a5-8840-d2a330b8ea40" 41 | } 42 | -------------------------------------------------------------------------------- /blockchains/rise/config.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "databaseName": "crypticle_rise_prod", 3 | "databaseHost": "rethinkdb-proxy", 4 | "databasePort": 28015, 5 | "authTokenExpiry": 600000, 6 | "authTokenRenewalInterval": 20000, 7 | "maxSocketBackpressure": 1000, 8 | "transactionSettlementInterval": 5000, 9 | "withdrawalProcessingInterval": 20000, 10 | "maxTransactionSettlementsPerAccount": 10, 11 | "maxConcurrentWithdrawalsPerAccount": 10, 12 | "maxConcurrentDebitsPerAccount": 50, 13 | "blockchainSync": true, 14 | "blockchainNodeAddress": "https://wallet.rise.vision", 15 | "blockPollInterval": 5000, 16 | "blockFetchLimit": 100, 17 | "blockchainWithdrawalMaxAttempts": 20, 18 | "bcryptPasswordRounds": 10, 19 | "publicInfo": { 20 | "cryptocurrency": { 21 | "name": "Rise", 22 | "symbol": "RISE", 23 | "unit": "100000000" 24 | }, 25 | "mainWalletAddress": "6255037810762443539R", 26 | "requiredDepositBlockConfirmations": 102, 27 | "requiredWithdrawalBlockConfirmations": 3, 28 | "paginationShowTotalCounts": false, 29 | "maxPageSize": 100, 30 | "alwaysRequireSecretSignupKey": false, 31 | "enableAdminAccountSignup": true, 32 | "withdrawalFees": "10000000", 33 | "depositFees": "20000000", 34 | "termsOfServiceURL": null, 35 | "privacyPolicyURL": null 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /kubernetes/agc-broker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: agc-broker 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | component: agc-broker 10 | template: 11 | metadata: 12 | labels: 13 | component: agc-broker 14 | spec: 15 | containers: 16 | - 17 | name: agc-broker 18 | image: 'socketcluster/agc-broker:v6.0.0' 19 | ports: 20 | - 21 | name: agc-broker 22 | containerPort: 8888 23 | env: 24 | - 25 | name: AGC_STATE_SERVER_HOST 26 | value: agc-state 27 | - 28 | name: AGC_INSTANCE_IP 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: status.podIP 32 | - 33 | name: AGC_BROKER_SERVER_LOG_LEVEL 34 | value: '2' 35 | readinessProbe: 36 | httpGet: 37 | path: /health-check 38 | port: agc-broker 39 | initialDelaySeconds: 5 40 | timeoutSeconds: 10 41 | livenessProbe: 42 | httpGet: 43 | path: /health-check 44 | port: agc-broker 45 | initialDelaySeconds: 15 46 | timeoutSeconds: 20 47 | -------------------------------------------------------------------------------- /kubernetes/agc-broker-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: agc-broker 5 | spec: 6 | type: ClusterIP 7 | ports: 8 | - 9 | port: 8888 10 | targetPort: 8888 11 | selector: 12 | component: agc-broker 13 | -------------------------------------------------------------------------------- /kubernetes/agc-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: agc-ingress 5 | annotations: 6 | kubernetes.io/ingress.allow-http: 'false' 7 | spec: 8 | # You should upload a certificate to your Kubernetes platform and refer to it here by secretName. 9 | # If you do not want to serve AGC over TLS, you can remove or comment out the 10 | # kubernetes.io/ingress.allow-http annotation above and the tls section below. 11 | # Note that raw (unencrypted) WebSockets may not work if you are behind a corporate proxy so 12 | # it is advised that you provide a cert for production and access the AGC service via https:// and wss:// 13 | tls: 14 | - 15 | secretName: agc-tls-credentials 16 | backend: 17 | serviceName: crypticle-worker 18 | servicePort: 8000 19 | -------------------------------------------------------------------------------- /kubernetes/agc-state-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: agc-state 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | component: agc-state 10 | template: 11 | metadata: 12 | labels: 13 | component: agc-state 14 | spec: 15 | containers: 16 | - 17 | name: agc-state 18 | image: 'socketcluster/agc-state:v6.0.0' 19 | ports: 20 | - 21 | name: agc-state 22 | containerPort: 7777 23 | readinessProbe: 24 | httpGet: 25 | path: /health-check 26 | port: agc-state 27 | initialDelaySeconds: 5 28 | timeoutSeconds: 10 29 | livenessProbe: 30 | httpGet: 31 | path: /health-check 32 | port: agc-state 33 | initialDelaySeconds: 15 34 | timeoutSeconds: 20 35 | -------------------------------------------------------------------------------- /kubernetes/agc-state-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: agc-state 5 | spec: 6 | type: ClusterIP 7 | ports: 8 | - 9 | port: 7777 10 | targetPort: 7777 11 | selector: 12 | component: agc-state 13 | -------------------------------------------------------------------------------- /kubernetes/crypticle-worker-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: crypticle-worker 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | component: crypticle-worker 10 | template: 11 | metadata: 12 | labels: 13 | component: crypticle-worker 14 | spec: 15 | containers: 16 | - 17 | name: crypticle-worker 18 | image: 'socketcluster/crypticle:v2.0.3' 19 | ports: 20 | - 21 | name: worker-port 22 | containerPort: 8000 23 | env: 24 | - 25 | name: AGC_STATE_SERVER_HOST 26 | value: agc-state 27 | - 28 | name: AGC_INSTANCE_IP 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: status.podIP 32 | - 33 | name: ENV 34 | value: prod 35 | readinessProbe: 36 | httpGet: 37 | path: /health-check 38 | port: worker-port 39 | initialDelaySeconds: 5 40 | timeoutSeconds: 10 41 | livenessProbe: 42 | httpGet: 43 | path: /health-check 44 | port: worker-port 45 | initialDelaySeconds: 15 46 | timeoutSeconds: 20 47 | volumeMounts: 48 | - 49 | name: config-volume 50 | mountPath: /usr/src/config 51 | volumes: 52 | - 53 | name: config-volume 54 | configMap: 55 | name: crypticle-config 56 | -------------------------------------------------------------------------------- /kubernetes/crypticle-worker-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: crypticle-worker 5 | spec: 6 | type: NodePort 7 | ports: 8 | - 9 | port: 8000 10 | targetPort: 8000 11 | selector: 12 | component: crypticle-worker 13 | -------------------------------------------------------------------------------- /kubernetes/gke/ingress-nginx-cloud-generic.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: ingress-nginx 5 | namespace: ingress-nginx 6 | labels: 7 | app.kubernetes.io/name: ingress-nginx 8 | app.kubernetes.io/part-of: ingress-nginx 9 | spec: 10 | externalTrafficPolicy: Local 11 | type: LoadBalancer 12 | selector: 13 | app.kubernetes.io/name: ingress-nginx 14 | app.kubernetes.io/part-of: ingress-nginx 15 | ports: 16 | - name: http 17 | port: 80 18 | targetPort: http 19 | - name: https 20 | port: 443 21 | targetPort: https 22 | 23 | --- 24 | -------------------------------------------------------------------------------- /kubernetes/gke/ingress-nginx-mandatory.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: ingress-nginx 5 | labels: 6 | app.kubernetes.io/name: ingress-nginx 7 | app.kubernetes.io/part-of: ingress-nginx 8 | 9 | --- 10 | 11 | kind: ConfigMap 12 | apiVersion: v1 13 | metadata: 14 | name: nginx-configuration 15 | namespace: ingress-nginx 16 | labels: 17 | app.kubernetes.io/name: ingress-nginx 18 | app.kubernetes.io/part-of: ingress-nginx 19 | 20 | --- 21 | kind: ConfigMap 22 | apiVersion: v1 23 | metadata: 24 | name: tcp-services 25 | namespace: ingress-nginx 26 | labels: 27 | app.kubernetes.io/name: ingress-nginx 28 | app.kubernetes.io/part-of: ingress-nginx 29 | 30 | --- 31 | kind: ConfigMap 32 | apiVersion: v1 33 | metadata: 34 | name: udp-services 35 | namespace: ingress-nginx 36 | labels: 37 | app.kubernetes.io/name: ingress-nginx 38 | app.kubernetes.io/part-of: ingress-nginx 39 | 40 | --- 41 | apiVersion: v1 42 | kind: ServiceAccount 43 | metadata: 44 | name: nginx-ingress-serviceaccount 45 | namespace: ingress-nginx 46 | labels: 47 | app.kubernetes.io/name: ingress-nginx 48 | app.kubernetes.io/part-of: ingress-nginx 49 | 50 | --- 51 | apiVersion: rbac.authorization.k8s.io/v1beta1 52 | kind: ClusterRole 53 | metadata: 54 | name: nginx-ingress-clusterrole 55 | labels: 56 | app.kubernetes.io/name: ingress-nginx 57 | app.kubernetes.io/part-of: ingress-nginx 58 | rules: 59 | - apiGroups: 60 | - "" 61 | resources: 62 | - configmaps 63 | - endpoints 64 | - nodes 65 | - pods 66 | - secrets 67 | verbs: 68 | - list 69 | - watch 70 | - apiGroups: 71 | - "" 72 | resources: 73 | - nodes 74 | verbs: 75 | - get 76 | - apiGroups: 77 | - "" 78 | resources: 79 | - services 80 | verbs: 81 | - get 82 | - list 83 | - watch 84 | - apiGroups: 85 | - "extensions" 86 | resources: 87 | - ingresses 88 | verbs: 89 | - get 90 | - list 91 | - watch 92 | - apiGroups: 93 | - "" 94 | resources: 95 | - events 96 | verbs: 97 | - create 98 | - patch 99 | - apiGroups: 100 | - "extensions" 101 | resources: 102 | - ingresses/status 103 | verbs: 104 | - update 105 | 106 | --- 107 | apiVersion: rbac.authorization.k8s.io/v1beta1 108 | kind: Role 109 | metadata: 110 | name: nginx-ingress-role 111 | namespace: ingress-nginx 112 | labels: 113 | app.kubernetes.io/name: ingress-nginx 114 | app.kubernetes.io/part-of: ingress-nginx 115 | rules: 116 | - apiGroups: 117 | - "" 118 | resources: 119 | - configmaps 120 | - pods 121 | - secrets 122 | - namespaces 123 | verbs: 124 | - get 125 | - apiGroups: 126 | - "" 127 | resources: 128 | - configmaps 129 | resourceNames: 130 | # Defaults to "-" 131 | # Here: "-" 132 | # This has to be adapted if you change either parameter 133 | # when launching the nginx-ingress-controller. 134 | - "ingress-controller-leader-nginx" 135 | verbs: 136 | - get 137 | - update 138 | - apiGroups: 139 | - "" 140 | resources: 141 | - configmaps 142 | verbs: 143 | - create 144 | - apiGroups: 145 | - "" 146 | resources: 147 | - endpoints 148 | verbs: 149 | - get 150 | 151 | --- 152 | apiVersion: rbac.authorization.k8s.io/v1beta1 153 | kind: RoleBinding 154 | metadata: 155 | name: nginx-ingress-role-nisa-binding 156 | namespace: ingress-nginx 157 | labels: 158 | app.kubernetes.io/name: ingress-nginx 159 | app.kubernetes.io/part-of: ingress-nginx 160 | roleRef: 161 | apiGroup: rbac.authorization.k8s.io 162 | kind: Role 163 | name: nginx-ingress-role 164 | subjects: 165 | - kind: ServiceAccount 166 | name: nginx-ingress-serviceaccount 167 | namespace: ingress-nginx 168 | 169 | --- 170 | apiVersion: rbac.authorization.k8s.io/v1beta1 171 | kind: ClusterRoleBinding 172 | metadata: 173 | name: nginx-ingress-clusterrole-nisa-binding 174 | labels: 175 | app.kubernetes.io/name: ingress-nginx 176 | app.kubernetes.io/part-of: ingress-nginx 177 | roleRef: 178 | apiGroup: rbac.authorization.k8s.io 179 | kind: ClusterRole 180 | name: nginx-ingress-clusterrole 181 | subjects: 182 | - kind: ServiceAccount 183 | name: nginx-ingress-serviceaccount 184 | namespace: ingress-nginx 185 | 186 | --- 187 | 188 | apiVersion: apps/v1 189 | kind: Deployment 190 | metadata: 191 | name: nginx-ingress-controller 192 | namespace: ingress-nginx 193 | labels: 194 | app.kubernetes.io/name: ingress-nginx 195 | app.kubernetes.io/part-of: ingress-nginx 196 | spec: 197 | replicas: 1 198 | selector: 199 | matchLabels: 200 | app.kubernetes.io/name: ingress-nginx 201 | app.kubernetes.io/part-of: ingress-nginx 202 | template: 203 | metadata: 204 | labels: 205 | app.kubernetes.io/name: ingress-nginx 206 | app.kubernetes.io/part-of: ingress-nginx 207 | annotations: 208 | prometheus.io/port: "10254" 209 | prometheus.io/scrape: "true" 210 | spec: 211 | serviceAccountName: nginx-ingress-serviceaccount 212 | containers: 213 | - name: nginx-ingress-controller 214 | image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.23.0 215 | args: 216 | - /nginx-ingress-controller 217 | - --configmap=$(POD_NAMESPACE)/nginx-configuration 218 | - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services 219 | - --udp-services-configmap=$(POD_NAMESPACE)/udp-services 220 | - --publish-service=$(POD_NAMESPACE)/ingress-nginx 221 | - --annotations-prefix=nginx.ingress.kubernetes.io 222 | securityContext: 223 | allowPrivilegeEscalation: true 224 | capabilities: 225 | drop: 226 | - ALL 227 | add: 228 | - NET_BIND_SERVICE 229 | # www-data -> 33 230 | runAsUser: 33 231 | env: 232 | - name: POD_NAME 233 | valueFrom: 234 | fieldRef: 235 | fieldPath: metadata.name 236 | - name: POD_NAMESPACE 237 | valueFrom: 238 | fieldRef: 239 | fieldPath: metadata.namespace 240 | ports: 241 | - name: http 242 | containerPort: 80 243 | - name: https 244 | containerPort: 443 245 | livenessProbe: 246 | failureThreshold: 3 247 | httpGet: 248 | path: /healthz 249 | port: 10254 250 | scheme: HTTP 251 | initialDelaySeconds: 10 252 | periodSeconds: 10 253 | successThreshold: 1 254 | timeoutSeconds: 10 255 | readinessProbe: 256 | failureThreshold: 3 257 | httpGet: 258 | path: /healthz 259 | port: 10254 260 | scheme: HTTP 261 | periodSeconds: 10 262 | successThreshold: 1 263 | timeoutSeconds: 10 264 | 265 | --- 266 | -------------------------------------------------------------------------------- /kubernetes/gke/rethinkdb-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: StatefulSet 3 | metadata: 4 | labels: 5 | db: rethinkdb 6 | name: rethinkdb 7 | spec: 8 | replicas: 1 9 | # Must match RETHINKDB-CLUSTER env and service name. 10 | serviceName: rethinkdb 11 | template: 12 | metadata: 13 | labels: 14 | db: rethinkdb 15 | role: replica 16 | spec: 17 | containers: 18 | - image: socketcluster/rethinkdb-kubernetes:v2.3.5 19 | args: 20 | - "--cache-size" 21 | - "100" 22 | imagePullPolicy: Always 23 | name: rethinkdb 24 | env: 25 | - name: POD_NAMESPACE 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.namespace 29 | - name: POD_NAME 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: POD_IP 34 | valueFrom: 35 | fieldRef: 36 | apiVersion: v1 37 | fieldPath: status.podIP 38 | - name: RETHINK_CLUSTER 39 | value: rethinkdb 40 | - name: USE_SERVICE_LOOKUP 41 | value: "yes" 42 | ports: 43 | - containerPort: 8080 44 | name: admin 45 | - containerPort: 28015 46 | name: driver 47 | - containerPort: 29015 48 | name: cluster 49 | resources: 50 | limits: 51 | cpu: 100m 52 | memory: 256Mi 53 | requests: 54 | cpu: 100m 55 | memory: 256Mi 56 | volumeMounts: 57 | - mountPath: /data 58 | name: data 59 | readinessProbe: 60 | exec: 61 | command: 62 | - /probe 63 | failureThreshold: 3 64 | initialDelaySeconds: 15 65 | periodSeconds: 10 66 | successThreshold: 1 67 | timeoutSeconds: 5 68 | livenessProbe: 69 | exec: 70 | command: 71 | - /probe 72 | failureThreshold: 3 73 | initialDelaySeconds: 15 74 | periodSeconds: 10 75 | successThreshold: 1 76 | timeoutSeconds: 5 77 | dnsPolicy: ClusterFirst 78 | restartPolicy: Always 79 | volumeClaimTemplates: 80 | - metadata: 81 | name: data 82 | annotations: 83 | volume.alpha.kubernetes.io/storage-class: anything 84 | spec: 85 | accessModes: 86 | - ReadWriteOnce 87 | resources: 88 | requests: 89 | storage: 10Gi 90 | -------------------------------------------------------------------------------- /kubernetes/local-path-storage.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: local-path-storage 5 | --- 6 | apiVersion: v1 7 | kind: ServiceAccount 8 | metadata: 9 | name: local-path-provisioner-service-account 10 | namespace: local-path-storage 11 | --- 12 | apiVersion: rbac.authorization.k8s.io/v1beta1 13 | kind: ClusterRole 14 | metadata: 15 | name: local-path-provisioner-role 16 | namespace: local-path-storage 17 | rules: 18 | - apiGroups: [""] 19 | resources: ["nodes", "persistentvolumeclaims"] 20 | verbs: ["get", "list", "watch"] 21 | - apiGroups: [""] 22 | resources: ["endpoints", "persistentvolumes", "pods"] 23 | verbs: ["*"] 24 | - apiGroups: [""] 25 | resources: ["events"] 26 | verbs: ["create", "patch"] 27 | - apiGroups: ["storage.k8s.io"] 28 | resources: ["storageclasses"] 29 | verbs: ["get", "list", "watch"] 30 | --- 31 | apiVersion: rbac.authorization.k8s.io/v1beta1 32 | kind: ClusterRoleBinding 33 | metadata: 34 | name: local-path-provisioner-bind 35 | namespace: local-path-storage 36 | roleRef: 37 | apiGroup: rbac.authorization.k8s.io 38 | kind: ClusterRole 39 | name: local-path-provisioner-role 40 | subjects: 41 | - kind: ServiceAccount 42 | name: local-path-provisioner-service-account 43 | namespace: local-path-storage 44 | --- 45 | apiVersion: apps/v1beta2 46 | kind: Deployment 47 | metadata: 48 | name: local-path-provisioner 49 | namespace: local-path-storage 50 | spec: 51 | replicas: 1 52 | selector: 53 | matchLabels: 54 | app: local-path-provisioner 55 | template: 56 | metadata: 57 | labels: 58 | app: local-path-provisioner 59 | spec: 60 | serviceAccountName: local-path-provisioner-service-account 61 | containers: 62 | - name: local-path-provisioner 63 | image: rancher/local-path-provisioner:v0.0.9 64 | imagePullPolicy: Always 65 | command: 66 | - local-path-provisioner 67 | - --debug 68 | - start 69 | - --config 70 | - /etc/config/config.json 71 | volumeMounts: 72 | - name: config-volume 73 | mountPath: /etc/config/ 74 | env: 75 | - name: POD_NAMESPACE 76 | valueFrom: 77 | fieldRef: 78 | fieldPath: metadata.namespace 79 | volumes: 80 | - name: config-volume 81 | configMap: 82 | name: local-path-config 83 | --- 84 | apiVersion: storage.k8s.io/v1 85 | kind: StorageClass 86 | metadata: 87 | name: local-path 88 | provisioner: rancher.io/local-path 89 | volumeBindingMode: WaitForFirstConsumer 90 | reclaimPolicy: Delete 91 | --- 92 | kind: ConfigMap 93 | apiVersion: v1 94 | metadata: 95 | name: local-path-config 96 | namespace: local-path-storage 97 | data: 98 | config.json: |- 99 | { 100 | "nodePathMap":[ 101 | { 102 | "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES", 103 | "paths":["/opt/local-path-provisioner"] 104 | } 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /kubernetes/rethinkdb-admin-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | db: rethinkdb 6 | name: rethinkdb-admin 7 | spec: 8 | template: 9 | metadata: 10 | labels: 11 | db: rethinkdb 12 | role: admin 13 | spec: 14 | serviceAccount: rethinkdb 15 | containers: 16 | - image: socketcluster/rethinkdb-kubernetes:v2.3.5 17 | imagePullPolicy: Always 18 | name: rethinkdb 19 | env: 20 | - name: PROXY 21 | value: "true" 22 | - name: POD_NAMESPACE 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.namespace 26 | - name: POD_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name 30 | - name: POD_IP 31 | valueFrom: 32 | fieldRef: 33 | apiVersion: v1 34 | fieldPath: status.podIP 35 | - name: RETHINK_CLUSTER 36 | value: rethinkdb 37 | ports: 38 | - containerPort: 8080 39 | name: admin 40 | - containerPort: 28015 41 | name: driver 42 | - containerPort: 29015 43 | name: cluster 44 | livenessProbe: 45 | exec: 46 | command: 47 | - /probe 48 | failureThreshold: 3 49 | initialDelaySeconds: 15 50 | periodSeconds: 10 51 | successThreshold: 1 52 | timeoutSeconds: 5 53 | readinessProbe: 54 | exec: 55 | command: 56 | - /probe 57 | failureThreshold: 3 58 | initialDelaySeconds: 15 59 | periodSeconds: 10 60 | successThreshold: 1 61 | timeoutSeconds: 5 62 | volumeMounts: 63 | - mountPath: /data/rethinkdb_data 64 | name: storage 65 | volumes: 66 | - name: storage 67 | emptyDir: {} 68 | -------------------------------------------------------------------------------- /kubernetes/rethinkdb-admin-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | db: rethinkdb 6 | name: rethinkdb-admin 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - port: 8080 11 | targetPort: admin 12 | selector: 13 | db: rethinkdb 14 | role: admin 15 | -------------------------------------------------------------------------------- /kubernetes/rethinkdb-proxy-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | db: rethinkdb 6 | name: rethinkdb-proxy 7 | spec: 8 | template: 9 | metadata: 10 | labels: 11 | db: rethinkdb 12 | role: proxy 13 | spec: 14 | serviceAccount: rethinkdb 15 | containers: 16 | - image: socketcluster/rethinkdb-kubernetes:v2.3.5 17 | imagePullPolicy: Always 18 | name: rethinkdb 19 | env: 20 | - name: PROXY 21 | value: "true" 22 | - name: POD_NAMESPACE 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.namespace 26 | - name: POD_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name 30 | - name: POD_IP 31 | valueFrom: 32 | fieldRef: 33 | apiVersion: v1 34 | fieldPath: status.podIP 35 | - name: RETHINK_CLUSTER 36 | value: rethinkdb 37 | ports: 38 | - containerPort: 8080 39 | name: admin 40 | - containerPort: 28015 41 | name: driver 42 | - containerPort: 29015 43 | name: cluster 44 | livenessProbe: 45 | exec: 46 | command: 47 | - /probe 48 | failureThreshold: 3 49 | initialDelaySeconds: 15 50 | periodSeconds: 10 51 | successThreshold: 1 52 | timeoutSeconds: 5 53 | readinessProbe: 54 | exec: 55 | command: 56 | - /probe 57 | failureThreshold: 3 58 | initialDelaySeconds: 15 59 | periodSeconds: 10 60 | successThreshold: 1 61 | timeoutSeconds: 5 62 | volumeMounts: 63 | - mountPath: /data/rethinkdb_data 64 | name: storage 65 | volumes: 66 | - name: storage 67 | emptyDir: {} 68 | -------------------------------------------------------------------------------- /kubernetes/rethinkdb-proxy-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | db: rethinkdb 6 | name: rethinkdb-proxy 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - port: 28015 11 | targetPort: driver 12 | selector: 13 | db: rethinkdb 14 | role: proxy 15 | -------------------------------------------------------------------------------- /kubernetes/rethinkdb-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: rethinkdb 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1beta1 7 | kind: ClusterRole 8 | metadata: 9 | name: rethinkdb 10 | rules: 11 | - apiGroups: [""] # "" indicates the core API group 12 | resources: ["*"] 13 | verbs: ["get", "watch", "list"] 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1beta1 16 | kind: RoleBinding 17 | metadata: 18 | name: rethinkdb 19 | roleRef: 20 | apiGroup: rbac.authorization.k8s.io 21 | kind: ClusterRole 22 | name: rethinkdb 23 | subjects: 24 | - kind: ServiceAccount 25 | name: rethinkdb 26 | -------------------------------------------------------------------------------- /kubernetes/rethinkdb-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | db: rethinkdb 6 | name: rethinkdb 7 | spec: 8 | clusterIP: None 9 | ports: 10 | - port: 29015 11 | selector: 12 | db: rethinkdb 13 | role: replica 14 | -------------------------------------------------------------------------------- /kubernetes/rethinkdb-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: StatefulSet 3 | metadata: 4 | labels: 5 | db: rethinkdb 6 | name: rethinkdb 7 | spec: 8 | replicas: 1 9 | # Must match RETHINKDB-CLUSTER env and service name. 10 | serviceName: rethinkdb 11 | template: 12 | metadata: 13 | labels: 14 | db: rethinkdb 15 | role: replica 16 | spec: 17 | containers: 18 | - image: socketcluster/rethinkdb-kubernetes:v2.3.5 19 | args: 20 | - "--cache-size" 21 | - "100" 22 | imagePullPolicy: Always 23 | name: rethinkdb 24 | env: 25 | - name: POD_NAMESPACE 26 | valueFrom: 27 | fieldRef: 28 | fieldPath: metadata.namespace 29 | - name: POD_NAME 30 | valueFrom: 31 | fieldRef: 32 | fieldPath: metadata.name 33 | - name: POD_IP 34 | valueFrom: 35 | fieldRef: 36 | apiVersion: v1 37 | fieldPath: status.podIP 38 | - name: RETHINK_CLUSTER 39 | value: rethinkdb 40 | - name: USE_SERVICE_LOOKUP 41 | value: "yes" 42 | ports: 43 | - containerPort: 8080 44 | name: admin 45 | - containerPort: 28015 46 | name: driver 47 | - containerPort: 29015 48 | name: cluster 49 | resources: 50 | limits: 51 | cpu: 100m 52 | memory: 256Mi 53 | requests: 54 | cpu: 100m 55 | memory: 256Mi 56 | volumeMounts: 57 | - mountPath: /data 58 | name: data 59 | readinessProbe: 60 | exec: 61 | command: 62 | - /probe 63 | failureThreshold: 3 64 | initialDelaySeconds: 15 65 | periodSeconds: 10 66 | successThreshold: 1 67 | timeoutSeconds: 5 68 | livenessProbe: 69 | exec: 70 | command: 71 | - /probe 72 | failureThreshold: 3 73 | initialDelaySeconds: 15 74 | periodSeconds: 10 75 | successThreshold: 1 76 | timeoutSeconds: 5 77 | dnsPolicy: ClusterFirst 78 | restartPolicy: Always 79 | volumeClaimTemplates: 80 | - metadata: 81 | name: data 82 | spec: 83 | accessModes: 84 | - ReadWriteOnce 85 | storageClassName: local-path 86 | resources: 87 | requests: 88 | storage: 10Gi 89 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["public/*", "state.json"] 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypticle", 3 | "description": "Scalable multi-tenant pegged off-chain payment microservice with realtime API", 4 | "version": "3.0.0", 5 | "contributors": [ 6 | { 7 | "name": "Jonathan Gros-Dubois", 8 | "email": "grosjona@yahoo.com.au" 9 | } 10 | ], 11 | "license": "AGPL-3.0", 12 | "dependencies": { 13 | "@liskhq/lisk-api-client": "^3.0.1", 14 | "@liskhq/lisk-cryptography": "^2.4.2", 15 | "@liskhq/lisk-passphrase": "^2.0.3", 16 | "@liskhq/lisk-transactions": "^2.3.1", 17 | "ag-crud-rethink": "^2.5.0", 18 | "agc-broker-client": "^6.0.0", 19 | "async-stream-emitter": "^3.0.3", 20 | "asyngular-client": "^6.1.0", 21 | "asyngular-server": "^6.1.0", 22 | "bcryptjs": "^2.4.3", 23 | "connect": "^3.6.6", 24 | "cryptr": "^4.0.2", 25 | "eetase": "^3.0.1", 26 | "express": "^4.16.3", 27 | "fs-extra": "^8.0.1", 28 | "inquirer": "^6.5.0", 29 | "jsonschema": "^1.2.4", 30 | "morgan": "^1.7.0", 31 | "nodemon": "^1.18.9", 32 | "sc-errors": "^2.0.0", 33 | "sc-hasher": "^1.0.1", 34 | "serve-static": "^1.13.2", 35 | "uuid": "^3.3.2", 36 | "writable-consumable-stream": "^1.1.1", 37 | "yamljs": "^0.3.0" 38 | }, 39 | "keywords": [ 40 | "websocket", 41 | "server", 42 | "realtime", 43 | "cluster", 44 | "scalable" 45 | ], 46 | "readmeFilename": "README.md", 47 | "scripts": { 48 | "start": "node server.js", 49 | "start:watch": "./node_modules/nodemon/bin/nodemon.js server.js", 50 | "start:docker": "./node_modules/nodemon/bin/nodemon.js /usr/src/app/server.js" 51 | }, 52 | "bin": { 53 | "crypticle": "bin/cli.js" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/app-main.js: -------------------------------------------------------------------------------- 1 | import getHomePageComponent from '/pages/page-home.js'; 2 | import getLoginPageComponent from '/pages/page-login.js'; 3 | import getLoginImpersonatePageComponent from '/pages/page-impersonate.js'; 4 | import getSignupPageComponent from '/pages/page-signup.js'; 5 | import getSignupAdminPageComponent from '/pages/page-signup-admin.js'; 6 | import getDashboardPageComponent from '/pages/page-dashboard.js'; 7 | 8 | let socket = window.socket = asyngularClient.create({ 9 | batchInterval: 50 10 | }); 11 | socket.startBatching(); 12 | 13 | let pageOptions = { 14 | socket, 15 | publicInfo: { 16 | cryptocurrency: {}, 17 | mainWalletAddress: null, 18 | requiredDepositBlockConfirmations: null, 19 | requiredWithdrawalBlockConfirmations: null, 20 | paginationShowTotalCounts: false, 21 | alwaysRequireSecretSignupKey: false, 22 | enableAdminAccountSignup: false, 23 | withdrawalFees: null, 24 | depositFees: null, 25 | termsOfServiceURL: null, 26 | privacyPolicyURL: null 27 | } 28 | }; 29 | 30 | (async () => { 31 | let publicInfo; 32 | try { 33 | publicInfo = await socket.invoke('getPublicInfo'); 34 | } catch (error) { 35 | console.error(error); 36 | } 37 | Object.assign(pageOptions.publicInfo, publicInfo); 38 | })(); 39 | 40 | let PageHome = getHomePageComponent(pageOptions); 41 | let PageDashboard = getDashboardPageComponent(pageOptions); 42 | let PageLogin = getLoginPageComponent(pageOptions); 43 | let PageLoginImpersonate = getLoginImpersonatePageComponent(pageOptions); 44 | let PageSignup = getSignupPageComponent(pageOptions); 45 | let PageSignupAdmin = getSignupAdminPageComponent(pageOptions); 46 | 47 | function isSocketAuthenticated() { 48 | return socket.authState === 'authenticated'; 49 | } 50 | 51 | function isSocketOpen() { 52 | return socket.state === socket.OPEN; 53 | } 54 | 55 | let Console = { 56 | components: { 57 | 'page-login': PageLogin, 58 | 'page-dashboard': PageDashboard 59 | }, 60 | data: function () { 61 | return { 62 | isAuthenticated: false, 63 | isConnected: false 64 | }; 65 | }, 66 | created: function () { 67 | this.isAuthenticated = isSocketAuthenticated(); 68 | this.isConnected = isSocketOpen(); 69 | 70 | (async () => { 71 | for await (let event of socket.listener('authStateChange')) { 72 | this.isAuthenticated = isSocketAuthenticated(); 73 | } 74 | })(); 75 | 76 | (async () => { 77 | for await (let event of socket.listener('connect')) { 78 | this.isConnected = isSocketOpen(); 79 | } 80 | })(); 81 | 82 | (async () => { 83 | for await (let event of socket.listener('close')) { 84 | this.isConnected = isSocketOpen(); 85 | } 86 | })(); 87 | }, 88 | template: ` 89 |
90 | 93 | 101 |
102 | ` 103 | }; 104 | 105 | let routes = [ 106 | {path: '/', component: PageHome, props: true}, 107 | {path: '/signup', component: PageSignup, props: true}, 108 | {path: '/signup/admin', component: PageSignupAdmin, props: true}, 109 | {path: '/login', component: PageLogin, props: (route) => ({redirect: route.query.redirect})}, 110 | {path: '/impersonate', component: PageLoginImpersonate, props: (route) => ({redirect: route.query.redirect})}, 111 | { 112 | path: '/console', 113 | component: Console, 114 | props: true, 115 | children: [ 116 | {path: '/', component: PageDashboard, props: true} 117 | ] 118 | } 119 | ]; 120 | 121 | let router = new VueRouter({ 122 | routes 123 | }); 124 | 125 | new Vue({ 126 | el: '#app', 127 | router, 128 | data: function () { 129 | return { 130 | publicInfo: pageOptions.publicInfo, 131 | isAuthenticated: false, 132 | isAdmin: false 133 | }; 134 | }, 135 | computed: { 136 | loginPath: function (arg) { 137 | return `#/login?redirect=${encodeURIComponent('#' + this.$route.path)}`; 138 | }, 139 | impersonatePath: function () { 140 | return `#/impersonate?redirect=${encodeURIComponent('#' + this.$route.path)}`; 141 | } 142 | }, 143 | 144 | created: function () { 145 | this.isAuthenticated = isSocketAuthenticated(); 146 | 147 | (async () => { 148 | for await (let event of socket.listener('authStateChange')) { 149 | this.isAuthenticated = isSocketAuthenticated(); 150 | if (socket.authToken && socket.authToken.admin) { 151 | this.isAdmin = true; 152 | } else { 153 | this.isAdmin = false; 154 | } 155 | } 156 | })(); 157 | 158 | this._localStorageAuthHandler = (change) => { 159 | // In case the user logged in from a different tab 160 | if (change.key === socket.options.authTokenName) { 161 | if (this.isAuthenticated) { 162 | if (!change.newValue) { 163 | socket.deauthenticate(); 164 | } 165 | } else if (change.newValue) { 166 | socket.authenticate(change.newValue); 167 | } 168 | } 169 | }; 170 | window.addEventListener('storage', this._localStorageAuthHandler); 171 | }, 172 | destroyed: function () { 173 | window.removeEventListener('storage', this._localStorageAuthHandler); 174 | }, 175 | methods: { 176 | logout: function () { 177 | socket.deauthenticate(); 178 | }, 179 | isOnPage: function (pagePath) { 180 | return this.$route.path === pagePath; 181 | } 182 | }, 183 | template: ` 184 |
185 | 208 |
209 | 210 |
211 | ` 212 | }); 213 | -------------------------------------------------------------------------------- /public/components/component-account.js: -------------------------------------------------------------------------------- 1 | import AGModel from '/node_modules/ag-model/ag-model.js'; 2 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 3 | 4 | function getComponent(options) { 5 | let {socket, publicInfo} = options; 6 | 7 | return { 8 | data: function () { 9 | this.accountModel = new AGModel({ 10 | socket, 11 | type: 'Account', 12 | id: socket.authToken && socket.authToken.accountId, 13 | fields: ['balance', 'depositWalletAddress', 'admin'] 14 | }); 15 | (async () => { 16 | for await (let {error} of this.accountModel.listener('error')) { 17 | console.error(error); 18 | } 19 | })(); 20 | 21 | return { 22 | publicInfo, 23 | account: this.accountModel.value, 24 | isImpersonating: socket.authToken && !!socket.authToken.impersonator 25 | }; 26 | }, 27 | destroyed: function () { 28 | this.accountModel.destroy(); 29 | }, 30 | methods: { 31 | toBlockchainUnits: function (amount) { 32 | let value = Number(amount) / Number(publicInfo.cryptocurrency.unit); 33 | return Math.round(value * 10000) / 10000; 34 | }, 35 | capitalize: function (message) { 36 | return message.charAt(0).toUpperCase() + message.slice(1) 37 | } 38 | }, 39 | computed: { 40 | accountBalance: function () { 41 | if (this.account.balance == null) { 42 | return 'Loading...'; 43 | } 44 | return `${this.toBlockchainUnits(this.account.balance)} ${this.publicInfo.cryptocurrency.symbol}`; 45 | } 46 | }, 47 | template: ` 48 |
49 |

50 | Account 51 |

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 84 | 85 | 86 | 87 |
This account has admin privileges.
This account is being impersonated.
Account ID{{account.id}}
Balance{{accountBalance}}
70 | Deposit wallet address ({{publicInfo.cryptocurrency.symbol}}) 71 | {{account.depositWalletAddress}}
76 | Deposit fees 77 | {{toBlockchainUnits(publicInfo.depositFees)}} {{publicInfo.cryptocurrency.symbol}}
82 | Withdrawal fees 83 | {{toBlockchainUnits(publicInfo.withdrawalFees)}} {{publicInfo.cryptocurrency.symbol}}
88 |
89 | ` 90 | }; 91 | } 92 | 93 | export default getComponent; 94 | -------------------------------------------------------------------------------- /public/components/component-deposits.js: -------------------------------------------------------------------------------- 1 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 2 | 3 | function getComponent(options) { 4 | let {socket, publicInfo} = options; 5 | let view; 6 | 7 | if (options.type === 'pending') { 8 | view = 'accountDepositsPendingView'; 9 | } else if (options.type === 'settled') { 10 | view = 'accountDepositsSettledView'; 11 | } 12 | 13 | return { 14 | data: function () { 15 | this.depositCollection = new AGCollection({ 16 | socket, 17 | type: 'Deposit', 18 | view, 19 | viewParams: { 20 | accountId: socket.authToken && socket.authToken.accountId 21 | }, 22 | viewPrimaryKeys: ['accountId'], 23 | fields: ['transactionId', 'amount', 'height', 'canceled', 'createdDate'], 24 | pageOffset: 0, 25 | pageSize: 10, 26 | getCount: publicInfo.paginationShowTotalCounts 27 | }); 28 | (async () => { 29 | for await (let {error} of this.depositCollection.listener('error')) { 30 | console.error(error); 31 | } 32 | })(); 33 | 34 | return { 35 | publicInfo, 36 | deposits: this.depositCollection.value, 37 | depositsMeta: this.depositCollection.meta, 38 | depositType: options.type 39 | }; 40 | }, 41 | destroyed: function () { 42 | this.depositCollection.destroy(); 43 | }, 44 | methods: { 45 | toBlockchainUnits: function (amount) { 46 | let value = Number(amount) / Number(publicInfo.cryptocurrency.unit); 47 | return Math.round(value * 10000) / 10000; 48 | }, 49 | toSimpleDate: function (dateString) { 50 | return (new Date(dateString)).toLocaleString(); 51 | }, 52 | capitalize: function (message) { 53 | return message.charAt(0).toUpperCase() + message.slice(1) 54 | }, 55 | goToPrevPage: function () { 56 | this.depositCollection.fetchPreviousPage(); 57 | }, 58 | goToNextPage: function () { 59 | this.depositCollection.fetchNextPage(); 60 | } 61 | }, 62 | computed: { 63 | firstItemIndex: function () { 64 | if (!this.deposits.length) { 65 | return 0; 66 | } 67 | return this.depositsMeta.pageOffset + 1; 68 | }, 69 | lastItemIndex: function () { 70 | return this.depositsMeta.pageOffset + this.deposits.length; 71 | }, 72 | hasMultiplePages: function () { 73 | return this.depositsMeta.pageOffset > 0 || !this.depositsMeta.isLastPage; 74 | } 75 | }, 76 | watch: { 77 | 'publicInfo.paginationShowTotalCounts': function (value) { 78 | this.depositCollection.getCount = value; 79 | } 80 | }, 81 | template: ` 82 |
83 |

{{capitalize(depositType)}} deposits

84 |

Deposits

85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 104 | 105 | 106 | 107 | 108 |
Deposit IDAmountHeightDate
No {{depositType}} deposits
109 |
110 | 111 |
Items {{firstItemIndex}} to {{lastItemIndex}} of {{depositsMeta.count}}
112 | 113 |
114 | ` 115 | }; 116 | } 117 | 118 | export default getComponent; 119 | -------------------------------------------------------------------------------- /public/components/component-make-transfer.js: -------------------------------------------------------------------------------- 1 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 2 | 3 | function getComponent(options) { 4 | let {socket, publicInfo} = options; 5 | 6 | return { 7 | data: function () { 8 | this.accountCollection = new AGCollection({ 9 | socket, 10 | type: 'Account', 11 | view: 'accountIdSearchView', 12 | viewParams: { 13 | searchString: '', 14 | }, 15 | viewPrimaryKeys: null, 16 | fields: [], 17 | pageOffset: 0, 18 | realtimeCollection: false, 19 | pageSize: 4 20 | }); 21 | (async () => { 22 | for await (let {error} of this.accountCollection.listener('error')) { 23 | console.error(error); 24 | } 25 | })(); 26 | 27 | return { 28 | publicInfo, 29 | accountId: null, 30 | dropdownActive: false, 31 | amount: null, 32 | data: null, 33 | error: null, 34 | accounts: this.accountCollection.value, 35 | isTransferModalActive: false, 36 | isDebitModalActive: false 37 | }; 38 | }, 39 | destroyed: function () { 40 | this.accountCollection.destroy(); 41 | }, 42 | methods: { 43 | openTransferModal: function () { 44 | this.isTransferModalActive = true; 45 | }, 46 | closeTransferModal: function () { 47 | this.clearForm(); 48 | this.isTransferModalActive = false; 49 | }, 50 | openDebitModal: function () { 51 | this.isDebitModalActive = true; 52 | }, 53 | closeDebitModal: function () { 54 | this.clearForm(); 55 | this.isDebitModalActive = false; 56 | }, 57 | clearForm: function () { 58 | this.error = null; 59 | this.accountId = null; 60 | this.amount = null; 61 | this.data = null; 62 | }, 63 | hideDropdown: function () { 64 | this.dropdownActive = false; 65 | }, 66 | searchForAccount: function () { 67 | this.dropdownActive = true; 68 | this.accountCollection.viewParams = { 69 | searchString: this.accountId 70 | }; 71 | this.accountCollection.reloadCurrentPage(); 72 | }, 73 | selectAccount: function (accountId) { 74 | this.dropdownActive = false; 75 | this.accountId = accountId; 76 | }, 77 | sendTransfer: async function () { 78 | if (!this.accountId) { 79 | this.error = 'Could not execute the transfer. The account ID was not provided.'; 80 | return; 81 | } 82 | if (!this.amount || this.amount < 0) { 83 | this.error = 'Could not execute the transfer. The amount was not provided or was invalid.'; 84 | return; 85 | } 86 | if (publicInfo.cryptocurrency.unit == null) { 87 | this.error = 'Could not execute the transfer. The cryptocurrency unit value could not be determined.'; 88 | return; 89 | } 90 | let accountId = this.accountId.trim(); 91 | let unitAmount = parseFloat(this.amount); 92 | let totalAmount = Math.round(unitAmount * parseInt(publicInfo.cryptocurrency.unit)); 93 | let totalAmountString = totalAmount.toString(); 94 | 95 | try { 96 | await socket.invoke('transfer', { 97 | amount: totalAmountString, 98 | toAccountId: accountId, 99 | data: this.data 100 | }); 101 | } catch (error) { 102 | this.error = error.message; 103 | return; 104 | } 105 | this.clearForm(); 106 | this.closeTransferModal(); 107 | }, 108 | sendDebit: async function () { 109 | if (!this.amount || this.amount < 0) { 110 | this.error = 'Could not execute the debit. The amount was not provided or was invalid.'; 111 | return; 112 | } 113 | if (publicInfo.cryptocurrency.unit == null) { 114 | this.error = 'Could not execute the debit. The cryptocurrency unit value could not be determined.'; 115 | return; 116 | } 117 | let unitAmount = parseFloat(this.amount); 118 | let totalAmount = Math.round(unitAmount * parseInt(publicInfo.cryptocurrency.unit)); 119 | let totalAmountString = totalAmount.toString(); 120 | 121 | try { 122 | await socket.invoke('debit', { 123 | amount: totalAmountString, 124 | data: this.data 125 | }); 126 | } catch (error) { 127 | this.error = error.message; 128 | return; 129 | } 130 | this.clearForm(); 131 | this.closeDebitModal(); 132 | } 133 | }, 134 | template: ` 135 |
136 | 137 | 138 | 139 |
140 | 141 | 187 |
188 | 189 |
190 | 191 | 218 |
219 |
220 | ` 221 | }; 222 | } 223 | 224 | export default getComponent; 225 | -------------------------------------------------------------------------------- /public/components/component-make-withdrawal.js: -------------------------------------------------------------------------------- 1 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 2 | 3 | function getComponent(options) { 4 | let {socket, publicInfo} = options; 5 | 6 | return { 7 | data: function () { 8 | return { 9 | publicInfo, 10 | walletAddress: null, 11 | amount: null, 12 | error: null, 13 | isModalActive: false 14 | }; 15 | }, 16 | methods: { 17 | toBlockchainUnits: function (amount, recordType) { 18 | let value = Number(amount) / Number(publicInfo.cryptocurrency.unit); 19 | if (recordType === 'debit') { 20 | value *= -1; 21 | } 22 | return Math.round(value * 10000) / 10000; 23 | }, 24 | openModal: function () { 25 | this.isModalActive = true; 26 | }, 27 | closeModal: function () { 28 | this.clearForm(); 29 | this.isModalActive = false; 30 | }, 31 | clearForm: function () { 32 | this.error = null; 33 | this.walletAddress = null; 34 | this.amount = null; 35 | }, 36 | sendWithdrawal: async function () { 37 | if (!this.walletAddress) { 38 | this.error = 'Could not execute the withdrawal. The wallet address was not provided.'; 39 | return; 40 | } 41 | if (!this.amount || this.amount < 0) { 42 | this.error = 'Could not execute the withdrawal. The amount was not provided or was invalid.'; 43 | return; 44 | } 45 | if (publicInfo.cryptocurrency.unit == null) { 46 | this.error = 'Could not execute the withdrawal. The cryptocurrency unit value could not be determined.'; 47 | return; 48 | } 49 | let walletAddress = this.walletAddress.trim(); 50 | let unitAmount = parseFloat(this.amount); 51 | let totalAmount = Math.round(unitAmount * parseInt(publicInfo.cryptocurrency.unit)); 52 | let totalAmountString = totalAmount.toString(); 53 | 54 | try { 55 | await socket.invoke('withdraw', { 56 | amount: totalAmountString, 57 | toWalletAddress: walletAddress 58 | }); 59 | } catch (error) { 60 | this.error = error.message; 61 | return; 62 | } 63 | this.clearForm(); 64 | this.closeModal(); 65 | } 66 | }, 67 | template: ` 68 |
69 | 70 | 71 |
72 | 73 | 100 |
101 |
102 | ` 103 | }; 104 | } 105 | 106 | export default getComponent; 107 | -------------------------------------------------------------------------------- /public/components/component-transfers.js: -------------------------------------------------------------------------------- 1 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 2 | 3 | function getComponent(options) { 4 | let {socket, publicInfo} = options; 5 | let view; 6 | 7 | if (options.type === 'pending') { 8 | view = 'accountTransfersPendingView'; 9 | } else if (options.type === 'settled') { 10 | view = 'accountTransfersSettledView'; 11 | } 12 | 13 | return { 14 | data: function () { 15 | this.transactionCollection = new AGCollection({ 16 | socket, 17 | type: 'Transaction', 18 | view, 19 | viewParams: { 20 | accountId: socket.authToken && socket.authToken.accountId 21 | }, 22 | viewPrimaryKeys: ['accountId'], 23 | fields: ['type', 'recordType', 'amount', 'counterpartyAccountId', 'data', 'canceled', 'createdDate'], 24 | pageOffset: 0, 25 | pageSize: 10, 26 | getCount: publicInfo.paginationShowTotalCounts 27 | }); 28 | (async () => { 29 | for await (let {error} of this.transactionCollection.listener('error')) { 30 | console.error(error); 31 | } 32 | })(); 33 | 34 | return { 35 | publicInfo, 36 | transactions: this.transactionCollection.value, 37 | transactionsMeta: this.transactionCollection.meta, 38 | transactionType: options.type 39 | }; 40 | }, 41 | destroyed: function () { 42 | this.transactionCollection.destroy(); 43 | }, 44 | methods: { 45 | toBlockchainUnits: function (amount, recordType) { 46 | let value = Number(amount) / Number(publicInfo.cryptocurrency.unit); 47 | if (recordType === 'debit') { 48 | value *= -1; 49 | } 50 | return Math.round(value * 10000) / 10000; 51 | }, 52 | toSimpleDate: function (dateString) { 53 | return (new Date(dateString)).toLocaleString(); 54 | }, 55 | capitalize: function (message) { 56 | return message.charAt(0).toUpperCase() + message.slice(1) 57 | }, 58 | goToPrevPage: function () { 59 | this.transactionCollection.fetchPreviousPage(); 60 | }, 61 | goToNextPage: function () { 62 | this.transactionCollection.fetchNextPage(); 63 | } 64 | }, 65 | computed: { 66 | firstItemIndex: function () { 67 | if (!this.transactions.length) { 68 | return 0; 69 | } 70 | return this.transactionsMeta.pageOffset + 1; 71 | }, 72 | lastItemIndex: function () { 73 | return this.transactionsMeta.pageOffset + this.transactions.length; 74 | }, 75 | hasMultiplePages: function () { 76 | return this.transactionsMeta.pageOffset > 0 || !this.transactionsMeta.isLastPage; 77 | } 78 | }, 79 | watch: { 80 | 'publicInfo.paginationShowTotalCounts': function (value) { 81 | this.transactionCollection.getCount = value; 82 | } 83 | }, 84 | template: ` 85 |
86 |

{{capitalize(transactionType)}} transfers

87 |

Transfers

88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 109 | 110 | 111 | 112 | 113 |
Transaction IDCounterparty account IDDataAmountDate
No {{transactionType}} transfers
114 |
115 | 116 |
Items {{firstItemIndex}} to {{lastItemIndex}} of {{transactionsMeta.count}}
117 | 118 |
119 | ` 120 | }; 121 | } 122 | 123 | export default getComponent; 124 | -------------------------------------------------------------------------------- /public/components/component-withdrawals.js: -------------------------------------------------------------------------------- 1 | import AGModel from '/node_modules/ag-model/ag-model.js'; 2 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 3 | 4 | function getComponent(options) { 5 | let {socket, publicInfo} = options; 6 | let view; 7 | 8 | if (options.type === 'pending') { 9 | view = 'accountWithdrawalsPendingView'; 10 | } else if (options.type === 'settled') { 11 | view = 'accountWithdrawalsSettledView'; 12 | } 13 | 14 | return { 15 | data: function () { 16 | this.withdrawalCollection = new AGCollection({ 17 | socket, 18 | type: 'Withdrawal', 19 | view, 20 | viewParams: { 21 | accountId: socket.authToken && socket.authToken.accountId 22 | }, 23 | viewPrimaryKeys: ['accountId'], 24 | fields: ['transactionId', 'amount', 'height', 'toWalletAddress', 'attemptCount', 'canceled', 'createdDate'], 25 | pageOffset: 0, 26 | pageSize: 10, 27 | getCount: publicInfo.paginationShowTotalCounts 28 | }); 29 | (async () => { 30 | for await (let {error} of this.withdrawalCollection.listener('error')) { 31 | console.error(error); 32 | } 33 | })(); 34 | 35 | return { 36 | publicInfo, 37 | withdrawals: this.withdrawalCollection.value, 38 | withdrawalsMeta: this.withdrawalCollection.meta, 39 | withdrawalType: options.type 40 | }; 41 | }, 42 | destroyed: function () { 43 | this.withdrawalCollection.destroy(); 44 | }, 45 | methods: { 46 | toBlockchainUnits: function (amount) { 47 | let value = Number(amount) / Number(publicInfo.cryptocurrency.unit); 48 | return Math.round(value * 10000) / 10000; 49 | }, 50 | toSimpleDate: function (dateString) { 51 | return (new Date(dateString)).toLocaleString(); 52 | }, 53 | capitalize: function (message) { 54 | return message.charAt(0).toUpperCase() + message.slice(1) 55 | }, 56 | goToPrevPage: function () { 57 | this.withdrawalCollection.fetchPreviousPage(); 58 | }, 59 | goToNextPage: function () { 60 | this.withdrawalCollection.fetchNextPage(); 61 | } 62 | }, 63 | computed: { 64 | firstItemIndex: function () { 65 | if (!this.withdrawals.length) { 66 | return 0; 67 | } 68 | return this.withdrawalsMeta.pageOffset + 1; 69 | }, 70 | lastItemIndex: function () { 71 | return this.withdrawalsMeta.pageOffset + this.withdrawals.length; 72 | }, 73 | hasMultiplePages: function () { 74 | return this.withdrawalsMeta.pageOffset > 0 || !this.withdrawalsMeta.isLastPage; 75 | } 76 | }, 77 | watch: { 78 | 'publicInfo.paginationShowTotalCounts': function (value) { 79 | this.withdrawalCollection.getCount = value; 80 | } 81 | }, 82 | template: ` 83 |
84 |

{{capitalize(withdrawalType)}} withdrawals

85 |

Withdrawals

86 |
87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 109 | 110 | 111 | 112 | 113 |
Withdrawal IDAmountHeightTo wallet addressAttemptsDate
No {{withdrawalType}} withdrawals
114 |
115 | 116 |
Items {{firstItemIndex}} to {{lastItemIndex}} of {{withdrawalsMeta.count}}
117 | 118 |
119 | ` 120 | }; 121 | } 122 | 123 | export default getComponent; 124 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capitalisk/crypticle/13040e809bbe0ff76fa40fc8135d33612d0ca911/public/favicon.png -------------------------------------------------------------------------------- /public/img/crypticle-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capitalisk/crypticle/13040e809bbe0ff76fa40fc8135d33612d0ca911/public/img/crypticle-logo-small.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Crypticle 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 27 | 28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /public/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "public", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ag-channel": { 8 | "version": "4.0.2", 9 | "resolved": "https://registry.npmjs.org/ag-channel/-/ag-channel-4.0.2.tgz", 10 | "integrity": "sha512-GU20s/ZGjt0L1H+OJ+bISlXnReWAh5t+HRHAMrBLWaAlKOnvkRr7hQ7Bv+1O/TPGmGA7hJGRiqNtoZkAnE9/MQ==", 11 | "requires": { 12 | "consumable-stream": "^1.0.0" 13 | } 14 | }, 15 | "ag-collection": { 16 | "version": "1.4.5", 17 | "resolved": "https://registry.npmjs.org/ag-collection/-/ag-collection-1.4.5.tgz", 18 | "integrity": "sha512-EsHcWquEBKVmEk+5PT0Mc6tQoz7Xa/zCAQu1PTplS7+8Bi5ZpFKY/m39PqOqLaM0cpPFkRn5kHdnIsIa1HtHYw==", 19 | "requires": { 20 | "ag-model": "^1.1.2", 21 | "async-stream-emitter": "^3.0.3", 22 | "sc-json-stable-stringify": "^2.0.0" 23 | } 24 | }, 25 | "ag-model": { 26 | "version": "1.1.2", 27 | "resolved": "https://registry.npmjs.org/ag-model/-/ag-model-1.1.2.tgz", 28 | "integrity": "sha512-Y3Rxi2+tg/JLeWkkvytbmH61w8k9uQMFUZcOd/w1y1B/+TlzQtMfFepD/hyWw30CqPt4Zb3F7zdEjXCM1MJobA==", 29 | "requires": { 30 | "async-stream-emitter": "^3.0.3", 31 | "sc-json-stable-stringify": "^2.0.0" 32 | } 33 | }, 34 | "ag-request": { 35 | "version": "1.0.0", 36 | "resolved": "https://registry.npmjs.org/ag-request/-/ag-request-1.0.0.tgz", 37 | "integrity": "sha512-2f7I0cQLMVyGAqjSewVMEFuAsJsIY6egdE16UHS636r+8c6Oevrv0j6SrOIXyRN6yuNT4PBuhiKmrhHbh9OvEg==", 38 | "requires": { 39 | "sc-errors": "^2.0.0" 40 | } 41 | }, 42 | "async-limiter": { 43 | "version": "1.0.0", 44 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 45 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 46 | }, 47 | "async-stream-emitter": { 48 | "version": "3.0.3", 49 | "resolved": "https://registry.npmjs.org/async-stream-emitter/-/async-stream-emitter-3.0.3.tgz", 50 | "integrity": "sha512-zXfmhWvPFWwzp5KEpdGrH3GDs1t0WZM6/Q2QoMerYFpc2z0H2KnXl4yNsN5PYNfcEym8leBWhoNxnijv646U0g==", 51 | "requires": { 52 | "stream-demux": "^7.0.1" 53 | } 54 | }, 55 | "asyngular-client": { 56 | "version": "6.0.0", 57 | "resolved": "https://registry.npmjs.org/asyngular-client/-/asyngular-client-6.0.0.tgz", 58 | "integrity": "sha512-FecOq6f3z+UV+1/nZlXzdB0HBVDW07GH1vcWNdxcN42+ZqhkEJ2MQLB/kn9O1xMFafAl+94g134tbaKYS+vC3Q==", 59 | "requires": { 60 | "ag-channel": "^4.0.2", 61 | "ag-request": "^1.0.0", 62 | "async-stream-emitter": "^3.0.2", 63 | "buffer": "^5.2.1", 64 | "linked-list": "^0.1.0", 65 | "lodash.clonedeep": "^4.5.0", 66 | "querystring": "^0.2.0", 67 | "sc-errors": "^2.0.0", 68 | "sc-formatter": "^3.0.2", 69 | "stream-demux": "^7.0.1", 70 | "uuid": "^3.2.1", 71 | "ws": "^6.1.2" 72 | } 73 | }, 74 | "base64-js": { 75 | "version": "1.3.0", 76 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", 77 | "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" 78 | }, 79 | "buffer": { 80 | "version": "5.2.1", 81 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", 82 | "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", 83 | "requires": { 84 | "base64-js": "^1.0.2", 85 | "ieee754": "^1.1.4" 86 | } 87 | }, 88 | "bulma": { 89 | "version": "0.7.5", 90 | "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.7.5.tgz", 91 | "integrity": "sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw==" 92 | }, 93 | "consumable-stream": { 94 | "version": "1.0.0", 95 | "resolved": "https://registry.npmjs.org/consumable-stream/-/consumable-stream-1.0.0.tgz", 96 | "integrity": "sha512-CwX4ZzpSMWxTG9Q3tNM+UNlevMfWX1hjR033MlKkT8ChfH9R+Rl7YQu7FCeMMv4bRv9yO33p8aYoX0EwdijaUA==" 97 | }, 98 | "ieee754": { 99 | "version": "1.1.13", 100 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 101 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 102 | }, 103 | "linked-list": { 104 | "version": "0.1.0", 105 | "resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz", 106 | "integrity": "sha1-eYsP+X0bkqT9CEgPVa6k6dSdN78=" 107 | }, 108 | "lodash.clonedeep": { 109 | "version": "4.5.0", 110 | "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", 111 | "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" 112 | }, 113 | "querystring": { 114 | "version": "0.2.0", 115 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 116 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 117 | }, 118 | "sc-errors": { 119 | "version": "2.0.0", 120 | "resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-2.0.0.tgz", 121 | "integrity": "sha512-zLIg4GskHvkBM7gpKl7JrdU1FXVYsYCavsUeTILFIi/YsuOHLN9OTlFcMp6otb+ebpNEnpcDJI395YXZPif+fw==" 122 | }, 123 | "sc-formatter": { 124 | "version": "3.0.2", 125 | "resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.2.tgz", 126 | "integrity": "sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==" 127 | }, 128 | "sc-json-stable-stringify": { 129 | "version": "2.0.0", 130 | "resolved": "https://registry.npmjs.org/sc-json-stable-stringify/-/sc-json-stable-stringify-2.0.0.tgz", 131 | "integrity": "sha512-24p2gE5mZr9Lhc34HyHki5+hQXF5so0C9xM4+TVT6gohasKzfQ/fNKngB21e4gJ+mzeam/hE9f055VR/U84iGA==" 132 | }, 133 | "stream-demux": { 134 | "version": "7.0.1", 135 | "resolved": "https://registry.npmjs.org/stream-demux/-/stream-demux-7.0.1.tgz", 136 | "integrity": "sha512-IEWUmiXyMPgAlCG+YhpZpG4S6PMhzYXZITWz0OrqXdzBGpV5vWYiLRc31CJhE9E95dIh4Slwuo0aG/RT/1OvJA==", 137 | "requires": { 138 | "consumable-stream": "^1.0.0", 139 | "writable-consumable-stream": "^1.1.1" 140 | } 141 | }, 142 | "uuid": { 143 | "version": "3.3.2", 144 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 145 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 146 | }, 147 | "vue": { 148 | "version": "2.6.10", 149 | "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", 150 | "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" 151 | }, 152 | "vue-router": { 153 | "version": "3.0.2", 154 | "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.0.2.tgz", 155 | "integrity": "sha512-opKtsxjp9eOcFWdp6xLQPLmRGgfM932Tl56U9chYTnoWqKxQ8M20N7AkdEbM5beUh6wICoFGYugAX9vQjyJLFg==" 156 | }, 157 | "writable-consumable-stream": { 158 | "version": "1.1.1", 159 | "resolved": "https://registry.npmjs.org/writable-consumable-stream/-/writable-consumable-stream-1.1.1.tgz", 160 | "integrity": "sha512-qARFG8dkQo2kX72UB2gnNwyCBS4x64lFjaJgRQnfbmrJVnfMpmYCUlqLD4kwmsFw6LcWo0Qv5MzkHET2L99ezw==", 161 | "requires": { 162 | "consumable-stream": "^1.0.0" 163 | } 164 | }, 165 | "ws": { 166 | "version": "6.2.1", 167 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", 168 | "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", 169 | "requires": { 170 | "async-limiter": "~1.0.0" 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /public/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "public", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "dependencies": { 7 | "ag-collection": "^1.4.5", 8 | "ag-model": "^1.1.2", 9 | "async-stream-emitter": "^3.0.3", 10 | "asyngular-client": "^6.0.0", 11 | "bulma": "^0.7.5", 12 | "vue": "^2.5.17", 13 | "vue-router": "^3.0.1" 14 | }, 15 | "devDependencies": {}, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "Jonathan Gros-Dubois", 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /public/pages/page-dashboard.js: -------------------------------------------------------------------------------- 1 | import getAccountComponent from '/components/component-account.js'; 2 | import getTransfersComponent from '/components/component-transfers.js'; 3 | import getWithdrawalsComponent from '/components/component-withdrawals.js'; 4 | import getDepositsComponent from '/components/component-deposits.js'; 5 | import getMakeTransferComponent from '/components/component-make-transfer.js'; 6 | import getMakeWithdrawalComponent from '/components/component-make-withdrawal.js'; 7 | 8 | function getPageComponent(pageOptions) { 9 | let {socket, publicInfo} = pageOptions; 10 | 11 | return { 12 | components: { 13 | 'component-account': getAccountComponent({ 14 | socket, 15 | publicInfo 16 | }), 17 | 'component-settled-deposits': getDepositsComponent({ 18 | socket, 19 | publicInfo, 20 | type: 'settled' 21 | }), 22 | 'component-pending-deposits': getDepositsComponent({ 23 | socket, 24 | publicInfo, 25 | type: 'pending' 26 | }), 27 | 'component-settled-withdrawals': getWithdrawalsComponent({ 28 | socket, 29 | publicInfo, 30 | type: 'settled' 31 | }), 32 | 'component-pending-withdrawals': getWithdrawalsComponent({ 33 | socket, 34 | publicInfo, 35 | type: 'pending' 36 | }), 37 | 'component-settled-transfers': getTransfersComponent({ 38 | socket, 39 | publicInfo, 40 | type: 'settled' 41 | }), 42 | 'component-pending-transfers': getTransfersComponent({ 43 | socket, 44 | publicInfo, 45 | type: 'pending' 46 | }), 47 | 'component-make-transfer': getMakeTransferComponent({ 48 | socket, 49 | publicInfo 50 | }), 51 | 'component-make-withdrawal': getMakeWithdrawalComponent({ 52 | socket, 53 | publicInfo 54 | }) 55 | }, 56 | data: function () { 57 | return { 58 | accountId: socket.authToken && socket.authToken.accountId 59 | }; 60 | }, 61 | template: ` 62 |
63 |

Dashboard

64 | 65 |
66 | 67 | 68 | 69 |
70 | 71 | 72 |
73 | 74 | 75 |
76 | 77 | 78 | 79 |
80 | 81 | 82 |
83 | 84 | 85 |
86 | 87 | 88 |
89 | 90 | 91 |
92 | 93 | 94 | 95 |
96 |
97 | ` 98 | }; 99 | } 100 | 101 | export default getPageComponent; 102 | -------------------------------------------------------------------------------- /public/pages/page-home.js: -------------------------------------------------------------------------------- 1 | 2 | function getPageComponent(pageOptions) { 3 | let {socket} = pageOptions; 4 | 5 | return { 6 | data: function () { 7 | return {}; 8 | }, 9 | methods: {}, 10 | template: ` 11 |
12 |
13 |

API overview

14 | 15 |

16 | Crypticle exposes a WebSocket API for reading and manipulating resources within the service and also for listening for realtime changes. 17 | The API adheres to the SocketCluster protocol. 18 | The following examples make use of the Asyngular JavaScript client. 19 |

20 |

21 | For more details about the API including the realtime CRUD API, visit crypticle.io. 22 |

23 | 24 |
25 | 26 |

Account RPCs

27 |
Signup
28 |

 29 |     try {
 30 |       let {accountId} = await socket.invoke('signup', {
 31 |         accountId: 'alice123',
 32 |         password: 'password123',
 33 |         admin: false,
 34 |         secretSignupKey: 'f502b122-5d7a-48cc-a0df-82d2a82465bd'
 35 |       });
 36 |     } catch (error) {
 37 |       // Handle failure
 38 |     }
 39 |           
40 |
    41 |
  • accountId is the account id.
  • 42 |
  • password is the account password.
  • 43 |
  • admin is a boolean which indicates whether or not the account should have admin privileges.
  • 44 |
  • 45 |

    46 | secretSignupKey is a secret key which needs to be provided if either: 47 |

    48 |
      49 |
    • 50 | The account being created is an admin account. 51 |
    • 52 |
    • 53 | The server side alwaysRequireSecretSignupKey config is true. 54 |
    • 55 |
    56 |

    57 | If required, this key needs to match secretSignupKey from the service config file. 58 |

    59 |
  • 60 |
61 |

62 | Returns a Promise which will resolve with an object containing the accountId or which will be rejected if the signup operation fails on the server. 63 |

64 | 65 |
66 | 67 |
Login
68 |

 69 |     try {
 70 |       let {accountId} = await socket.invoke('login', {
 71 |         accountId: 'alice123',
 72 |         password: 'password123'
 73 |       });
 74 |     } catch (error) {
 75 |       // Handle login failure.
 76 |     }
 77 |           
78 |
    79 |
  • accountId is the account id.
  • 80 |
  • password is the account password.
  • 81 |
82 |

83 | Returns a Promise which will resolve with an object containing the accountId or which will be rejected if the login operation fails on the server. 84 |

85 | 86 |
87 | 88 |
Transfer
89 |

 90 |     let {creditId, debitId} = await socket.invoke('transfer', {
 91 |       amount: '1000000000',
 92 |       toAccountId: 'alice123',
 93 |       data: 'Notes...'
 94 |     });
 95 |           
96 |
    97 |
  • amount is the amount of funds to send to the specified Crypticle account - It is expressed in the smallest possible cryptocurrency unit.
  • 98 |
  • toAccountId is the ID of the Crypticle account to send the funds to.
  • 99 |
  • data is an optional custom string to add to both the debit and credit transactions which will be created as a result of the transfer.
  • 100 |
  • debitId is an optional ID (string in UUID format) to use for the underlying debit transaction. If not provided, it will be automatically generated on the backend.
  • 101 |
  • creditId is an optional ID (string in UUID format) to use for the underlying credit transaction. If not provided, it will be automatically generated on the backend.
  • 102 |
103 |

Returns a Promise which will resolve with an object containing the creditId and debitId (transaction IDs) of the underlying transactions.

104 | 105 |
106 | 107 |
Debit
108 |

109 |     let {debitId} = await socket.invoke('debit', {
110 |       amount: '1000000000',
111 |       data: 'Notes...'
112 |     });
113 |           
114 |
    115 |
  • amount is the amount of funds to debit from the current authenticated account - It is expressed in the smallest possible cryptocurrency unit.
  • 116 |
  • data is an optional custom string to add to the debit transaction.
  • 117 |
  • debitId is an optional ID (string in UUID format) to use for the underlying debit transaction. If not provided, it will be automatically generated on the backend.
  • 118 |
119 |

Returns a Promise which will resolve with an object containing the debitId (transaction ID) of the underlying transaction.

120 | 121 |
122 | 123 |
Get balance
124 |

125 |     let balance = await socket.invoke('getBalance');
126 |           
127 |

128 | Returns a Promise which will resolve with the current logged in user's account balance as a string. 129 |

130 | 131 |
132 | 133 |
Withdraw
134 |

135 |     let {withdrawalId} = await socket.invoke('withdraw', {
136 |       amount: '1100000000',
137 |       toWalletAddress: '6942317426094516776R'
138 |     });
139 |           
140 |
    141 |
  • amount is the amount of funds to withdraw from your Crypticle account - It is expressed in the smallest possible cryptocurrency unit.
  • 142 |
  • toWalletAddress is the blockchain wallet address to send the funds to.
  • 143 |
144 |

Returns a Promise which will resolve with an object containing the withdrawalId as a string.

145 | 146 |
147 | 148 |
Deposit
149 |

150 | To make a deposit, send a blockchain transaction to the deposit address of your Crypticle account (as shown on your console dashboard). The deposit wallet address for your account is shown on your console dashboard. 151 |

152 |
153 | 154 |
155 | 156 |
157 |

Admin RPCs

158 | 159 |
Impersonate
160 |

161 |     try {
162 |       let {accountId} = await socket.invoke('adminImpersonate', {
163 |         accountId: 'alice123'
164 |       });
165 |     } catch (error) {
166 |       // Handle impersonation failure.
167 |     }
168 |           
169 |
    170 |
  • accountId is the id of the account to impersonate.
  • 171 |
172 |

173 | The Promise will resolve with an object containing the accountId or which will be rejected if the impersonate operation fails on the server. 174 |

175 | 176 |
177 | 178 |
Transfer
179 |

180 |     let {creditId, debitId} = await socket.invoke('adminTransfer', {
181 |       amount: '20000000',
182 |       fromAccountId: 'bob456',
183 |       toAccountId: 'alice123',
184 |       data: 'Notes...'
185 |     });
186 |           
187 |
    188 |
  • amount is the amount of funds to send to the specified Crypticle account - It is expressed in the smallest possible cryptocurrency unit.
  • 189 |
  • fromAccountId is the ID of the Crypticle account from which to take the funds.
  • 190 |
  • toAccountId is the ID of the Crypticle account to send the funds to.
  • 191 |
  • data is an optional custom string to add to both the debit and credit transactions which will be created as a result of the transfer.
  • 192 |
  • debitId is an optional ID (string in UUID format) to use for the underlying debit transaction. If not provided, it will be automatically generated on the backend.
  • 193 |
  • creditId is an optional ID (string in UUID format) to use for the underlying credit transaction. If not provided, it will be automatically generated on the backend.
  • 194 |
195 |

Returns a Promise which will resolve with an object containing the creditId and debitId (transaction IDs) of the underlying transactions.

196 | 197 |
198 | 199 |
Debit
200 |

201 |     let {debitId} = await socket.invoke('adminDebit', {
202 |       amount: '1000000000',
203 |       fromAccountId: 'bob456',
204 |       data: 'Notes...'
205 |     });
206 |           
207 |
    208 |
  • amount is the amount of funds to debit from the specified account - It is expressed in the smallest possible cryptocurrency unit.
  • 209 |
  • fromAccountId is the ID of the Crypticle account from which to debit the funds.
  • 210 |
  • data is an optional custom string to add to the debit transaction.
  • 211 |
  • debitId is an optional ID (string in UUID format) to use for the underlying debit transaction. If not provided, it will be automatically generated on the backend.
  • 212 |
213 |

Returns a Promise which will resolve with an object containing the debitId (transaction ID) of the underlying transaction.

214 | 215 |
216 | 217 |
Credit
218 |

219 |     let {creditId} = await socket.invoke('adminCredit', {
220 |       amount: '1000000000',
221 |       toAccountId: 'alice123',
222 |       data: 'Notes...'
223 |     });
224 |           
225 |
    226 |
  • amount is the amount of funds to credit to the specified account - It is expressed in the smallest possible cryptocurrency unit.
  • 227 |
  • toAccountId is the ID of the Crypticle account to credit the funds to.
  • 228 |
  • data is an optional custom string to add to the credit transaction.
  • 229 |
  • creditId is an optional ID (string in UUID format) to use for the underlying credit transaction. If not provided, it will be automatically generated on the backend.
  • 230 |
231 |

Returns a Promise which will resolve with an object containing the creditId (transaction ID) of the underlying transaction.

232 | 233 |
234 | 235 |
Get balance
236 |

237 |     let balance = await socket.invoke('adminGetBalance', {
238 |       accountId: 'bob456'
239 |     });
240 |           
241 |
    242 |
  • accountId is the ID of the Crypticle account to get the balance from.
  • 243 |
244 |

245 | Returns a Promise which will resolve with the current balance of the specified account as a string. 246 |

247 | 248 |
249 | 250 |
Withdraw
251 |

252 |     let {withdrawalId} = await socket.invoke('adminWithdraw', {
253 |       amount: '234000000',
254 |       fromAccountId: 'bob456',
255 |       toWalletAddress: '6942317426094516776R'
256 |     });
257 |           
258 |
    259 |
  • amount is the amount of funds to withdraw from the specified Crypticle account - It is expressed in the smallest possible cryptocurrency unit.
  • 260 |
  • fromAccountId is the ID of the Crypticle account from which to withdraw the funds.
  • 261 |
  • toWalletAddress is the blockchain wallet address to send the funds to.
  • 262 |
263 |

Returns a Promise which will resolve with an object containing the withdrawalId as a string.

264 |
265 | 266 |
267 | 268 |
269 |

Realtime CRUD channels

270 | 271 |

272 | Modifying data within Crypticle will cause change notifications to be published to specific channels. 273 | A channel can either represent a field of a specific resource or an entire view (collection). 274 |

275 | 276 |
277 | 278 |
Resource field changes
279 |

280 | You can subscribe to and consume realtime changes from a resource field channel like this: 281 |

282 | 283 |

284 |     // This example shows how to detect when the 'settled' property of the
285 |     // transaction with ID 1336b876-fda0-42dc-8834-b407d4d9d5fc has changed.
286 | 
287 |     let transactionSettledChannel = socket.subscribe(
288 |       'crud>Transaction/1336b876-fda0-42dc-8834-b407d4d9d5fc/settled'
289 |     );
290 | 
291 |     (async () => {
292 |       for await (let {value} of transactionSettledChannel) {
293 |         // value will be true when the transaction has settled.
294 |       }
295 |     })();
296 |           
297 |

298 | Note that it's possible to subscribe to a channel for any resource field defined in schema-data.js provided that the current authenticated user has the required access rights. 299 |

300 | 301 |
302 | 303 |
View changes
304 |

305 | You can subscribe to and consume realtime change notifications from a view channel like this: 306 |

307 | 308 |

309 |     // This example shows how to detect when a transaction has been added to the
310 |     // 'lastSettledTransactions' view for our account with ID 'bob456'.
311 | 
312 |     let lastSettledTransactionsChannel = socket.subscribe(
313 |       'crud>lastSettledTransactions({"accountId":"bob456"}):Transaction'
314 |     );
315 | 
316 |     (async () => {
317 |       for await (let data of lastSettledTransactionsChannel) {
318 |         // This loop will iterate once when whenever the view has been
319 |         // modified (e.g. a new transaction was added).
320 |         // It's a good time to re-fetch our latest account balance.
321 |         let balance = await socket.invoke('getBalance');
322 |       }
323 |     })();
324 |           
325 |

326 | Note that it's possible to subscribe to a channel for any view defined in schema-data.js provided that the current authenticated user has the required access rights. 327 |

328 |
329 |
330 | ` 331 | }; 332 | } 333 | 334 | export default getPageComponent; 335 | -------------------------------------------------------------------------------- /public/pages/page-impersonate.js: -------------------------------------------------------------------------------- 1 | 2 | function getPageComponent(pageOptions) { 3 | let {socket} = pageOptions; 4 | 5 | return { 6 | props: { 7 | redirect: { 8 | type: String, 9 | default: null 10 | } 11 | }, 12 | data: function () { 13 | return { 14 | error: null, 15 | accountId: '' 16 | }; 17 | }, 18 | methods: { 19 | login: async function () { 20 | var details = { 21 | accountId: this.accountId 22 | }; 23 | try { 24 | await socket.invoke('adminImpersonate', details); 25 | } catch (error) { 26 | this.error = `Failed to login. ${error.message}`; 27 | return; 28 | } 29 | this.error = ''; 30 | 31 | if (this.redirect != null) { 32 | window.location.href = decodeURIComponent(this.redirect); 33 | } 34 | } 35 | }, 36 | template: ` 37 | 52 | ` 53 | }; 54 | } 55 | 56 | export default getPageComponent; 57 | -------------------------------------------------------------------------------- /public/pages/page-login.js: -------------------------------------------------------------------------------- 1 | 2 | function getPageComponent(pageOptions) { 3 | let {socket} = pageOptions; 4 | 5 | return { 6 | props: { 7 | redirect: { 8 | type: String, 9 | default: null 10 | } 11 | }, 12 | data: function () { 13 | return { 14 | error: null, 15 | accountId: '', 16 | password: '' 17 | }; 18 | }, 19 | methods: { 20 | login: async function () { 21 | var details = { 22 | accountId: this.accountId, 23 | password: this.password 24 | }; 25 | try { 26 | await socket.invoke('login', details); 27 | } catch (error) { 28 | this.error = `Failed to login. ${error.message}`; 29 | return; 30 | } 31 | this.error = ''; 32 | 33 | if (this.redirect != null) { 34 | window.location.href = decodeURIComponent(this.redirect); 35 | } 36 | } 37 | }, 38 | template: ` 39 | 60 | ` 61 | }; 62 | } 63 | 64 | export default getPageComponent; 65 | -------------------------------------------------------------------------------- /public/pages/page-signup-admin.js: -------------------------------------------------------------------------------- 1 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 2 | 3 | function getPageComponent(pageOptions) { 4 | let {socket, publicInfo} = pageOptions; 5 | 6 | return { 7 | data: function () { 8 | this.accountCollection = new AGCollection({ 9 | socket, 10 | type: 'Account', 11 | fields: ['password'], 12 | writeOnly: true 13 | }); 14 | 15 | return { 16 | success: null, 17 | error: null, 18 | accountId: '', 19 | password: '', 20 | secretSignupKey: null, 21 | showConsoleLink: false, 22 | publicInfo 23 | }; 24 | }, 25 | destroyed: function () { 26 | this.accountCollection.destroy(); 27 | }, 28 | methods: { 29 | signup: async function () { 30 | let details = { 31 | accountId: this.accountId, 32 | password: this.password, 33 | admin: true, 34 | secretSignupKey: this.secretSignupKey 35 | }; 36 | try { 37 | await socket.invoke('signup', details); 38 | } catch (error) { 39 | this.error = `Failed to sign up. ${error.message}`; 40 | this.showConsoleLink = false; 41 | this.success = null; 42 | return; 43 | } 44 | 45 | this.error = null; 46 | this.success = 'Account was created successfully.'; 47 | this.showConsoleLink = true; 48 | } 49 | }, 50 | template: ` 51 | 87 | ` 88 | }; 89 | } 90 | 91 | export default getPageComponent; 92 | -------------------------------------------------------------------------------- /public/pages/page-signup.js: -------------------------------------------------------------------------------- 1 | import AGCollection from '/node_modules/ag-collection/ag-collection.js'; 2 | 3 | function getPageComponent(pageOptions) { 4 | let {socket, publicInfo} = pageOptions; 5 | 6 | return { 7 | data: function () { 8 | this.accountCollection = new AGCollection({ 9 | socket, 10 | type: 'Account', 11 | fields: ['password'], 12 | writeOnly: true 13 | }); 14 | 15 | return { 16 | success: null, 17 | error: null, 18 | accountId: '', 19 | password: '', 20 | secretSignupKey: null, 21 | showConsoleLink: false, 22 | publicInfo 23 | }; 24 | }, 25 | destroyed: function () { 26 | this.accountCollection.destroy(); 27 | }, 28 | methods: { 29 | signup: async function () { 30 | let details = { 31 | accountId: this.accountId, 32 | password: this.password, 33 | admin: false, 34 | secretSignupKey: this.secretSignupKey 35 | }; 36 | try { 37 | await socket.invoke('signup', details); 38 | } catch (error) { 39 | this.error = `Failed to sign up. ${error.message}`; 40 | this.showConsoleLink = false; 41 | this.success = null; 42 | return; 43 | } 44 | 45 | this.error = null; 46 | this.success = 'Account was created successfully.'; 47 | this.showConsoleLink = true; 48 | } 49 | }, 50 | template: ` 51 | 87 | ` 88 | }; 89 | } 90 | 91 | export default getPageComponent; 92 | -------------------------------------------------------------------------------- /public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | .app-wrapper { 2 | padding: 20px; 3 | } 4 | 5 | .modal-content { 6 | width: 500px; 7 | height: 400px; 8 | } 9 | 10 | .navbar-crypticle-logo { 11 | width: 30px; 12 | height: 30px; 13 | } 14 | 15 | .app-wrapper .navbar .navbar-brand { 16 | margin-left: 0px; 17 | } 18 | 19 | .navbar-logo-area { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | } 24 | 25 | .navbar-crypticle-title { 26 | color: #3EBB90; 27 | } 28 | 29 | .table .table-cell-amount { 30 | text-align: right; 31 | } 32 | 33 | .table .table-cell-date { 34 | text-align: right; 35 | } 36 | 37 | .table .table-cell-data { 38 | white-space: nowrap; 39 | text-overflow: ellipsis; 40 | max-width: 300px; 41 | overflow: hidden; 42 | } 43 | 44 | .spacer { 45 | height: 24px; 46 | width: 100%; 47 | } 48 | 49 | .table .table-row-failure { 50 | background: #FFECEC !important; 51 | } 52 | 53 | .table .table-row-success { 54 | background: #ECFFEC !important; 55 | } 56 | 57 | .transfers-paginated-table-container { 58 | min-height: 452px; 59 | } 60 | 61 | .deposits-paginated-table-container { 62 | min-height: 452px; 63 | } 64 | 65 | .withdrawals-paginated-table-container { 66 | min-height: 452px; 67 | } 68 | 69 | .table.transfers-table { 70 | margin-bottom: 15px; 71 | } 72 | 73 | .table.deposits-table { 74 | margin-bottom: 15px; 75 | } 76 | 77 | .table.withdrawals-table { 78 | margin-bottom: 15px; 79 | } 80 | 81 | .paginator-container { 82 | margin-bottom: 24px; 83 | } 84 | 85 | .paginator-text { 86 | display: inline-block; 87 | line-height: 36px; 88 | margin-left: 10px; 89 | margin-right: 10px; 90 | } 91 | 92 | .login-container { 93 | max-width: 500px; 94 | } 95 | 96 | .signup-container { 97 | max-width: 500px; 98 | } 99 | 100 | .hr-big-spacing { 101 | margin-top: 70px; 102 | margin-bottom: 70px; 103 | } 104 | 105 | .hr-medium-spacing { 106 | margin-top: 50px; 107 | margin-bottom: 50px; 108 | } 109 | 110 | .code-snippet { 111 | padding: 0; 112 | } 113 | 114 | a[disabled="disabled"] { 115 | pointer-events: none; 116 | } 117 | 118 | .make-transfer-dropdown { 119 | width: 100%; 120 | } 121 | 122 | .make-transfer-dropdown-trigger { 123 | width: 100%; 124 | } 125 | 126 | .make-transfer-account-id-display { 127 | color: #999999; 128 | } 129 | 130 | .make-transfer-dropdown-menu { 131 | width: 100%; 132 | } 133 | 134 | .terms-footer { 135 | margin-top: 10px; 136 | float: right; 137 | } 138 | 139 | .terms-footer-separator { 140 | margin-left: 5px; 141 | margin-right: 5px; 142 | } 143 | -------------------------------------------------------------------------------- /schemas/data-schema.js: -------------------------------------------------------------------------------- 1 | const agCrudRethink = require('ag-crud-rethink'); 2 | const thinky = agCrudRethink.thinky; 3 | const type = thinky.type; 4 | 5 | let allowedAccountReadFields = { 6 | depositWalletAddress: true, 7 | active: true, 8 | withdrawalsDisabled: true, 9 | admin: true, 10 | balance: true, 11 | createdDate: true 12 | }; 13 | 14 | let allowedAccountUpdateFields = {}; 15 | 16 | let allowedAdminAccountUpdateFields = { 17 | depositWalletAddress: true, 18 | depositWalletEncryptedPassphrase: true, 19 | depositWalletPublicKey: true, 20 | password: true, 21 | passwordResetKey: true, 22 | passwordResetExpiry: true, 23 | active: true, 24 | withdrawalsDisabled: true 25 | }; 26 | 27 | let allowedAdminAccountReadFields = { 28 | depositWalletAddress: true, 29 | depositWalletEncryptedPassphrase: true, 30 | depositWalletPublicKey: true, 31 | passwordResetKey: true, 32 | passwordResetExpiry: true, 33 | active: true, 34 | withdrawalsDisabled: true, 35 | admin: true, 36 | balance: true, 37 | createdDate: true 38 | }; 39 | 40 | function getSchema(options) { 41 | let {maxPageSize} = options; 42 | 43 | function validateQuery(req) { 44 | let query = req.query || {}; 45 | if ( 46 | query.view && 47 | typeof query.pageSize === 'number' && 48 | query.pageSize > maxPageSize 49 | ) { 50 | let error = new Error( 51 | `The specified page size of ${ 52 | query.pageSize 53 | } for the ${ 54 | query.view 55 | } view exceeded the maximum page size of ${ 56 | maxPageSize 57 | }` 58 | ); 59 | error.name = 'ForbiddenCRUDError'; 60 | error.isClientError = true; 61 | throw error; 62 | } 63 | } 64 | 65 | function accountAccessController(req) { 66 | validateQuery(req); 67 | 68 | let query = req.query || {}; 69 | let isOwnAccount = req.authToken && req.authToken.accountId === query.id; 70 | let isAdmin = req.authToken && req.authToken.admin; 71 | let isView = !!query.view; 72 | let isIdRead = req.action === 'read' && query.field === 'id'; 73 | 74 | if (isIdRead) { 75 | return; 76 | } 77 | 78 | if (isOwnAccount) { 79 | if (req.action === 'read' || req.action === 'subscribe') { 80 | if (allowedAccountReadFields[query.field]) { 81 | return; 82 | } 83 | } 84 | if (req.action === 'update') { 85 | if (allowedAccountUpdateFields[query.field]) { 86 | return; 87 | } 88 | } 89 | } 90 | if (isAdmin) { 91 | if (req.action === 'read' || req.action === 'subscribe') { 92 | if (isView || allowedAdminAccountReadFields[query.field]) { 93 | return; 94 | } 95 | } 96 | if (req.action === 'update') { 97 | if (allowedAdminAccountUpdateFields[query.field]) { 98 | return; 99 | } 100 | } 101 | } 102 | 103 | if ( 104 | query.view === 'accountIdSearchView' && 105 | (req.action === 'read' || req.action === 'subscribe') 106 | ) { 107 | return; 108 | } 109 | 110 | let error = new Error('Not allowed to perform CRUD operation'); 111 | error.name = 'ForbiddenCRUDError'; 112 | error.isClientError = true; 113 | throw error; 114 | } 115 | 116 | function privateResourceAccessController(req) { 117 | let query = req.query || {}; 118 | let isLoggedIn = req.authToken; 119 | let isAdmin = req.authToken && req.authToken.admin; 120 | let viewParams = query.viewParams || {}; 121 | let isOwnResource = req.authToken && ( 122 | (req.resource && req.authToken.accountId === req.resource.accountId) || 123 | (req.authToken.accountId === viewParams.accountId) 124 | ); 125 | 126 | if (isOwnResource) { 127 | if (req.action === 'read' || req.action === 'subscribe') { 128 | return; 129 | } 130 | } 131 | if (isAdmin) { 132 | if (req.action === 'read' || req.action === 'subscribe') { 133 | return; 134 | } 135 | if (req.action === 'update') { 136 | if (query.type === 'Withdrawal' && query.field === 'canceled') { 137 | return; 138 | } 139 | } 140 | } 141 | 142 | let error = new Error('Not allowed to perform CRUD operation'); 143 | error.name = 'ForbiddenCRUDError'; 144 | error.isClientError = true; 145 | throw error; 146 | } 147 | 148 | function computeDateFromParams(params, r) { 149 | if (typeof params.fromAge === 'number') { 150 | return r.now().sub(params.fromAge); 151 | } 152 | return typeof params.fromCreatedDate === 'string' ? new Date(params.fromCreatedDate) : r.minval; 153 | } 154 | 155 | return { 156 | Account: { 157 | fields: { 158 | depositWalletAddress: type.string(), 159 | depositWalletEncryptedPassphrase: type.string(), 160 | depositWalletPublicKey: type.string(), 161 | password: type.string(), 162 | passwordResetKey: type.string().optional(), 163 | passwordResetExpiry: type.date().optional(), 164 | maxConcurrentWithdrawals: type.number().optional(), 165 | maxConcurrentDebits: type.number().optional(), 166 | maxSocketBackpressure: type.number().optional(), 167 | withdrawalsDisabled: type.boolean().default(false), 168 | active: type.boolean().default(true), 169 | admin: type.boolean().default(false), 170 | balance: type.string().default('0'), 171 | createdDate: type.date() 172 | }, 173 | indexes: ['depositWalletAddress'], 174 | access: { 175 | pre: accountAccessController 176 | }, 177 | views: { 178 | accountIdSearchView: { 179 | paramFields: ['searchString'], 180 | primaryKeys: [], 181 | transform: function (fullTableQuery, r, params) { 182 | if (params.searchString === '') { 183 | return fullTableQuery.limit(0); 184 | } 185 | return fullTableQuery 186 | .between( 187 | params.searchString, 188 | r.maxval, 189 | {index: 'id', rightBound: 'closed'} 190 | ) 191 | .orderBy({index: 'id'}) 192 | .limit(20) 193 | .filter((doc) => { 194 | return doc('id').match(`^${params.searchString}`); 195 | }); 196 | } 197 | } 198 | } 199 | }, 200 | Transaction: { 201 | fields: { 202 | accountId: type.string(), 203 | type: type.string(), // Can be 'deposit', 'withdrawal' or 'transfer' 204 | recordType: type.string(), // Can be 'credit' or 'debit' 205 | amount: type.string(), 206 | counterpartyAccountId: type.string().optional(), 207 | counterpartyTransactionId: type.string().optional(), 208 | data: type.string().optional(), 209 | balance: type.string().optional(), 210 | settled: type.boolean().default(false), 211 | settledDate: type.date().optional(), 212 | settlementShardKey: type.number().optional(), 213 | canceled: type.boolean().default(false), 214 | createdDate: type.date() 215 | }, 216 | indexes: [ 217 | 'accountId', 218 | 'settlementShardKey', 219 | 'createdDate', 220 | { 221 | name: 'accountIdTypeSettledCreatedDate', 222 | type: 'compound', // Compound indexes are ordered lexicographically 223 | fn: function (r) { 224 | return [r.row('accountId'), r.row('type'), r.row('settled'), r.row('createdDate')]; 225 | } 226 | }, 227 | { 228 | name: 'accountIdSettledDate', 229 | type: 'compound', // Compound indexes are ordered lexicographically 230 | fn: function (r) { 231 | return [r.row('accountId'), r.row('settledDate')]; 232 | } 233 | } 234 | ], 235 | access: { 236 | pre: validateQuery, 237 | post: privateResourceAccessController 238 | }, 239 | views: { 240 | lastSettledTransactions: { 241 | paramFields: ['accountId'], 242 | affectingFields: ['settledDate'], 243 | transform: function (fullTableQuery, r, params) { 244 | return fullTableQuery 245 | .between( 246 | [params.accountId, r.minval], 247 | [params.accountId, r.maxval], 248 | {index: 'accountIdSettledDate', rightBound: 'closed'} 249 | ) 250 | .orderBy({index: r.desc('accountIdSettledDate')}) 251 | .limit(10); // This limit is necessary for performance reasons. 252 | } 253 | }, 254 | accountTransfersPendingView: { 255 | paramFields: ['accountId', 'fromCreatedDate', 'fromAge'], 256 | primaryKeys: ['accountId'], 257 | affectingFields: ['settled'], 258 | transform: function (fullTableQuery, r, params) { 259 | let startTime = computeDateFromParams(params, r); 260 | return fullTableQuery 261 | .between( 262 | [params.accountId, 'transfer', false, startTime], 263 | [params.accountId, 'transfer', false, r.maxval], 264 | {index: 'accountIdTypeSettledCreatedDate', rightBound: 'closed'} 265 | ) 266 | .orderBy({index: r.desc('accountIdTypeSettledCreatedDate')}); 267 | } 268 | }, 269 | accountTransfersSettledView: { 270 | paramFields: ['accountId', 'fromCreatedDate', 'fromAge'], 271 | primaryKeys: ['accountId'], 272 | affectingFields: ['settled'], 273 | transform: function (fullTableQuery, r, params) { 274 | let startTime = computeDateFromParams(params, r); 275 | return fullTableQuery 276 | .between( 277 | [params.accountId, 'transfer', true, startTime], 278 | [params.accountId, 'transfer', true, r.maxval], 279 | {index: 'accountIdTypeSettledCreatedDate', rightBound: 'closed'} 280 | ) 281 | .orderBy({index: r.desc('accountIdTypeSettledCreatedDate')}); 282 | } 283 | } 284 | } 285 | }, 286 | Deposit: { 287 | fields: { 288 | accountId: type.string(), 289 | transactionId: type.string(), 290 | height: type.number(), 291 | amount: type.string(), 292 | settled: type.boolean().default(false), 293 | settledDate: type.date().optional(), 294 | settlementShardKey: type.number().optional(), 295 | canceled: type.boolean().default(false), 296 | createdDate: type.date() 297 | }, 298 | indexes: [ 299 | 'accountId', 300 | 'transactionId', 301 | 'createdDate', 302 | 'settlementShardKey', 303 | { 304 | name: 'accountIdSettledCreatedDate', 305 | type: 'compound', // Compound indexes are ordered lexicographically 306 | fn: function (r) { 307 | return [r.row('accountId'), r.row('settled'), r.row('createdDate')]; 308 | } 309 | } 310 | ], 311 | access: { 312 | pre: validateQuery, 313 | post: privateResourceAccessController 314 | }, 315 | relations: { 316 | Transaction: { 317 | accountId: function (transaction) { 318 | return transaction.accountId; 319 | }, 320 | transactionId: function (transaction) { 321 | return transaction.id; 322 | } 323 | } 324 | }, 325 | views: { 326 | accountDepositsPendingView: { 327 | paramFields: ['accountId', 'fromCreatedDate', 'fromAge'], 328 | primaryKeys: ['accountId'], 329 | affectingFields: ['settled'], 330 | transform: function (fullTableQuery, r, params) { 331 | let startTime = computeDateFromParams(params, r); 332 | return fullTableQuery 333 | .between( 334 | [params.accountId, false, startTime], 335 | [params.accountId, false, r.maxval], 336 | {index: 'accountIdSettledCreatedDate', rightBound: 'closed'} 337 | ) 338 | .orderBy({index: r.desc('accountIdSettledCreatedDate')}); 339 | } 340 | }, 341 | accountDepositsSettledView: { 342 | paramFields: ['accountId', 'fromCreatedDate', 'fromAge'], 343 | primaryKeys: ['accountId'], 344 | affectingFields: ['settled'], 345 | transform: function (fullTableQuery, r, params) { 346 | let startTime = computeDateFromParams(params, r); 347 | return fullTableQuery 348 | .between( 349 | [params.accountId, true, startTime], 350 | [params.accountId, true, r.maxval], 351 | {index: 'accountIdSettledCreatedDate', rightBound: 'closed'} 352 | ) 353 | .orderBy({index: r.desc('accountIdSettledCreatedDate')}); 354 | } 355 | } 356 | } 357 | }, 358 | Withdrawal: { 359 | fields: { 360 | accountId: type.string(), 361 | transactionId: type.string(), 362 | height: type.number().default(null), 363 | attemptCount: type.number().default(0), 364 | signedTransaction: type.string(), 365 | toWalletAddress: type.string(), 366 | fromWalletAddress: type.string(), 367 | amount: type.string(), 368 | fees: type.string(), 369 | canceled: type.boolean().default(false), 370 | data: type.string().optional(), 371 | settled: type.boolean().default(false), 372 | settledDate: type.date().optional(), 373 | settlementShardKey: type.number().optional(), 374 | createdDate: type.date() 375 | }, 376 | indexes: [ 377 | 'accountId', 378 | 'transactionId', 379 | 'createdDate', 380 | 'settlementShardKey', 381 | { 382 | name: 'accountIdSettledCreatedDate', 383 | type: 'compound', // Compound indexes are ordered lexicographically 384 | fn: function (r) { 385 | return [r.row('accountId'), r.row('settled'), r.row('createdDate')]; 386 | } 387 | } 388 | ], 389 | access: { 390 | pre: validateQuery, 391 | post: privateResourceAccessController 392 | }, 393 | views: { 394 | accountWithdrawalsPendingView: { 395 | paramFields: ['accountId', 'fromCreatedDate', 'fromAge'], 396 | primaryKeys: ['accountId'], 397 | affectingFields: ['settled'], 398 | transform: function (fullTableQuery, r, params) { 399 | let startTime = computeDateFromParams(params, r); 400 | return fullTableQuery 401 | .between( 402 | [params.accountId, false, startTime], 403 | [params.accountId, false, r.maxval], 404 | {index: 'accountIdSettledCreatedDate', rightBound: 'closed'} 405 | ) 406 | .orderBy({index: r.desc('accountIdSettledCreatedDate')}); 407 | } 408 | }, 409 | accountWithdrawalsSettledView: { 410 | paramFields: ['accountId', 'fromCreatedDate', 'fromAge'], 411 | primaryKeys: ['accountId'], 412 | affectingFields: ['settled'], 413 | transform: function (fullTableQuery, r, params) { 414 | let startTime = computeDateFromParams(params, r); 415 | return fullTableQuery 416 | .between( 417 | [params.accountId, true, startTime], 418 | [params.accountId, true, r.maxval], 419 | {index: 'accountIdSettledCreatedDate', rightBound: 'closed'} 420 | ) 421 | .orderBy({index: r.desc('accountIdSettledCreatedDate')}); 422 | } 423 | } 424 | } 425 | } 426 | }; 427 | } 428 | 429 | module.exports = getSchema; 430 | -------------------------------------------------------------------------------- /schemas/rpc-schema.js: -------------------------------------------------------------------------------- 1 | let accountIdSchema = { 2 | type: 'string', 3 | minLength: 3, 4 | maxLength: 40 5 | }; 6 | let passwordSchema = { 7 | type: 'string', 8 | minLength: 7, 9 | maxLength: 100 10 | }; 11 | let uuidSchema = { 12 | type: 'string', 13 | pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' 14 | }; 15 | let amountSchema = { 16 | type: 'string', 17 | pattern: '^[0-9]*$', 18 | minLength: 1, 19 | maxLength: 30 20 | }; 21 | let walletAddressSchema = { 22 | type: 'string', 23 | minLength: 1, 24 | maxLength: 100 25 | }; 26 | let customDataSchema = { 27 | type: ['string', 'null'], 28 | maxLength: 200 29 | }; 30 | 31 | let requestSchema = { 32 | getPublicInfo: {}, 33 | signup: { 34 | type: 'object', 35 | properties: { 36 | accountId: accountIdSchema, 37 | password: passwordSchema, 38 | admin: {type: 'boolean'}, 39 | secretSignupKey: customDataSchema 40 | }, 41 | required: ['accountId', 'password'] 42 | }, 43 | login: { 44 | type: 'object', 45 | properties: { 46 | accountId: accountIdSchema, 47 | password: passwordSchema 48 | }, 49 | required: ['accountId', 'password'] 50 | }, 51 | withdraw: { 52 | type: 'object', 53 | properties: { 54 | amount: amountSchema, 55 | toWalletAddress: walletAddressSchema, 56 | }, 57 | required: ['amount', 'toWalletAddress'] 58 | }, 59 | transfer: { 60 | type: 'object', 61 | properties: { 62 | amount: amountSchema, 63 | toAccountId: accountIdSchema, 64 | data: customDataSchema, 65 | debitId: uuidSchema, 66 | creditId: uuidSchema 67 | }, 68 | required: ['amount', 'toAccountId'] 69 | }, 70 | debit: { 71 | type: 'object', 72 | properties: { 73 | amount: amountSchema, 74 | data: customDataSchema, 75 | debitId: uuidSchema 76 | }, 77 | required: ['amount'] 78 | }, 79 | getBalance: {}, 80 | adminImpersonate: { 81 | type: 'object', 82 | properties: { 83 | accountId: accountIdSchema 84 | }, 85 | required: ['accountId'] 86 | }, 87 | adminWithdraw: { 88 | type: 'object', 89 | properties: { 90 | amount: amountSchema, 91 | fromAccountId: accountIdSchema, 92 | toWalletAddress: walletAddressSchema, 93 | }, 94 | required: ['amount', 'fromAccountId', 'toWalletAddress'] 95 | }, 96 | adminTransfer: { 97 | type: 'object', 98 | properties: { 99 | amount: amountSchema, 100 | fromAccountId: accountIdSchema, 101 | toAccountId: accountIdSchema, 102 | data: customDataSchema, 103 | debitId: uuidSchema, 104 | creditId: uuidSchema 105 | }, 106 | required: ['amount', 'fromAccountId', 'toAccountId'] 107 | }, 108 | adminDebit: { 109 | type: 'object', 110 | properties: { 111 | amount: amountSchema, 112 | fromAccountId: accountIdSchema, 113 | data: customDataSchema, 114 | debitId: uuidSchema 115 | }, 116 | required: ['amount', 'fromAccountId'] 117 | }, 118 | adminCredit: { 119 | type: 'object', 120 | properties: { 121 | amount: amountSchema, 122 | toAccountId: accountIdSchema, 123 | data: customDataSchema, 124 | creditId: uuidSchema 125 | }, 126 | required: ['amount', 'toAccountId'] 127 | }, 128 | adminGetBalance: { 129 | type: 'object', 130 | properties: { 131 | accountId: accountIdSchema 132 | }, 133 | required: ['accountId'] 134 | } 135 | }; 136 | 137 | function getSchema() { 138 | return requestSchema; 139 | } 140 | 141 | module.exports = getSchema; 142 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const path = require('path'); 3 | const eetase = require('eetase'); 4 | const asyngularServer = require('asyngular-server'); 5 | const express = require('express'); 6 | const serveStatic = require('serve-static'); 7 | const morgan = require('morgan'); 8 | const uuid = require('uuid'); 9 | const agcBrokerClient = require('agc-broker-client'); 10 | const agCrudRethink = require('ag-crud-rethink'); 11 | const Validator = require('jsonschema').Validator; 12 | const AccountService = require('./services/account-service'); 13 | 14 | const getDataSchema = require('./schemas/data-schema'); 15 | const getRPCSchema = require('./schemas/rpc-schema'); 16 | 17 | const ENVIRONMENT = process.env.ENV || 'dev'; 18 | const BLOCKCHAIN = process.env.BLOCKCHAIN || 'lisk'; 19 | const SYNC_FROM_BLOCK_HEIGHT = parseInt(process.env.SYNC_FROM_BLOCK_HEIGHT) || null; 20 | const { 21 | SECRET_SIGNUP_KEY, 22 | AUTH_KEY, 23 | BLOCKCHAIN_WALLET_PASSPHRASE, 24 | STORAGE_ENCRYPTION_KEY 25 | } = process.env; 26 | 27 | const ASYNGULAR_PORT = process.env.ASYNGULAR_PORT || 8000; 28 | const ASYNGULAR_WS_ENGINE = process.env.ASYNGULAR_WS_ENGINE || 'ws'; 29 | const ASYNGULAR_SOCKET_CHANNEL_LIMIT = Number(process.env.ASYNGULAR_SOCKET_CHANNEL_LIMIT) || 1000; 30 | const ASYNGULAR_LOG_LEVEL = process.env.ASYNGULAR_LOG_LEVEL || 2; 31 | 32 | const AGC_INSTANCE_ID = uuid.v4(); 33 | const AGC_STATE_SERVER_HOST = process.env.AGC_STATE_SERVER_HOST || null; 34 | const AGC_STATE_SERVER_PORT = process.env.AGC_STATE_SERVER_PORT || null; 35 | const AGC_MAPPING_ENGINE = process.env.AGC_MAPPING_ENGINE || null; 36 | const AGC_CLIENT_POOL_SIZE = process.env.AGC_CLIENT_POOL_SIZE || null; 37 | const AGC_AUTH_KEY = process.env.AGC_AUTH_KEY || null; 38 | const AGC_INSTANCE_IP = process.env.AGC_INSTANCE_IP || null; 39 | const AGC_INSTANCE_IP_FAMILY = process.env.AGC_INSTANCE_IP_FAMILY || null; 40 | const AGC_STATE_SERVER_CONNECT_TIMEOUT = Number(process.env.AGC_STATE_SERVER_CONNECT_TIMEOUT) || null; 41 | const AGC_STATE_SERVER_ACK_TIMEOUT = Number(process.env.AGC_STATE_SERVER_ACK_TIMEOUT) || null; 42 | const AGC_STATE_SERVER_RECONNECT_RANDOMNESS = Number(process.env.AGC_STATE_SERVER_RECONNECT_RANDOMNESS) || null; 43 | const AGC_PUB_SUB_BATCH_DURATION = Number(process.env.AGC_PUB_SUB_BATCH_DURATION) || null; 44 | const AGC_BROKER_RETRY_DELAY = Number(process.env.AGC_BROKER_RETRY_DELAY) || null; 45 | 46 | let envConfig; 47 | 48 | try { 49 | // A config file attached via a Docker/K8s volume have priority. 50 | envConfig = require(`./config/config.${ENVIRONMENT}.json`); 51 | } catch (error) { 52 | const configDev = require(`./blockchains/${BLOCKCHAIN}/config.dev.json`); 53 | const configProd = require(`./blockchains/${BLOCKCHAIN}/config.prod.json`); 54 | const config = { 55 | dev: configDev, 56 | prod: configProd 57 | }; 58 | 59 | envConfig = config[ENVIRONMENT]; 60 | } 61 | 62 | const databaseName = envConfig.databaseName || 'crypticle'; 63 | const authTokenExpiry = Math.round(envConfig.authTokenExpiry / 1000); 64 | 65 | if ( 66 | ENVIRONMENT === 'prod' && 67 | ( 68 | envConfig.secretSignupKey || 69 | envConfig.authKey || 70 | envConfig.blockchainWalletPassphrase || 71 | envConfig.storageEncryptionKey 72 | ) 73 | ) { 74 | throw new Error( 75 | 'The secretSignupKey, authKey, blockchainWalletPassphrase and storageEncryptionKey ' + 76 | 'properties should not be present in the config file for a production environment. ' + 77 | 'Use SECRET_SIGNUP_KEY, AUTH_KEY, BLOCKCHAIN_WALLET_PASSPHRASE and ' + 78 | 'STORAGE_ENCRYPTION_KEY environment variables instead.' 79 | ); 80 | } 81 | 82 | let secretSignupKey = envConfig.secretSignupKey || SECRET_SIGNUP_KEY; 83 | let authKey = envConfig.authKey || AUTH_KEY; 84 | let blockchainWalletPassphrase = envConfig.blockchainWalletPassphrase || BLOCKCHAIN_WALLET_PASSPHRASE; 85 | let storageEncryptionKey = envConfig.storageEncryptionKey || STORAGE_ENCRYPTION_KEY; 86 | 87 | if (secretSignupKey == null) { 88 | throw new Error( 89 | 'The secret signup key was not specified. During development, ' + 90 | 'it must be provided either through the secretSignupKey config ' + 91 | 'option or through the SECRET_SIGNUP_KEY environment variable.' + 92 | 'In production, it must be provided through the SECRET_SIGNUP_KEY environment ' + 93 | 'variable for security reasons.' 94 | ); 95 | } 96 | 97 | if (authKey == null) { 98 | throw new Error( 99 | 'The auth key was not specified. During development, ' + 100 | 'it must be provided either through the authKey config ' + 101 | 'option or through the AUTH_KEY environment variable.' + 102 | 'In production, it must be provided through the AUTH_KEY environment ' + 103 | 'variable for security reasons.' 104 | ); 105 | } 106 | 107 | if (blockchainWalletPassphrase == null) { 108 | throw new Error( 109 | 'The blockchain wallet passphrase was not specified. During development, ' + 110 | 'it must be provided either through the blockchainWalletPassphrase config ' + 111 | 'option or through the BLOCKCHAIN_WALLET_PASSPHRASE environment variable.' + 112 | 'In production, it must be provided through the BLOCKCHAIN_WALLET_PASSPHRASE environment ' + 113 | 'variable for security reasons.' 114 | ); 115 | } 116 | 117 | const dataSchema = getDataSchema({ 118 | dbName: databaseName, 119 | maxPageSize: envConfig.publicInfo.maxPageSize 120 | }); 121 | 122 | let rpcValidator = new Validator(); 123 | let rpcSchema = getRPCSchema(); 124 | 125 | let agOptions = { 126 | batchInterval: 50, 127 | authKey 128 | }; 129 | 130 | if (process.env.ASYNGULAR_OPTIONS) { 131 | let envOptions = JSON.parse(process.env.ASYNGULAR_OPTIONS); 132 | Object.assign(agOptions, envOptions); 133 | } 134 | 135 | function monitorBackpressure(socket) { 136 | let authToken = socket.authToken; 137 | let maxBackpressure = ( 138 | authToken && authToken.maxSocketBackpressure 139 | ) || envConfig.maxSocketBackpressure; 140 | let isAdmin = authToken && authToken.admin; 141 | 142 | if (!isAdmin && socket.getBackpressure() > maxBackpressure) { 143 | throw new Error( 144 | `The total backpressure of socket ${ 145 | socket.id 146 | } with account ID ${ 147 | socket.authToken && socket.authToken.accountId 148 | } exceeded the maximum threshold of ${ 149 | maxBackpressure 150 | } operations` 151 | ); 152 | } 153 | } 154 | 155 | let httpServer = eetase(http.createServer()); 156 | let agServer = asyngularServer.attach(httpServer, agOptions); 157 | 158 | agServer.setMiddleware(agServer.MIDDLEWARE_INBOUND_RAW, async (middlewareStream) => { 159 | for await (let action of middlewareStream) { 160 | let {socket} = action; 161 | try { 162 | monitorBackpressure(socket); 163 | } catch (error) { 164 | console.warn('[BackpressureMonitor]', error); 165 | let clientError = new Error('Socket consumed too many resources'); 166 | clientError.name = 'ResourceOveruseError'; 167 | clientError.isClientError = true; 168 | action.block(clientError); 169 | socket.disconnect(4500, 'Socket consumed too many resources'); 170 | continue; 171 | } 172 | action.allow(); 173 | } 174 | }); 175 | 176 | let crudOptions = { 177 | defaultPageSize: 10, 178 | schema: dataSchema, 179 | thinkyOptions: { 180 | host: envConfig.databaseHost || '127.0.0.1', 181 | db: databaseName, 182 | port: envConfig.databasePort || 28015 183 | } 184 | }; 185 | 186 | let crud = agCrudRethink.attach(agServer, crudOptions); 187 | 188 | (async () => { 189 | for await (let {error} of crud.listener('error')) { 190 | console.warn('[CRUD]', error); 191 | } 192 | })(); 193 | 194 | let shardInfo = { 195 | shardIndex: null, 196 | shardCount: null 197 | }; 198 | 199 | let accountService = new AccountService({ 200 | transactionSettlementInterval: envConfig.transactionSettlementInterval, 201 | withdrawalProcessingInterval: envConfig.withdrawalProcessingInterval, 202 | maxTransactionSettlementsPerAccount: envConfig.maxTransactionSettlementsPerAccount, 203 | maxConcurrentWithdrawalsPerAccount: envConfig.maxConcurrentWithdrawalsPerAccount, 204 | maxConcurrentDebitsPerAccount: envConfig.maxConcurrentDebitsPerAccount, 205 | blockchainSync: envConfig.blockchainSync, 206 | blockchainNodeAddress: envConfig.blockchainNodeAddress, 207 | blockPollInterval: envConfig.blockPollInterval, 208 | blockFetchLimit: envConfig.blockFetchLimit, 209 | blockchainWithdrawalMaxAttempts: envConfig.blockchainWithdrawalMaxAttempts, 210 | bcryptPasswordRounds: envConfig.bcryptPasswordRounds, 211 | thinky: crud.thinky, 212 | crud, 213 | publicInfo: envConfig.publicInfo, 214 | shardInfo, 215 | blockchainWalletPassphrase, 216 | secretSignupKey, 217 | storageEncryptionKey, 218 | blockchainAdapterPath: path.resolve(__dirname, 'blockchains', BLOCKCHAIN, `adapter.js`), 219 | syncFromBlockHeight: SYNC_FROM_BLOCK_HEIGHT 220 | }); 221 | 222 | (async () => { 223 | for await (let {error} of accountService.listener('error')) { 224 | console.error('[AccountService]', error); 225 | } 226 | })(); 227 | 228 | (async () => { 229 | for await (let {info} of accountService.listener('info')) { 230 | console.info('[AccountService]', info); 231 | } 232 | })(); 233 | 234 | (async () => { 235 | for await (let {block} of accountService.listener('processBlock')) { 236 | console.log('[AccountService]', `Processed block at height ${block.height}`); 237 | } 238 | })(); 239 | 240 | let expressApp = express(); 241 | if (ENVIRONMENT === 'dev') { 242 | // Log every HTTP request. See https://github.com/expressjs/morgan for other 243 | // available formats. 244 | expressApp.use(morgan('dev')); 245 | } 246 | expressApp.use(serveStatic(path.resolve(__dirname, 'public'))); 247 | 248 | // Add GET /health-check express route 249 | expressApp.get('/health-check', (req, res) => { 250 | res.status(200).send('OK'); 251 | }); 252 | 253 | function generateMessageFromSchemaError(error) { 254 | return `The ${error.property.split('.')[1]} field ${error.message}`; 255 | } 256 | 257 | function validateRPCSchema(request) { 258 | let schema = rpcSchema[request.procedure]; 259 | if (!schema) { 260 | let error = new Error(`Could not find a schema for the ${request.procedure} procedure.`); 261 | error.name = 'NoMatchingRequestSchemaError'; 262 | error.isClientError = true; 263 | throw error; 264 | } 265 | let validationResult = rpcValidator.validate(request.data, schema); 266 | if (!validationResult.valid) { 267 | let errorsMessage = validationResult.errors.map(error => generateMessageFromSchemaError(error)).join('. '); 268 | let error = new Error(`${errorsMessage}.`); 269 | error.name = 'RequestSchemaValidationError'; 270 | error.errors = validationResult.errors; 271 | error.isClientError = true; 272 | throw error; 273 | } 274 | } 275 | 276 | function verifyUserAuth(request, socket) { 277 | if (!socket.authToken) { 278 | let error = new Error( 279 | `Cannot invoke the ${ 280 | request.procedure 281 | } procedure while not authenticated.` 282 | ); 283 | error.name = 'NotAuthenticatedError'; 284 | error.isClientError = true; 285 | throw error; 286 | } 287 | } 288 | 289 | function verifyAdminAuth(request, socket) { 290 | if (!socket.authToken || !socket.authToken.admin) { 291 | let error = new Error( 292 | `Cannot invoke the ${ 293 | request.procedure 294 | } procedure while not authenticated as an admin.` 295 | ); 296 | error.name = 'NotAuthenticatedAsAdminError'; 297 | error.isClientError = true; 298 | throw error; 299 | } 300 | } 301 | 302 | // HTTP request handling loop. 303 | (async () => { 304 | for await (let requestData of httpServer.listener('request')) { 305 | expressApp.apply(null, requestData); 306 | } 307 | })(); 308 | 309 | (async () => { 310 | for await (let {socket} of agServer.listener('disconnection')) { 311 | if (socket.authTokenRenewalIntervalId != null) { 312 | clearInterval(socket.authTokenRenewalIntervalId); 313 | } 314 | } 315 | })(); 316 | 317 | function renewAuthToken(socket) { 318 | if (socket.authToken) { 319 | let {exp, iat, ...tokenData} = socket.authToken; 320 | socket.setAuthToken(tokenData, {expiresIn: authTokenExpiry}); 321 | } 322 | } 323 | 324 | // Asyngular/WebSocket connection handling loop. 325 | (async () => { 326 | for await (let {socket} of agServer.listener('connection')) { 327 | // Handle socket connection. 328 | 329 | // Batch everything to improve performance. 330 | socket.startBatching(); 331 | 332 | renewAuthToken(socket); 333 | 334 | // Refresh the token on an interval so long as the socket is connected. 335 | socket.authTokenRenewalIntervalId = setInterval(() => { 336 | renewAuthToken(socket); 337 | }, envConfig.authTokenRenewalInterval); 338 | 339 | (async () => { 340 | for await (let request of socket.procedure('signup')) { 341 | try { 342 | validateRPCSchema(request); 343 | } catch (error) { 344 | request.error(error); 345 | console.error(error); 346 | continue; 347 | } 348 | 349 | let accountData; 350 | let accountId; 351 | try { 352 | accountData = await accountService.sanitizeSignupCredentials(request.data); 353 | accountId = await crud.create({ 354 | type: 'Account', 355 | value: accountData 356 | }); 357 | } catch (error) { 358 | if (error.name === 'DuplicatePrimaryKeyError') { 359 | error = new Error('The specified account ID was already taken.'); 360 | error.name = 'AccountIdTakenError'; 361 | error.isClientError = true; 362 | } 363 | if (error.isClientError) { 364 | request.error(error); 365 | } else { 366 | let clientError = new Error('Server error.'); 367 | clientError.name = 'SignupError'; 368 | clientError.isClientError = true; 369 | request.error(clientError); 370 | } 371 | console.error(error); 372 | continue; 373 | } 374 | request.end({accountId}); 375 | } 376 | })(); 377 | 378 | (async () => { 379 | for await (let request of socket.procedure('login')) { 380 | try { 381 | validateRPCSchema(request); 382 | } catch (error) { 383 | request.error(error); 384 | console.error(error); 385 | continue; 386 | } 387 | 388 | let accountData; 389 | try { 390 | accountData = await accountService.verifyLoginCredentials(request.data); 391 | } catch (error) { 392 | if (error.isClientError) { 393 | request.error(error); 394 | } else { 395 | let clientError = new Error('Server error.'); 396 | clientError.name = 'LoginError'; 397 | clientError.isClientError = true; 398 | request.error(clientError); 399 | } 400 | console.error(error); 401 | continue; 402 | } 403 | let token = { 404 | accountId: accountData.id 405 | }; 406 | if (accountData.maxConcurrentDebits != null) { 407 | token.maxConcurrentDebits = accountData.maxConcurrentDebits; 408 | } 409 | if (accountData.maxConcurrentWithdrawals != null) { 410 | token.maxConcurrentWithdrawals = accountData.maxConcurrentWithdrawals; 411 | } 412 | if (accountData.maxSocketBackpressure != null) { 413 | token.maxSocketBackpressure = accountData.maxSocketBackpressure; 414 | } 415 | if (accountData.admin) { 416 | token.admin = true; 417 | } 418 | socket.setAuthToken(token, {expiresIn: authTokenExpiry}); 419 | request.end({accountId: accountData.id}); 420 | } 421 | })(); 422 | 423 | (async () => { 424 | for await (let request of socket.procedure('getPublicInfo')) { 425 | try { 426 | validateRPCSchema(request); 427 | } catch (error) { 428 | request.error(error); 429 | console.error(error); 430 | continue; 431 | } 432 | request.end(envConfig.publicInfo); 433 | } 434 | })(); 435 | 436 | (async () => { 437 | for await (let request of socket.procedure('withdraw')) { 438 | try { 439 | verifyUserAuth(request, socket); 440 | validateRPCSchema(request); 441 | } catch (error) { 442 | request.error(error); 443 | console.error(error); 444 | continue; 445 | } 446 | 447 | let withdrawalData = request.data; 448 | let result; 449 | try { 450 | result = await accountService.attemptWithdrawal({ 451 | amount: withdrawalData.amount, 452 | fromAccountId: socket.authToken.accountId, 453 | toWalletAddress: withdrawalData.toWalletAddress 454 | }, socket.authToken.maxConcurrentWithdrawals); 455 | } catch (error) { 456 | if (error.isClientError) { 457 | request.error(error); 458 | } else { 459 | let clientError = new Error('Failed to execute withdrawal due to a server error'); 460 | clientError.name = 'WithdrawError'; 461 | clientError.isClientError = true; 462 | request.error(clientError); 463 | } 464 | console.error(error); 465 | continue; 466 | } 467 | request.end(result); 468 | } 469 | })(); 470 | 471 | (async () => { 472 | for await (let request of socket.procedure('transfer')) { 473 | try { 474 | verifyUserAuth(request, socket); 475 | validateRPCSchema(request); 476 | } catch (error) { 477 | request.error(error); 478 | console.error(error); 479 | continue; 480 | } 481 | 482 | let transferData = request.data; 483 | let result; 484 | try { 485 | result = await accountService.attemptTransfer({ 486 | amount: transferData.amount, 487 | fromAccountId: socket.authToken.accountId, 488 | toAccountId: transferData.toAccountId, 489 | debitId: transferData.debitId, 490 | creditId: transferData.creditId, 491 | data: transferData.data 492 | }, socket.authToken.maxConcurrentDebits); 493 | } catch (error) { 494 | if (error.isClientError) { 495 | request.error(error); 496 | } else { 497 | let clientError = new Error('Failed to execute transfer due to a server error'); 498 | clientError.name = 'TransferError'; 499 | clientError.isClientError = true; 500 | request.error(clientError); 501 | } 502 | console.error(error); 503 | continue; 504 | } 505 | request.end(result); 506 | } 507 | })(); 508 | 509 | (async () => { 510 | for await (let request of socket.procedure('debit')) { 511 | try { 512 | verifyUserAuth(request, socket); 513 | validateRPCSchema(request); 514 | } catch (error) { 515 | request.error(error); 516 | console.error(error); 517 | continue; 518 | } 519 | 520 | let debitData = request.data; 521 | let result; 522 | try { 523 | result = await accountService.attemptDirectDebit({ 524 | amount: debitData.amount, 525 | fromAccountId: socket.authToken.accountId, 526 | debitId: debitData.debitId, 527 | data: debitData.data 528 | }, socket.authToken.maxConcurrentDebits); 529 | } catch (error) { 530 | if (error.isClientError) { 531 | request.error(error); 532 | } else { 533 | let clientError = new Error('Failed to execute debit due to a server error'); 534 | clientError.name = 'DebitError'; 535 | clientError.isClientError = true; 536 | request.error(clientError); 537 | } 538 | console.error(error); 539 | continue; 540 | } 541 | request.end(result); 542 | } 543 | })(); 544 | 545 | (async () => { 546 | for await (let request of socket.procedure('getBalance')) { 547 | try { 548 | verifyUserAuth(request, socket); 549 | validateRPCSchema(request); 550 | } catch (error) { 551 | request.error(error); 552 | console.error(error); 553 | continue; 554 | } 555 | 556 | let balance; 557 | try { 558 | balance = await accountService.fetchAccountBalance(socket.authToken.accountId); 559 | } catch (error) { 560 | if (error.isClientError) { 561 | request.error(error); 562 | } else { 563 | let clientError = new Error('Failed to get account balance due to a server error'); 564 | clientError.name = 'GetBalanceError'; 565 | clientError.isClientError = true; 566 | request.error(clientError); 567 | } 568 | console.error(error); 569 | continue; 570 | } 571 | request.end(balance); 572 | } 573 | })(); 574 | 575 | (async () => { 576 | for await (let request of socket.procedure('adminImpersonate')) { 577 | try { 578 | verifyAdminAuth(request, socket); 579 | validateRPCSchema(request); 580 | } catch (error) { 581 | request.error(error); 582 | console.error(error); 583 | continue; 584 | } 585 | 586 | let accountData; 587 | try { 588 | accountData = await accountService.verifyLoginCredentialsAccountId(request.data); 589 | } catch (error) { 590 | if (error.isClientError) { 591 | request.error(error); 592 | } else { 593 | let clientError = new Error(`Failed to login to account ${request.data.accountId}.`); 594 | clientError.name = 'AdminLoginError'; 595 | clientError.isClientError = true; 596 | request.error(clientError); 597 | } 598 | console.error(error); 599 | continue; 600 | } 601 | let realAccountId = socket.authToken.impersonator || socket.authToken.accountId; 602 | let isOwnAdminAccount = accountData.id === realAccountId; 603 | if (accountData.admin && !isOwnAdminAccount) { 604 | let clientError = new Error( 605 | `Failed to login to account ${ 606 | request.data.accountId 607 | } because other admin accounts cannot be impersonated.` 608 | ); 609 | clientError.name = 'AdminLoginError'; 610 | clientError.isClientError = true; 611 | request.error(clientError); 612 | console.error(clientError); 613 | continue; 614 | } 615 | let token = { 616 | accountId: accountData.id, 617 | admin: true 618 | }; 619 | if (!isOwnAdminAccount) { 620 | token.impersonator = realAccountId; 621 | } 622 | socket.setAuthToken(token, {expiresIn: authTokenExpiry}); 623 | request.end({accountId: accountData.id}); 624 | } 625 | })(); 626 | 627 | (async () => { 628 | for await (let request of socket.procedure('adminWithdraw')) { 629 | try { 630 | verifyAdminAuth(request, socket); 631 | validateRPCSchema(request); 632 | } catch (error) { 633 | request.error(error); 634 | console.error(error); 635 | continue; 636 | } 637 | 638 | let withdrawalData = request.data; 639 | let result; 640 | try { 641 | result = await accountService.execWithdrawal({ 642 | amount: withdrawalData.amount, 643 | fromAccountId: withdrawalData.fromAccountId, 644 | toWalletAddress: withdrawalData.toWalletAddress 645 | }); 646 | } catch (error) { 647 | if (error.isClientError) { 648 | request.error(error); 649 | } else { 650 | let clientError = new Error('Failed to execute withdrawal due to a server error'); 651 | clientError.name = 'AdminWithdrawError'; 652 | clientError.isClientError = true; 653 | request.error(clientError); 654 | } 655 | console.error(error); 656 | continue; 657 | } 658 | request.end(result); 659 | } 660 | })(); 661 | 662 | (async () => { 663 | for await (let request of socket.procedure('adminTransfer')) { 664 | try { 665 | verifyAdminAuth(request, socket); 666 | validateRPCSchema(request); 667 | } catch (error) { 668 | request.error(error); 669 | console.error(error); 670 | continue; 671 | } 672 | 673 | let transferData = request.data; 674 | let result; 675 | try { 676 | result = await accountService.execTransfer({ 677 | amount: transferData.amount, 678 | fromAccountId: transferData.fromAccountId, 679 | toAccountId: transferData.toAccountId, 680 | debitId: transferData.debitId, 681 | creditId: transferData.creditId, 682 | data: transferData.data 683 | }); 684 | } catch (error) { 685 | if (error.isClientError) { 686 | request.error(error); 687 | } else { 688 | let clientError = new Error('Failed to execute transfer due to a server error'); 689 | clientError.name = 'AdminTransferError'; 690 | clientError.isClientError = true; 691 | request.error(clientError); 692 | } 693 | console.error(error); 694 | continue; 695 | } 696 | request.end(result); 697 | } 698 | })(); 699 | 700 | (async () => { 701 | for await (let request of socket.procedure('adminDebit')) { 702 | try { 703 | verifyAdminAuth(request, socket); 704 | validateRPCSchema(request); 705 | } catch (error) { 706 | request.error(error); 707 | console.error(error); 708 | continue; 709 | } 710 | 711 | let debitData = request.data; 712 | let result; 713 | try { 714 | result = await accountService.execDirectDebit({ 715 | amount: debitData.amount, 716 | fromAccountId: debitData.fromAccountId, 717 | debitId: debitData.debitId, 718 | data: debitData.data 719 | }); 720 | } catch (error) { 721 | if (error.isClientError) { 722 | request.error(error); 723 | } else { 724 | let clientError = new Error('Failed to execute debit due to a server error'); 725 | clientError.name = 'DebitError'; 726 | clientError.isClientError = true; 727 | request.error(clientError); 728 | } 729 | console.error(error); 730 | continue; 731 | } 732 | request.end(result); 733 | } 734 | })(); 735 | 736 | (async () => { 737 | for await (let request of socket.procedure('adminCredit')) { 738 | try { 739 | verifyAdminAuth(request, socket); 740 | validateRPCSchema(request); 741 | } catch (error) { 742 | request.error(error); 743 | console.error(error); 744 | continue; 745 | } 746 | 747 | let creditData = request.data; 748 | let result; 749 | try { 750 | result = await accountService.execDirectCredit({ 751 | amount: creditData.amount, 752 | toAccountId: creditData.toAccountId, 753 | creditId: creditData.creditId, 754 | data: creditData.data 755 | }); 756 | } catch (error) { 757 | if (error.isClientError) { 758 | request.error(error); 759 | } else { 760 | let clientError = new Error('Failed to execute credit due to a server error'); 761 | clientError.name = 'CreditError'; 762 | clientError.isClientError = true; 763 | request.error(clientError); 764 | } 765 | console.error(error); 766 | continue; 767 | } 768 | request.end(result); 769 | } 770 | })(); 771 | 772 | (async () => { 773 | for await (let request of socket.procedure('adminGetBalance')) { 774 | try { 775 | verifyAdminAuth(request, socket); 776 | validateRPCSchema(request); 777 | } catch (error) { 778 | request.error(error); 779 | console.error(error); 780 | continue; 781 | } 782 | 783 | let getBalanceData = request.data; 784 | let balance; 785 | try { 786 | balance = await accountService.fetchAccountBalance(getBalanceData.accountId); 787 | } catch (error) { 788 | if (error.isClientError) { 789 | request.error(error); 790 | } else { 791 | let clientError = new Error('Failed to get account balance due to a server error'); 792 | clientError.name = 'AdminGetBalanceError'; 793 | clientError.isClientError = true; 794 | request.error(clientError); 795 | } 796 | 797 | console.error(error); 798 | continue; 799 | } 800 | request.end(balance); 801 | } 802 | })(); 803 | 804 | } 805 | })(); 806 | 807 | (async () => { 808 | await crud.thinky.dbReady(); 809 | httpServer.listen(ASYNGULAR_PORT); 810 | })(); 811 | 812 | if (ASYNGULAR_LOG_LEVEL >= 1) { 813 | (async () => { 814 | for await (let {error} of agServer.listener('error')) { 815 | console.error(error); 816 | } 817 | })(); 818 | } 819 | 820 | if (ASYNGULAR_LOG_LEVEL >= 2) { 821 | console.log( 822 | ` ${colorText('[Active]', 32)} Asyngular worker with PID ${process.pid} is listening on port ${ASYNGULAR_PORT}` 823 | ); 824 | 825 | (async () => { 826 | for await (let {warning} of agServer.listener('warning')) { 827 | console.warn(warning); 828 | } 829 | })(); 830 | } 831 | 832 | function colorText(message, color) { 833 | if (color) { 834 | return `\x1b[${color}m${message}\x1b[0m`; 835 | } 836 | return message; 837 | } 838 | 839 | if (AGC_STATE_SERVER_HOST) { 840 | // Setup broker client to connect to the Asyngular cluster (AGC). 841 | let agcClient = agcBrokerClient.attach(agServer.brokerEngine, { 842 | instanceId: AGC_INSTANCE_ID, 843 | instancePort: ASYNGULAR_PORT, 844 | instanceIp: AGC_INSTANCE_IP, 845 | instanceIpFamily: AGC_INSTANCE_IP_FAMILY, 846 | pubSubBatchDuration: AGC_PUB_SUB_BATCH_DURATION, 847 | stateServerHost: AGC_STATE_SERVER_HOST, 848 | stateServerPort: AGC_STATE_SERVER_PORT, 849 | mappingEngine: AGC_MAPPING_ENGINE, 850 | clientPoolSize: AGC_CLIENT_POOL_SIZE, 851 | authKey: AGC_AUTH_KEY, 852 | stateServerConnectTimeout: AGC_STATE_SERVER_CONNECT_TIMEOUT, 853 | stateServerAckTimeout: AGC_STATE_SERVER_ACK_TIMEOUT, 854 | stateServerReconnectRandomness: AGC_STATE_SERVER_RECONNECT_RANDOMNESS, 855 | brokerRetryDelay: AGC_BROKER_RETRY_DELAY 856 | }); 857 | 858 | (async () => { 859 | for await (let event of agcClient.listener('updateWorkers')) { 860 | let sortedWorkerURIs = event.workerURIs.sort(); 861 | let workerCount = sortedWorkerURIs.length; 862 | let currentWorkerIndex = event.workerURIs.indexOf(event.sourceWorkerURI); 863 | shardInfo.shardIndex = currentWorkerIndex; 864 | shardInfo.shardCount = workerCount; 865 | } 866 | })(); 867 | 868 | if (ASYNGULAR_LOG_LEVEL >= 1) { 869 | (async () => { 870 | for await (let {error} of agcClient.listener('error')) { 871 | error.name = 'AGCError'; 872 | console.error(error); 873 | } 874 | })(); 875 | } 876 | } else { 877 | shardInfo.shardIndex = 0; 878 | shardInfo.shardCount = 1; 879 | } 880 | 881 | process.on('uncaughtException', (error) => { 882 | console.error('[uncaughtException]', error); 883 | process.exit(1); 884 | }); 885 | 886 | process.on('unhandledRejection', (error) => { 887 | console.error('[unhandledRejection]', error); 888 | process.exit(1); 889 | }); 890 | -------------------------------------------------------------------------------- /utils/sharding.js: -------------------------------------------------------------------------------- 1 | const scHasher = require('sc-hasher'); 2 | const crypto = require('crypto'); 3 | 4 | const MAX_SHARD_KEY = Math.pow(2, 30); 5 | 6 | function getShardKey(key) { 7 | let cryptoHasher = crypto.createHash('md5'); 8 | cryptoHasher.update(key); 9 | let baseHash = cryptoHasher.digest('hex'); 10 | return scHasher.hash(baseHash, MAX_SHARD_KEY); 11 | } 12 | 13 | function getShardRange(shardIndex, shardCount) { 14 | let shardSize = MAX_SHARD_KEY / shardCount; 15 | let shardStart = shardIndex * shardSize; 16 | return { 17 | start: shardStart, 18 | end: shardStart + shardSize 19 | }; 20 | } 21 | 22 | module.exports = { 23 | getShardKey, 24 | getShardRange 25 | }; 26 | --------------------------------------------------------------------------------