@circle-fin/app-kit 1.15.2 → 1.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -44514,7 +44514,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
44514
44514
  };
44515
44515
 
44516
44516
  var name$2 = "@circle-fin/earn-kit";
44517
- var version$2 = "1.7.0";
44517
+ var version$2 = "1.8.0";
44518
44518
  var pkg$2 = {
44519
44519
  name: name$2,
44520
44520
  version: version$2};
@@ -45053,6 +45053,142 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
45053
45053
  };
45054
45054
  }
45055
45055
 
45056
+ /**
45057
+ * Fallback gas reserve expressed in ERC-20 deposit token base units when no
45058
+ * quote gas fees are available. 50_000 = 0.05 USDC at the 6-decimal ERC-20
45059
+ * USDC scale used on Arc (and the scale of `balance` / `depositAmountRaw`).
45060
+ *
45061
+ * @internal
45062
+ */ const DEFAULT_GAS_RESERVE = 50_000n;
45063
+ /**
45064
+ * Standard ERC-20 decimal precision for USDC and EURC. Both tokens use 6
45065
+ * decimals on all EVM chains, regardless of how many decimals the native
45066
+ * gas-token representation uses.
45067
+ *
45068
+ * @internal
45069
+ */ const STABLECOIN_ERC20_DECIMALS = 6;
45070
+ /**
45071
+ * Compute the gas reserve in ERC-20 deposit token base units.
45072
+ *
45073
+ * `EstimatedGas.fee` is denominated in the chain's native currency base units
45074
+ * (e.g. 18-decimal USDC on Arc). The deposit token balance and
45075
+ * `depositAmountRaw` are in ERC-20 token base units (e.g. 6-decimal USDC).
45076
+ * This function converts the native fee total to the same scale so both sides
45077
+ * of the `balance < required` comparison are commensurable.
45078
+ *
45079
+ * Conversion: `fee_token_units = fee_native_units / 10^(nativeDecimals - tokenDecimals)`
45080
+ *
45081
+ * Falls back to {@link DEFAULT_GAS_RESERVE} when:
45082
+ * - `gasFees` is absent or empty
45083
+ * - all fee entries have `fees: null` (estimation failed)
45084
+ * - the converted total rounds to zero (native fee smaller than 1 token unit)
45085
+ *
45086
+ * @internal
45087
+ */ function computeGasReserve(gasFees, nativeDecimals, tokenDecimals) {
45088
+ if (gasFees === undefined || gasFees.length === 0) {
45089
+ return DEFAULT_GAS_RESERVE;
45090
+ }
45091
+ let totalNative = 0n;
45092
+ let hasNullEntry = false;
45093
+ for (const entry of gasFees){
45094
+ if (entry.fees === null) {
45095
+ hasNullEntry = true;
45096
+ } else {
45097
+ totalNative += BigInt(entry.fees.fee);
45098
+ }
45099
+ }
45100
+ const decimalDiff = nativeDecimals - tokenDecimals;
45101
+ // Use ceiling division when reducing precision so we never under-estimate.
45102
+ const scale = decimalDiff > 0 ? 10n ** BigInt(decimalDiff) : 1n;
45103
+ let knownToken;
45104
+ if (totalNative === 0n) {
45105
+ knownToken = 0n;
45106
+ } else if (decimalDiff > 0) {
45107
+ knownToken = (totalNative + scale - 1n) / scale;
45108
+ } else {
45109
+ knownToken = totalNative * 10n ** BigInt(-decimalDiff);
45110
+ }
45111
+ // When any estimate is missing the true total is unknown, so reserve at
45112
+ // least DEFAULT_GAS_RESERVE on top of whatever was already known.
45113
+ // BigInt is not supported by Math.max, so we use an explicit comparison.
45114
+ if (hasNullEntry) {
45115
+ return knownToken > DEFAULT_GAS_RESERVE ? knownToken : DEFAULT_GAS_RESERVE // NOSONAR
45116
+ ;
45117
+ }
45118
+ return knownToken > 0n ? knownToken : DEFAULT_GAS_RESERVE;
45119
+ }
45120
+ /**
45121
+ * Build a symbol → ERC-20 address map from the chain's known token addresses.
45122
+ * Used to resolve whether the native gas token corresponds to the deposit
45123
+ * token's address.
45124
+ *
45125
+ * @internal
45126
+ */ function buildSymbolAddressMap(chain) {
45127
+ const map = new Map();
45128
+ if (chain.usdcAddress != null) {
45129
+ map.set('USDC', chain.usdcAddress.toLowerCase());
45130
+ }
45131
+ if (chain.eurcAddress != null) {
45132
+ map.set('EURC', chain.eurcAddress.toLowerCase());
45133
+ }
45134
+ return map;
45135
+ }
45136
+ /**
45137
+ * Pre-broadcast balance check for EOA wallets on chains where the native gas
45138
+ * token and the deposit token are the same asset (e.g. Arc: USDC gas + USDC
45139
+ * deposit).
45140
+ *
45141
+ * When the deposit would consume the entire balance, the adapter locks gas
45142
+ * before `transferFrom`, causing an `ERC20: transfer amount exceeds balance`
45143
+ * revert on-chain. This function reads the wallet balance before broadcast and
45144
+ * throws {@link BalanceError.INSUFFICIENT_TOKEN} (code 9001) when
45145
+ * `balance < depositAmount + gasReserve`, giving callers a clean pre-broadcast
45146
+ * error instead of an opaque chain revert.
45147
+ *
45148
+ * The check is skipped when the chain's native gas currency symbol does not
45149
+ * match the deposit token symbol (derived by address lookup). Examples of
45150
+ * chains where the check is skipped: EURC deposit on Arc, any ETH-gas chain.
45151
+ *
45152
+ * Gas fees from the quote (`gasFees`) are in native-currency base units
45153
+ * (18-decimal on Arc) and are scaled down to ERC-20 token units (6-decimal)
45154
+ * before being added to `depositAmountRaw`.
45155
+ *
45156
+ * @param params - Adapter context, chain, deposit token, amount, and optional
45157
+ * quote gas fees.
45158
+ * @throws {@link KitError} (code 9001) when the balance is insufficient to
45159
+ * cover both the deposit and the gas reserve.
45160
+ *
45161
+ * @internal
45162
+ */ async function checkGasReserveForDeposit(params) {
45163
+ const { adapter, chain, address, depositTokenAddress, depositAmountRaw, gasFees } = params;
45164
+ const nativeSymbol = chain.nativeCurrency.symbol.toUpperCase();
45165
+ const symbolAddressMap = buildSymbolAddressMap(chain);
45166
+ const nativeTokenAddress = symbolAddressMap.get(nativeSymbol);
45167
+ // Only applies when native gas currency and deposit token are the same asset.
45168
+ if (nativeTokenAddress === undefined || depositTokenAddress.toLowerCase() !== nativeTokenAddress) {
45169
+ return;
45170
+ }
45171
+ const rawBalance = await executeAdapterReadAction(adapter, 'token.balanceOf', {
45172
+ tokenAddress: depositTokenAddress,
45173
+ walletAddress: address
45174
+ }, {
45175
+ chain,
45176
+ address
45177
+ });
45178
+ const balance = BigInt(String(rawBalance));
45179
+ // Both USDC and EURC use 6 ERC-20 decimals; the check only reaches here when
45180
+ // the deposit token is the chain's native gas asset (USDC or EURC).
45181
+ const gasReserve = computeGasReserve(gasFees, chain.nativeCurrency.decimals, STABLECOIN_ERC20_DECIMALS);
45182
+ const required = depositAmountRaw + gasReserve;
45183
+ if (balance < required) {
45184
+ throw new KitError({
45185
+ ...BalanceError.INSUFFICIENT_TOKEN,
45186
+ recoverability: 'FATAL',
45187
+ message: `Insufficient ${chain.nativeCurrency.symbol} balance to cover deposit and gas. Balance: ${balance.toString()}, required: ${required.toString()} (deposit: ${depositAmountRaw.toString()} + gas reserve: ${gasReserve.toString()})`
45188
+ });
45189
+ }
45190
+ }
45191
+
45056
45192
  /** @internal */ function isSameAddress(actual, expected) {
45057
45193
  return actual.toLowerCase() === expected.toLowerCase();
45058
45194
  }
