@flashnet/sdk 0.3.24 → 0.3.26

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 (56) hide show
  1. package/dist/cjs/index.d.ts +2 -2
  2. package/dist/cjs/index.d.ts.map +1 -1
  3. package/dist/cjs/src/api/typed-endpoints.d.ts +15 -0
  4. package/dist/cjs/src/api/typed-endpoints.d.ts.map +1 -1
  5. package/dist/cjs/src/api/typed-endpoints.js +22 -0
  6. package/dist/cjs/src/api/typed-endpoints.js.map +1 -1
  7. package/dist/cjs/src/client/FlashnetClient.d.ts +18 -0
  8. package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
  9. package/dist/cjs/src/client/FlashnetClient.js +338 -88
  10. package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
  11. package/dist/cjs/src/config/index.js +2 -2
  12. package/dist/cjs/src/config/index.js.map +1 -1
  13. package/dist/cjs/src/types/index.d.ts +19 -0
  14. package/dist/cjs/src/types/index.d.ts.map +1 -1
  15. package/dist/cjs/src/types/index.js.map +1 -1
  16. package/dist/cjs/src/utils/auth.d.ts.map +1 -1
  17. package/dist/cjs/src/utils/auth.js +5 -3
  18. package/dist/cjs/src/utils/auth.js.map +1 -1
  19. package/dist/cjs/src/utils/hex.d.ts +3 -0
  20. package/dist/cjs/src/utils/hex.d.ts.map +1 -0
  21. package/dist/cjs/src/utils/hex.js +17 -0
  22. package/dist/cjs/src/utils/hex.js.map +1 -0
  23. package/dist/cjs/src/utils/index.js +2 -2
  24. package/dist/cjs/src/utils/spark-address.js +16 -16
  25. package/dist/cjs/src/utils/spark-address.js.map +1 -1
  26. package/dist/cjs/src/utils/tokenAddress.d.ts.map +1 -1
  27. package/dist/cjs/src/utils/tokenAddress.js +8 -8
  28. package/dist/cjs/src/utils/tokenAddress.js.map +1 -1
  29. package/dist/esm/index.d.ts +2 -2
  30. package/dist/esm/index.d.ts.map +1 -1
  31. package/dist/esm/src/api/typed-endpoints.d.ts +15 -0
  32. package/dist/esm/src/api/typed-endpoints.d.ts.map +1 -1
  33. package/dist/esm/src/api/typed-endpoints.js +22 -0
  34. package/dist/esm/src/api/typed-endpoints.js.map +1 -1
  35. package/dist/esm/src/client/FlashnetClient.d.ts +18 -0
  36. package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
  37. package/dist/esm/src/client/FlashnetClient.js +338 -88
  38. package/dist/esm/src/client/FlashnetClient.js.map +1 -1
  39. package/dist/esm/src/config/index.js +2 -2
  40. package/dist/esm/src/config/index.js.map +1 -1
  41. package/dist/esm/src/types/index.d.ts +19 -0
  42. package/dist/esm/src/types/index.d.ts.map +1 -1
  43. package/dist/esm/src/types/index.js.map +1 -1
  44. package/dist/esm/src/utils/auth.d.ts.map +1 -1
  45. package/dist/esm/src/utils/auth.js +5 -3
  46. package/dist/esm/src/utils/auth.js.map +1 -1
  47. package/dist/esm/src/utils/hex.d.ts +3 -0
  48. package/dist/esm/src/utils/hex.d.ts.map +1 -0
  49. package/dist/esm/src/utils/hex.js +14 -0
  50. package/dist/esm/src/utils/hex.js.map +1 -0
  51. package/dist/esm/src/utils/index.js +2 -2
  52. package/dist/esm/src/utils/spark-address.js +8 -8
  53. package/dist/esm/src/utils/spark-address.js.map +1 -1
  54. package/dist/esm/src/utils/tokenAddress.d.ts.map +1 -1
  55. package/dist/esm/src/utils/tokenAddress.js +2 -2
  56. package/package.json +7 -7
