@flashnet/sdk 0.3.23 → 0.3.25

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/README.md +2 -1
  2. package/dist/cjs/index.d.ts +2 -2
  3. package/dist/cjs/index.d.ts.map +1 -1
  4. package/dist/cjs/src/api/typed-endpoints.d.ts +15 -0
  5. package/dist/cjs/src/api/typed-endpoints.d.ts.map +1 -1
  6. package/dist/cjs/src/api/typed-endpoints.js +22 -0
  7. package/dist/cjs/src/api/typed-endpoints.js.map +1 -1
  8. package/dist/cjs/src/client/FlashnetClient.d.ts +18 -0
  9. package/dist/cjs/src/client/FlashnetClient.d.ts.map +1 -1
  10. package/dist/cjs/src/client/FlashnetClient.js +329 -65
  11. package/dist/cjs/src/client/FlashnetClient.js.map +1 -1
  12. package/dist/cjs/src/config/index.js +2 -2
  13. package/dist/cjs/src/config/index.js.map +1 -1
  14. package/dist/cjs/src/types/index.d.ts +19 -0
  15. package/dist/cjs/src/types/index.d.ts.map +1 -1
  16. package/dist/cjs/src/types/index.js.map +1 -1
  17. package/dist/cjs/src/utils/auth.d.ts.map +1 -1
  18. package/dist/cjs/src/utils/auth.js +5 -3
  19. package/dist/cjs/src/utils/auth.js.map +1 -1
  20. package/dist/cjs/src/utils/hex.d.ts +3 -0
  21. package/dist/cjs/src/utils/hex.d.ts.map +1 -0
  22. package/dist/cjs/src/utils/hex.js +17 -0
  23. package/dist/cjs/src/utils/hex.js.map +1 -0
  24. package/dist/cjs/src/utils/index.js +1 -1
  25. package/dist/cjs/src/utils/tokenAddress.d.ts.map +1 -1
  26. package/dist/cjs/src/utils/tokenAddress.js +1 -1
  27. package/dist/esm/index.d.ts +2 -2
  28. package/dist/esm/index.d.ts.map +1 -1
  29. package/dist/esm/src/api/typed-endpoints.d.ts +15 -0
  30. package/dist/esm/src/api/typed-endpoints.d.ts.map +1 -1
  31. package/dist/esm/src/api/typed-endpoints.js +22 -0
  32. package/dist/esm/src/api/typed-endpoints.js.map +1 -1
  33. package/dist/esm/src/client/FlashnetClient.d.ts +18 -0
  34. package/dist/esm/src/client/FlashnetClient.d.ts.map +1 -1
  35. package/dist/esm/src/client/FlashnetClient.js +329 -65
  36. package/dist/esm/src/client/FlashnetClient.js.map +1 -1
  37. package/dist/esm/src/config/index.js +2 -2
  38. package/dist/esm/src/config/index.js.map +1 -1
  39. package/dist/esm/src/types/index.d.ts +19 -0
  40. package/dist/esm/src/types/index.d.ts.map +1 -1
  41. package/dist/esm/src/types/index.js.map +1 -1
  42. package/dist/esm/src/utils/auth.d.ts.map +1 -1
  43. package/dist/esm/src/utils/auth.js +5 -3
  44. package/dist/esm/src/utils/auth.js.map +1 -1
  45. package/dist/esm/src/utils/hex.d.ts +3 -0
  46. package/dist/esm/src/utils/hex.d.ts.map +1 -0
  47. package/dist/esm/src/utils/hex.js +14 -0
  48. package/dist/esm/src/utils/hex.js.map +1 -0
  49. package/dist/esm/src/utils/index.js +1 -1
  50. package/dist/esm/src/utils/tokenAddress.d.ts.map +1 -1
  51. package/dist/esm/src/utils/tokenAddress.js +1 -1
  52. package/package.json +1 -1
@@ -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,38 +423,13 @@ 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
427
  !Number.isInteger(params.graduationThresholdPct) ||
415
428
  params.graduationThresholdPct <= 0) {
416
429
  throw new Error("Graduation threshold percentage must be a positive integer");
417
430
  }
418
- const supply = parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
419
- const targetB = parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
431
+ const supply = FlashnetClient.parsePositiveIntegerToBigInt(params.initialTokenSupply, "Initial token supply");
432
+ const targetB = FlashnetClient.parsePositiveIntegerToBigInt(params.targetRaise, "Target raise");
420
433
  const graduationThresholdPct = BigInt(params.graduationThresholdPct);