@@ -46442,12 +46578,19 @@ const EARN_OPERATIONS = new Set([
46442
46578
  writable: true,
46443
46579
  configurable: true
46444
46580
  });
46581
+ // Surface the tx hash in the message so callers can look it up without
46582
+ // digging into cause.trace. Only applies to on-chain reverts — other errors
46583
+ // either have no tx hash or already embed it in their message.
46584
+ let message = base.message;
46585
+ if (base.name === 'ONCHAIN_TRANSACTION_REVERTED' && typeof existingTrace['txHash'] === 'string' && existingTrace['txHash'] !== '') {
46586
+ message = `${message} (txHash: ${existingTrace['txHash']})`;
46587
+ }
46445
46588
  return new KitError({
46446
46589
  code: base.code,
46447
46590
  name: base.name,
46448
46591
  type: base.type,
46449
46592
  recoverability,
46450
- message: base.message,
46593
+ message,
46451
46594
  cause: {
46452
46595
  trace
46453
46596
  }
@@ -46563,10 +46706,8 @@ const bridgeFeeTokenSchema = hexAddressSchema;
46563
46706
  assetAddress: z.string(),
46564
46707
  lltv: z.number(),
46565
46708
  supplyUsd: z.number(),
46566
- // Optional during the expand/contract window (a backend that predates the
46567
- // field omits the key), mirroring the `.optional()` facets on the base
46568
- // schema; `null` when the product exposes no per-market allocation (V2).
46569
- allocationPct: z.number().nullable().optional()
46709
+ // `null` when the product exposes no per-market allocation (V2).
46710
+ allocationPct: z.number().nullable()
46570
46711
  });
