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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------