@keetanetwork/anchor 0.0.37 → 0.0.39
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/encrypted-container.d.ts +53 -3
- package/lib/encrypted-container.d.ts.map +1 -1
- package/lib/encrypted-container.js +549 -93
- package/lib/encrypted-container.js.map +1 -1
- package/lib/http-server/index.d.ts.map +1 -1
- package/lib/http-server/index.js +58 -5
- package/lib/http-server/index.js.map +1 -1
- package/lib/queue/drivers/queue_firestore.d.ts +29 -0
- package/lib/queue/drivers/queue_firestore.d.ts.map +1 -0
- package/lib/queue/drivers/queue_firestore.js +279 -0
- package/lib/queue/drivers/queue_firestore.js.map +1 -0
- package/lib/queue/index.d.ts +57 -0
- package/lib/queue/index.d.ts.map +1 -1
- package/lib/queue/index.js +127 -21
- package/lib/queue/index.js.map +1 -1
- package/lib/resolver.d.ts +4 -15
- package/lib/resolver.d.ts.map +1 -1
- package/lib/resolver.js +468 -636
- package/lib/resolver.js.map +1 -1
- package/lib/utils/signing.d.ts +12 -3
- package/lib/utils/signing.d.ts.map +1 -1
- package/lib/utils/signing.js +7 -13
- package/lib/utils/signing.js.map +1 -1
- package/lib/utils/types.d.ts +14 -2
- package/lib/utils/types.d.ts.map +1 -1
- package/lib/utils/types.js.map +1 -1
- package/npm-shrinkwrap.json +7 -7
- package/package.json +3 -2
- package/services/asset-movement/client.d.ts +2 -2
- package/services/asset-movement/client.d.ts.map +1 -1
- package/services/asset-movement/client.js +2 -2
- package/services/asset-movement/client.js.map +1 -1
- package/services/asset-movement/common.d.ts +201 -24
- package/services/asset-movement/common.d.ts.map +1 -1
- package/services/asset-movement/common.js +305 -80
- package/services/asset-movement/common.js.map +1 -1
- package/services/fx/client.d.ts +38 -11
- package/services/fx/client.d.ts.map +1 -1
- package/services/fx/client.js +187 -42
- package/services/fx/client.js.map +1 -1
- package/services/fx/common.d.ts +55 -6
- package/services/fx/common.d.ts.map +1 -1
- package/services/fx/common.js +142 -16
- package/services/fx/common.js.map +1 -1
- package/services/fx/server.d.ts +51 -7
- package/services/fx/server.d.ts.map +1 -1
- package/services/fx/server.js +333 -109
- package/services/fx/server.js.map +1 -1
- package/services/fx/util.d.ts +31 -0
- package/services/fx/util.d.ts.map +1 -0
- package/services/fx/util.js +132 -0
- package/services/fx/util.js.map +1 -0
package/services/fx/server.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { __addDisposableResource, __disposeResources } from "tslib";
|
|
1
2
|
import * as __typia_transform__assertGuard from "typia/lib/internal/_assertGuard.js";
|
|
2
3
|
import * as KeetaAnchorHTTPServer from '../../lib/http-server/index.js';
|
|
3
4
|
import { KeetaNet } from '../../client/index.js';
|
|
4
|
-
import { KeetaAnchorUserError } from '../../lib/error.js';
|
|
5
|
-
import { assertConversionInputCanonicalJSON,
|
|
5
|
+
import { KeetaAnchorError, KeetaAnchorUserError } from '../../lib/error.js';
|
|
6
|
+
import { assertConversionInputCanonicalJSON, assertKeetaFXAnchorClientCreateExchangeRequestJSON, Errors } from './common.js';
|
|
6
7
|
import * as Signing from '../../lib/utils/signing.js';
|
|
7
8
|
import { KeetaAnchorQueueRunner, KeetaAnchorQueueStorageDriverMemory } from '../../lib/queue/index.js';
|
|
8
9
|
import { KeetaAnchorQueuePipelineAdvanced } from '../../lib/queue/pipeline.js';
|
|
9
10
|
import { assertNever } from '../../lib/utils/never.js';
|
|
10
11
|
import * as typia from 'typia';
|
|
12
|
+
import { assertExchangeBlockParametersAndComputeRefund, convertQuoteToExpectedSwapWithoutCost } from './util.js';
|
|
13
|
+
import { AsyncDisposableStack } from '../../lib/utils/defer.js';
|
|
14
|
+
import { asleep } from '../../lib/utils/asleep.js';
|
|
11
15
|
/**
|
|
12
16
|
* Enable additional runtime "paranoid" checks in the FX server.
|
|
13
17
|
*
|
|
@@ -49,7 +53,7 @@ async function requestToAccounts(config, request) {
|
|
|
49
53
|
let account;
|
|
50
54
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
51
55
|
if (config.account !== undefined) {
|
|
52
|
-
const rateFee = await config.fx.getConversionRateAndFee(request);
|
|
56
|
+
const rateFee = await config.fx.getConversionRateAndFee(request, { purpose: 'estimate' });
|
|
53
57
|
account = rateFee.account;
|
|
54
58
|
}
|
|
55
59
|
else {
|
|
@@ -75,6 +79,31 @@ async function requestToAccounts(config, request) {
|
|
|
75
79
|
signer: signer
|
|
76
80
|
});
|
|
77
81
|
}
|
|
82
|
+
export function toValidateQuoteInput(input) {
|
|
83
|
+
const ret = {
|
|
84
|
+
account: KeetaNet.lib.Account.toAccount(input.account),
|
|
85
|
+
convertedAmount: BigInt(input.convertedAmount),
|
|
86
|
+
cost: {
|
|
87
|
+
amount: BigInt(input.cost.amount),
|
|
88
|
+
token: KeetaNet.lib.Account.toAccount(input.cost.token)
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
if ('convertedAmountBound' in input && input.convertedAmountBound !== undefined) {
|
|
92
|
+
ret.convertedAmountBound = BigInt(input.convertedAmountBound);
|
|
93
|
+
}
|
|
94
|
+
if ('signed' in input && input.signed !== undefined) {
|
|
95
|
+
ret.signed = input.signed;
|
|
96
|
+
}
|
|
97
|
+
if ('request' in input && input.request !== undefined) {
|
|
98
|
+
ret.request = {
|
|
99
|
+
from: KeetaNet.lib.Account.toAccount(input.request.from),
|
|
100
|
+
to: KeetaNet.lib.Account.toAccount(input.request.to),
|
|
101
|
+
amount: BigInt(input.request.amount),
|
|
102
|
+
affinity: input.request.affinity
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return (ret);
|
|
106
|
+
}
|
|
78
107
|
class KeetaFXAnchorQueuePipelineStage1 extends KeetaAnchorQueueRunner {
|
|
79
108
|
serverConfig;
|
|
80
109
|
sequential = true;
|
|
@@ -107,13 +136,18 @@ class KeetaFXAnchorQueuePipelineStage1 extends KeetaAnchorQueueRunner {
|
|
|
107
136
|
* on the network and marks the job as completed if so.
|
|
108
137
|
*/
|
|
109
138
|
async processor(entry) {
|
|
110
|
-
const { block,
|
|
111
|
-
const expectedToken = expected.token;
|
|
112
|
-
const expectedAmount = expected.amount;
|
|
139
|
+
const { block, request } = entry.request;
|
|
113
140
|
const config = this.serverConfig;
|
|
114
141
|
let userClient;
|
|
115
142
|
if (KeetaNet.UserClient.isInstance(config.client)) {
|
|
116
143
|
userClient = config.client;
|
|
144
|
+
if (!(userClient.account.comparePublicKey(entry.request.account))) {
|
|
145
|
+
return ({
|
|
146
|
+
status: 'failed_permanently',
|
|
147
|
+
output: null,
|
|
148
|
+
error: `Mismatched account for FX request with configured UserClient account`
|
|
149
|
+
});
|
|
150
|
+
}
|
|
117
151
|
}
|
|
118
152
|
else {
|
|
119
153
|
const { signer, account: checkAccount } = await requestToAccounts(config, request);
|
|
@@ -206,14 +240,55 @@ class KeetaFXAnchorQueuePipelineStage1 extends KeetaAnchorQueueRunner {
|
|
|
206
240
|
}
|
|
207
241
|
}
|
|
208
242
|
/* We are clear to attempt the swap now */
|
|
209
|
-
const
|
|
243
|
+
const builder = userClient.initBuilder();
|
|
244
|
+
let expected = entry.request.expected;
|
|
245
|
+
/**
|
|
246
|
+
* We only want to refund excess for cost/paying token if it is not a fixed rate
|
|
247
|
+
* as if it is a fixed rate transfer, you can assume the client did not send unintentionally
|
|
248
|
+
*
|
|
249
|
+
* Additionally, other FX anchors are not forced to refund any, so there is no guarantee to client that refunds will occur
|
|
250
|
+
*/
|
|
251
|
+
if (expected === null) {
|
|
252
|
+
const quote = await this.serverConfig.fx.getConversionRateAndFee(request, { purpose: 'exchange', request: entry.request });
|
|
253
|
+
const { refunds } = assertExchangeBlockParametersAndComputeRefund({
|
|
254
|
+
block: block,
|
|
255
|
+
liquidityAccount: entry.request.account,
|
|
256
|
+
allowedLiquidityAccounts: null,
|
|
257
|
+
checks: { quote, request },
|
|
258
|
+
isQuoteBasedExchange: false
|
|
259
|
+
});
|
|
260
|
+
for (const refund of refunds) {
|
|
261
|
+
builder.send(block.account, refund.amount, refund.token);
|
|
262
|
+
}
|
|
263
|
+
expected = convertQuoteToExpectedSwapWithoutCost({ quote, request });
|
|
264
|
+
}
|
|
265
|
+
builder.send(block.account, expected.send.amount, expected.send.token);
|
|
266
|
+
const sendBlock = await builder.computeBlocks();
|
|
267
|
+
const swapBlocks = [...sendBlock.blocks, block];
|
|
210
268
|
const publishOptions = {};
|
|
211
269
|
if (userClient.config.generateFeeBlock !== undefined) {
|
|
212
270
|
publishOptions.generateFeeBlock = userClient.config.generateFeeBlock;
|
|
213
271
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
272
|
+
try {
|
|
273
|
+
const publishResult = await userClient.client.transmit(swapBlocks, publishOptions);
|
|
274
|
+
if (!publishResult.publish) {
|
|
275
|
+
throw (new Error('Exchange Publish Failed'));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (KeetaNet.lib.Error.isInstance(error) &&
|
|
280
|
+
'shouldRetry' in error &&
|
|
281
|
+
// Disable this warning as there is nothing stopping this from not being a boolean
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
|
283
|
+
error.shouldRetry === false) {
|
|
284
|
+
this.serverConfig.logger?.warn('KeetaFXAnchorQueuePipelineStage1::processor', 'Non-retryable error publishing swap blocks:', error);
|
|
285
|
+
return ({
|
|
286
|
+
status: 'failed_permanently',
|
|
287
|
+
output: null,
|
|
288
|
+
error: `${error.code} ${error.message}`
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
throw (error);
|
|
217
292
|
}
|
|
218
293
|
/* Set the output and mark the job as pending so we can run the queue again and check for completion */
|
|
219
294
|
return ({
|
|
@@ -227,15 +302,29 @@ class KeetaFXAnchorQueuePipelineStage1 extends KeetaAnchorQueueRunner {
|
|
|
227
302
|
});
|
|
228
303
|
}
|
|
229
304
|
encodeRequest(request) {
|
|
305
|
+
let expected;
|
|
306
|
+
if (request.expected === null) {
|
|
307
|
+
expected = null;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
expected = {
|
|
311
|
+
receive: {
|
|
312
|
+
token: request.expected.receive.token.publicKeyString.get(),
|
|
313
|
+
amount: request.expected.receive.amount.toString()
|
|
314
|
+
},
|
|
315
|
+
send: {
|
|
316
|
+
token: request.expected.send.token.publicKeyString.get(),
|
|
317
|
+
amount: request.expected.send.amount.toString()
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
;
|
|
230
322
|
const retval = {
|
|
231
323
|
version: 1,
|
|
232
324
|
account: request.account.publicKeyString.get(),
|
|
233
325
|
block: Buffer.from(request.block.toBytes()).toString('base64'),
|
|
234
326
|
request: request.request,
|
|
235
|
-
expected:
|
|
236
|
-
token: request.expected.token.publicKeyString.get(),
|
|
237
|
-
amount: request.expected.amount.toString()
|
|
238
|
-
}
|
|
327
|
+
expected: expected
|
|
239
328
|
};
|
|
240
329
|
return (retval);
|
|
241
330
|
}
|
|
@@ -249,14 +338,27 @@ class KeetaFXAnchorQueuePipelineStage1 extends KeetaAnchorQueueRunner {
|
|
|
249
338
|
if (reqJSON.version !== 1) {
|
|
250
339
|
throw (new Error(`Unsupported KeetaFXAnchorQueueStage1Request version ${reqJSON.version}`));
|
|
251
340
|
}
|
|
341
|
+
let expected;
|
|
342
|
+
if (reqJSON.expected === null) {
|
|
343
|
+
expected = null;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
expected = {
|
|
347
|
+
receive: {
|
|
348
|
+
token: KeetaNet.lib.Account.fromPublicKeyString(reqJSON.expected.receive.token).assertKeyType(KeetaNet.lib.Account.AccountKeyAlgorithm.TOKEN),
|
|
349
|
+
amount: BigInt(reqJSON.expected.receive.amount)
|
|
350
|
+
},
|
|
351
|
+
send: {
|
|
352
|
+
token: KeetaNet.lib.Account.fromPublicKeyString(reqJSON.expected.send.token).assertKeyType(KeetaNet.lib.Account.AccountKeyAlgorithm.TOKEN),
|
|
353
|
+
amount: BigInt(reqJSON.expected.send.amount)
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
252
357
|
const retval = {
|
|
253
358
|
account: KeetaNet.lib.Account.fromPublicKeyString(reqJSON.account),
|
|
254
359
|
block: new KeetaNet.lib.Block(reqJSON.block),
|
|
255
360
|
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
|
-
}
|
|
361
|
+
expected: expected
|
|
260
362
|
};
|
|
261
363
|
return (retval);
|
|
262
364
|
}
|
|
@@ -365,13 +467,16 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
365
467
|
quoteSigner;
|
|
366
468
|
fx;
|
|
367
469
|
pipeline;
|
|
368
|
-
|
|
470
|
+
quoteConfiguration;
|
|
471
|
+
autoRun;
|
|
472
|
+
autoRunRunning = false;
|
|
369
473
|
constructor(config) {
|
|
370
474
|
super(config);
|
|
371
475
|
this.homepage = config.homepage ?? '';
|
|
372
476
|
this.client = config.client;
|
|
373
477
|
this.fx = config.fx;
|
|
374
478
|
this.quoteSigner = config.quoteSigner;
|
|
479
|
+
this.quoteConfiguration = config.quoteConfiguration ?? { requiresQuote: true };
|
|
375
480
|
/*
|
|
376
481
|
* Setup the accounts
|
|
377
482
|
*/
|
|
@@ -407,9 +512,9 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
407
512
|
* If no storage driver is provided, we default to an in-memory
|
|
408
513
|
* that we auto-run
|
|
409
514
|
*/
|
|
410
|
-
|
|
515
|
+
this.autoRun = config.storage?.autoRun ?? false;
|
|
411
516
|
if (config.storage === undefined) {
|
|
412
|
-
|
|
517
|
+
this.autoRun = true;
|
|
413
518
|
}
|
|
414
519
|
/*
|
|
415
520
|
* Create the pipeline to process transactions
|
|
@@ -424,33 +529,6 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
424
529
|
logger: this.logger,
|
|
425
530
|
serverConfig: this
|
|
426
531
|
});
|
|
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
|
-
}
|
|
454
532
|
}
|
|
455
533
|
async initRoutes(config) {
|
|
456
534
|
const routes = {};
|
|
@@ -482,6 +560,16 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
482
560
|
});
|
|
483
561
|
};
|
|
484
562
|
}
|
|
563
|
+
async function getUnsignedQuoteData(conversion, purpose) {
|
|
564
|
+
const rateAndFee = await config.fx.getConversionRateAndFee(conversion, { purpose });
|
|
565
|
+
if (PARANOID) {
|
|
566
|
+
const quoteAccount = rateAndFee.account;
|
|
567
|
+
if (!instance.accounts.has(quoteAccount)) {
|
|
568
|
+
throw (new Error('"getConversionRateAndFee" returned an account not configured for this server'));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return (rateAndFee);
|
|
572
|
+
}
|
|
485
573
|
/**
|
|
486
574
|
* Setup the request handler for an estimate request
|
|
487
575
|
*/
|
|
@@ -493,17 +581,37 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
493
581
|
throw (new Error('POST data missing request'));
|
|
494
582
|
}
|
|
495
583
|
const conversion = assertConversionInputCanonicalJSON(postData.request);
|
|
496
|
-
const rateAndFee = await
|
|
584
|
+
const rateAndFee = await getUnsignedQuoteData(conversion, 'estimate');
|
|
585
|
+
let requiresQuoteBody;
|
|
586
|
+
if (instance.quoteConfiguration.requiresQuote) {
|
|
587
|
+
requiresQuoteBody = { requiresQuote: true };
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
if (rateAndFee.convertedAmountBound === undefined) {
|
|
591
|
+
instance.logger.warn('POST /api/getEstimate', 'FX configuration indicates quotes are not required, but "convertedAmountBound" was not provided in the rate and fee response');
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
if (conversion.affinity === 'to' && (BigInt(conversion.amount) > rateAndFee.convertedAmountBound)) {
|
|
595
|
+
throw (new KeetaAnchorError('Affinity is to, but bound is less than estimated sent amount'));
|
|
596
|
+
}
|
|
597
|
+
if (conversion.affinity === 'from' && (BigInt(conversion.amount) < rateAndFee.convertedAmountBound)) {
|
|
598
|
+
throw (new KeetaAnchorError('Affinity is from, but bound is greater than estimated received amount'));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
requiresQuoteBody = { requiresQuote: false, account: rateAndFee.account };
|
|
602
|
+
}
|
|
497
603
|
const estimateResponse = {
|
|
498
604
|
ok: true,
|
|
499
605
|
estimate: KeetaNet.lib.Utils.Conversion.toJSONSerializable({
|
|
500
606
|
request: conversion,
|
|
501
607
|
convertedAmount: rateAndFee.convertedAmount,
|
|
608
|
+
convertedAmountBound: rateAndFee.convertedAmountBound,
|
|
502
609
|
expectedCost: {
|
|
503
610
|
min: rateAndFee.cost.amount,
|
|
504
611
|
max: rateAndFee.cost.amount,
|
|
505
612
|
token: rateAndFee.cost.token
|
|
506
|
-
}
|
|
613
|
+
},
|
|
614
|
+
...requiresQuoteBody
|
|
507
615
|
})
|
|
508
616
|
};
|
|
509
617
|
return ({
|
|
@@ -511,6 +619,9 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
511
619
|
});
|
|
512
620
|
};
|
|
513
621
|
routes['POST /api/getQuote'] = async function (_ignore_params, postData) {
|
|
622
|
+
if (!instance.quoteConfiguration.requiresQuote && !instance.quoteConfiguration.issueQuotes) {
|
|
623
|
+
throw (new Errors.QuoteIssuanceDisabled());
|
|
624
|
+
}
|
|
514
625
|
if (!postData || typeof postData !== 'object') {
|
|
515
626
|
throw (new Error('No POST data provided'));
|
|
516
627
|
}
|
|
@@ -518,17 +629,14 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
518
629
|
throw (new Error('POST data missing request'));
|
|
519
630
|
}
|
|
520
631
|
const conversion = assertConversionInputCanonicalJSON(postData.request);
|
|
521
|
-
const rateAndFee = await
|
|
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
|
-
}
|
|
632
|
+
const rateAndFee = await getUnsignedQuoteData(conversion, 'quote');
|
|
528
633
|
const unsignedQuote = KeetaNet.lib.Utils.Conversion.toJSONSerializable({
|
|
529
634
|
request: conversion,
|
|
530
635
|
...rateAndFee
|
|
531
636
|
});
|
|
637
|
+
if (config.quoteSigner === null) {
|
|
638
|
+
throw (new Error('Quote signer not configured, this is required when issuing quotes'));
|
|
639
|
+
}
|
|
532
640
|
const signedQuote = await generateSignedQuote(config.quoteSigner, unsignedQuote);
|
|
533
641
|
const quoteResponse = {
|
|
534
642
|
ok: true,
|
|
@@ -545,71 +653,161 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
545
653
|
if (!('request' in postData)) {
|
|
546
654
|
throw (new Error('POST data missing request'));
|
|
547
655
|
}
|
|
548
|
-
const request = postData.request;
|
|
549
|
-
if (!request || typeof request !== 'object') {
|
|
550
|
-
throw (new Error('Request is not an object'));
|
|
551
|
-
}
|
|
552
|
-
if (!('quote' in request)) {
|
|
553
|
-
throw (new Error('Quote is missing from request'));
|
|
554
|
-
}
|
|
656
|
+
const request = assertKeetaFXAnchorClientCreateExchangeRequestJSON(postData.request);
|
|
555
657
|
if (!('block' in request) || typeof request.block !== 'string') {
|
|
556
658
|
throw (new Error('Block was not provided in exchange request'));
|
|
557
659
|
}
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
660
|
+
const block = new KeetaNet.lib.Block(request.block);
|
|
661
|
+
let quoteInput;
|
|
662
|
+
let conversionInput;
|
|
663
|
+
let shouldValidateQuote;
|
|
664
|
+
let liquidityAccount;
|
|
665
|
+
let expectedConversion;
|
|
666
|
+
let isQuoteBasedExchange;
|
|
667
|
+
if ('quote' in request && 'estimate' in request && request.quote && request.estimate) {
|
|
668
|
+
throw (new Error('Request cannot contain both quote and estimate'));
|
|
562
669
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
670
|
+
else if ('quote' in request && request.quote) {
|
|
671
|
+
isQuoteBasedExchange = true;
|
|
672
|
+
shouldValidateQuote = true;
|
|
673
|
+
quoteInput = request.quote;
|
|
674
|
+
conversionInput = quoteInput.request;
|
|
675
|
+
const isValidQuote = await (async () => {
|
|
676
|
+
if (config.quoteSigner === null) {
|
|
677
|
+
return (false);
|
|
678
|
+
}
|
|
679
|
+
return (await verifySignedData(config.quoteSigner, quoteInput));
|
|
680
|
+
})();
|
|
681
|
+
if (!isValidQuote) {
|
|
567
682
|
throw (new Errors.QuoteValidationFailed());
|
|
568
683
|
}
|
|
684
|
+
liquidityAccount = quoteInput.account;
|
|
685
|
+
expectedConversion = convertQuoteToExpectedSwapWithoutCost({
|
|
686
|
+
quote: toValidateQuoteInput(quoteInput),
|
|
687
|
+
request: conversionInput
|
|
688
|
+
});
|
|
569
689
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
/* If token is different then check block operations for matching amount and token */
|
|
690
|
+
else if ('request' in request && request.request) {
|
|
691
|
+
isQuoteBasedExchange = false;
|
|
692
|
+
if (instance.quoteConfiguration.requiresQuote) {
|
|
693
|
+
throw (new Errors.QuoteRequired());
|
|
694
|
+
}
|
|
695
|
+
conversionInput = request.request;
|
|
696
|
+
quoteInput = await getUnsignedQuoteData(conversionInput, 'estimate');
|
|
697
|
+
if (instance.quoteConfiguration.validateQuoteBeforeExchange !== undefined) {
|
|
698
|
+
shouldValidateQuote = instance.quoteConfiguration.validateQuoteBeforeExchange;
|
|
580
699
|
}
|
|
581
700
|
else {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
701
|
+
shouldValidateQuote = config.fx.validateQuote !== undefined;
|
|
702
|
+
}
|
|
703
|
+
for (const operation of block.operations) {
|
|
704
|
+
if (operation.type === KeetaNet.lib.Block.OperationType.SEND) {
|
|
705
|
+
if (!config.accounts) {
|
|
706
|
+
throw (new Error('No accounts configured for FX server, cannot infer liquidity account from block'));
|
|
707
|
+
}
|
|
708
|
+
if (config.accounts.has(operation.to)) {
|
|
709
|
+
liquidityAccount = operation.to;
|
|
710
|
+
break;
|
|
591
711
|
}
|
|
592
712
|
}
|
|
593
|
-
if (!requestIncludesCost) {
|
|
594
|
-
throw (new Error('Exchange missing required cost'));
|
|
595
|
-
}
|
|
596
713
|
}
|
|
714
|
+
if (!liquidityAccount) {
|
|
715
|
+
throw (new KeetaAnchorUserError('Could not determine liquidity account from exchange block'));
|
|
716
|
+
}
|
|
717
|
+
// No expected conversion provided when using estimate, we determine the rate when processing the exchange
|
|
718
|
+
expectedConversion = null;
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
throw (new Error('Either quote or request must be provided (but not both) in exchange request'));
|
|
722
|
+
}
|
|
723
|
+
const parsedQuote = toValidateQuoteInput(quoteInput);
|
|
724
|
+
/* Validate the quote using the optional callback */
|
|
725
|
+
if (config.fx.validateQuote !== undefined && shouldValidateQuote) {
|
|
726
|
+
const isAcceptable = await config.fx.validateQuote(parsedQuote);
|
|
727
|
+
if (!isAcceptable) {
|
|
728
|
+
throw (new Errors.QuoteValidationFailed());
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const liquidityAccountInstance = KeetaNet.lib.Account.toAccount(liquidityAccount);
|
|
732
|
+
let allowedLiquidityAccounts;
|
|
733
|
+
if (config.accounts) {
|
|
734
|
+
allowedLiquidityAccounts = config.accounts;
|
|
735
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
736
|
+
}
|
|
737
|
+
else if (config.account) {
|
|
738
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
739
|
+
allowedLiquidityAccounts = new KeetaNet.lib.Account.Set([config.account]);
|
|
597
740
|
}
|
|
741
|
+
else {
|
|
742
|
+
throw (new Error('config.account or config.accounts must be provided'));
|
|
743
|
+
}
|
|
744
|
+
assertExchangeBlockParametersAndComputeRefund({
|
|
745
|
+
block: block,
|
|
746
|
+
liquidityAccount: liquidityAccountInstance,
|
|
747
|
+
allowedLiquidityAccounts: allowedLiquidityAccounts,
|
|
748
|
+
checks: { quote: parsedQuote, request: conversionInput },
|
|
749
|
+
isQuoteBasedExchange: isQuoteBasedExchange
|
|
750
|
+
});
|
|
598
751
|
/* Enqueue the exchange request */
|
|
599
752
|
const exchangeID = await instance.pipeline.add({
|
|
600
|
-
account:
|
|
753
|
+
account: liquidityAccountInstance,
|
|
601
754
|
block: block,
|
|
602
|
-
request:
|
|
603
|
-
expected:
|
|
604
|
-
token: expectedToken,
|
|
605
|
-
amount: BigInt(expectedAmount)
|
|
606
|
-
}
|
|
755
|
+
request: conversionInput,
|
|
756
|
+
expected: expectedConversion
|
|
607
757
|
});
|
|
608
758
|
const exchangeResponse = {
|
|
609
759
|
ok: true,
|
|
610
760
|
exchangeID: exchangeID.toString(),
|
|
611
761
|
status: 'pending'
|
|
612
762
|
};
|
|
763
|
+
if (instance.autoRun && !instance.autoRunRunning) {
|
|
764
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
765
|
+
try {
|
|
766
|
+
/*
|
|
767
|
+
* Keep track of how many times, consecutively, the queue was empty when we
|
|
768
|
+
* went to run it
|
|
769
|
+
*/
|
|
770
|
+
let noMoreJobsCount = 0;
|
|
771
|
+
/*
|
|
772
|
+
* Create a mutex around the queue running so we don't have
|
|
773
|
+
* lock contention for the worker ID if multiple requests
|
|
774
|
+
* are being served by the same instance
|
|
775
|
+
*/
|
|
776
|
+
instance.autoRunRunning = true;
|
|
777
|
+
const cleanup = __addDisposableResource(env_1, new AsyncDisposableStack(), true);
|
|
778
|
+
cleanup.defer(async function () {
|
|
779
|
+
instance.autoRunRunning = false;
|
|
780
|
+
});
|
|
781
|
+
/*
|
|
782
|
+
* For up to 15s process the queue, stopping only when
|
|
783
|
+
* we've had 2 consecutive runs that indicate there is
|
|
784
|
+
* no more in the queue to process
|
|
785
|
+
*/
|
|
786
|
+
for (const startTime = Date.now(); Date.now() - startTime < 15000;) {
|
|
787
|
+
const more = await instance.pipeline.run({ timeoutMs: 1500 });
|
|
788
|
+
if (!more) {
|
|
789
|
+
noMoreJobsCount++;
|
|
790
|
+
if (noMoreJobsCount >= 2) {
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
noMoreJobsCount = 0;
|
|
796
|
+
}
|
|
797
|
+
await instance.pipeline.maintain();
|
|
798
|
+
await asleep(100);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch (e_1) {
|
|
802
|
+
env_1.error = e_1;
|
|
803
|
+
env_1.hasError = true;
|
|
804
|
+
}
|
|
805
|
+
finally {
|
|
806
|
+
const result_1 = __disposeResources(env_1);
|
|
807
|
+
if (result_1)
|
|
808
|
+
await result_1;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
613
811
|
return ({
|
|
614
812
|
output: JSON.stringify(exchangeResponse)
|
|
615
813
|
});
|
|
@@ -695,10 +893,6 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
695
893
|
});
|
|
696
894
|
}
|
|
697
895
|
async stop() {
|
|
698
|
-
if (this.pipelineAutoRunInterval !== null) {
|
|
699
|
-
clearInterval(this.pipelineAutoRunInterval);
|
|
700
|
-
this.pipelineAutoRunInterval = null;
|
|
701
|
-
}
|
|
702
896
|
await this.pipeline.destroy();
|
|
703
897
|
await super.stop();
|
|
704
898
|
}
|
|
@@ -709,7 +903,7 @@ export class KeetaNetFXAnchorHTTPServer extends KeetaAnchorHTTPServer.KeetaNetAn
|
|
|
709
903
|
* in a ".generated.ts" file but for simplicity of internal types
|
|
710
904
|
* we keep them here.
|
|
711
905
|
*/
|
|
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, {
|
|
906
|
+
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)) && (null === input.expected || "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 => "object" === typeof input.receive && null !== input.receive && _io3(input.receive) && ("object" === typeof input.send && null !== input.send && _io4(input.send)); const _io3 = input => "string" === typeof input.token && (RegExp(/^keeta_am(.*)/).test(input.token) || RegExp(/^keeta_an(.*)/).test(input.token) || RegExp(/^keeta_ao(.*)/).test(input.token) || RegExp(/^keeta_ap(.*)/).test(input.token) || RegExp(/^tyblocks_am(.*)/).test(input.token) || RegExp(/^tyblocks_an(.*)/).test(input.token) || RegExp(/^tyblocks_ao(.*)/).test(input.token) || RegExp(/^tyblocks_ap(.*)/).test(input.token)) && "string" === typeof input.amount; const _io4 = input => "string" === typeof input.token && (RegExp(/^keeta_am(.*)/).test(input.token) || RegExp(/^keeta_an(.*)/).test(input.token) || RegExp(/^keeta_ao(.*)/).test(input.token) || RegExp(/^keeta_ap(.*)/).test(input.token) || RegExp(/^tyblocks_am(.*)/).test(input.token) || RegExp(/^tyblocks_an(.*)/).test(input.token) || RegExp(/^tyblocks_ao(.*)/).test(input.token) || RegExp(/^tyblocks_ap(.*)/).test(input.token)) && "string" === typeof input.amount; const _ao0 = (input, _path, _exceptionable = true) => (1 === input.version || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
713
907
|
method: "typia.createAssert",
|
|
714
908
|
path: _path + ".version",
|
|
715
909
|
expected: "1",
|
|
@@ -734,15 +928,15 @@ const assertKeetaFXAnchorQueueStage1RequestJSON = (() => { const _io0 = input =>
|
|
|
734
928
|
path: _path + ".request",
|
|
735
929
|
expected: "__type",
|
|
736
930
|
value: input.request
|
|
737
|
-
}, _errorFactory)) && (("object" === typeof input.expected && null !== input.expected || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
931
|
+
}, _errorFactory)) && (null === input.expected || ("object" === typeof input.expected && null !== input.expected || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
738
932
|
method: "typia.createAssert",
|
|
739
933
|
path: _path + ".expected",
|
|
740
|
-
expected: "__type.o1",
|
|
934
|
+
expected: "(__type.o1 | null)",
|
|
741
935
|
value: input.expected
|
|
742
936
|
}, _errorFactory)) && _ao2(input.expected, _path + ".expected", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
743
937
|
method: "typia.createAssert",
|
|
744
938
|
path: _path + ".expected",
|
|
745
|
-
expected: "__type.o1",
|
|
939
|
+
expected: "(__type.o1 | null)",
|
|
746
940
|
value: input.expected
|
|
747
941
|
}, _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
942
|
method: "typia.createAssert",
|
|
@@ -764,10 +958,40 @@ const assertKeetaFXAnchorQueueStage1RequestJSON = (() => { const _io0 = input =>
|
|
|
764
958
|
path: _path + ".affinity",
|
|
765
959
|
expected: "(\"from\" | \"to\")",
|
|
766
960
|
value: input.affinity
|
|
767
|
-
}, _errorFactory)); const _ao2 = (input, _path, _exceptionable = true) => ("
|
|
961
|
+
}, _errorFactory)); const _ao2 = (input, _path, _exceptionable = true) => (("object" === typeof input.receive && null !== input.receive || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
962
|
+
method: "typia.createAssert",
|
|
963
|
+
path: _path + ".receive",
|
|
964
|
+
expected: "__type.o2",
|
|
965
|
+
value: input.receive
|
|
966
|
+
}, _errorFactory)) && _ao3(input.receive, _path + ".receive", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
967
|
+
method: "typia.createAssert",
|
|
968
|
+
path: _path + ".receive",
|
|
969
|
+
expected: "__type.o2",
|
|
970
|
+
value: input.receive
|
|
971
|
+
}, _errorFactory)) && (("object" === typeof input.send && null !== input.send || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
972
|
+
method: "typia.createAssert",
|
|
973
|
+
path: _path + ".send",
|
|
974
|
+
expected: "__type.o3",
|
|
975
|
+
value: input.send
|
|
976
|
+
}, _errorFactory)) && _ao4(input.send, _path + ".send", true && _exceptionable) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
977
|
+
method: "typia.createAssert",
|
|
978
|
+
path: _path + ".send",
|
|
979
|
+
expected: "__type.o3",
|
|
980
|
+
value: input.send
|
|
981
|
+
}, _errorFactory)); const _ao3 = (input, _path, _exceptionable = true) => ("string" === typeof input.token && (RegExp(/^keeta_am(.*)/).test(input.token) || RegExp(/^keeta_an(.*)/).test(input.token) || RegExp(/^keeta_ao(.*)/).test(input.token) || RegExp(/^keeta_ap(.*)/).test(input.token) || RegExp(/^tyblocks_am(.*)/).test(input.token) || RegExp(/^tyblocks_an(.*)/).test(input.token) || RegExp(/^tyblocks_ao(.*)/).test(input.token) || RegExp(/^tyblocks_ap(.*)/).test(input.token)) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
768
982
|
method: "typia.createAssert",
|
|
769
983
|
path: _path + ".token",
|
|
984
|
+
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}`)",
|
|
985
|
+
value: input.token
|
|
986
|
+
}, _errorFactory)) && ("string" === typeof input.amount || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
987
|
+
method: "typia.createAssert",
|
|
988
|
+
path: _path + ".amount",
|
|
770
989
|
expected: "string",
|
|
990
|
+
value: input.amount
|
|
991
|
+
}, _errorFactory)); const _ao4 = (input, _path, _exceptionable = true) => ("string" === typeof input.token && (RegExp(/^keeta_am(.*)/).test(input.token) || RegExp(/^keeta_an(.*)/).test(input.token) || RegExp(/^keeta_ao(.*)/).test(input.token) || RegExp(/^keeta_ap(.*)/).test(input.token) || RegExp(/^tyblocks_am(.*)/).test(input.token) || RegExp(/^tyblocks_an(.*)/).test(input.token) || RegExp(/^tyblocks_ao(.*)/).test(input.token) || RegExp(/^tyblocks_ap(.*)/).test(input.token)) || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
992
|
+
method: "typia.createAssert",
|
|
993
|
+
path: _path + ".token",
|
|
994
|
+
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}`)",
|
|
771
995
|
value: input.token
|
|
772
996
|
}, _errorFactory)) && ("string" === typeof input.amount || __typia_transform__assertGuard._assertGuard(_exceptionable, {
|
|
773
997
|
method: "typia.createAssert",
|