46571
46712
  /**
46572
46713
  * Zod schema for a Morpho vault warning in the API response.
@@ -46673,19 +46814,20 @@ const bridgeFeeTokenSchema = hexAddressSchema;
46673
46814
  earnKitWarnings: z.array(z.string()).optional()
46674
46815
  });
46675
46816
  /**
46676
- * Shared base schema: existing flat fields (kept) plus the new nested
46677
- * facets and neutral identity. Facets are `.optional()` during the
46678
- * transition; flip to required once the backend is confirmed emitting.
46817
+ * Shared base schema: existing flat fields (kept as deprecated aliases) plus
46818
+ * the nested facets. Zenith emits all facets on every response; required here
46819
+ * so a partial emission is caught at parse time rather than surfacing as a
46820
+ * runtime `undefined` access.
46679
46821
  *
46680
46822
  * @internal
46681
46823
  */ const earnBaseSchema = vaultInfoResponseSchema.extend({
46682
- address: z.string().optional(),
46683
- asOf: z.string().optional(),
46684
- manager: managerSchema.nullable().optional(),
46685
- apyProfile: apyProfileSchema.optional(),
46686
- fee: feeInfoSchema.optional(),
46687
- liquidityProfile: liquidityProfileSchema.optional(),
46688
- riskSignals: riskSignalsSchema.optional()
46824
+ address: z.string(),
46825
+ asOf: z.string(),
46826
+ manager: managerSchema.nullable(),
46827
+ apyProfile: apyProfileSchema,
46828
+ fee: feeInfoSchema,
46829
+ liquidityProfile: liquidityProfileSchema,
46830
+ riskSignals: riskSignalsSchema
46689
46831
  });
