@keetanetwork/anchor 0.0.33 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/http-server/index.d.ts +7 -1
- package/lib/http-server/index.d.ts.map +1 -1
- package/lib/http-server/index.js +2 -0
- package/lib/http-server/index.js.map +1 -1
- package/lib/queue/common.d.ts +26 -0
- package/lib/queue/common.d.ts.map +1 -0
- package/lib/queue/common.js +47 -0
- package/lib/queue/common.js.map +1 -0
- package/lib/queue/drivers/queue_file.d.ts +17 -0
- package/lib/queue/drivers/queue_file.d.ts.map +1 -0
- package/lib/queue/drivers/queue_file.js +100 -0
- package/lib/queue/drivers/queue_file.js.map +1 -0
- package/lib/queue/drivers/queue_postgres.d.ts +28 -0
- package/lib/queue/drivers/queue_postgres.d.ts.map +1 -0
- package/lib/queue/drivers/queue_postgres.js +360 -0
- package/lib/queue/drivers/queue_postgres.js.map +1 -0
- package/lib/queue/drivers/queue_redis.d.ts +27 -0
- package/lib/queue/drivers/queue_redis.d.ts.map +1 -0
- package/lib/queue/drivers/queue_redis.js +359 -0
- package/lib/queue/drivers/queue_redis.js.map +1 -0
- package/lib/queue/drivers/queue_sqlite3.d.ts +28 -0
- package/lib/queue/drivers/queue_sqlite3.d.ts.map +1 -0
- package/lib/queue/drivers/queue_sqlite3.js +378 -0
- package/lib/queue/drivers/queue_sqlite3.js.map +1 -0
- package/lib/queue/index.d.ts +341 -0
- package/lib/queue/index.d.ts.map +1 -0
- package/lib/queue/index.js +946 -0
- package/lib/queue/index.js.map +1 -0
- package/lib/queue/internal.d.ts +20 -0
- package/lib/queue/internal.d.ts.map +1 -0
- package/lib/queue/internal.js +66 -0
- package/lib/queue/internal.js.map +1 -0
- package/lib/queue/pipeline.d.ts +152 -0
- package/lib/queue/pipeline.d.ts.map +1 -0
- package/lib/queue/pipeline.js +296 -0
- package/lib/queue/pipeline.js.map +1 -0
- package/lib/resolver.d.ts +1 -1
- package/lib/resolver.d.ts.map +1 -1
- package/lib/resolver.js.map +1 -1
- package/lib/utils/asleep.d.ts +2 -0
- package/lib/utils/asleep.d.ts.map +1 -0
- package/lib/utils/asleep.js +3 -0
- package/lib/utils/asleep.js.map +1 -0
- package/lib/utils/defer.d.ts +4 -0
- package/lib/utils/defer.d.ts.map +1 -0
- package/lib/utils/defer.js +3 -0
- package/lib/utils/defer.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/services/fx/client.d.ts +1 -1
- package/services/fx/client.d.ts.map +1 -1
- package/services/fx/client.js +2 -2
- package/services/fx/client.js.map +1 -1
- package/services/fx/common.d.ts +19 -4
- package/services/fx/common.d.ts.map +1 -1
- package/services/fx/common.js +8 -5
- package/services/fx/common.js.map +1 -1
- package/services/fx/server.d.ts +105 -8
- package/services/fx/server.d.ts.map +1 -1
- package/services/fx/server.js +609 -43
- package/services/fx/server.js.map +1 -1
package/services/fx/server.js
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
import * as __typia_transform__assertGuard from "typia/lib/internal/_assertGuard.js";
|
|
1
2
|
import * as KeetaAnchorHTTPServer from '../../lib/http-server/index.js';
|
|
2
3
|
import { KeetaNet } from '../../client/index.js';
|
|
3
4
|
import { KeetaAnchorUserError } from '../../lib/error.js';
|
|
4
5
|
import { assertConversionInputCanonicalJSON, assertConversionQuoteJSON, Errors } from './common.js';
|
|
5
6
|
import * as Signing from '../../lib/utils/signing.js';
|
|
7
|
+
import { KeetaAnchorQueueRunner, KeetaAnchorQueueStorageDriverMemory } from '../../lib/queue/index.js';
|
|
8
|
+
import { KeetaAnchorQueuePipelineAdvanced } from '../../lib/queue/pipeline.js';
|
|
9
|
+
import { assertNever } from '../../lib/utils/never.js';
|
|
10
|
+
import * as typia from 'typia';
|
|
11
|
+
/**
|
|
12
|
+
* Enable additional runtime "paranoid" checks in the FX server.
|
|
13
|
+
*
|
|
14
|
+
* This may have a small performance impact but increases safety
|
|
15
|
+
* by ensuring that the accounts used in quotes are actually
|
|
16
|
+
* configured in the server.
|
|
17
|
+
*
|
|
18
|
+
* During the transition to multiple accounts this may help catch
|
|
19
|
+
* misconfigurations so it is enabled by default for now.
|
|
20
|
+
*/
|
|
21
|
+
const PARANOID = true;
|
|
6
22
|
;
|
|
7
23
|
async function formatQuoteSignable(unsignedQuote) {
|
|
8
24
|
const retval = [
|
|
@@ -30,40 +46,421 @@ async function verifySignedData(signedBy, quote) {
|
|
|
30
46
|
return (await Signing.VerifySignedData(signedBy, signableQuote, quote.signed));
|
|
31
47
|
}
|
|
32
48
|
async function requestToAccounts(config, request) {
|
|
33
|
-
|
|
49
|
+
let account;
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
51
|
+
if (config.account !== undefined) {
|
|
52
|
+
const rateFee = await config.fx.getConversionRateAndFee(request);
|
|
53
|
+
account = rateFee.account;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
account = null;
|
|
57
|
+
}
|
|
34
58
|
let signer = null;
|
|
35
59
|
if (config.signer !== undefined) {
|
|
36
60
|
signer = (KeetaNet.lib.Account.isInstance(config.signer) ? config.signer : await config.signer(request)).assertAccount();
|
|
37
61
|
}
|
|
38
|
-
if (
|
|
39
|
-
signer
|
|
62
|
+
if (account !== null) {
|
|
63
|
+
if (signer === null) {
|
|
64
|
+
signer = account.assertAccount();
|
|
65
|
+
}
|
|
66
|
+
if (!account.isAccount() && !account.isStorage()) {
|
|
67
|
+
throw (new Error('FX Account should be an Account or Storage Account'));
|
|
68
|
+
}
|
|
40
69
|
}
|
|
41
|
-
if (
|
|
42
|
-
throw (new Error('
|
|
70
|
+
else if (signer === null) {
|
|
71
|
+
throw (new Error('Either account or signer must be provided'));
|
|
43
72
|
}
|
|
44
73
|
return ({
|
|
45
|
-
|
|
46
|
-
|
|
74
|
+
account: account,
|
|
75
|
+
signer: signer
|
|
47
76
|
});
|
|
48
77
|
}
|
|
78
|
+
class KeetaFXAnchorQueuePipelineStage1 extends KeetaAnchorQueueRunner {
|
|
79
|
+
serverConfig;
|
|
80
|
+
sequential = true;
|
|
81
|
+
/**
|
|
82
|
+
* Timeout for processing a single job -- if exceeded the job is marked as aborted
|
|
83
|
+
*/
|
|
84
|
+
processTimeout = 60 * 1000;
|
|
85
|
+
constructor(config) {
|
|
86
|
+
super(config);
|
|
87
|
+
this.serverConfig = config.serverConfig;
|
|
88
|
+
this.processorAborted = this.processorStuck.bind(this);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Handles both stuck (no status update after a long period) and
|
|
92
|
+
* aborted (timeout while processing an entry) states.
|
|
93
|
+
*
|
|
94
|
+
* We just put the job back into pending because the processor
|
|
95
|
+
* will check the network state again.
|
|
96
|
+
*/
|
|
97
|
+
async processorStuck(entry) {
|
|
98
|
+
return ({
|
|
99
|
+
status: 'pending',
|
|
100
|
+
output: entry.output
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Process the entry, attempting to submit the swap block(s)
|
|
105
|
+
* to the network. Verifies the block can be submitted before
|
|
106
|
+
* attempting submission. Also verifies if the block is already
|
|
107
|
+
* on the network and marks the job as completed if so.
|
|
108
|
+
*/
|
|
109
|
+
async processor(entry) {
|
|
110
|
+
const { block, expected, request } = entry.request;
|
|
111
|
+
const expectedToken = expected.token;
|
|
112
|
+
const expectedAmount = expected.amount;
|
|
113
|
+
const config = this.serverConfig;
|
|
114
|
+
let userClient;
|
|
115
|
+
if (KeetaNet.UserClient.isInstance(config.client)) {
|
|
116
|
+
userClient = config.client;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const { signer, account: checkAccount } = await requestToAccounts(config, request);
|
|
120
|
+
if (checkAccount === null) {
|
|
121
|
+
if (this.serverConfig.accounts === undefined) {
|
|
122
|
+
throw (new Error('No accounts configured for FX server'));
|
|
123
|
+
}
|
|
124
|
+
if (!this.serverConfig.accounts.has(entry.request.account)) {
|
|
125
|
+
return ({
|
|
126
|
+
status: 'failed_permanently',
|
|
127
|
+
output: null,
|
|
128
|
+
error: `Mismatched account for FX request and configured account (no matching account found)`
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (!checkAccount.comparePublicKey(entry.request.account)) {
|
|
133
|
+
return ({
|
|
134
|
+
status: 'failed_permanently',
|
|
135
|
+
output: null,
|
|
136
|
+
error: `Mismatched account for FX request and configured account (single account not matched)`
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
userClient = new KeetaNet.UserClient({
|
|
140
|
+
client: config.client.client,
|
|
141
|
+
network: config.client.network,
|
|
142
|
+
networkAlias: config.client.networkAlias,
|
|
143
|
+
account: entry.request.account,
|
|
144
|
+
signer: signer
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/* Check for the block already being on the network, if so we can mark this job as completed */
|
|
148
|
+
const blockExists = await userClient.block(block.hash);
|
|
149
|
+
if (blockExists !== null) {
|
|
150
|
+
const existingOutput = entry.output;
|
|
151
|
+
const blocks = existingOutput?.blocks ?? [Buffer.from(block.toBytes()).toString('base64')];
|
|
152
|
+
return ({
|
|
153
|
+
status: 'completed',
|
|
154
|
+
output: {
|
|
155
|
+
blockhash: block.hash.toString(),
|
|
156
|
+
blocks: blocks
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/* Get the current head block of the account in the block */
|
|
161
|
+
const accountHead = await userClient.head({ account: block.account });
|
|
162
|
+
let isHeadBlock = false;
|
|
163
|
+
if (accountHead !== null) {
|
|
164
|
+
isHeadBlock = accountHead.compare(block.previous);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
isHeadBlock = block['$opening'];
|
|
168
|
+
}
|
|
169
|
+
/* If the account's head block is not the block's previous, see if the block's previous exists on the network */
|
|
170
|
+
if (!isHeadBlock) {
|
|
171
|
+
let previousBlockOnNetwork = false;
|
|
172
|
+
if (block['$opening']) {
|
|
173
|
+
/* Opening block means that there is no previous block, so we are free to proceed */
|
|
174
|
+
previousBlockOnNetwork = true;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const previousBlock = await userClient.block(block.previous);
|
|
178
|
+
if (previousBlock !== null) {
|
|
179
|
+
previousBlockOnNetwork = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let blockPreviousString;
|
|
183
|
+
if (block['$opening']) {
|
|
184
|
+
blockPreviousString = '<opening>';
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
blockPreviousString = block.previous.toString();
|
|
188
|
+
}
|
|
189
|
+
/* If block.previous exists on the network, and it's not the account head block we can mark this job as failed_permanently */
|
|
190
|
+
if (previousBlockOnNetwork) {
|
|
191
|
+
/* Block's previous exists on the network, but is not the account head -- mark as failed_permanently */
|
|
192
|
+
return ({
|
|
193
|
+
status: 'failed_permanently',
|
|
194
|
+
output: null,
|
|
195
|
+
error: `Block previous (${blockPreviousString}) exists on the network but is not the account head`
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
/* If block.previous does not exist on the network we can mark this job as failed_temporarily -- it can't process until the missing block is added */
|
|
200
|
+
/* Block's previous does not exist on the network -- mark as failed_temporarily */
|
|
201
|
+
return ({
|
|
202
|
+
status: 'failed_temporarily',
|
|
203
|
+
output: null,
|
|
204
|
+
error: `Block previous (${blockPreviousString}) does not exist on the network`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/* We are clear to attempt the swap now */
|
|
209
|
+
const swapBlocks = await userClient.acceptSwapRequest({ block, expected: { token: expectedToken, amount: BigInt(expectedAmount) } });
|
|
210
|
+
const publishOptions = {};
|
|
211
|
+
if (userClient.config.generateFeeBlock !== undefined) {
|
|
212
|
+
publishOptions.generateFeeBlock = userClient.config.generateFeeBlock;
|
|
213
|
+
}
|
|
214
|
+
const publishResult = await userClient.client.transmit(swapBlocks, publishOptions);
|
|
215
|
+
if (!publishResult.publish) {
|
|
216
|
+
throw (new Error('Exchange Publish Failed'));
|
|
217
|
+
}
|
|
218
|
+
/* Set the output and mark the job as pending so we can run the queue again and check for completion */
|
|
219
|
+
return ({
|
|
220
|
+
status: 'pending',
|
|
221
|
+
output: {
|
|
222
|
+
blockhash: block.hash.toString(),
|
|
223
|
+
blocks: swapBlocks.map(function (block) {
|
|
224
|
+
return (Buffer.from(block.toBytes()).toString('base64'));
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
encodeRequest(request) {
|
|
230
|
+
const retval = {
|
|
231
|
+
version: 1,
|
|
232
|
+
account: request.account.publicKeyString.get(),
|
|
233
|
+
block: Buffer.from(request.block.toBytes()).toString('base64'),
|
|
234
|
+
request: request.request,
|
|
235
|
+
expected: {
|
|
236
|
+
token: request.expected.token.publicKeyString.get(),
|
|
237
|
+
amount: request.expected.amount.toString()
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
return (retval);
|
|
241
|
+
}
|
|
242
|
+
encodeResponse(response) {
|
|
243
|
+
return (response);
|
|
244
|
+
}
|
|
245
|
+
decodeRequest(request) {
|
|
246
|
+
/* See note at bottom of file */
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
248
|
+
const reqJSON = assertKeetaFXAnchorQueueStage1RequestJSON(request);
|
|
249
|
+
if (reqJSON.version !== 1) {
|
|
250
|
+
throw (new Error(`Unsupported KeetaFXAnchorQueueStage1Request version ${reqJSON.version}`));
|
|
251
|
+
}
|
|
252
|
+
const retval = {
|
|
253
|
+
account: KeetaNet.lib.Account.fromPublicKeyString(reqJSON.account),
|
|
254
|
+
block: new KeetaNet.lib.Block(reqJSON.block),
|
|
255
|
+
request: reqJSON.request,
|
|
256
|
+
expected: {
|
|
257
|
+
token: KeetaNet.lib.Account.fromPublicKeyString(reqJSON.expected.token).assertKeyType(KeetaNet.lib.Account.AccountKeyAlgorithm.TOKEN),
|
|
258
|
+
amount: BigInt(reqJSON.expected.amount)
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
return (retval);
|
|
262
|
+
}
|
|
263
|
+
decodeResponse(response) {
|
|
264
|
+
/* See note at bottom of file */
|
|
265
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
266
|
+
return (assertKeetaFXAnchorQueueStage1ResponseOrNull(response));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
class KeetaFXAnchorQueuePipeline extends KeetaAnchorQueuePipelineAdvanced {
|
|
270
|
+
serverConfig;
|
|
271
|
+
accounts;
|
|
272
|
+
runners = {};
|
|
273
|
+
constructor(options) {
|
|
274
|
+
super(options);
|
|
275
|
+
this.serverConfig = options.serverConfig;
|
|
276
|
+
this.accounts = options.accounts;
|
|
277
|
+
}
|
|
278
|
+
async createPipeline() {
|
|
279
|
+
for (const account of this.accounts) {
|
|
280
|
+
const queue = await this.baseQueue.partition(account.publicKeyAndTypeString);
|
|
281
|
+
this.queues.push(queue);
|
|
282
|
+
const runner = new KeetaFXAnchorQueuePipelineStage1({
|
|
283
|
+
id: `keeta-fx-anchor-runner-${account.publicKeyAndTypeString}`,
|
|
284
|
+
queue: queue,
|
|
285
|
+
logger: this.logger,
|
|
286
|
+
serverConfig: this.serverConfig
|
|
287
|
+
});
|
|
288
|
+
this.runners[account.publicKeyAndTypeString] = runner;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
getStage(_ignore_stageID) {
|
|
292
|
+
throw (new Error('method not supported'));
|
|
293
|
+
}
|
|
294
|
+
async add(request) {
|
|
295
|
+
await super.init();
|
|
296
|
+
const account = request.account.publicKeyAndTypeString;
|
|
297
|
+
const runner = this.runners[account];
|
|
298
|
+
if (runner === undefined) {
|
|
299
|
+
throw (new Error(`No queue runner for account ${account}`));
|
|
300
|
+
}
|
|
301
|
+
return (await runner.add(request));
|
|
302
|
+
}
|
|
303
|
+
async get(id) {
|
|
304
|
+
await super.init();
|
|
305
|
+
for (const account of this.accounts) {
|
|
306
|
+
const runner = this.runners[account.publicKeyAndTypeString];
|
|
307
|
+
if (runner === undefined) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const entry = await runner.get(id);
|
|
311
|
+
if (entry !== null) {
|
|
312
|
+
return (entry);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return (null);
|
|
316
|
+
}
|
|
317
|
+
async run(options) {
|
|
318
|
+
await super.init();
|
|
319
|
+
let retval = false;
|
|
320
|
+
for (const account of this.accounts) {
|
|
321
|
+
const runner = this.runners[account.publicKeyAndTypeString];
|
|
322
|
+
if (runner === undefined) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const more = await runner.run(options);
|
|
326
|
+
if (more) {
|
|
327
|
+
retval = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return (retval);
|
|
331
|
+
}
|
|
332
|
+
async maintain() {
|
|
333
|
+
await super.init();
|
|
334
|
+
for (const account of this.accounts) {
|
|
335
|
+
const runner = this.runners[account.publicKeyAndTypeString];
|
|
336
|
+
if (runner === undefined) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
await runner.maintain();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async destroy() {
|
|
343
|
+
const logger = this.methodLogger('destroy');
|
|
344
|
+
if (this.destroyed) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.destroyed = true;
|
|
348
|
+
await super.destroy();
|
|
349
|
+
for (const runner of Object.values(this.runners)) {
|
|
350
|
+
try {
|
|
351
|
+
await runner.destroy();
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
logger?.error('Error destroying runner:', error);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
49
359
|
export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAnchorHTTPServer {
|
|
50
360
|
homepage;
|
|
51
361
|
client;
|
|
52
|
-
|
|
362
|
+
accounts;
|
|
363
|
+
account = undefined;
|
|
53
364
|
signer;
|
|
54
365
|
quoteSigner;
|
|
55
366
|
fx;
|
|
367
|
+
pipeline;
|
|
368
|
+
pipelineAutoRunInterval = null;
|
|
56
369
|
constructor(config) {
|
|
57
370
|
super(config);
|
|
58
371
|
this.homepage = config.homepage ?? '';
|
|
59
372
|
this.client = config.client;
|
|
60
373
|
this.fx = config.fx;
|
|
61
|
-
this.account = config.account;
|
|
62
|
-
this.signer = config.signer ?? config.account;
|
|
63
374
|
this.quoteSigner = config.quoteSigner;
|
|
375
|
+
/*
|
|
376
|
+
* Setup the accounts
|
|
377
|
+
*/
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
379
|
+
if (config.account !== undefined && config.accounts === undefined) {
|
|
380
|
+
/*
|
|
381
|
+
* Deprecated: If a single account is provided, use that
|
|
382
|
+
* along with the signer to create the accounts set
|
|
383
|
+
*/
|
|
384
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
385
|
+
this.accounts = new KeetaNet.lib.Account.Set([config.account]);
|
|
386
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
387
|
+
this.signer = config.signer ?? config.account;
|
|
388
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
389
|
+
}
|
|
390
|
+
else if (config.accounts !== undefined && config.account === undefined) {
|
|
391
|
+
/*
|
|
392
|
+
* Only allow either "account" or "accounts"+"signer" to be provided
|
|
393
|
+
*/
|
|
394
|
+
if (config.signer === undefined) {
|
|
395
|
+
throw (new Error('If "accounts" is provided, "signer" must also be provided'));
|
|
396
|
+
}
|
|
397
|
+
this.accounts = config.accounts;
|
|
398
|
+
this.signer = config.signer;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
throw (new Error('Either "account" (and optional "signer") or "accounts" and "signer" must be provided, but not both "account" and "accounts"'));
|
|
402
|
+
}
|
|
403
|
+
if (this.accounts.size === 0) {
|
|
404
|
+
throw (new Error('No FX accounts provided'));
|
|
405
|
+
}
|
|
406
|
+
/*
|
|
407
|
+
* If no storage driver is provided, we default to an in-memory
|
|
408
|
+
* that we auto-run
|
|
409
|
+
*/
|
|
410
|
+
let autorun = config.storage?.autoRun ?? false;
|
|
411
|
+
if (config.storage === undefined) {
|
|
412
|
+
autorun = true;
|
|
413
|
+
}
|
|
414
|
+
/*
|
|
415
|
+
* Create the pipeline to process transactions
|
|
416
|
+
*/
|
|
417
|
+
this.pipeline = new KeetaFXAnchorQueuePipeline({
|
|
418
|
+
id: 'keeta-fx-anchor-queue-pipeline',
|
|
419
|
+
baseQueue: config.storage?.queue ?? new KeetaAnchorQueueStorageDriverMemory({
|
|
420
|
+
id: 'keeta-fx-anchor-queue-pipeline-memory-driver',
|
|
421
|
+
logger: this.logger
|
|
422
|
+
}),
|
|
423
|
+
accounts: this.accounts,
|
|
424
|
+
logger: this.logger,
|
|
425
|
+
serverConfig: this
|
|
426
|
+
});
|
|
427
|
+
/*
|
|
428
|
+
* If auto-run is enabled, setup the interval to run the pipeline
|
|
429
|
+
*/
|
|
430
|
+
if (autorun) {
|
|
431
|
+
let running = false;
|
|
432
|
+
this.pipelineAutoRunInterval = setInterval(async () => {
|
|
433
|
+
if (running) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
running = true;
|
|
437
|
+
try {
|
|
438
|
+
await this.pipeline.maintain();
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
this.logger.error('KeetaNetFXAnchorHTTPServer::pipelineAutoRunInterval', 'Error maintaining pipeline:', error);
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await this.pipeline.run({ timeoutMs: 5000 });
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
this.logger.error('KeetaNetFXAnchorHTTPServer::pipelineAutoRunInterval', 'Error running pipeline:', error);
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
running = false;
|
|
451
|
+
}
|
|
452
|
+
}, 1000);
|
|
453
|
+
}
|
|
64
454
|
}
|
|
65
455
|
async initRoutes(config) {
|
|
66
456
|
const routes = {};
|
|
457
|
+
const logger = this.logger;
|
|
458
|
+
/*
|
|
459
|
+
* To use the instance within the route handlers, we need to
|
|
460
|
+
* make a local reference to it.
|
|
461
|
+
*/
|
|
462
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
463
|
+
const instance = this;
|
|
67
464
|
/**
|
|
68
465
|
* If a homepage is provided, setup the route for it
|
|
69
466
|
*/
|
|
@@ -122,6 +519,12 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
122
519
|
}
|
|
123
520
|
const conversion = assertConversionInputCanonicalJSON(postData.request);
|
|
124
521
|
const rateAndFee = await config.fx.getConversionRateAndFee(conversion);
|
|
522
|
+
if (PARANOID) {
|
|
523
|
+
const quoteAccount = rateAndFee.account;
|
|
524
|
+
if (!instance.accounts.has(quoteAccount)) {
|
|
525
|
+
throw (new Error('"getConversionRateAndFee" returned an account not configured for this server'));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
125
528
|
const unsignedQuote = KeetaNet.lib.Utils.Conversion.toJSONSerializable({
|
|
126
529
|
request: conversion,
|
|
127
530
|
...rateAndFee
|
|
@@ -165,20 +568,6 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
165
568
|
}
|
|
166
569
|
}
|
|
167
570
|
const block = new KeetaNet.lib.Block(request.block);
|
|
168
|
-
let userClient;
|
|
169
|
-
if (KeetaNet.UserClient.isInstance(config.client)) {
|
|
170
|
-
userClient = config.client;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
const { account, signer } = await requestToAccounts(config, quote.request);
|
|
174
|
-
userClient = new KeetaNet.UserClient({
|
|
175
|
-
client: config.client.client,
|
|
176
|
-
network: config.client.network,
|
|
177
|
-
networkAlias: config.client.networkAlias,
|
|
178
|
-
account: account,
|
|
179
|
-
signer: signer
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
571
|
/* Get Expected Amount and Token to Verify Swap */
|
|
183
572
|
const expectedToken = KeetaNet.lib.Account.fromPublicKeyString(quote.request.from);
|
|
184
573
|
let expectedAmount = quote.request.affinity === 'from' ? BigInt(quote.request.amount) : BigInt(quote.convertedAmount);
|
|
@@ -206,19 +595,20 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
206
595
|
}
|
|
207
596
|
}
|
|
208
597
|
}
|
|
209
|
-
/*
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
598
|
+
/* Enqueue the exchange request */
|
|
599
|
+
const exchangeID = await instance.pipeline.add({
|
|
600
|
+
account: KeetaNet.lib.Account.fromPublicKeyString(quote.account),
|
|
601
|
+
block: block,
|
|
602
|
+
request: quote.request,
|
|
603
|
+
expected: {
|
|
604
|
+
token: expectedToken,
|
|
605
|
+
amount: BigInt(expectedAmount)
|
|
606
|
+
}
|
|
607
|
+
});
|
|
219
608
|
const exchangeResponse = {
|
|
220
609
|
ok: true,
|
|
221
|
-
exchangeID:
|
|
610
|
+
exchangeID: exchangeID.toString(),
|
|
611
|
+
status: 'pending'
|
|
222
612
|
};
|
|
223
613
|
return ({
|
|
224
614
|
output: JSON.stringify(exchangeResponse)
|
|
@@ -232,14 +622,57 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
232
622
|
if (typeof exchangeID !== 'string') {
|
|
233
623
|
throw (new Error('Missing exchangeID in params'));
|
|
234
624
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
625
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
626
|
+
const queueEntryInfo = await instance.pipeline.get(exchangeID);
|
|
627
|
+
const exchangeResponse = (function () {
|
|
628
|
+
const inputStatus = queueEntryInfo?.status;
|
|
629
|
+
switch (inputStatus) {
|
|
630
|
+
case undefined:
|
|
631
|
+
throw (new KeetaAnchorUserError('Exchange ID not found'));
|
|
632
|
+
case 'pending':
|
|
633
|
+
case 'processing':
|
|
634
|
+
case 'stuck':
|
|
635
|
+
case 'aborted':
|
|
636
|
+
case 'failed_temporarily':
|
|
637
|
+
return ({
|
|
638
|
+
ok: true,
|
|
639
|
+
status: 'pending',
|
|
640
|
+
exchangeID: exchangeID
|
|
641
|
+
});
|
|
642
|
+
case 'completed': {
|
|
643
|
+
const blockhash = queueEntryInfo?.output?.blockhash;
|
|
644
|
+
if (blockhash === undefined) {
|
|
645
|
+
return ({
|
|
646
|
+
ok: true,
|
|
647
|
+
status: 'pending',
|
|
648
|
+
exchangeID: exchangeID
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
return ({
|
|
653
|
+
ok: true,
|
|
654
|
+
status: 'completed',
|
|
655
|
+
exchangeID: exchangeID,
|
|
656
|
+
blockhash: blockhash
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
case 'failed_permanently':
|
|
661
|
+
return ({
|
|
662
|
+
ok: false,
|
|
663
|
+
exchangeID: exchangeID,
|
|
664
|
+
status: 'failed',
|
|
665
|
+
error: 'Exchange failed'
|
|
666
|
+
});
|
|
667
|
+
case 'moved':
|
|
668
|
+
throw (new Error('Exchange ID has been moved'));
|
|
669
|
+
case '@internal':
|
|
670
|
+
throw (new Error('Invalid exchange status @internal'));
|
|
671
|
+
default:
|
|
672
|
+
assertNever(inputStatus);
|
|
673
|
+
}
|
|
674
|
+
})();
|
|
675
|
+
logger.debug('GET /api/getExchangeStatus/:id', 'Exchange Status for ID', exchangeID, 'is', exchangeResponse, 'based on jobInfo', queueEntryInfo);
|
|
243
676
|
return ({
|
|
244
677
|
output: JSON.stringify(exchangeResponse)
|
|
245
678
|
});
|
|
@@ -261,5 +694,138 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
261
694
|
operations: operations
|
|
262
695
|
});
|
|
263
696
|
}
|
|
697
|
+
async stop() {
|
|
698
|
+
if (this.pipelineAutoRunInterval !== null) {
|
|
699
|
+
clearInterval(this.pipelineAutoRunInterval);
|
|
700
|
+
this.pipelineAutoRunInterval = null;
|
|
701
|
+
}
|
|
702
|
+
await this.pipeline.destroy();
|
|
703
|
+
await super.stop();
|
|
704
|
+
}
|
|
264
705
|
}
|
|
706
|
+
/*
|
|
707
|
+
* These are placed at the bottom of the file because the generation
|
|
708
|
+
* breaks the code coverage computation; Normally, we'd place these
|
|
709
|
+
* in a ".generated.ts" file but for simplicity of internal types
|
|
710
|
+
* we keep them here.
|
|
711
|
+
*/
|
|
712
|
+
const assertKeetaFXAnchorQueueStage1RequestJSON = (() => { const _io0 = input => 1 === input.version && "string" === typeof input.account && "string" === typeof input.block && ("object" === typeof input.request && null !== input.request && _io1(input.request)) && ("object" === typeof input.expected && null !== input.expected && _io2(input.expected)); const _io1 = input => "string" === typeof input.from && (RegExp(/^keeta_am(.*)/).test(input.from) || RegExp(/^keeta_an(.*)/).test(input.from) || RegExp(/^keeta_ao(.*)/).test(input.from) || RegExp(/^keeta_ap(.*)/).test(input.from) || RegExp(/^tyblocks_am(.*)/).test(input.from) || RegExp(/^tyblocks_an(.*)/).test(input.from) || RegExp(/^tyblocks_ao(.*)/).test(input.from) || RegExp(/^tyblocks_ap(.*)/).test(input.from)) && ("string" === typeof input.to && (RegExp(/^keeta_am(.*)/).test(input.to) || RegExp(/^keeta_an(.*)/).test(input.to) || RegExp(/^keeta_ao(.*)/).test(input.to) || RegExp(/^keeta_ap(.*)/).test(input.to) || RegExp(/^tyblocks_am(.*)/).test(input.to) || RegExp(/^tyblocks_an(.*)/).test(input.to) || RegExp(/^tyblocks_ao(.*)/).test(input.to) || RegExp(/^tyblocks_ap(.*)/).test(input.to))) && "string" === typeof input.amount && ("from" === input.affinity || "to" === input.affinity); const _io2 = input => "string" === typeof input.token && "string" === typeof input.amount; const _ao0 = (input, _path, _exceptionable = true) => (1 === input.version || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
713
|
+
method: "typia.createAssert",
|
|
714
|
+
path: _path + ".version",
|
|
715
|
+
expected: "1",
|
|
716
|
+
value: input.version
|
|
717
|
+
}, _errorFactory)) && ("string" === typeof input.account || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
718
|
+
method: "typia.createAssert",
|
|
719
|
+
path: _path + ".account",
|
|
720
|
+
expected: "string",
|
|
721
|
+
value: input.account
|
|
722
|
+
}, _errorFactory)) && ("string" === typeof input.block || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
723
|
+
method: "typia.createAssert",
|
|
724
|
+
path: _path + ".block",
|
|
725
|
+
expected: "string",
|
|
726
|
+
value: input.block
|
|
727
|
+
}, _errorFactory)) && (("object" === typeof input.request && null !== input.request || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
728
|
+
method: "typia.createAssert",
|
|
729
|
+
path: _path + ".request",
|
|
730
|
+
expected: "__type",
|
|
731
|
+
value: input.request
|
|
732
|
+
}, _errorFactory)) && _ao1(input.request, _path + ".request", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
733
|
+
method: "typia.createAssert",
|
|
734
|
+
path: _path + ".request",
|
|
735
|
+
expected: "__type",
|
|
736
|
+
value: input.request
|
|
737
|
+
}, _errorFactory)) && (("object" === typeof input.expected && null !== input.expected || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
738
|
+
method: "typia.createAssert",
|
|
739
|
+
path: _path + ".expected",
|
|
740
|
+
expected: "__type.o1",
|
|
741
|
+
value: input.expected
|
|
742
|
+
}, _errorFactory)) && _ao2(input.expected, _path + ".expected", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
743
|
+
method: "typia.createAssert",
|
|
744
|
+
path: _path + ".expected",
|
|
745
|
+
expected: "__type.o1",
|
|
746
|
+
value: input.expected
|
|
747
|
+
}, _errorFactory)); const _ao1 = (input, _path, _exceptionable = true) => ("string" === typeof input.from && (RegExp(/^keeta_am(.*)/).test(input.from) || RegExp(/^keeta_an(.*)/).test(input.from) || RegExp(/^keeta_ao(.*)/).test(input.from) || RegExp(/^keeta_ap(.*)/).test(input.from) || RegExp(/^tyblocks_am(.*)/).test(input.from) || RegExp(/^tyblocks_an(.*)/).test(input.from) || RegExp(/^tyblocks_ao(.*)/).test(input.from) || RegExp(/^tyblocks_ap(.*)/).test(input.from)) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
748
|
+
method: "typia.createAssert",
|
|
749
|
+
path: _path + ".from",
|
|
750
|
+
expected: "(`keeta_am${string}` | `keeta_an${string}` | `keeta_ao${string}` | `keeta_ap${string}` | `tyblocks_am${string}` | `tyblocks_an${string}` | `tyblocks_ao${string}` | `tyblocks_ap${string}`)",
|
|
751
|
+
value: input.from
|
|
752
|
+
}, _errorFactory)) && ("string" === typeof input.to && (RegExp(/^keeta_am(.*)/).test(input.to) || RegExp(/^keeta_an(.*)/).test(input.to) || RegExp(/^keeta_ao(.*)/).test(input.to) || RegExp(/^keeta_ap(.*)/).test(input.to) || RegExp(/^tyblocks_am(.*)/).test(input.to) || RegExp(/^tyblocks_an(.*)/).test(input.to) || RegExp(/^tyblocks_ao(.*)/).test(input.to) || RegExp(/^tyblocks_ap(.*)/).test(input.to)) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
753
|
+
method: "typia.createAssert",
|
|
754
|
+
path: _path + ".to",
|
|
755
|
+
expected: "(`keeta_am${string}` | `keeta_an${string}` | `keeta_ao${string}` | `keeta_ap${string}` | `tyblocks_am${string}` | `tyblocks_an${string}` | `tyblocks_ao${string}` | `tyblocks_ap${string}`)",
|
|
756
|
+
value: input.to
|
|
757
|
+
}, _errorFactory)) && ("string" === typeof input.amount || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
758
|
+
method: "typia.createAssert",
|
|
759
|
+
path: _path + ".amount",
|
|
760
|
+
expected: "string",
|
|
761
|
+
value: input.amount
|
|
762
|
+
}, _errorFactory)) && ("from" === input.affinity || "to" === input.affinity || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
763
|
+
method: "typia.createAssert",
|
|
764
|
+
path: _path + ".affinity",
|
|
765
|
+
expected: "(\"from\" | \"to\")",
|
|
766
|
+
value: input.affinity
|
|
767
|
+
}, _errorFactory)); const _ao2 = (input, _path, _exceptionable = true) => ("string" === typeof input.token || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
768
|
+
method: "typia.createAssert",
|
|
769
|
+
path: _path + ".token",
|
|
770
|
+
expected: "string",
|
|
771
|
+
value: input.token
|
|
772
|
+
}, _errorFactory)) && ("string" === typeof input.amount || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
773
|
+
method: "typia.createAssert",
|
|
774
|
+
path: _path + ".amount",
|
|
775
|
+
expected: "string",
|
|
776
|
+
value: input.amount
|
|
777
|
+
}, _errorFactory)); const __is = input => "object" === typeof input && null !== input && _io0(input); let _errorFactory; return (input, errorFactory) => {
|
|
778
|
+
if (false === __is(input)) {
|
|
779
|
+
_errorFactory = errorFactory;
|
|
780
|
+
((input, _path, _exceptionable = true) => ("object" === typeof input && null !== input || __typia_transform__assertGuard._assertGuard(true, {
|
|
781
|
+
method: "typia.createAssert",
|
|
782
|
+
path: _path + "",
|
|
783
|
+
expected: "KeetaFXAnchorQueueStage1RequestJSON",
|
|
784
|
+
value: input
|
|
785
|
+
}, _errorFactory)) && _ao0(input, _path + "", true) || __typia_transform__assertGuard._assertGuard(true, {
|
|
786
|
+
method: "typia.createAssert",
|
|
787
|
+
path: _path + "",
|
|
788
|
+
expected: "KeetaFXAnchorQueueStage1RequestJSON",
|
|
789
|
+
value: input
|
|
790
|
+
}, _errorFactory))(input, "$input", true);
|
|
791
|
+
}
|
|
792
|
+
return input;
|
|
793
|
+
}; })();
|
|
794
|
+
const assertKeetaFXAnchorQueueStage1ResponseOrNull = (() => { const _io0 = input => Array.isArray(input.blocks) && input.blocks.every(elem => "string" === typeof elem) && "string" === typeof input.blockhash; const _ao0 = (input, _path, _exceptionable = true) => ((Array.isArray(input.blocks) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
795
|
+
method: "typia.createAssert",
|
|
796
|
+
path: _path + ".blocks",
|
|
797
|
+
expected: "Array<string>",
|
|
798
|
+
value: input.blocks
|
|
799
|
+
}, _errorFactory)) && input.blocks.every((elem, _index2) => "string" === typeof elem || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
800
|
+
method: "typia.createAssert",
|
|
801
|
+
path: _path + ".blocks[" + _index2 + "]",
|
|
802
|
+
expected: "string",
|
|
803
|
+
value: elem
|
|
804
|
+
}, _errorFactory)) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
805
|
+
method: "typia.createAssert",
|
|
806
|
+
path: _path + ".blocks",
|
|
807
|
+
expected: "Array<string>",
|
|
808
|
+
value: input.blocks
|
|
809
|
+
}, _errorFactory)) && ("string" === typeof input.blockhash || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
810
|
+
method: "typia.createAssert",
|
|
811
|
+
path: _path + ".blockhash",
|
|
812
|
+
expected: "string",
|
|
813
|
+
value: input.blockhash
|
|
814
|
+
}, _errorFactory)); const __is = input => null === input || "object" === typeof input && null !== input && _io0(input); let _errorFactory; return (input, errorFactory) => {
|
|
815
|
+
if (false === __is(input)) {
|
|
816
|
+
_errorFactory = errorFactory;
|
|
817
|
+
((input, _path, _exceptionable = true) => null === input || ("object" === typeof input && null !== input || __typia_transform__assertGuard._assertGuard(true, {
|
|
818
|
+
method: "typia.createAssert",
|
|
819
|
+
path: _path + "",
|
|
820
|
+
expected: "(KeetaFXAnchorQueueStage1Response | null)",
|
|
821
|
+
value: input
|
|
822
|
+
}, _errorFactory)) && _ao0(input, _path + "", true) || __typia_transform__assertGuard._assertGuard(true, {
|
|
823
|
+
method: "typia.createAssert",
|
|
824
|
+
path: _path + "",
|
|
825
|
+
expected: "(KeetaFXAnchorQueueStage1Response | null)",
|
|
826
|
+
value: input
|
|
827
|
+
}, _errorFactory))(input, "$input", true);
|
|
828
|
+
}
|
|
829
|
+
return input;
|
|
830
|
+
}; })();
|
|
265
831
|
//# sourceMappingURL=server.js.map
|