@@ -4,6 +4,7 @@ import { getClientEnvironmentName, resolveClientNetworkConfig, getClientNetworkC
4
4
  import { getSparkNetworkFromLegacy, getClientEnvironmentFromLegacy, Network } from '../types/index.js';
5
5
  import { generateNonce, compareDecimalStrings } from '../utils/index.js';
6
6
  import { AuthManager } from '../utils/auth.js';
7
+ import { getHexFromUint8Array } from '../utils/hex.js';
7
8
  import { generateConstantProductPoolInitializationIntentMessage, generatePoolInitializationIntentMessage, generatePoolConfirmInitialDepositIntentMessage, generatePoolSwapIntentMessage, generateRouteSwapIntentMessage, generateAddLiquidityIntentMessage, generateRemoveLiquidityIntentMessage, generateRegisterHostIntentMessage, generateWithdrawHostFeesIntentMessage, generateWithdrawIntegratorFeesIntentMessage, generateCreateEscrowIntentMessage, generateFundEscrowIntentMessage, generateClaimEscrowIntentMessage, generateClawbackIntentMessage } from '../utils/intents.js';
8
9
  import { createWalletSigner } from '../utils/signer.js';
9
10
  import { getSparkNetworkFromAddress, encodeSparkAddressNew } from '../utils/spark-address.js';
@@ -29,6 +30,16 @@ class FlashnetClient {
29
30
  publicKey = "";
30
31
  sparkAddress = "";
31
32
  isAuthenticated = false;
33
+ // Ephemeral caches for config endpoints and ping
34
+ featureStatusCache;
35
+ minAmountsCache;
36
+ allowedAssetsCache;
37
+ pingCache;
38
+ // TTLs (milliseconds)
39
+ static FEATURE_STATUS_TTL_MS = 5000; // 5s
40
+ static MIN_AMOUNTS_TTL_MS = 5000; // 5s
41
+ static ALLOWED_ASSETS_TTL_MS = 60000; // 60s
42
+ static PING_TTL_MS = 2000; // 2s
32
43
  /**
33
44
  * Get the underlying wallet instance for direct wallet operations
34
45
  */
@@ -225,7 +236,7 @@ class FlashnetClient {
225
236
  for (const [tokenPubkey, tokenData] of balance.tokenBalances.entries()) {
226
237
  const info = tokenData.tokenMetadata;
227
238
  // Convert raw token identifier to hex and human-readable forms
228
- const tokenIdentifierHex = Buffer.from(info.rawTokenIdentifier).toString("hex");
239
+ const tokenIdentifierHex = getHexFromUint8Array(info.rawTokenIdentifier);
229
240
  const tokenAddress = encodeSparkHumanReadableTokenIdentifier(info.rawTokenIdentifier, this.sparkNetwork);
230
241
  tokenBalances.set(tokenPubkey, {
231
242
  balance: BigInt(tokenData.balance),
@@ -323,6 +334,8 @@ class FlashnetClient {
323
334
  */
324
335
  async createConstantProductPool(params) {
325
336
  await this.ensureInitialized();
337
+ await this.ensureAmmOperationAllowed("allow_pool_creation");
338
+ await this.assertAllowedAssetBForPoolCreation(this.toHexTokenIdentifier(params.assetBAddress));
326
339
  // Check if we need to add initial liquidity
327
340
  if (params.initialLiquidity) {
328
341
  await this.checkBalance({
@@ -362,7 +375,7 @@ class FlashnetClient {
362
375
  totalHostFeeRateBps: params.totalHostFeeRateBps.toString(),
363
376
  hostNamespace: params.hostNamespace || "",
364
377
  nonce,
365
- signature: Buffer.from(signature).toString("hex"),
378
+ signature: getHexFromUint8Array(signature),
366
379
  };
367
380
  const response = await this.typedApi.createConstantProductPool(request);
368
381
  // Add initial liquidity if specified
@@ -371,6 +384,31 @@ class FlashnetClient {
371
384
  }
372
385
  return response;
373
386
  }
387
+ // Validate and normalize inputs to bigint
388
+ static parsePositiveIntegerToBigInt(value, name) {
389
+ if (typeof value === "bigint") {
390
+ if (value <= 0n) {
391
+ throw new Error(`${name} must be positive integer`);
392
+ }
393
+ return value;
394
+ }
395
+ if (typeof value === "number") {
396
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
397
+ throw new Error(`${name} must be positive integer`);
398
+ }
399
+ return BigInt(value);
400
+ }
401
+ try {
402
+ const v = BigInt(value);
403
+ if (v <= 0n) {
404
+ throw new Error(`${name} must be positive integer`);
405
+ }
406
+ return v;
407
+ }
408
+ catch {
409
+ throw new Error(`${name} must be positive integer`);
410
+ }
411
+ }
374
412
  /**
375
413
  * Calculates virtual reserves for a bonding curve AMM.
376
414
  *
@@ -385,69 +423,36 @@ class FlashnetClient {
385
423
  * @returns An object containing `virtualReserveA`, `virtualReserveB`, and `threshold`.
386
424
  */
387
425
  static calculateVirtualReserves(params) {
388
- // Validate and normalize inputs to bigint
389
- const parsePositiveIntegerToBigInt = (value, name) => {
390
- if (typeof value === "bigint") {
391
- if (value <= 0n) {
392
- throw new Error(`${name} must be positive integer`);
393
- }
394
- return value;
395
- }
396
- if (typeof value === "number") {
397
- if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
398
- throw new Error(`${name} must be positive integer`);
399
- }
400
- return BigInt(value);
401
- }
402
- try {
403
- const v = BigInt(value);
404
- if (v <= 0n) {
405
- throw new Error(`${name} must be positive integer`);
406
- }
407
- return v;
408
- }
409
- catch {
410
- throw new Error(`${name} must be positive integer`);
411
- }
412
- };
413
426
  if (!Number.isFinite(params.graduationThresholdPct) ||
414
- !Number.isInteger(params.graduationThresholdPct) ||
415
- params.graduationThresholdPct <= 0) {
416
- throw new Error("Graduation threshold percentage must be a positive integer");
427
+ !Number.isInteger(params.graduationThresholdPct)) {
428
+ throw new Error("Graduation threshold percentage must be an integer number of percent");
417
429
  }
418
- const supply = parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
419
- const targetB = parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
430
+ const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
431
+ const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
420
432
  const graduationThresholdPct = BigInt(params.graduationThresholdPct);
421
- // Check feasibility: f - g*(1-f) > 0 where f is graduationThresholdPct/100 and g is 1
422
- const MIN_GRADUATION_THRESHOLD_PCT = 50n;
423
- const MAX_GRADUATION_THRESHOLD_PCT = 95n;
424
- if (graduationThresholdPct <= MIN_GRADUATION_THRESHOLD_PCT ||
425
- graduationThresholdPct > MAX_GRADUATION_THRESHOLD_PCT) {
426
- throw new Error(`Graduation threshold percentage must be greater than ${MIN_GRADUATION_THRESHOLD_PCT} ` +
427
- `and not greater than ${MAX_GRADUATION_THRESHOLD_PCT}`);
428
- }
429
- // f is graduationThresholdPct / 100n
430
- // denom is (2n * graduationThresholdPct / 100n) - 1n
431
- // (2n * graduationThresholdPct - 100n) / 100n
432
- // 1/denom is 100n / (2n * graduationThresholdPct - 100n)
433
- // Calculate virtual reserves and round down to integers
434
- // virtualA is (supply * f * f) / denom
435
- const virtualANumerator = BigInt(supply) * graduationThresholdPct * graduationThresholdPct;
436
- const virtualADenominator = 200n * graduationThresholdPct - 10000n;
437
- const virtualARemainder = virtualANumerator % virtualADenominator;
438
- const virtualA = (virtualANumerator - virtualARemainder) / virtualADenominator;
439
- // virtualB is (targetB * g * (1 - f)) / denom
440
- const virtualBNumerator = BigInt(targetB) * (100n - graduationThresholdPct);
441
- const virtualBDenominator = 2n * graduationThresholdPct - 100n;
442
- const virtualBRemainder = virtualBNumerator % virtualBDenominator;
443
- const virtualB = (virtualBNumerator - virtualBRemainder) / virtualBDenominator;
444
- // Calculate threshold as amount of asset A (not percentage)
445
- const threshold = (BigInt(supply) * graduationThresholdPct) / 100n;
446
- return {
447
- virtualReserveA: virtualA,
448
- virtualReserveB: virtualB,
449
- threshold: threshold,
450
- };
433
+ // Align bounds with Rust AMM (20%..95%), then check feasibility for g=1 (requires >50%).
434
+ const MIN_PCT = 20n;
435
+ const MAX_PCT = 95n;
436
+ if (graduationThresholdPct < MIN_PCT || graduationThresholdPct > MAX_PCT) {
437
+ throw new Error(`Graduation threshold percentage must be between ${MIN_PCT} and ${MAX_PCT}`);
438
+ }
439
+ // Feasibility: denom = f - g*(1-f) > 0 with g=1 -> 2f - 1 > 0 -> pct > 50
440
+ const denomNormalized = 2n * graduationThresholdPct - 100n; // equals 100*(f - (1-f))
441
+ if (denomNormalized <= 0n) {
442
+ throw new Error("Invalid configuration: threshold must be greater than 50% when LP fraction is 1.0");
443
+ }
444
+ // v_A = S * f^2 / (f - (1-f)) ; using integer math with pct where
445
+ // v_A = S * p^2 / (100 * (2p - 100))
446
+ const vANumerator = supply * graduationThresholdPct * graduationThresholdPct;
447
+ const vADenominator = 100n * denomNormalized;
448
+ const virtualA = vANumerator / vADenominator; // floor
449
+ // v_B = T * (1 - f) / (f - (1-f)) ; with pct => T * (100 - p) / (2p - 100)
450
+ const vBNumerator = targetB * (100n - graduationThresholdPct);
451
+ const vBDenominator = denomNormalized;
452
+ const virtualB = vBNumerator / vBDenominator; // floor
453
+ // Threshold amount in A
454
+ const threshold = (supply * graduationThresholdPct) / 100n;
455
+ return { virtualReserveA: virtualA, virtualReserveB: virtualB, threshold };
451
456
  }
452
457
  /**
453
458
  * Create a single-sided pool with automatic initial deposit
@@ -457,18 +462,20 @@ class FlashnetClient {
457
462
  */
458
463
  async createSingleSidedPool(params) {
459
464
  await this.ensureInitialized();
465
+ await this.ensureAmmOperationAllowed("allow_pool_creation");
466
+ await this.assertAllowedAssetBForPoolCreation(this.toHexTokenIdentifier(params.assetBAddress));
460
467
  if (!params.hostNamespace && params.totalHostFeeRateBps < 10) {
461
468
  throw new Error(`Host fee must be greater than 10 bps when no host namespace is provided`);
462
469
  }
463
- // Clip decimals off reserves before any operations
464
- const clippedAssetAInitialReserve = Math.floor(Number(params.assetAInitialReserve)).toString();
465
- const clippedVirtualReserveA = Math.floor(Number(params.virtualReserveA)).toString();
466
- const clippedVirtualReserveB = Math.floor(Number(params.virtualReserveB)).toString();
470
+ // Validate reserves are valid positive integers before any operations
471
+ const assetAInitialReserve = FlashnetClient.parsePositiveIntegerToBigInt(params.assetAInitialReserve, "Asset A Initial Reserve").toString();
472
+ const virtualReserveA = FlashnetClient.parsePositiveIntegerToBigInt(params.virtualReserveA, "Virtual Reserve A").toString();
473
+ const virtualReserveB = FlashnetClient.parsePositiveIntegerToBigInt(params.virtualReserveB, "Virtual Reserve B").toString();
467
474
  await this.checkBalance({
468
475
  balancesToCheck: [
469
476
  {
470
477
  assetAddress: params.assetAAddress,
471
- amount: clippedAssetAInitialReserve,
478
+ amount: assetAInitialReserve,
472
479
  },
473
480
  ],
474
481
  errorPrefix: "Insufficient balance for pool creation: ",
@@ -480,9 +487,9 @@ class FlashnetClient {
480
487
  poolOwnerPublicKey,
481
488
  assetAAddress: this.toHexTokenIdentifier(params.assetAAddress),
482
489
  assetBAddress: this.toHexTokenIdentifier(params.assetBAddress),
483
- assetAInitialReserve: clippedAssetAInitialReserve,
484
- virtualReserveA: clippedVirtualReserveA,
485
- virtualReserveB: clippedVirtualReserveB,
490
+ assetAInitialReserve,
491
+ virtualReserveA,
492
+ virtualReserveB,
486
493
  threshold: params.threshold.toString(),
487
494
  lpFeeRateBps: params.lpFeeRateBps.toString(),
488
495
  totalHostFeeRateBps: params.totalHostFeeRateBps.toString(),
@@ -495,15 +502,15 @@ class FlashnetClient {
495
502
  poolOwnerPublicKey,
496
503
  assetAAddress: this.toHexTokenIdentifier(params.assetAAddress),
497
504
  assetBAddress: this.toHexTokenIdentifier(params.assetBAddress),
498
- assetAInitialReserve: clippedAssetAInitialReserve,
499
- virtualReserveA: clippedVirtualReserveA,
500
- virtualReserveB: clippedVirtualReserveB,
505
+ assetAInitialReserve,
506
+ virtualReserveA,
507
+ virtualReserveB,
501
508
  threshold: params.threshold.toString(),
502
509
  lpFeeRateBps: params.lpFeeRateBps.toString(),
503
510
  totalHostFeeRateBps: params.totalHostFeeRateBps.toString(),
504
511
  hostNamespace: params.hostNamespace,
505
512
  nonce,
506
- signature: Buffer.from(signature).toString("hex"),
513
+ signature: getHexFromUint8Array(signature),
507
514
  };
508
515
  const createResponse = await this.typedApi.createSingleSidedPool(request);
509
516
  if (params.disableInitialDeposit) {
@@ -517,7 +524,7 @@ class FlashnetClient {
517
524
  const assetATransferId = await this.transferAsset({
518
525
  receiverSparkAddress: lpSparkAddress,
519
526
  assetAddress: params.assetAAddress,
520
- amount: clippedAssetAInitialReserve,
527
+ amount: assetAInitialReserve,
521
528
  });
522
529
  const confirmResponse = await this.confirmInitialDeposit(createResponse.poolId, assetATransferId, poolOwnerPublicKey);
523
530
  if (!confirmResponse.confirmed) {
@@ -546,7 +553,7 @@ class FlashnetClient {
546
553
  poolId,
547
554
  assetASparkTransferId,
548
555
  nonce,
549
- signature: Buffer.from(signature).toString("hex"),
556
+ signature: getHexFromUint8Array(signature),
550
557
  poolOwnerPublicKey: poolOwnerPublicKey ?? this.publicKey,
551
558
  };
552
559
  return this.typedApi.confirmInitialDeposit(request);
@@ -557,6 +564,7 @@ class FlashnetClient {
557
564
  */
558
565
  async simulateSwap(params) {
559
566
  await this.ensureInitialized();
567
+ await this.ensurePingOk();
560
568
  return this.typedApi.simulateSwap(params);
561
569
  }
562
570
  /**
@@ -564,6 +572,14 @@ class FlashnetClient {
564
572
  */
565
573
  async executeSwap(params) {
566
574
  await this.ensureInitialized();
575
+ // Gate by feature flags and ping, and enforce min-amount policy before transfers
576
+ await this.ensureAmmOperationAllowed("allow_swaps");
577
+ await this.assertSwapMeetsMinAmounts({
578
+ assetInAddress: params.assetInAddress,
579
+ assetOutAddress: params.assetOutAddress,
580
+ amountIn: params.amountIn,
581
+ minAmountOut: params.minAmountOut,
582
+ });
567
583
  // Transfer assets to pool using new address encoding
568
584
  const lpSparkAddress = encodeSparkAddressNew({
569
585
  identityPublicKey: params.poolId,
@@ -582,6 +598,14 @@ class FlashnetClient {
582
598
  }
583
599
  async executeSwapIntent(params) {
584
600
  await this.ensureInitialized();
601
+ // Also enforce gating and min amounts for direct intent usage
602
+ await this.ensureAmmOperationAllowed("allow_swaps");
603
+ await this.assertSwapMeetsMinAmounts({
604
+ assetInAddress: params.assetInAddress,
605
+ assetOutAddress: params.assetOutAddress,
606
+ amountIn: params.amountIn,
607
+ minAmountOut: params.minAmountOut,
608
+ });
585
609
  // Generate swap intent
586
610
  const nonce = generateNonce();
587
611
  const intentMessage = generatePoolSwapIntentMessage({
@@ -611,7 +635,7 @@ class FlashnetClient {
611
635
  totalIntegratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
612
636
  integratorPublicKey: params.integratorPublicKey || "",
613
637
  nonce,
614
- signature: Buffer.from(signature).toString("hex"),
638
+ signature: getHexFromUint8Array(signature),
615
639
  };
616
640
  const response = await this.typedApi.executeSwap(request);
617
641
  // Check if the swap was accepted
@@ -632,6 +656,7 @@ class FlashnetClient {
632
656
  throw new Error("Route swap cannot have more than 4 hops");
633
657
  }
634
658
  await this.ensureInitialized();
659
+ await this.ensurePingOk();
635
660
  return this.typedApi.simulateRouteSwap(params);
636
661
  }
637
662
  /**
@@ -639,6 +664,18 @@ class FlashnetClient {
639
664
  */
640
665
  async executeRouteSwap(params) {
641
666
  await this.ensureInitialized();
667
+ await this.ensureAmmOperationAllowed("allow_route_swaps");
668
+ // Validate min-amount policy for route: check initial input and final output asset
669
+ const finalOutputAsset = params.hops[params.hops.length - 1]?.assetOutAddress;
670
+ if (!finalOutputAsset) {
671
+ throw new Error("Route swap requires at least one hop with output asset");
672
+ }
673
+ await this.assertSwapMeetsMinAmounts({
674
+ assetInAddress: params.initialAssetAddress,
675
+ assetOutAddress: finalOutputAsset,
676
+ amountIn: params.inputAmount,
677
+ minAmountOut: params.minAmountOut,
678
+ });
642
679
  // Validate hops array
643
680
  if (params.hops.length > 4) {
644
681
  throw new Error("Route swap cannot have more than 4 hops");
@@ -708,7 +745,7 @@ class FlashnetClient {
708
745
  maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
709
746
  minAmountOut: params.minAmountOut,
710
747
  nonce,
711
- signature: Buffer.from(signature).toString("hex"),
748
+ signature: getHexFromUint8Array(signature),
712
749
  integratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
713
750
  integratorPublicKey: params.integratorPublicKey || "",
714
751
  };
@@ -729,6 +766,7 @@ class FlashnetClient {
729
766
  */
730
767
  async simulateAddLiquidity(params) {
731
768
  await this.ensureInitialized();
769
+ await this.ensurePingOk();
732
770
  return this.typedApi.simulateAddLiquidity(params);
733
771
  }
734
772
  /**
@@ -736,8 +774,15 @@ class FlashnetClient {
736
774
  */
737
775
  async addLiquidity(params) {
738
776
  await this.ensureInitialized();
777
+ await this.ensureAmmOperationAllowed("allow_add_liquidity");
739
778
  // Get pool details to know which assets we're dealing with
740
779
  const pool = await this.getPool(params.poolId);
780
+ // Enforce min-amount policy for inputs based on pool assets
781
+ await this.assertAddLiquidityMeetsMinAmounts({
782
+ poolId: params.poolId,
783
+ assetAAmount: params.assetAAmount,
784
+ assetBAmount: params.assetBAmount,
785
+ });
741
786
  // Transfer assets to pool using new address encoding
742
787
  const lpSparkAddress = encodeSparkAddressNew({
743
788
  identityPublicKey: params.poolId,
@@ -781,7 +826,7 @@ class FlashnetClient {
781
826
  assetAMinAmountIn: params.assetAMinAmountIn.toString(),
782
827
  assetBMinAmountIn: params.assetBMinAmountIn.toString(),
783
828
  nonce,
784
- signature: Buffer.from(signature).toString("hex"),
829
+ signature: getHexFromUint8Array(signature),
785
830
  };
786
831
  const response = await this.typedApi.addLiquidity(request);
787
832
  // Check if the liquidity addition was accepted
@@ -799,6 +844,7 @@ class FlashnetClient {
799
844
  */
800
845
  async simulateRemoveLiquidity(params) {
801
846
  await this.ensureInitialized();
847
+ await this.ensurePingOk();
802
848
  return this.typedApi.simulateRemoveLiquidity(params);
803
849
  }
804
850
  /**
@@ -806,6 +852,7 @@ class FlashnetClient {
806
852
  */
807
853
  async removeLiquidity(params) {
808
854
  await this.ensureInitialized();
855
+ await this.ensureAmmOperationAllowed("allow_withdraw_liquidity");
809
856
  // Check LP token balance
810
857
  const position = await this.getLpPosition(params.poolId);
811
858
  const lpTokensOwned = position.lpTokensOwned;
@@ -813,6 +860,11 @@ class FlashnetClient {
813
860
  if (compareDecimalStrings(lpTokensOwned, tokensToRemove) < 0) {
814
861
  throw new Error(`Insufficient LP tokens. Owned: ${lpTokensOwned}, Requested: ${tokensToRemove}`);
815
862
  }
863
+ // Pre-simulate and enforce min-amount policy for outputs
864
+ await this.assertRemoveLiquidityMeetsMinAmounts({
865
+ poolId: params.poolId,
866
+ lpTokensToRemove: params.lpTokensToRemove,
867
+ });
816
868
  // Generate remove liquidity intent
817
869
  const nonce = generateNonce();
818
870
  const intentMessage = generateRemoveLiquidityIntentMessage({
@@ -829,7 +881,7 @@ class FlashnetClient {
829
881
  poolId: params.poolId,
830
882
  lpTokensToRemove: params.lpTokensToRemove,
831
883
  nonce,
832
- signature: Buffer.from(signature).toString("hex"),
884
+ signature: getHexFromUint8Array(signature),
833
885
  };
834
886
  const response = await this.typedApi.removeLiquidity(request);
835
887
  // Check if the liquidity removal was accepted
@@ -845,6 +897,7 @@ class FlashnetClient {
845
897
  */
846
898
  async registerHost(params) {
847
899
  await this.ensureInitialized();
900
+ await this.ensurePingOk();
848
901
  const feeRecipient = params.feeRecipientPublicKey || this.publicKey;
849
902
  const nonce = generateNonce();
850
903
  // Generate intent
@@ -862,7 +915,7 @@ class FlashnetClient {
862
915
  minFeeBps: params.minFeeBps,
863
916
  feeRecipientPublicKey: feeRecipient,
864
917
  nonce,
865
- signature: Buffer.from(signature).toString("hex"),
918
+ signature: getHexFromUint8Array(signature),
866
919
  };
867
920
  return this.typedApi.registerHost(request);
868
921
  }
@@ -878,6 +931,7 @@ class FlashnetClient {
878
931
  */
879
932
  async getPoolHostFees(hostNamespace, poolId) {
880
933
  await this.ensureInitialized();
934
+ await this.ensurePingOk();
881
935
  return this.typedApi.getPoolHostFees({ hostNamespace, poolId });
882
936
  }
883
937
  /**
@@ -892,6 +946,7 @@ class FlashnetClient {
892
946
  */
893
947
  async withdrawHostFees(params) {
894
948
  await this.ensureInitialized();
949
+ await this.ensureAmmOperationAllowed("allow_withdraw_fees");
895
950
  const nonce = generateNonce();
896
951
  const intentMessage = generateWithdrawHostFeesIntentMessage({
897
952
  hostPublicKey: this.publicKey,
@@ -906,7 +961,7 @@ class FlashnetClient {
906
961
  lpIdentityPublicKey: params.lpIdentityPublicKey,
907
962
  assetBAmount: params.assetBAmount,
908
963
  nonce,
909
- signature: Buffer.from(signature).toString("hex"),
964
+ signature: getHexFromUint8Array(signature),
910
965
  };
911
966
  const response = await this.typedApi.withdrawHostFees(request);
912
967
  // Check if the withdrawal was accepted
@@ -921,6 +976,7 @@ class FlashnetClient {
921
976
  */
922
977
  async getHostFees(hostNamespace) {
923
978
  await this.ensureInitialized();
979
+ await this.ensurePingOk();
924
980
  const request = {
925
981
  hostNamespace,
926
982
  };
@@ -938,6 +994,7 @@ class FlashnetClient {
938
994
  */
939
995
  async getPoolIntegratorFees(poolId) {
940
996
  await this.ensureInitialized();
997
+ await this.ensurePingOk();
941
998
  return this.typedApi.getPoolIntegratorFees({ poolId });
942
999
  }
943
1000
  /**
@@ -945,6 +1002,7 @@ class FlashnetClient {
945
1002
  */
946
1003
  async withdrawIntegratorFees(params) {
947
1004
  await this.ensureInitialized();
1005
+ await this.ensureAmmOperationAllowed("allow_withdraw_fees");
948
1006
  const nonce = generateNonce();
949
1007
  const intentMessage = generateWithdrawIntegratorFeesIntentMessage({
950
1008
  integratorPublicKey: this.publicKey,
@@ -960,7 +1018,7 @@ class FlashnetClient {
960
1018
  lpIdentityPublicKey: params.lpIdentityPublicKey,
961
1019
  assetBAmount: params.assetBAmount,
962
1020
  nonce,
963
- signature: Buffer.from(signature).toString("hex"),
1021
+ signature: getHexFromUint8Array(signature),
964
1022
  };
965
1023
  const response = await this.typedApi.withdrawIntegratorFees(request);
966
1024
  // Check if the withdrawal was accepted
@@ -986,6 +1044,7 @@ class FlashnetClient {
986
1044
  */
987
1045
  async createEscrow(params) {
988
1046
  await this.ensureInitialized();
1047
+ await this.ensurePingOk();
989
1048
  const nonce = generateNonce();
990
1049
  // The intent message requires a different structure for recipients and conditions
991
1050
  const intentRecipients = params.recipients.map((r) => ({
@@ -1014,7 +1073,7 @@ class FlashnetClient {
1014
1073
  abandonHost: params.abandonHost,
1015
1074
  abandonConditions: params.abandonConditions,
1016
1075
  nonce,
1017
- signature: Buffer.from(signature).toString("hex"),
1076
+ signature: getHexFromUint8Array(signature),
1018
1077
  };
1019
1078
  const createResponse = await this.typedApi.createEscrow(request);
1020
1079
  const autoFund = params.autoFund !== false;
@@ -1037,6 +1096,7 @@ class FlashnetClient {
1037
1096
  */
1038
1097
  async fundEscrow(params) {
1039
1098
  await this.ensureInitialized();
1099
+ await this.ensurePingOk();
1040
1100
  // 1. Balance check
1041
1101
  await this.checkBalance({
1042
1102
  balancesToCheck: [
@@ -1061,6 +1121,7 @@ class FlashnetClient {
1061
1121
  });
1062
1122
  }
1063
1123
  async executeFundEscrowIntent(params) {
1124
+ await this.ensurePingOk();
1064
1125
  // Generate intent
1065
1126
  const nonce = generateNonce();
1066
1127
  const intentMessage = generateFundEscrowIntentMessage({
@@ -1075,7 +1136,7 @@ class FlashnetClient {
1075
1136
  const request = {
1076
1137
  ...params,
1077
1138
  nonce,
1078
- signature: Buffer.from(signature).toString("hex"),
1139
+ signature: getHexFromUint8Array(signature),
1079
1140
  };
1080
1141
  return this.typedApi.fundEscrow(request);
1081
1142
  }
@@ -1087,6 +1148,7 @@ class FlashnetClient {
1087
1148
  */
1088
1149
  async claimEscrow(params) {
1089
1150
  await this.ensureInitialized();
1151
+ await this.ensurePingOk();
1090
1152
  const nonce = generateNonce();
1091
1153
  const intentMessage = generateClaimEscrowIntentMessage({
1092
1154
  escrowId: params.escrowId,
@@ -1098,7 +1160,7 @@ class FlashnetClient {
1098
1160
  const request = {
1099
1161
  escrowId: params.escrowId,
1100
1162
  nonce,
1101
- signature: Buffer.from(signature).toString("hex"),
1163
+ signature: getHexFromUint8Array(signature),
1102
1164
  };
1103
1165
  return this.typedApi.claimEscrow(request);
1104
1166
  }
@@ -1141,6 +1203,7 @@ class FlashnetClient {
1141
1203
  */
1142
1204
  async clawback(params) {
1143
1205
  await this.ensureInitialized();
1206
+ await this.ensurePingOk();
1144
1207
  const nonce = generateNonce();
1145
1208
  const intentMessage = generateClawbackIntentMessage({
1146
1209
  senderPublicKey: this.publicKey,
@@ -1155,7 +1218,7 @@ class FlashnetClient {
1155
1218
  sparkTransferId: params.sparkTransferId,
1156
1219
  lpIdentityPublicKey: params.lpIdentityPublicKey,
1157
1220
  nonce,
1158
- signature: Buffer.from(signature).toString("hex"),
1221
+ signature: getHexFromUint8Array(signature),
1159
1222
  };
1160
1223
  const response = await this.typedApi.clawback(request);
1161
1224
  if (!response.accepted) {
@@ -1250,6 +1313,13 @@ class FlashnetClient {
1250
1313
  * Helper method to add initial liquidity after pool creation
1251
1314
  */
1252
1315
  async addInitialLiquidity(poolId, assetAAddress, assetBAddress, assetAAmount, assetBAmount, assetAMinAmountIn, assetBMinAmountIn) {
1316
+ // Enforce gating and min-amount policy for initial liquidity
1317
+ await this.ensureAmmOperationAllowed("allow_add_liquidity");
1318
+ await this.assertAddLiquidityMeetsMinAmounts({
1319
+ poolId,
1320
+ assetAAmount,
1321
+ assetBAmount,
1322
+ });
1253
1323
  const lpSparkAddress = encodeSparkAddressNew({
1254
1324
  identityPublicKey: poolId,
1255
1325
  network: this.sparkNetwork,
@@ -1291,7 +1361,7 @@ class FlashnetClient {
1291
1361
  assetAMinAmountIn: assetAMinAmountIn.toString(),
1292
1362
  assetBMinAmountIn: assetBMinAmountIn.toString(),
1293
1363
  nonce,
1294
- signature: Buffer.from(signature).toString("hex"),
1364
+ signature: getHexFromUint8Array(signature),
1295
1365
  };
1296
1366
  const response = await this.typedApi.addLiquidity(request);
1297
1367
  // Check if the initial liquidity addition was accepted
@@ -1306,6 +1376,186 @@ class FlashnetClient {
1306
1376
  async cleanup() {
1307
1377
  await this._wallet.cleanupConnections();
1308
1378
  }
1379
+ // ===== Config and Policy Enforcement Helpers =====
1380
+ async ensureAmmOperationAllowed(requiredFeature) {
1381
+ await this.ensurePingOk();
1382
+ const featureMap = await this.getFeatureStatusMap();
1383
+ if (featureMap.get("master_kill_switch")) {
1384
+ throw new Error("Service is temporarily disabled by master kill switch");
1385
+ }
1386
+ if (!featureMap.get(requiredFeature)) {
1387
+ throw new Error(`Operation not allowed: feature '${requiredFeature}' is disabled`);
1388
+ }
1389
+ }
1390
+ async ensurePingOk() {
1391
+ const now = Date.now();
1392
+ if (this.pingCache && this.pingCache.expiryMs > now) {
1393
+ if (!this.pingCache.ok) {
1394
+ throw new Error("Settlement service unavailable. Only read (GET) operations are allowed right now.");
1395
+ }
1396
+ return;
1397
+ }
1398
+ const ping = await this.typedApi.ping();
1399
+ const ok = !!ping &&
1400
+ typeof ping.status === "string" &&
1401
+ ping.status.toLowerCase() === "ok";
1402
+ this.pingCache = { ok, expiryMs: now + FlashnetClient.PING_TTL_MS };
1403
+ if (!ok) {
1404
+ throw new Error("Settlement service unavailable. Only read (GET) operations are allowed right now.");
1405
+ }
1406
+ }
1407
+ async getFeatureStatusMap() {
1408
+ const now = Date.now();
1409
+ if (this.featureStatusCache && this.featureStatusCache.expiryMs > now) {
1410
+ const map = new Map();
1411
+ for (const item of this.featureStatusCache.data) {
1412
+ map.set(item.feature_name, Boolean(item.enabled));
1413
+ }
1414
+ return map;
1415
+ }
1416
+ const data = await this.typedApi.getFeatureStatus();
1417
+ this.featureStatusCache = {
1418
+ data,
1419
+ expiryMs: now + FlashnetClient.FEATURE_STATUS_TTL_MS,
1420
+ };
1421
+ const map = new Map();
1422
+ for (const item of data) {
1423
+ map.set(item.feature_name, Boolean(item.enabled));
1424
+ }
1425
+ return map;
1426
+ }
1427
+ async getEnabledMinAmountsMap() {
1428
+ const now = Date.now();
1429
+ if (this.minAmountsCache && this.minAmountsCache.expiryMs > now) {
1430
+ return this.minAmountsCache.map;
1431
+ }
1432
+ const config = await this.typedApi.getMinAmounts();
1433
+ const map = new Map();
1434
+ for (const item of config) {
1435
+ if (item.enabled) {
1436
+ const key = item.asset_identifier.toLowerCase();
1437
+ const value = BigInt(String(item.min_amount));
1438
+ map.set(key, value);
1439
+ }
1440
+ }
1441
+ this.minAmountsCache = {
1442
+ map,
1443
+ expiryMs: now + FlashnetClient.MIN_AMOUNTS_TTL_MS,
1444
+ };
1445
+ return map;
1446
+ }
1447
+ getHexAddress(addr) {
1448
+ return this.toHexTokenIdentifier(addr).toLowerCase();
1449
+ }
1450
+ async assertSwapMeetsMinAmounts(params) {
1451
+ const minMap = await this.getEnabledMinAmountsMap();
1452
+ if (minMap.size === 0) {
1453
+ return;
1454
+ }
1455
+ const inHex = this.getHexAddress(params.assetInAddress);
1456
+ const outHex = this.getHexAddress(params.assetOutAddress);
1457
+ const minIn = minMap.get(inHex);
1458
+ const minOut = minMap.get(outHex);
1459
+ const amountIn = BigInt(String(params.amountIn));
1460
+ const minAmountOut = BigInt(String(params.minAmountOut));
1461
+ if (minIn && minOut) {
1462
+ if (amountIn < minIn) {
1463
+ throw new Error(`Minimum amount not met for input asset. Required \
1464
+ ${minIn.toString()}, provided ${amountIn.toString()}`);
1465
+ }
1466
+ return;
1467
+ }
1468
+ if (minIn) {
1469
+ if (amountIn < minIn) {
1470
+ throw new Error(`Minimum amount not met for input asset. Required \
1471
+ ${minIn.toString()}, provided ${amountIn.toString()}`);
1472
+ }
1473
+ return;
1474
+ }
1475
+ if (minOut) {
1476
+ const relaxed = minOut / 2n; // 50% relaxation for slippage
1477
+ if (minAmountOut < relaxed) {
1478
+ throw new Error(`Minimum amount not met for output asset. Required at least \
1479
+ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toString()}`);
1480
+ }
1481
+ }
1482
+ }
1483
+ async assertAddLiquidityMeetsMinAmounts(params) {
1484
+ const minMap = await this.getEnabledMinAmountsMap();
1485
+ if (minMap.size === 0) {
1486
+ return;
1487
+ }
1488
+ const pool = await this.getPool(params.poolId);
1489
+ const aHex = pool.assetAAddress.toLowerCase();
1490
+ const bHex = pool.assetBAddress.toLowerCase();
1491
+ const aMin = minMap.get(aHex);
1492
+ const bMin = minMap.get(bHex);
1493
+ if (aMin) {
1494
+ const aAmt = BigInt(String(params.assetAAmount));
1495
+ if (aAmt < aMin) {
1496
+ throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
1497
+ }
1498
+ }
1499
+ if (bMin) {
1500
+ const bAmt = BigInt(String(params.assetBAmount));
1501
+ if (bAmt < bMin) {
1502
+ throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
1503
+ }
1504
+ }
1505
+ }
1506
+ async assertRemoveLiquidityMeetsMinAmounts(params) {
1507
+ const minMap = await this.getEnabledMinAmountsMap();
1508
+ if (minMap.size === 0) {
1509
+ return;
1510
+ }
1511
+ const simulation = await this.simulateRemoveLiquidity({
1512
+ poolId: params.poolId,
1513
+ providerPublicKey: this.publicKey,
1514
+ lpTokensToRemove: String(params.lpTokensToRemove),
1515
+ });
1516
+ const pool = await this.getPool(params.poolId);
1517
+ const aHex = pool.assetAAddress.toLowerCase();
1518
+ const bHex = pool.assetBAddress.toLowerCase();
1519
+ const aMin = minMap.get(aHex);
1520
+ const bMin = minMap.get(bHex);
1521
+ if (aMin) {
1522
+ const predictedAOut = BigInt(String(simulation.assetAAmount));
1523
+ const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
1524
+ if (predictedAOut < relaxedA) {
1525
+ throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
1526
+ }
1527
+ }
1528
+ if (bMin) {
1529
+ const predictedBOut = BigInt(String(simulation.assetBAmount));
1530
+ const relaxedB = bMin / 2n;
1531
+ if (predictedBOut < relaxedB) {
1532
+ throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
1533
+ }
1534
+ }
1535
+ }
1536
+ async assertAllowedAssetBForPoolCreation(assetBHex) {
1537
+ const now = Date.now();
1538
+ let allowed;
1539
+ if (this.allowedAssetsCache && this.allowedAssetsCache.expiryMs > now) {
1540
+ allowed = this.allowedAssetsCache.data;
1541
+ }
1542
+ else {
1543
+ allowed = await this.typedApi.getAllowedAssets();
1544
+ this.allowedAssetsCache = {
1545
+ data: allowed,
1546
+ expiryMs: now + FlashnetClient.ALLOWED_ASSETS_TTL_MS,
1547
+ };
1548
+ }
1549
+ if (!allowed || allowed.length === 0) {
1550
+ // Wildcard allowance
1551
+ return;
1552
+ }
1553
+ const isAllowed = allowed.some((it) => it.enabled &&
1554
+ it.asset_identifier.toLowerCase() === assetBHex.toLowerCase());
1555
+ if (!isAllowed) {
1556
+ throw new Error(`Asset B is not allowed for pool creation: ${assetBHex}`);
1557
+ }
1558
+ }
1309
1559
  }
1310
1560
 
1311
1561
  export { FlashnetClient };