46690
46832
  /**
46691
46833
  * Zod schema for the `vault` opportunity variant.
@@ -47473,7 +47615,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
47473
47615
  }
47474
47616
 
47475
47617
  var name$1 = "@circle-fin/provider-earn-service";
47476
- var version$1 = "1.6.0";
47618
+ var version$1 = "1.7.0";
47477
47619
  var pkg$1 = {
47478
47620
  name: name$1,
47479
47621
  version: version$1};
@@ -47621,23 +47763,16 @@ var pkg$1 = {
47621
47763
  if (chain === undefined) {
47622
47764
  throw createInvalidChainError(vault.chain, 'Chain returned by the Earn Service is not supported by the SDK');
47623
47765
  }
47624
- // The nested facets are `.optional()` in the schema (a backend that predates
47625
- // them omits them) and are typed optional on `EarnOpportunity` to match.
47626
- // Convert the nested liquidity amounts when present and pass the remaining
47627
- // facets straight through; each absent facet stays absent rather than being
47628
- // asserted present by a cast.
47629
47766
  return {
47630
47767
  ...vault,
47631
47768
  chain,
47632
47769
  totalDeposits: Amount.fromJSON(totalDeposits),
47633
47770
  liquidity: Amount.fromJSON(liquidity),
47634
- ...liquidityProfile !== undefined && {
47635
- liquidityProfile: {
47636
- ...liquidityProfile,
47637
- totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
47638
- available: Amount.fromJSON(liquidityProfile.available),
47639
- totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
47640
- }
47771
+ liquidityProfile: {
47772
+ ...liquidityProfile,
47773
+ totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
47774
+ available: Amount.fromJSON(liquidityProfile.available),
47775
+ totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
47641
47776
  }
47642
47777
  };
47643
47778
  }
@@ -49153,6 +49288,24 @@ function finishElapsedWait(lastStatus, lastError) {
49153
49288
  amount: params.amount
49154
49289
  };
49155
49290
  }
49291
+ // Only EOA wallets pay native gas from their own balance — SCAs have
49292
+ // their gas sponsored externally. Detect EOA by checking that the
49293
+ // adapter supports readBytecode and the wallet address has no on-chain
49294
+ // bytecode (empty code ⟹ EOA; non-empty ⟹ smart-contract account).
49295
+ // EIP-7702-delegated addresses are treated as EOA by isContractSigner.
49296
+ // If the adapter does not expose readBytecode (non-EVM adapter) the
49297
+ // check is skipped conservatively.
49298
+ if (approvalToken !== undefined && requiredAllowance > 0n && chain.type === 'evm' && isEvmAdapterLike(adapter) && !await isContractSigner(adapter, address, chain)) {
49299
+ const sameChainParams = params;
49300
+ await checkGasReserveForDeposit({
49301
+ adapter,
49302
+ chain,
49303
+ address,
49304
+ depositTokenAddress: approvalToken,
49305
+ depositAmountRaw: requiredAllowance,
49306
+ gasFees: sameChainParams.gasFees
49307
+ });
49308
+ }
49156
49309
  if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
49157
49310
  await this.runPhase(ctx, 'approve', 'approve', async ()=>{
49158
49311
  try {
@@ -49559,7 +49712,7 @@ function finishElapsedWait(lastStatus, lastError) {
49559
49712
  // Gas is estimated server-side by the Earn Service and returned on the quote, so the
49560
49713
  // SDK no longer simulates it locally. `chainDefinition` lets the fetch fill
49561
49714
  // the native token symbol / blockchain on each gas entry.
49562
- return fetchDepositQuote({
49715
+ const quote = await fetchDepositQuote({
49563
49716
  vaultAddress: params.vaultAddress,
49564
49717
  amount: params.amount,
49565
49718
  address,
@@ -49567,6 +49720,34 @@ function finishElapsedWait(lastStatus, lastError) {
49567
49720
  config,
49568
49721
  chainDefinition
49569
49722
  });
49723
+ // Run the same pre-broadcast gas reserve check here so callers get an early
49724
+ // warning at quote time — before they call deposit() and hit the real check.
49725
+ // Only applies on stablecoin-gas chains (e.g. Arc) where the deposit token
49726
+ // is also the native gas token, and only for EOA wallets that pay native gas
49727
+ // from their own balance. Skips silently on all other chains and wallet types.
49728
+ if (chainDefinition?.type === 'evm' && isEvmAdapterLike(params.from.adapter) && !await isContractSigner(params.from.adapter, address, chainDefinition)) {
49729
+ const symbol = quote.deposit.symbol.toUpperCase();
49730
+ let depositTokenAddress = null;
49731
+ if (symbol === 'USDC') {
49732
+ depositTokenAddress = chainDefinition.usdcAddress;
49733
+ } else if (symbol === 'EURC') {
49734
+ depositTokenAddress = chainDefinition.eurcAddress;
49735
+ }
49736
+ if (depositTokenAddress != null) {
49737
+ const amountObj = Amount.parse(params.amount, {
49738
+ decimals: 6
49739
+ });
49740
+ await checkGasReserveForDeposit({
49741
+ adapter: params.from.adapter,
49742
+ chain: chainDefinition,
49743
+ address,
49744
+ depositTokenAddress,
49745
+ depositAmountRaw: amountObj.raw,
49746
+ gasFees: quote.gasFees
49747
+ });
49748
+ }
49749
+ }
49750
+ return quote;
49570
49751
  }
49571
49752
  /** {@inheritdoc} */ async getWithdrawalQuote(params) {
49572
49753
  const config = this.resolveConfig(params.config);
@@ -49866,21 +50047,15 @@ function formatPositionPnL(pnl) {
49866
50047
  // consumers keep receiving them until Contract.
49867
50048
  // eslint-disable-next-line @typescript-eslint/no-deprecated
49868
50049
  const { totalDeposits, liquidity, liquidityProfile, ...rest } = vault;
49869
- // `liquidityProfile` is `.optional()` in the response schema during the
49870
- // expand/contract window (an old backend that predates the nested facets
49871
- // omits it), so only format and re-attach it when present — matching the
49872
- // provider-side `toVaultInfo` mapper.
49873
50050
  return {
49874
50051
  ...rest,
49875
50052
  totalDeposits: formatAmount$1(totalDeposits),
49876
50053
  liquidity: formatAmount$1(liquidity),
49877
- ...liquidityProfile !== undefined && {
49878
- liquidityProfile: {
49879
- ...liquidityProfile,
49880
- totalDeposits: formatAmount$1(liquidityProfile.totalDeposits),
49881
- available: formatAmount$1(liquidityProfile.available),
49882
- totalSupply: formatAmount$1(liquidityProfile.totalSupply)
49883
- }
50054
+ liquidityProfile: {
50055
+ ...liquidityProfile,
50056
+ totalDeposits: formatAmount$1(liquidityProfile.totalDeposits),
50057
+ available: formatAmount$1(liquidityProfile.available),
50058
+ totalSupply: formatAmount$1(liquidityProfile.totalSupply)
49884
50059
  }
49885
50060
  };
49886
50061
  }
@@ -50366,6 +50541,49 @@ const sourceAdapterContextSchema = z.object({
50366
50541
  vaultAddress: vaultAddressSchema,
50367
50542
  config: earnConfigSchema.optional()
50368
50543
  });
50544
+ /**
50545
+ * Schema for a successful gas fee entry (fees is non-null).
50546
+ *
50547
+ * Validates the kit shape — {@link EarnEstimatedGas} uses decimal strings for
50548
+ * `gas`, `gasPrice`, and `fee`. Only digits are accepted so that the deposit
50549
+ * operation can safely convert them to `bigint` before passing them to the
50550
+ * provider's `computeGasReserve`.
50551
+ *
50552
+ * @internal
50553
+ */ const earnGasFeeEstimateSuccessSchema = z.object({
50554
+ name: z.string(),
50555
+ token: z.string(),
50556
+ blockchain: z.string(),
50557
+ fees: z.object({
50558
+ gas: z.string().regex(/^\d+$/, 'gas must be a non-negative integer string'),
50559
+ gasPrice: z.string().regex(/^\d+$/, 'gasPrice must be a non-negative integer string'),
50560
+ fee: z.string().regex(/^\d+$/, 'fee must be a non-negative integer string')
50561
+ }),
50562
+ error: z.undefined().optional()
50563
+ }).strict();
50564
+ /**
50565
+ * Schema for a failed gas fee entry (fees is null, error is a string).
50566
+ *
50567
+ * @internal
50568
+ */ const earnGasFeeEstimateFailureSchema = z.object({
50569
+ name: z.string(),
50570
+ token: z.string(),
50571
+ blockchain: z.string(),
50572
+ fees: z.null(),
50573
+ error: z.string()
50574
+ }).strict();
50575
+ /**
50576
+ * Schema for a single kit-level gas fee estimate entry.
50577
+ *
50578
+ * Rejects arbitrary inputs (null, numbers, empty objects, non-numeric fee
50579
+ * strings) so callers receive a descriptive validation error instead of a
50580
+ * raw `BigInt` conversion error inside `computeGasReserve`.
50581
+ *
50582
+ * @internal
50583
+ */ const earnGasFeeEstimateSchema = z.union([
50584
+ earnGasFeeEstimateSuccessSchema,
50585
+ earnGasFeeEstimateFailureSchema
50586
+ ]);
50369
50587
  /**
50370
50588
  * Validation schema for the same-chain variant of DepositParams.
50371
50589
  *
@@ -50378,6 +50596,7 @@ const sourceAdapterContextSchema = z.object({
50378
50596
  to: z.undefined().optional(),
50379
50597
  vaultAddress: vaultAddressSchema,
50380
50598
  amount: amountSchema$1,
50599
+ gasFees: z.array(earnGasFeeEstimateSchema).optional(),
50381
50600
  config: earnConfigSchema.optional()
50382
50601
  }).strict();
50383
50602
  const crossChainDepositDestinationSchema = z.object({
@@ -50998,6 +51217,9 @@ async function deposit$3(context, params) {
50998
51217
  },
50999
51218
  vaultAddress: params.vaultAddress,
51000
51219
  amount: params.amount,
51220
+ ...!hasExplicitDestination && params.gasFees !== undefined && {
51221
+ gasFees: toProviderGasFees(params.gasFees)
51222
+ },
51001
51223
  ...params.config !== undefined && {
51002
51224
  config: params.config
51003
51225
  }
@@ -51020,6 +51242,21 @@ async function deposit$3(context, params) {
51020
51242
  }
51021
51243
  return provider.deposit(baseParams);
51022
51244
  }
51245
+ function toProviderGasFees(gasFees) {
51246
+ return gasFees.map((entry)=>{
51247
+ if (entry.fees === null) {
51248
+ return entry; // NOSONAR
51249
+ }
51250
+ return {
51251
+ ...entry,
51252
+ fees: {
51253
+ ...entry.fees,
51254
+ gas: BigInt(entry.fees.gas),
51255
+ gasPrice: BigInt(entry.fees.gasPrice)
51256
+ }
51257
+ };
51258
+ });
51259
+ }
51023
51260
  function hasCrossChainDestination(params) {
51024
51261
  // Runs before schema validation, so guard against non-object params from
51025
51262
  // untyped callers; assertEarnParams reports those with a structured error.
@@ -52495,10 +52732,10 @@ function readUserAgent(hostWindow) {
52495
52732
  * @param widgetPath - Path appended to `widgetBaseUrl`. Leading `/`
52496
52733
  * is required by the URL semantics below — see remarks.
52497
52734
  * @param params - Additional query parameters to set on the launch URL
52498
- * (e.g. `destinationWallet`, `hostApp`). Entries whose value is
52499
- * `undefined` or empty are skipped. The widget reads these from the
52500
- * query string — `destinationWallet` in particular is **required** by
52501
- * the widget, so callers that have it must pass it here.
52735
+ * (e.g. the asset-scoping `chains` / `tokens` / `pairs`). Entries whose
52736
+ * value is `undefined` or empty are skipped. Launch URLs end up in
52737
+ * browser history and access logs, so never pass user identifiers
52738
+ * such as wallet addresses here.
52502
52739
  * @returns A fully-qualified launch URL.
52503
52740
  *
52504
52741
  * @remarks
@@ -52522,9 +52759,9 @@ function readUserAgent(hostWindow) {
52522
52759
  * @example
52523
52760
  * ```ts
52524
52761
  * composeWidgetUrl('https://onramp.arc.io', 'tok-1', '', {
52525
- * destinationWallet: '0xabc',
52762
+ * tokens: 'USDC',
52526
52763
  * })
52527
- * // → 'https://onramp.arc.io/?sessionToken=tok-1&destinationWallet=0xabc'
52764
+ * // → 'https://onramp.arc.io/?sessionToken=tok-1&tokens=USDC'
52528
52765
  * ```
52529
52766
  *
52530
52767
  * @internal Implementation detail shared by the server kit
@@ -52533,13 +52770,6 @@ function readUserAgent(hostWindow) {
52533
52770
  */ function composeWidgetUrl(widgetBaseUrl, sessionToken, widgetPath, params) {
52534
52771
  const url = new URL(widgetPath, widgetBaseUrl);
52535
52772
  url.searchParams.set('sessionToken', sessionToken);
52536
- if (params !== undefined) {
52537
- for (const [key, value] of Object.entries(params)){
52538
- if (typeof value === 'string' && value.length > 0) {
52539
- url.searchParams.set(key, value);
52540
- }
52541
- }
52542
- }
52543
52773
  return url.toString();
52544
52774
  }
52545
52775
 
@@ -52749,12 +52979,10 @@ function readUserAgent(hostWindow) {
52749
52979
  assertSafeWidgetUrl(session.widgetUrl, expectedOrigin);
52750
52980
  return session.widgetUrl;
52751
52981
  }
52752
- // Fallback path: compose from a bare session token. The widget reads
52753
- // the destination wallet from the URL, so forward it when the session
52754
- // carries one (server-minted sessions always do).
52755
- return composeWidgetUrl(widgetBaseUrl, session.sessionToken, ONRAMP_WIDGET_PATH, {
52756
- destinationWallet: typeof session.destinationWallet === 'string' ? session.destinationWallet : undefined
52757
- });
52982
+ // Fallback path: compose from a bare session token. The widget reads the
52983
+ // destination wallet from the token's `walletAddress` claim, so
52984
+ // `session.destinationWallet` must not be added to the URL.
52985
+ return composeWidgetUrl(widgetBaseUrl, session.sessionToken, ONRAMP_WIDGET_PATH);
52758
52986
  }
52759
52987
 
52760
52988
  const ALLOWED_TRANSITIONS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circle-fin/app-kit",
3
- "version": "1.15.2",
3
+ "version": "1.15.3",
4
4
  "description": "A one-stop Circle SDK solution for building stablecoin (e.g. USDC) applications, with bridging, swapping, and more on-chain operations",
5
5
  "keywords": [
6
6
  "circle",
@@ -36,10 +36,10 @@
36
36
  "module": "./index.mjs",
37
37
  "types": "./index.d.cts",
38
38
  "dependencies": {
39
- "@circle-fin/onramp-kit": "1.0.1",
39
+ "@circle-fin/onramp-kit": "1.0.2",
40
40
  "@circle-fin/unified-balance-kit": "1.7.0",
41
41
  "@circle-fin/provider-gateway-v1": "1.5.0",
42
- "@circle-fin/earn-kit": "1.7.0",
42
+ "@circle-fin/earn-kit": "1.8.0",
43
43
  "@circle-fin/bridge-kit": "1.15.1",
44
44
  "@circle-fin/swap-kit": "1.7.0",
45
45
  "@coral-xyz/anchor": "^0.31.1",
package/server.cjs CHANGED
@@ -8650,7 +8650,7 @@ const swapTokenEnumSchema = zod.z.enum([
8650
8650
  return kit;
8651
8651
  }
8652
8652
 
8653
- var version = "1.0.1";
8653
+ var version = "1.0.2";
8654
8654
  var pkg = {
8655
8655
  version: version};
8656
8656
 
@@ -9166,10 +9166,10 @@ function isAbortLikeError(err) {
9166
9166
  * @param widgetPath - Path appended to `widgetBaseUrl`. Leading `/`
9167
9167
  * is required by the URL semantics below — see remarks.
9168
9168
  * @param params - Additional query parameters to set on the launch URL
9169
- * (e.g. `destinationWallet`, `hostApp`). Entries whose value is
9170
- * `undefined` or empty are skipped. The widget reads these from the
9171
- * query string — `destinationWallet` in particular is **required** by
9172
- * the widget, so callers that have it must pass it here.
9169
+ * (e.g. the asset-scoping `chains` / `tokens` / `pairs`). Entries whose
9170
+ * value is `undefined` or empty are skipped. Launch URLs end up in
9171
+ * browser history and access logs, so never pass user identifiers
9172
+ * such as wallet addresses here.
9173
9173
  * @returns A fully-qualified launch URL.
9174
9174
  *
9175
9175
  * @remarks
@@ -9193,9 +9193,9 @@ function isAbortLikeError(err) {
9193
9193
  * @example
9194
9194
  * ```ts
9195
9195
  * composeWidgetUrl('https://onramp.arc.io', 'tok-1', '', {
9196
- * destinationWallet: '0xabc',
9196
+ * tokens: 'USDC',
9197
9197
  * })
9198
- * // → 'https://onramp.arc.io/?sessionToken=tok-1&destinationWallet=0xabc'
9198
+ * // → 'https://onramp.arc.io/?sessionToken=tok-1&tokens=USDC'
9199
9199
  * ```
9200
9200
  *
9201
9201
  * @internal Implementation detail shared by the server kit
@@ -9434,17 +9434,13 @@ function isAbortLikeError(err) {
9434
9434
  const session = unwrapped !== null && typeof unwrapped === 'object' ? unwrapped : {};
9435
9435
  const sessionToken = typeof session['sessionToken'] === 'string' ? session['sessionToken'] : undefined;
9436
9436
  const existingWidgetUrl = typeof session['widgetUrl'] === 'string' && session['widgetUrl'].length > 0 ? session['widgetUrl'] : undefined;
9437
- // The widget reads the destination wallet from the launch URL
9438
- // (`destinationWallet` query param), NOT from the session token —
9439
- // the `/v1/stablecoinKits/sessions` body is anonymous. We must
9440
- // therefore bake it into the composed URL or the widget reports
9441
- // "Destination wallet is required." mid-flow. Host-app asset scoping
9442
- // (`assets`) rides the same launch URL as `chains`/`tokens`/`pairs`.
9437
+ // The launch URL carries only the session token and host-app asset
9438
+ // scoping (`chains`/`tokens`/`pairs`). The destination wallet must
9439
+ // stay out of it: the widget reads the wallet from the signed session
9440
+ // token's `walletAddress` claim, and a query param would only leak
9441
+ // the address into browser history and access logs.
9443
9442
  const assetParams = serializeAssetSelection(params.assets);
9444
- const composedWidgetUrl = sessionToken === undefined ? undefined : composeWidgetUrl(widgetBaseUrl, sessionToken, ONRAMP_WIDGET_PATH, {
9445
- destinationWallet: params.destinationAddress,
9446
- ...assetParams
9447
- });
9443
+ const composedWidgetUrl = sessionToken === undefined ? undefined : composeWidgetUrl(widgetBaseUrl, sessionToken, ONRAMP_WIDGET_PATH, assetParams);
9448
9444
  // When wallets-api returns a `widgetUrl`, asset scoping must still be
9449
9445
  // applied — `assets` is a display-only widget concern and is never
9450
9446
  // forwarded upstream, so we merge the params directly into the returned
package/server.d.cts CHANGED
@@ -1785,17 +1785,17 @@ interface KitEventSubscription<M extends EventMap = KitEventMap> {
1785
1785
  * embeds. Never sent back by the client; it is redeemed inside the
1786
1786
  * widget origin only.
1787
1787
  * - `widgetUrl` — fully composed launch URL (origin + path + token +
1788
- * `destinationWallet`). The server kit prefers to compose this itself
1788
+ * asset-scoping params). The server kit prefers to compose this itself
1789
1789
  * so the client kit has no exposure to the widget origin. The client
1790
1790
  * still accepts a bare `sessionToken` and composes a URL locally as a
1791
1791
  * fallback. `getOnrampUrl` is the single place that enforces the
1792
1792
  * https-scheme / origin-pin safety checks before the URL reaches the
1793
1793
  * DOM — that is a security boundary, not response validation.
1794
1794
  * - `destinationWallet` — the wallet that receives funds. The widget
1795
- * requires it as a launch query param, so the server kit echoes the
1796
- * `destinationAddress` it was minted with here (and bakes it into
1797
- * `widgetUrl`). Used by the client fallback path when it has to
1798
- * compose the URL from a bare `sessionToken`.
1795
+ * reads it from the signed session token, not the launch URL, so the
1796
+ * kit never adds it there. The server kit still echoes the
1797
+ * `destinationAddress` it was minted with here for the host's
1798
+ * convenience.
1799
1799
  * - `expiresAt` — RFC 3339 timestamp. The client refuses to launch an
1800
1800
  * expired session and throws an `INPUT` `KitError` so the host can
1801
1801
  * re-mint.
@@ -1819,8 +1819,8 @@ interface OnrampSession {
1819
1819
  */
1820
1820
  readonly sessionToken: string;
1821
1821
  /**
1822
- * Fully composed launch URL (origin + path + token +
1823
- * `destinationWallet`).
1822
+ * Fully composed launch URL (origin + path + token + optional
1823
+ * asset-scoping params).
1824
1824
  *
1825
1825
  * @remarks
1826
1826
  * The server kit prefers to compose this itself; the client falls
@@ -1833,8 +1833,9 @@ interface OnrampSession {
1833
1833
  *
1834
1834
  * @remarks
1835
1835
  * Echoed verbatim from the `destinationAddress` the server kit
1836
- * minted with. Used by the client kit's URL-composition fallback
1837
- * when `widgetUrl` is absent.
1836
+ * minted with, for the host's convenience (e.g. to display it). Never
1837
+ * added to the launch URL: the widget reads the destination wallet
1838
+ * from the signed session token.
1838
1839
  */
1839
1840
  readonly destinationWallet?: string;
1840
1841
  /**