421
434
  // Check feasibility: f - g*(1-f) > 0 where f is graduationThresholdPct/100 and g is 1
422
435
  const MIN_GRADUATION_THRESHOLD_PCT = 50n;
@@ -431,16 +444,22 @@ class FlashnetClient {
431
444
  // (2n * graduationThresholdPct - 100n) / 100n
432
445
  // 1/denom is 100n / (2n * graduationThresholdPct - 100n)
433
446
  // 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;
447
+ // totalA is (supply * f * f) / denom
448
+ const totalANumerator = BigInt(supply) * graduationThresholdPct * graduationThresholdPct;
449
+ const totalADenominator = 200n * graduationThresholdPct - 10000n;
450
+ const totalARemainder = totalANumerator % totalADenominator;
451
+ const totalA = (totalANumerator - totalARemainder) / totalADenominator;
452
+ // Since there is an initial supply of tokens, we subtract that amount
453
+ // to get the remaining "virtual" amount for pricing
454
+ const virtualA = totalA - supply;
455
+ // totalB is (targetB * g * (1 - f)) / denom
456
+ const totalBNumerator = BigInt(targetB) * (100n - graduationThresholdPct);
457
+ const totalBDenominator = 2n * graduationThresholdPct - 100n;
458
+ const totalBRemainder = totalBNumerator % totalBDenominator;
459
+ const totalB = (totalBNumerator - totalBRemainder) / totalBDenominator;
460
+ // Bonding curve starts with no deposited amount of asset B,
461
+ // so virtualB is all of totalB
462
+ const virtualB = totalB;
444
463
  // Calculate threshold as amount of asset A (not percentage)
445
464
  const threshold = (BigInt(supply) * graduationThresholdPct) / 100n;
446
465
  return {
@@ -457,18 +476,20 @@ class FlashnetClient {
457
476
  */
458
477
  async createSingleSidedPool(params) {
459
478
  await this.ensureInitialized();
479
+ await this.ensureAmmOperationAllowed("allow_pool_creation");
480
+ await this.assertAllowedAssetBForPoolCreation(this.toHexTokenIdentifier(params.assetBAddress));
460
481
  if (!params.hostNamespace && params.totalHostFeeRateBps < 10) {
461
482
  throw new Error(`Host fee must be greater than 10 bps when no host namespace is provided`);
462
483
  }
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();
484
+ // Validate reserves are valid positive integers before any operations
485
+ const assetAInitialReserve = FlashnetClient.parsePositiveIntegerToBigInt(params.assetAInitialReserve, "Asset A Initial Reserve").toString();
486
+ const virtualReserveA = FlashnetClient.parsePositiveIntegerToBigInt(params.virtualReserveA, "Virtual Reserve A").toString();
487
+ const virtualReserveB = FlashnetClient.parsePositiveIntegerToBigInt(params.virtualReserveB, "Virtual Reserve B").toString();
467
488
  await this.checkBalance({
468
489
  balancesToCheck: [
469
490
  {
470
491
  assetAddress: params.assetAAddress,
471
- amount: clippedAssetAInitialReserve,
492
+ amount: assetAInitialReserve,
472
493
  },
473
494
  ],
474
495
  errorPrefix: "Insufficient balance for pool creation: ",
@@ -480,9 +501,9 @@ class FlashnetClient {
480
501
  poolOwnerPublicKey,
481
502
  assetAAddress: this.toHexTokenIdentifier(params.assetAAddress),
482
503
  assetBAddress: this.toHexTokenIdentifier(params.assetBAddress),
483
- assetAInitialReserve: clippedAssetAInitialReserve,
484
- virtualReserveA: clippedVirtualReserveA,
485
- virtualReserveB: clippedVirtualReserveB,
504
+ assetAInitialReserve,
505
+ virtualReserveA,
506
+ virtualReserveB,
486
507
  threshold: params.threshold.toString(),
487
508
  lpFeeRateBps: params.lpFeeRateBps.toString(),
488
509
  totalHostFeeRateBps: params.totalHostFeeRateBps.toString(),
@@ -495,15 +516,15 @@ class FlashnetClient {
495
516
  poolOwnerPublicKey,
496
517
  assetAAddress: this.toHexTokenIdentifier(params.assetAAddress),
497
518
  assetBAddress: this.toHexTokenIdentifier(params.assetBAddress),
498
- assetAInitialReserve: clippedAssetAInitialReserve,
499
- virtualReserveA: clippedVirtualReserveA,
500
- virtualReserveB: clippedVirtualReserveB,
519
+ assetAInitialReserve,
520
+ virtualReserveA,
521
+ virtualReserveB,
501
522
  threshold: params.threshold.toString(),
502
523
  lpFeeRateBps: params.lpFeeRateBps.toString(),
503
524
  totalHostFeeRateBps: params.totalHostFeeRateBps.toString(),
504
525
  hostNamespace: params.hostNamespace,
505
526
  nonce,
506
- signature: Buffer.from(signature).toString("hex"),
527
+ signature: getHexFromUint8Array(signature),
507
528
  };
508
529
  const createResponse = await this.typedApi.createSingleSidedPool(request);
509
530
  if (params.disableInitialDeposit) {
@@ -517,7 +538,7 @@ class FlashnetClient {
517
538
  const assetATransferId = await this.transferAsset({
518
539
  receiverSparkAddress: lpSparkAddress,
519
540
  assetAddress: params.assetAAddress,
520
- amount: clippedAssetAInitialReserve,
541
+ amount: assetAInitialReserve,
521
542
  });
522
543
  const confirmResponse = await this.confirmInitialDeposit(createResponse.poolId, assetATransferId, poolOwnerPublicKey);
523
544
  if (!confirmResponse.confirmed) {
@@ -546,7 +567,7 @@ class FlashnetClient {
546
567
  poolId,
547
568
  assetASparkTransferId,
548
569
  nonce,
549
- signature: Buffer.from(signature).toString("hex"),
570
+ signature: getHexFromUint8Array(signature),
550
571
  poolOwnerPublicKey: poolOwnerPublicKey ?? this.publicKey,
551
572
  };
552
573
  return this.typedApi.confirmInitialDeposit(request);
@@ -557,6 +578,7 @@ class FlashnetClient {
557
578
  */
558
579
  async simulateSwap(params) {
559
580
  await this.ensureInitialized();
581
+ await this.ensurePingOk();
560
582
  return this.typedApi.simulateSwap(params);
561
583
  }
562
584
  /**
@@ -564,6 +586,14 @@ class FlashnetClient {
564
586
  */
565
587
  async executeSwap(params) {
566
588
  await this.ensureInitialized();
589
+ // Gate by feature flags and ping, and enforce min-amount policy before transfers
590
+ await this.ensureAmmOperationAllowed("allow_swaps");
591
+ await this.assertSwapMeetsMinAmounts({
592
+ assetInAddress: params.assetInAddress,
593
+ assetOutAddress: params.assetOutAddress,
594
+ amountIn: params.amountIn,
595
+ minAmountOut: params.minAmountOut,
596
+ });
567
597
  // Transfer assets to pool using new address encoding
568
598
  const lpSparkAddress = encodeSparkAddressNew({
569
599
  identityPublicKey: params.poolId,
@@ -582,6 +612,14 @@ class FlashnetClient {
582
612
  }
583
613
  async executeSwapIntent(params) {
584
614
  await this.ensureInitialized();
615
+ // Also enforce gating and min amounts for direct intent usage
616
+ await this.ensureAmmOperationAllowed("allow_swaps");
617
+ await this.assertSwapMeetsMinAmounts({
618
+ assetInAddress: params.assetInAddress,
619
+ assetOutAddress: params.assetOutAddress,
620
+ amountIn: params.amountIn,
621
+ minAmountOut: params.minAmountOut,
622
+ });
585
623
  // Generate swap intent
586
624
  const nonce = generateNonce();
587
625
  const intentMessage = generatePoolSwapIntentMessage({
@@ -611,7 +649,7 @@ class FlashnetClient {
611
649
  totalIntegratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
612
650
  integratorPublicKey: params.integratorPublicKey || "",
613
651
  nonce,
614
- signature: Buffer.from(signature).toString("hex"),
652
+ signature: getHexFromUint8Array(signature),
615
653
  };
616
654
  const response = await this.typedApi.executeSwap(request);
617
655
  // Check if the swap was accepted
@@ -632,6 +670,7 @@ class FlashnetClient {
632
670
  throw new Error("Route swap cannot have more than 4 hops");
633
671
  }
634
672
  await this.ensureInitialized();
673
+ await this.ensurePingOk();
635
674
  return this.typedApi.simulateRouteSwap(params);
636
675
  }
637
676
  /**
@@ -639,6 +678,18 @@ class FlashnetClient {
639
678
  */
640
679
  async executeRouteSwap(params) {
641
680
  await this.ensureInitialized();
681
+ await this.ensureAmmOperationAllowed("allow_route_swaps");
682
+ // Validate min-amount policy for route: check initial input and final output asset
683
+ const finalOutputAsset = params.hops[params.hops.length - 1]?.assetOutAddress;
684
+ if (!finalOutputAsset) {
685
+ throw new Error("Route swap requires at least one hop with output asset");
686
+ }
687
+ await this.assertSwapMeetsMinAmounts({
688
+ assetInAddress: params.initialAssetAddress,
689
+ assetOutAddress: finalOutputAsset,
690
+ amountIn: params.inputAmount,
691
+ minAmountOut: params.minAmountOut,
692
+ });
642
693
  // Validate hops array
643
694
  if (params.hops.length > 4) {
644
695
  throw new Error("Route swap cannot have more than 4 hops");
@@ -708,7 +759,7 @@ class FlashnetClient {
708
759
  maxRouteSlippageBps: params.maxRouteSlippageBps.toString(),
709
760
  minAmountOut: params.minAmountOut,
710
761
  nonce,
711
- signature: Buffer.from(signature).toString("hex"),
762
+ signature: getHexFromUint8Array(signature),
712
763
  integratorFeeRateBps: params.integratorFeeRateBps?.toString() || "0",
713
764
  integratorPublicKey: params.integratorPublicKey || "",
714
765
  };
@@ -729,6 +780,7 @@ class FlashnetClient {
729
780
  */
730
781
  async simulateAddLiquidity(params) {
731
782
  await this.ensureInitialized();
783
+ await this.ensurePingOk();
732
784
  return this.typedApi.simulateAddLiquidity(params);
733
785
  }
734
786
  /**
@@ -736,8 +788,15 @@ class FlashnetClient {
736
788
  */
737
789
  async addLiquidity(params) {
738
790
  await this.ensureInitialized();
791
+ await this.ensureAmmOperationAllowed("allow_add_liquidity");
739
792
  // Get pool details to know which assets we're dealing with
740
793
  const pool = await this.getPool(params.poolId);
794
+ // Enforce min-amount policy for inputs based on pool assets
795
+ await this.assertAddLiquidityMeetsMinAmounts({
796
+ poolId: params.poolId,
797
+ assetAAmount: params.assetAAmount,
798
+ assetBAmount: params.assetBAmount,
799
+ });
741
800
  // Transfer assets to pool using new address encoding
742
801
  const lpSparkAddress = encodeSparkAddressNew({
743
802
  identityPublicKey: params.poolId,
@@ -781,7 +840,7 @@ class FlashnetClient {
781
840
  assetAMinAmountIn: params.assetAMinAmountIn.toString(),
782
841
  assetBMinAmountIn: params.assetBMinAmountIn.toString(),
783
842
  nonce,
784
- signature: Buffer.from(signature).toString("hex"),
843
+ signature: getHexFromUint8Array(signature),
785
844
  };
786
845
  const response = await this.typedApi.addLiquidity(request);
787
846
  // Check if the liquidity addition was accepted
@@ -799,6 +858,7 @@ class FlashnetClient {
799
858
  */
800
859
  async simulateRemoveLiquidity(params) {
801
860
  await this.ensureInitialized();
861
+ await this.ensurePingOk();
802
862
  return this.typedApi.simulateRemoveLiquidity(params);
803
863
  }
804
864
  /**
@@ -806,6 +866,7 @@ class FlashnetClient {
806
866
  */
807
867
  async removeLiquidity(params) {
808
868
  await this.ensureInitialized();
869
+ await this.ensureAmmOperationAllowed("allow_withdraw_liquidity");
809
870
  // Check LP token balance
810
871
  const position = await this.getLpPosition(params.poolId);
811
872
  const lpTokensOwned = position.lpTokensOwned;
@@ -813,6 +874,11 @@ class FlashnetClient {
813
874
  if (compareDecimalStrings(lpTokensOwned, tokensToRemove) < 0) {
814
875
  throw new Error(`Insufficient LP tokens. Owned: ${lpTokensOwned}, Requested: ${tokensToRemove}`);
815
876
  }
877
+ // Pre-simulate and enforce min-amount policy for outputs
878
+ await this.assertRemoveLiquidityMeetsMinAmounts({
879
+ poolId: params.poolId,
880
+ lpTokensToRemove: params.lpTokensToRemove,
881
+ });
816
882
  // Generate remove liquidity intent
817
883
  const nonce = generateNonce();
818
884
  const intentMessage = generateRemoveLiquidityIntentMessage({
@@ -829,7 +895,7 @@ class FlashnetClient {
829
895
  poolId: params.poolId,
830
896
  lpTokensToRemove: params.lpTokensToRemove,
831
897
  nonce,
832
- signature: Buffer.from(signature).toString("hex"),
898
+ signature: getHexFromUint8Array(signature),
833
899
  };
834
900
  const response = await this.typedApi.removeLiquidity(request);
835
901
  // Check if the liquidity removal was accepted
@@ -845,6 +911,7 @@ class FlashnetClient {
845
911
  */
846
912
  async registerHost(params) {
847
913
  await this.ensureInitialized();
914
+ await this.ensurePingOk();
848
915
  const feeRecipient = params.feeRecipientPublicKey || this.publicKey;
849
916
  const nonce = generateNonce();
850
917
  // Generate intent
@@ -862,7 +929,7 @@ class FlashnetClient {
862
929
  minFeeBps: params.minFeeBps,
863
930
  feeRecipientPublicKey: feeRecipient,
864
931
  nonce,
865
- signature: Buffer.from(signature).toString("hex"),
932
+ signature: getHexFromUint8Array(signature),
866
933
  };
867
934
  return this.typedApi.registerHost(request);
868
935
  }
@@ -878,6 +945,7 @@ class FlashnetClient {
878
945
  */
879
946
  async getPoolHostFees(hostNamespace, poolId) {
880
947
  await this.ensureInitialized();
948
+ await this.ensurePingOk();
881
949
  return this.typedApi.getPoolHostFees({ hostNamespace, poolId });
882
950
  }
883
951
  /**
@@ -892,6 +960,7 @@ class FlashnetClient {
892
960
  */
893
961
  async withdrawHostFees(params) {
894
962
  await this.ensureInitialized();
963
+ await this.ensureAmmOperationAllowed("allow_withdraw_fees");
895
964
  const nonce = generateNonce();
896
965
  const intentMessage = generateWithdrawHostFeesIntentMessage({
897
966
  hostPublicKey: this.publicKey,
@@ -906,7 +975,7 @@ class FlashnetClient {
906
975
  lpIdentityPublicKey: params.lpIdentityPublicKey,
907
976
  assetBAmount: params.assetBAmount,
908
977
  nonce,
909
- signature: Buffer.from(signature).toString("hex"),
978
+ signature: getHexFromUint8Array(signature),
910
979
  };
911
980
  const response = await this.typedApi.withdrawHostFees(request);
912
981
  // Check if the withdrawal was accepted
@@ -921,6 +990,7 @@ class FlashnetClient {
921
990
  */
922
991
  async getHostFees(hostNamespace) {
923
992
  await this.ensureInitialized();
993
+ await this.ensurePingOk();
924
994
  const request = {
925
995
  hostNamespace,
926
996
  };
@@ -938,6 +1008,7 @@ class FlashnetClient {
938
1008
  */
939
1009
  async getPoolIntegratorFees(poolId) {
940
1010
  await this.ensureInitialized();
1011
+ await this.ensurePingOk();
941
1012
  return this.typedApi.getPoolIntegratorFees({ poolId });
942
1013
  }
943
1014
  /**
@@ -945,6 +1016,7 @@ class FlashnetClient {
945
1016
  */
946
1017
  async withdrawIntegratorFees(params) {
947
1018
  await this.ensureInitialized();
1019
+ await this.ensureAmmOperationAllowed("allow_withdraw_fees");
948
1020
  const nonce = generateNonce();
949
1021
  const intentMessage = generateWithdrawIntegratorFeesIntentMessage({
950
1022
  integratorPublicKey: this.publicKey,
@@ -960,7 +1032,7 @@ class FlashnetClient {
960
1032
  lpIdentityPublicKey: params.lpIdentityPublicKey,
961
1033
  assetBAmount: params.assetBAmount,
962
1034
  nonce,
963
- signature: Buffer.from(signature).toString("hex"),
1035
+ signature: getHexFromUint8Array(signature),
964
1036
  };
965
1037
  const response = await this.typedApi.withdrawIntegratorFees(request);
966
1038
  // Check if the withdrawal was accepted
@@ -986,6 +1058,7 @@ class FlashnetClient {
986
1058
  */
987
1059
  async createEscrow(params) {
988
1060
  await this.ensureInitialized();
1061
+ await this.ensurePingOk();
989
1062
  const nonce = generateNonce();
990
1063
  // The intent message requires a different structure for recipients and conditions
991
1064
  const intentRecipients = params.recipients.map((r) => ({
@@ -1014,7 +1087,7 @@ class FlashnetClient {
1014
1087
  abandonHost: params.abandonHost,
1015
1088
  abandonConditions: params.abandonConditions,
1016
1089
  nonce,
1017
- signature: Buffer.from(signature).toString("hex"),
1090
+ signature: getHexFromUint8Array(signature),
1018
1091
  };
1019
1092
  const createResponse = await this.typedApi.createEscrow(request);
1020
1093
  const autoFund = params.autoFund !== false;
@@ -1037,6 +1110,7 @@ class FlashnetClient {
1037
1110
  */
1038
1111
  async fundEscrow(params) {
1039
1112
  await this.ensureInitialized();
1113
+ await this.ensurePingOk();
1040
1114
  // 1. Balance check
1041
1115
  await this.checkBalance({
1042
1116
  balancesToCheck: [
@@ -1061,6 +1135,7 @@ class FlashnetClient {
1061
1135
  });
1062
1136
  }
1063
1137
  async executeFundEscrowIntent(params) {
1138
+ await this.ensurePingOk();
1064
1139
  // Generate intent
1065
1140
  const nonce = generateNonce();
1066
1141
  const intentMessage = generateFundEscrowIntentMessage({
@@ -1075,7 +1150,7 @@ class FlashnetClient {
1075
1150
  const request = {
1076
1151
  ...params,
1077
1152
  nonce,
1078
- signature: Buffer.from(signature).toString("hex"),
1153
+ signature: getHexFromUint8Array(signature),
1079
1154
  };
1080
1155
  return this.typedApi.fundEscrow(request);
1081
1156
  }
@@ -1087,6 +1162,7 @@ class FlashnetClient {
1087
1162
  */
1088
1163
  async claimEscrow(params) {
1089
1164
  await this.ensureInitialized();
1165
+ await this.ensurePingOk();
1090
1166
  const nonce = generateNonce();
1091
1167
  const intentMessage = generateClaimEscrowIntentMessage({
1092
1168
  escrowId: params.escrowId,
@@ -1098,7 +1174,7 @@ class FlashnetClient {
1098
1174
  const request = {
1099
1175
  escrowId: params.escrowId,
1100
1176
  nonce,
1101
- signature: Buffer.from(signature).toString("hex"),
1177
+ signature: getHexFromUint8Array(signature),
1102
1178
  };
1103
1179
  return this.typedApi.claimEscrow(request);
1104
1180
  }
@@ -1141,6 +1217,7 @@ class FlashnetClient {
1141
1217
  */
1142
1218
  async clawback(params) {
1143
1219
  await this.ensureInitialized();
1220
+ await this.ensurePingOk();
1144
1221
  const nonce = generateNonce();
1145
1222
  const intentMessage = generateClawbackIntentMessage({
1146
1223
  senderPublicKey: this.publicKey,
@@ -1155,7 +1232,7 @@ class FlashnetClient {
1155
1232
  sparkTransferId: params.sparkTransferId,
1156
1233
  lpIdentityPublicKey: params.lpIdentityPublicKey,
1157
1234
  nonce,
1158
- signature: Buffer.from(signature).toString("hex"),
1235
+ signature: getHexFromUint8Array(signature),
1159
1236
  };
1160
1237
  const response = await this.typedApi.clawback(request);
1161
1238
  if (!response.accepted) {
@@ -1250,6 +1327,13 @@ class FlashnetClient {
1250
1327
  * Helper method to add initial liquidity after pool creation
1251
1328
  */
1252
1329
  async addInitialLiquidity(poolId, assetAAddress, assetBAddress, assetAAmount, assetBAmount, assetAMinAmountIn, assetBMinAmountIn) {
1330
+ // Enforce gating and min-amount policy for initial liquidity
1331
+ await this.ensureAmmOperationAllowed("allow_add_liquidity");
1332
+ await this.assertAddLiquidityMeetsMinAmounts({
1333
+ poolId,
1334
+ assetAAmount,
1335
+ assetBAmount,
1336
+ });
1253
1337
  const lpSparkAddress = encodeSparkAddressNew({
1254
1338
  identityPublicKey: poolId,
1255
1339
  network: this.sparkNetwork,
@@ -1291,7 +1375,7 @@ class FlashnetClient {
1291
1375
  assetAMinAmountIn: assetAMinAmountIn.toString(),
1292
1376
  assetBMinAmountIn: assetBMinAmountIn.toString(),
1293
1377
  nonce,
1294
- signature: Buffer.from(signature).toString("hex"),
1378
+ signature: getHexFromUint8Array(signature),
1295
1379
  };
1296
1380
  const response = await this.typedApi.addLiquidity(request);
1297
1381
  // Check if the initial liquidity addition was accepted
@@ -1306,6 +1390,186 @@ class FlashnetClient {
1306
1390
  async cleanup() {
1307
1391
  await this._wallet.cleanupConnections();
1308
1392
  }
1393
+ // ===== Config and Policy Enforcement Helpers =====
1394
+ async ensureAmmOperationAllowed(requiredFeature) {
1395
+ await this.ensurePingOk();
1396
+ const featureMap = await this.getFeatureStatusMap();
1397
+ if (featureMap.get("master_kill_switch")) {
1398
+ throw new Error("Service is temporarily disabled by master kill switch");
1399
+ }
1400
+ if (!featureMap.get(requiredFeature)) {
1401
+ throw new Error(`Operation not allowed: feature '${requiredFeature}' is disabled`);
1402
+ }
1403
+ }
1404
+ async ensurePingOk() {
1405
+ const now = Date.now();
1406
+ if (this.pingCache && this.pingCache.expiryMs > now) {
1407
+ if (!this.pingCache.ok) {
1408
+ throw new Error("Settlement service unavailable. Only read (GET) operations are allowed right now.");
1409
+ }
1410
+ return;
1411
+ }
1412
+ const ping = await this.typedApi.ping();
1413
+ const ok = !!ping &&
1414
+ typeof ping.status === "string" &&
1415
+ ping.status.toLowerCase() === "ok";
1416
+ this.pingCache = { ok, expiryMs: now + FlashnetClient.PING_TTL_MS };
1417
+ if (!ok) {
1418
+ throw new Error("Settlement service unavailable. Only read (GET) operations are allowed right now.");
1419
+ }
1420
+ }
1421
+ async getFeatureStatusMap() {
1422
+ const now = Date.now();
1423
+ if (this.featureStatusCache && this.featureStatusCache.expiryMs > now) {
1424
+ const map = new Map();
1425
+ for (const item of this.featureStatusCache.data) {
1426
+ map.set(item.feature_name, Boolean(item.enabled));
1427
+ }
1428
+ return map;
1429
+ }
1430
+ const data = await this.typedApi.getFeatureStatus();
1431
+ this.featureStatusCache = {
1432
+ data,
1433
+ expiryMs: now + FlashnetClient.FEATURE_STATUS_TTL_MS,
1434
+ };
1435
+ const map = new Map();
1436
+ for (const item of data) {
1437
+ map.set(item.feature_name, Boolean(item.enabled));
1438
+ }
1439
+ return map;
1440
+ }
1441
+ async getEnabledMinAmountsMap() {
1442
+ const now = Date.now();
1443
+ if (this.minAmountsCache && this.minAmountsCache.expiryMs > now) {
1444
+ return this.minAmountsCache.map;
1445
+ }
1446
+ const config = await this.typedApi.getMinAmounts();
1447
+ const map = new Map();
1448
+ for (const item of config) {
1449
+ if (item.enabled) {
1450
+ const key = item.asset_identifier.toLowerCase();
1451
+ const value = BigInt(String(item.min_amount));
1452
+ map.set(key, value);
1453
+ }
1454
+ }
1455
+ this.minAmountsCache = {
1456
+ map,
1457
+ expiryMs: now + FlashnetClient.MIN_AMOUNTS_TTL_MS,
1458
+ };
1459
+ return map;
1460
+ }
1461
+ getHexAddress(addr) {
1462
+ return this.toHexTokenIdentifier(addr).toLowerCase();
1463
+ }
1464
+ async assertSwapMeetsMinAmounts(params) {
1465
+ const minMap = await this.getEnabledMinAmountsMap();
1466
+ if (minMap.size === 0) {
1467
+ return;
1468
+ }
1469
+ const inHex = this.getHexAddress(params.assetInAddress);
1470
+ const outHex = this.getHexAddress(params.assetOutAddress);
1471
+ const minIn = minMap.get(inHex);
1472
+ const minOut = minMap.get(outHex);
1473
+ const amountIn = BigInt(String(params.amountIn));
1474
+ const minAmountOut = BigInt(String(params.minAmountOut));
1475
+ if (minIn && minOut) {
1476
+ if (amountIn < minIn) {
1477
+ throw new Error(`Minimum amount not met for input asset. Required \
1478
+ ${minIn.toString()}, provided ${amountIn.toString()}`);
1479
+ }
1480
+ return;
1481
+ }
1482
+ if (minIn) {
1483
+ if (amountIn < minIn) {
1484
+ throw new Error(`Minimum amount not met for input asset. Required \
1485
+ ${minIn.toString()}, provided ${amountIn.toString()}`);
1486
+ }
1487
+ return;
1488
+ }
1489
+ if (minOut) {
1490
+ const relaxed = minOut / 2n; // 50% relaxation for slippage
1491
+ if (minAmountOut < relaxed) {
1492
+ throw new Error(`Minimum amount not met for output asset. Required at least \
1493
+ ${relaxed.toString()} (50% relaxed), provided minAmountOut ${minAmountOut.toString()}`);
1494
+ }
1495
+ }
1496
+ }
1497
+ async assertAddLiquidityMeetsMinAmounts(params) {
1498
+ const minMap = await this.getEnabledMinAmountsMap();
1499
+ if (minMap.size === 0) {
1500
+ return;
1501
+ }
1502
+ const pool = await this.getPool(params.poolId);
1503
+ const aHex = pool.assetAAddress.toLowerCase();
1504
+ const bHex = pool.assetBAddress.toLowerCase();
1505
+ const aMin = minMap.get(aHex);
1506
+ const bMin = minMap.get(bHex);
1507
+ if (aMin) {
1508
+ const aAmt = BigInt(String(params.assetAAmount));
1509
+ if (aAmt < aMin) {
1510
+ throw new Error(`Minimum amount not met for Asset A. Required ${aMin.toString()}, provided ${aAmt.toString()}`);
1511
+ }
1512
+ }
1513
+ if (bMin) {
1514
+ const bAmt = BigInt(String(params.assetBAmount));
1515
+ if (bAmt < bMin) {
1516
+ throw new Error(`Minimum amount not met for Asset B. Required ${bMin.toString()}, provided ${bAmt.toString()}`);
1517
+ }
1518
+ }
1519
+ }
1520
+ async assertRemoveLiquidityMeetsMinAmounts(params) {
1521
+ const minMap = await this.getEnabledMinAmountsMap();
1522
+ if (minMap.size === 0) {
1523
+ return;
1524
+ }
1525
+ const simulation = await this.simulateRemoveLiquidity({
1526
+ poolId: params.poolId,
1527
+ providerPublicKey: this.publicKey,
1528
+ lpTokensToRemove: String(params.lpTokensToRemove),
1529
+ });
1530
+ const pool = await this.getPool(params.poolId);
1531
+ const aHex = pool.assetAAddress.toLowerCase();
1532
+ const bHex = pool.assetBAddress.toLowerCase();
1533
+ const aMin = minMap.get(aHex);
1534
+ const bMin = minMap.get(bHex);
1535
+ if (aMin) {
1536
+ const predictedAOut = BigInt(String(simulation.assetAAmount));
1537
+ const relaxedA = aMin / 2n; // apply 50% relaxation for outputs
1538
+ if (predictedAOut < relaxedA) {
1539
+ throw new Error(`Minimum amount not met for Asset A on withdrawal. Required at least ${relaxedA.toString()} (50% relaxed), predicted ${predictedAOut.toString()}`);
1540
+ }
1541
+ }
1542
+ if (bMin) {
1543
+ const predictedBOut = BigInt(String(simulation.assetBAmount));
1544
+ const relaxedB = bMin / 2n;
1545
+ if (predictedBOut < relaxedB) {
1546
+ throw new Error(`Minimum amount not met for Asset B on withdrawal. Required at least ${relaxedB.toString()} (50% relaxed), predicted ${predictedBOut.toString()}`);
1547
+ }
1548
+ }
1549
+ }
1550
+ async assertAllowedAssetBForPoolCreation(assetBHex) {
1551
+ const now = Date.now();
1552
+ let allowed;
1553
+ if (this.allowedAssetsCache && this.allowedAssetsCache.expiryMs > now) {
1554
+ allowed = this.allowedAssetsCache.data;
1555
+ }
1556
+ else {
1557
+ allowed = await this.typedApi.getAllowedAssets();
1558
+ this.allowedAssetsCache = {
1559
+ data: allowed,
1560
+ expiryMs: now + FlashnetClient.ALLOWED_ASSETS_TTL_MS,
1561
+ };
1562
+ }
1563
+ if (!allowed || allowed.length === 0) {
1564
+ // Wildcard allowance
1565
+ return;
1566
+ }
1567
+ const isAllowed = allowed.some((it) => it.enabled &&
1568
+ it.asset_identifier.toLowerCase() === assetBHex.toLowerCase());
1569
+ if (!isAllowed) {
1570
+ throw new Error(`Asset B is not allowed for pool creation: ${assetBHex}`);
1571
+ }
1572
+ }
1309
1573
  }
1310
1574
 
1311
1575
  export { FlashnetClient };