@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.
Files changed (52) hide show
  1. package/lib/encrypted-container.d.ts +53 -3
  2. package/lib/encrypted-container.d.ts.map +1 -1
  3. package/lib/encrypted-container.js +549 -93
  4. package/lib/encrypted-container.js.map +1 -1
  5. package/lib/http-server/index.d.ts.map +1 -1
  6. package/lib/http-server/index.js +58 -5
  7. package/lib/http-server/index.js.map +1 -1
  8. package/lib/queue/drivers/queue_firestore.d.ts +29 -0
  9. package/lib/queue/drivers/queue_firestore.d.ts.map +1 -0
  10. package/lib/queue/drivers/queue_firestore.js +279 -0
  11. package/lib/queue/drivers/queue_firestore.js.map +1 -0
  12. package/lib/queue/index.d.ts +57 -0
  13. package/lib/queue/index.d.ts.map +1 -1
  14. package/lib/queue/index.js +127 -21
  15. package/lib/queue/index.js.map +1 -1
  16. package/lib/resolver.d.ts +4 -15
  17. package/lib/resolver.d.ts.map +1 -1
  18. package/lib/resolver.js +468 -636
  19. package/lib/resolver.js.map +1 -1
  20. package/lib/utils/signing.d.ts +12 -3
  21. package/lib/utils/signing.d.ts.map +1 -1
  22. package/lib/utils/signing.js +7 -13
  23. package/lib/utils/signing.js.map +1 -1
  24. package/lib/utils/types.d.ts +14 -2
  25. package/lib/utils/types.d.ts.map +1 -1
  26. package/lib/utils/types.js.map +1 -1
  27. package/npm-shrinkwrap.json +7 -7
  28. package/package.json +3 -2
  29. package/services/asset-movement/client.d.ts +2 -2
  30. package/services/asset-movement/client.d.ts.map +1 -1
  31. package/services/asset-movement/client.js +2 -2
  32. package/services/asset-movement/client.js.map +1 -1
  33. package/services/asset-movement/common.d.ts +201 -24
  34. package/services/asset-movement/common.d.ts.map +1 -1
  35. package/services/asset-movement/common.js +305 -80
  36. package/services/asset-movement/common.js.map +1 -1
  37. package/services/fx/client.d.ts +38 -11
  38. package/services/fx/client.d.ts.map +1 -1
  39. package/services/fx/client.js +187 -42
  40. package/services/fx/client.js.map +1 -1
  41. package/services/fx/common.d.ts +55 -6
  42. package/services/fx/common.d.ts.map +1 -1
  43. package/services/fx/common.js +142 -16
  44. package/services/fx/common.js.map +1 -1
  45. package/services/fx/server.d.ts +51 -7
  46. package/services/fx/server.d.ts.map +1 -1
  47. package/services/fx/server.js +333 -109
  48. package/services/fx/server.js.map +1 -1
  49. package/services/fx/util.d.ts +31 -0
  50. package/services/fx/util.d.ts.map +1 -0
  51. package/services/fx/util.js +132 -0
  52. package/services/fx/util.js.map +1 -0
@@ -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, assertConversionQuoteJSON, Errors } from './common.js';
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, expected, request } = entry.request;
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 swapBlocks = await userClient.acceptSwapRequest({ block, expected: { token: expectedToken, amount: BigInt(expectedAmount) } });
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
- const publishResult = await userClient.client.transmit(swapBlocks, publishOptions);
215
- if (!publishResult.publish) {
216
- throw (new Error('Exchange Publish Failed'));
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
- pipelineAutoRunInterval = null;
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
- let autorun = config.storage?.autoRun ?? false;
515
+ this.autoRun = config.storage?.autoRun ?? false;
411
516
  if (config.storage === undefined) {
412
- autorun = true;
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 config.fx.getConversionRateAndFee(conversion);
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 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
- }
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 quote = assertConversionQuoteJSON(request.quote);
559
- const isValidQuote = await verifySignedData(config.quoteSigner, quote);
560
- if (!isValidQuote) {
561
- throw (new Error('Invalid quote signature'));
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
- /* Validate the quote using the optional callback */
564
- if (config.fx.validateQuote !== undefined) {
565
- const isAcceptable = await config.fx.validateQuote(quote);
566
- if (!isAcceptable) {
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
- const block = new KeetaNet.lib.Block(request.block);
571
- /* Get Expected Amount and Token to Verify Swap */
572
- const expectedToken = KeetaNet.lib.Account.fromPublicKeyString(quote.request.from);
573
- let expectedAmount = quote.request.affinity === 'from' ? BigInt(quote.request.amount) : BigInt(quote.convertedAmount);
574
- /* If cost is required verify the amounts and token. */
575
- if (BigInt(quote.cost.amount) > 0) {
576
- /* If swap token matches the cost token the add the amount since they should be combined in one block and will be checked in `acceptSwapRequest` */
577
- if (expectedToken.comparePublicKey(quote.cost.token)) {
578
- expectedAmount += BigInt(quote.cost.amount);
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
- let requestIncludesCost = false;
583
- for (const operation of block.operations) {
584
- if (operation.type === KeetaNet.lib.Block.OperationType.SEND) {
585
- const recipientMatches = operation.to.comparePublicKey(quote.account);
586
- const tokenMatches = operation.token.comparePublicKey(quote.cost.token);
587
- const amountMatches = operation.amount === BigInt(quote.cost.amount);
588
- if (recipientMatches && tokenMatches && amountMatches) {
589
- requestIncludesCost = true;
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: KeetaNet.lib.Account.fromPublicKeyString(quote.account),
753
+ account: liquidityAccountInstance,
601
754
  block: block,
602
- request: quote.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) => ("string" === typeof input.token || __typia_transform__assertGuard._assertGuard(_exceptionable, {
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",