@gibs/bridge-sdk 1.18.0 → 1.19.0

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/README.md CHANGED
@@ -441,6 +441,39 @@ if (plan.kind === 'refused') throw new Error(plan.refusal.reason);
441
441
  asked for your wallet, in `sections[i].aggregator`. The candidate's own quote
442
442
  priced the route on screen and is never signed.
443
443
 
444
+ ### Counting signatures, approvals included
445
+
446
+ `routeSignatureCount(route)` counts sections only, so a USDC route reads 1 and
447
+ then asks for two prompts. It leaves out approvals. Use
448
+ `routeSignatureCountForAccount` for the number the wallet will really ask for:
449
+
450
+ ```ts
451
+ import {
452
+ type AllowanceReader,
453
+ type ResolveNodeToken,
454
+ type RouteCandidate,
455
+ type SectionExecutionInputs,
456
+ routeSignatureCountForAccount,
457
+ } from '@gibs/bridge-sdk';
458
+ import type { Hex } from 'viem';
459
+
460
+ declare const candidate: RouteCandidate;
461
+ declare const account: Hex;
462
+ declare const readAllowance: AllowanceReader;
463
+ declare const resolveToken: ResolveNodeToken;
464
+ declare const sections: SectionExecutionInputs[];
465
+
466
+ const { signatures, approvals, total } = await routeSignatureCountForAccount({
467
+ candidate, account, readAllowance, resolveToken, sections,
468
+ });
469
+ ```
470
+
471
+ It takes the inputs `planCandidateExecution` takes and counts the plan, so it
472
+ uses the same allowance rule and the same spender (mediator, swap router, or
473
+ aggregator address). A native input and a covered allowance add no approval. A
474
+ short, non-zero allowance counts two approvals (reset, then approve). It throws
475
+ when a section cannot be planned.
476
+
444
477
  ## Overriding swap routers
445
478
 
446
479
  The package swaps on PulseX V1 and V2 on PulseChain by default. You can swap on
@@ -1041,6 +1041,9 @@ const parseLifiTokens = (payload, chainId) => {
1041
1041
  // Preserve the inline spot price the LI.FI list carries — it is the USD
1042
1042
  // price source for aggregator chains (the dapp image lists carry none).
1043
1043
  priceUSD: typeof entry.priceUSD === 'string' ? entry.priceUSD : null,
1044
+ // Additive: kept only when LI.FI sends them, so a token without them has the same shape as before.
1045
+ ...(typeof entry.coinKey === 'string' ? { coinKey: entry.coinKey } : {}),
1046
+ ...(typeof entry.verificationStatus === 'string' ? { verificationStatus: entry.verificationStatus } : {}),
1044
1047
  });
1045
1048
  }
1046
1049
  return parsed;
@@ -1,5 +1,18 @@
1
1
  import type { DeferredCrossing, RouteCandidate } from './routing/candidate.js';
2
2
  import type { RouteData } from './routing/route-data.js';
3
+ /**
4
+ * The name of a step that starts on one chain and ends on another (or the same).
5
+ *
6
+ * A step whose two chains differ is "Send to {end chain}". A step that never
7
+ * leaves its chain is a swap, so it is "Swap on {chain}". "Send to Ethereum"
8
+ * on a swap that starts and ends on Ethereum names a crossing that never
9
+ * happens.
10
+ *
11
+ * @param startChainId - the chain the step starts on
12
+ * @param endChainId - the chain the step ends on
13
+ * @returns the step's English label
14
+ */
15
+ export declare const sendOrSwapLabel: (startChainId: number, endChainId: number) => string;
3
16
  /**
4
17
  * Builds the two-signature route for a candidate whose aggregator cannot
5
18
  * execute the omnibridge entry call.
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.describeDeferredCrossing = exports.buildNearIntentsTwoStepRouteData = void 0;
36
+ exports.describeDeferredCrossing = exports.buildNearIntentsTwoStepRouteData = exports.sendOrSwapLabel = void 0;
37
37
  const imageLinks = __importStar(require("./image-links.js"));
38
38
  const producers_js_1 = require("./routing/producers.js");
39
39
  const providers_js_1 = require("./providers.js");
@@ -89,6 +89,22 @@ const tokenNode = (token, chainId) => ({
89
89
  logoUri: imageLinks.tokenImageOrChainFallback(token),
90
90
  chainLogoUri: imageLinks.networkOrPseudo(chainId),
91
91
  });
92
+ /**
93
+ * The name of a step that starts on one chain and ends on another (or the same).
94
+ *
95
+ * A step whose two chains differ is "Send to {end chain}". A step that never
96
+ * leaves its chain is a swap, so it is "Swap on {chain}". "Send to Ethereum"
97
+ * on a swap that starts and ends on Ethereum names a crossing that never
98
+ * happens.
99
+ *
100
+ * @param startChainId - the chain the step starts on
101
+ * @param endChainId - the chain the step ends on
102
+ * @returns the step's English label
103
+ */
104
+ const sendOrSwapLabel = (startChainId, endChainId) => startChainId === endChainId
105
+ ? `Swap on ${(0, chain_names_js_1.chainDisplayName)(endChainId)}`
106
+ : `Send to ${(0, chain_names_js_1.chainDisplayName)(endChainId)}`;
107
+ exports.sendOrSwapLabel = sendOrSwapLabel;
92
108
  /**
93
109
  * Parses one of the aggregator's own dollar figures.
94
110
  *
@@ -174,7 +190,7 @@ const buildNearIntentsTwoStepRouteData = (candidate) => {
174
190
  // silently naming a different provider.
175
191
  providerName: (0, providers_js_1.providerById)(candidate.quote.provider).label,
176
192
  providerKey: candidate.quote.provider,
177
- title: `Send to ${(0, chain_names_js_1.chainDisplayName)(candidate.entryChainId)}`,
193
+ title: (0, exports.sendOrSwapLabel)(originToken.chainId, candidate.entryChainId),
178
194
  conversion: `→ ${anchorToken.symbol}`,
179
195
  fromToken: originNode,
180
196
  toToken: anchorNode,
@@ -187,7 +203,7 @@ const buildNearIntentsTwoStepRouteData = (candidate) => {
187
203
  // Named for where the funds END UP, and pointedly NOT "Bridge to
188
204
  // PulseChain": this signature does not reach PulseChain and a label that
189
205
  // said so would be the single-signature claim this route exists to avoid.
190
- label: `Send to ${(0, chain_names_js_1.chainDisplayName)(candidate.entryChainId)}`,
206
+ label: (0, exports.sendOrSwapLabel)(originToken.chainId, candidate.entryChainId),
191
207
  providerKey: candidate.quote.provider,
192
208
  tokenFlow: [originNode, anchorNode],
193
209
  // WHAT THE READER IS LEFT HOLDING between the two signatures, in their own
@@ -1,3 +1,4 @@
1
+ import { type CandidateExecutionInputs } from '../execution.js';
1
2
  import type { RouteCandidate } from './candidate.js';
2
3
  import type { RouteData } from './route-data.js';
3
4
  /**
@@ -37,7 +38,51 @@ export declare const buildRouteDataForCandidate: (candidate: RouteCandidate) =>
37
38
  * site, so a surface that warns about a long journey and a surface that ranks
38
39
  * routes cannot come to different counts.
39
40
  *
41
+ * THIS COUNT LEAVES OUT APPROVALS. A token that is not the chain's native coin
42
+ * needs an `approve` transaction before its first section when the allowance
43
+ * is too small, and that approval is a signature too. Raw route data does not
44
+ * say who the spender is or what the account has approved already, so this
45
+ * function cannot count it. Use {@link routeSignatureCountForAccount} for the
46
+ * number the wallet will really ask for.
47
+ *
40
48
  * @param route - the raw route data
41
- * @returns the number of signatures, one per section
49
+ * @returns the number of signatures, one per section, with no approvals
42
50
  */
43
51
  export declare const routeSignatureCount: (route: RouteData) => number;
52
+ /** What {@link routeSignatureCountForAccount} reports. */
53
+ export type RouteSignatureCount = {
54
+ /** Signatures for the sections themselves: one per planned section. */
55
+ readonly signatures: number;
56
+ /** Approval transactions the account must sign first, given what it has approved already. */
57
+ readonly approvals: number;
58
+ /** `signatures + approvals`: every wallet prompt the planned sections cause. */
59
+ readonly total: number;
60
+ };
61
+ /**
62
+ * How many wallet signatures a route asks one account for, approvals included.
63
+ *
64
+ * {@link routeSignatureCount} counts sections only, so a USDC route reads "1"
65
+ * and then asks for two prompts: approve, then send. This function plans the
66
+ * sections with {@link planCandidateExecution} and counts what that plan
67
+ * contains, so it uses the same allowance rule and the same spender the
68
+ * execution planner uses. A native input needs no approval. An allowance that
69
+ * already covers the amount needs none. A smaller, non-zero allowance counts
70
+ * the reset and the new approval, the two transactions the planner emits. The
71
+ * spender is whatever the planner picks: the omnibridge mediator, a swap
72
+ * venue's router (including routers passed in `swapVenues`), or the
73
+ * aggregator's approval address.
74
+ *
75
+ * Pass every section of the route to count the whole route. Pass fewer to
76
+ * count only the window you plan now; each section is counted against the
77
+ * allowance on chain today, so a later section that spends a token an earlier
78
+ * section delivers is counted as if the approval is still needed.
79
+ *
80
+ * An aggregator section needs its `aggregator` inputs, a quote and the request
81
+ * it answered, exactly as {@link planCandidateExecution} does. Without them
82
+ * this function throws, because the approval spender comes from that quote.
83
+ *
84
+ * @param inputs - the same inputs {@link planCandidateExecution} takes
85
+ * @returns the signatures, approvals, and their total
86
+ * @throws when a section cannot be planned; the message names the section and the reason
87
+ */
88
+ export declare const routeSignatureCountForAccount: (inputs: CandidateExecutionInputs) => Promise<RouteSignatureCount>;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.routeSignatureCount = exports.buildRouteDataForCandidate = void 0;
3
+ exports.routeSignatureCountForAccount = exports.routeSignatureCount = exports.buildRouteDataForCandidate = void 0;
4
4
  const near_intents_route_js_1 = require("../near-intents-route.js");
5
+ const execution_js_1 = require("../execution.js");
5
6
  const producers_js_1 = require("./producers.js");
6
7
  /**
7
8
  * One door in front of the two base route builders.
@@ -43,8 +44,52 @@ exports.buildRouteDataForCandidate = buildRouteDataForCandidate;
43
44
  * site, so a surface that warns about a long journey and a surface that ranks
44
45
  * routes cannot come to different counts.
45
46
  *
47
+ * THIS COUNT LEAVES OUT APPROVALS. A token that is not the chain's native coin
48
+ * needs an `approve` transaction before its first section when the allowance
49
+ * is too small, and that approval is a signature too. Raw route data does not
50
+ * say who the spender is or what the account has approved already, so this
51
+ * function cannot count it. Use {@link routeSignatureCountForAccount} for the
52
+ * number the wallet will really ask for.
53
+ *
46
54
  * @param route - the raw route data
47
- * @returns the number of signatures, one per section
55
+ * @returns the number of signatures, one per section, with no approvals
48
56
  */
49
57
  const routeSignatureCount = (route) => route.sections.length;
50
58
  exports.routeSignatureCount = routeSignatureCount;
59
+ /**
60
+ * How many wallet signatures a route asks one account for, approvals included.
61
+ *
62
+ * {@link routeSignatureCount} counts sections only, so a USDC route reads "1"
63
+ * and then asks for two prompts: approve, then send. This function plans the
64
+ * sections with {@link planCandidateExecution} and counts what that plan
65
+ * contains, so it uses the same allowance rule and the same spender the
66
+ * execution planner uses. A native input needs no approval. An allowance that
67
+ * already covers the amount needs none. A smaller, non-zero allowance counts
68
+ * the reset and the new approval, the two transactions the planner emits. The
69
+ * spender is whatever the planner picks: the omnibridge mediator, a swap
70
+ * venue's router (including routers passed in `swapVenues`), or the
71
+ * aggregator's approval address.
72
+ *
73
+ * Pass every section of the route to count the whole route. Pass fewer to
74
+ * count only the window you plan now; each section is counted against the
75
+ * allowance on chain today, so a later section that spends a token an earlier
76
+ * section delivers is counted as if the approval is still needed.
77
+ *
78
+ * An aggregator section needs its `aggregator` inputs, a quote and the request
79
+ * it answered, exactly as {@link planCandidateExecution} does. Without them
80
+ * this function throws, because the approval spender comes from that quote.
81
+ *
82
+ * @param inputs - the same inputs {@link planCandidateExecution} takes
83
+ * @returns the signatures, approvals, and their total
84
+ * @throws when a section cannot be planned; the message names the section and the reason
85
+ */
86
+ const routeSignatureCountForAccount = async (inputs) => {
87
+ const plan = await (0, execution_js_1.planCandidateExecution)(inputs);
88
+ if (plan.kind === 'refused') {
89
+ throw new Error(`Cannot count signatures: section ${plan.sectionIndex} cannot be planned (${JSON.stringify(plan.refusal)}).`);
90
+ }
91
+ const approvals = plan.transactions.filter((transaction) => transaction.kind === 'approve').length;
92
+ const signatures = plan.transactions.length - approvals;
93
+ return { signatures, approvals, total: plan.transactions.length };
94
+ };
95
+ exports.routeSignatureCountForAccount = routeSignatureCountForAccount;
@@ -8,7 +8,7 @@ export { netDeliveredAmount, restateInDecimals, valueEquivalentAmount } from './
8
8
  export { omnibridgeProviderForEntryChain, pulseXConversionSwapPath } from './providers.js';
9
9
  export { buildDirectCrossingCandidate } from './candidate.js';
10
10
  export type { RouteCandidate, DeferredCrossing, DirectCrossingCandidateInputs } from './candidate.js';
11
- export { buildRouteDataForCandidate, routeSignatureCount } from './candidate-route.js';
11
+ export { buildRouteDataForCandidate, routeSignatureCount, routeSignatureCountForAccount, type RouteSignatureCount, } from './candidate-route.js';
12
12
  export { COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT, COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT, COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT, assessComposedRoute, composedRouteMayBeOffered, } from './composed-route-quality.js';
13
13
  export type { ComposedRouteVerdict } from './composed-route-quality.js';
14
14
  export { entryChainAnchorsForPair } from './entry-chain-anchor.js';
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.composeRouteDelivery = exports.composableHopBasis = exports.routePlanPathKey = exports.planIsRankable = exports.planIsConfirmed = exports.planComposedBasis = exports.isRateLimitedCarrier = exports.isRankableBasis = exports.isConfirmedBasis = exports.hopEdgeKey = exports.hopComposedBasis = exports.groupHopsBySignature = exports.compareRoutePlansByDeliveredAmount = exports.combineComposedPathBasis = exports.assetNodeKey = exports.RANKABLE_ESTIMATE_BASES = exports.candidatePaidUsd = exports.candidateOutsideFeesUsd = exports.candidateNetValueUsd = exports.candidateDeliveredUsd = exports.buildSameChainSwapRouteData = exports.buildOutboundRouteData = exports.buildPulseXSwapSectionData = exports.buildPulseXSwapEstimateData = exports.buildDirectRouteData = exports.buildConversionRouteData = exports.buildCandidateRouteData = exports.resolveEntryChainName = exports.providerDisplayName = exports.addUsdValues = exports.pulsechainChainId = exports.PULSEX_MARGINAL_REFERENCE_DIVISOR = exports.BRIDGE_SETTLEMENT_ETA_MINUTES = exports.formatUsdNullable = exports.formatUsd = exports.usdValueOfAmount = exports.entryChainAnchorsForPair = exports.composedRouteMayBeOffered = exports.assessComposedRoute = exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = exports.routeSignatureCount = exports.buildRouteDataForCandidate = exports.buildDirectCrossingCandidate = exports.pulseXConversionSwapPath = exports.omnibridgeProviderForEntryChain = exports.valueEquivalentAmount = exports.restateInDecimals = exports.netDeliveredAmount = void 0;
4
- exports.swapMinimumOut = exports.buildWrapTransaction = exports.buildPulseXSwapTransaction = exports.createWrapCarrier = exports.resolveSwapVenues = exports.findSwapVenue = exports.pulseXHubTokens = exports.candidatePulseXPaths = exports.bestPulseXPathQuote = exports.pulseXWrappedNativePath = exports.pulseXVenues = exports.pulseXSwapPathForHop = exports.venueForEdge = exports.pulseXRouterForEdge = exports.createPulseXCarrier = exports.withOmnibridgeEntryFusing = exports.createOmnibridgeCarrier = exports.derivePlanKind = exports.deriveViaChainIdsFromPlan = exports.deriveOmnibridgeProviderFromPlan = exports.deriveEntryChainIdFromPlan = exports.deriveDeferredCrossingFromPlan = exports.buildRouteScoreInputsFromPlan = exports.buildRouteDataFromPlan = exports.buildRouteCandidateFromPlan = exports.carrierStepKind = exports.carrierProviderKey = exports.WRAP_CARRIER_ID = exports.zeroGasCostFor = exports.waitPenaltyPpm = exports.sectionIsFundedByLanding = exports.scoreRoutePlan = exports.gasAdjustedDeliveredAmount = exports.compareRoutePlansByScoreAdjustedDeliveredAmount = exports.compareRoutePlansByGasAdjustedDeliveredAmount = exports.SIGNATURE_PENALTY_PPM = exports.SIGNATURE_PENALTY_FLOOR_USD = exports.GAS_TOPUP_PENALTY_PPM = exports.GAS_TOPUP_PENALTY_FLOOR_USD = exports.hopOutputAmount = void 0;
3
+ exports.composableHopBasis = exports.routePlanPathKey = exports.planIsRankable = exports.planIsConfirmed = exports.planComposedBasis = exports.isRateLimitedCarrier = exports.isRankableBasis = exports.isConfirmedBasis = exports.hopEdgeKey = exports.hopComposedBasis = exports.groupHopsBySignature = exports.compareRoutePlansByDeliveredAmount = exports.combineComposedPathBasis = exports.assetNodeKey = exports.RANKABLE_ESTIMATE_BASES = exports.candidatePaidUsd = exports.candidateOutsideFeesUsd = exports.candidateNetValueUsd = exports.candidateDeliveredUsd = exports.buildSameChainSwapRouteData = exports.buildOutboundRouteData = exports.buildPulseXSwapSectionData = exports.buildPulseXSwapEstimateData = exports.buildDirectRouteData = exports.buildConversionRouteData = exports.buildCandidateRouteData = exports.resolveEntryChainName = exports.providerDisplayName = exports.addUsdValues = exports.pulsechainChainId = exports.PULSEX_MARGINAL_REFERENCE_DIVISOR = exports.BRIDGE_SETTLEMENT_ETA_MINUTES = exports.formatUsdNullable = exports.formatUsd = exports.usdValueOfAmount = exports.entryChainAnchorsForPair = exports.composedRouteMayBeOffered = exports.assessComposedRoute = exports.COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT = exports.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT = exports.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT = exports.routeSignatureCountForAccount = exports.routeSignatureCount = exports.buildRouteDataForCandidate = exports.buildDirectCrossingCandidate = exports.pulseXConversionSwapPath = exports.omnibridgeProviderForEntryChain = exports.valueEquivalentAmount = exports.restateInDecimals = exports.netDeliveredAmount = void 0;
4
+ exports.swapMinimumOut = exports.buildWrapTransaction = exports.buildPulseXSwapTransaction = exports.createWrapCarrier = exports.resolveSwapVenues = exports.findSwapVenue = exports.pulseXHubTokens = exports.candidatePulseXPaths = exports.bestPulseXPathQuote = exports.pulseXWrappedNativePath = exports.pulseXVenues = exports.pulseXSwapPathForHop = exports.venueForEdge = exports.pulseXRouterForEdge = exports.createPulseXCarrier = exports.withOmnibridgeEntryFusing = exports.createOmnibridgeCarrier = exports.derivePlanKind = exports.deriveViaChainIdsFromPlan = exports.deriveOmnibridgeProviderFromPlan = exports.deriveEntryChainIdFromPlan = exports.deriveDeferredCrossingFromPlan = exports.buildRouteScoreInputsFromPlan = exports.buildRouteDataFromPlan = exports.buildRouteCandidateFromPlan = exports.carrierStepKind = exports.carrierProviderKey = exports.WRAP_CARRIER_ID = exports.zeroGasCostFor = exports.waitPenaltyPpm = exports.sectionIsFundedByLanding = exports.scoreRoutePlan = exports.gasAdjustedDeliveredAmount = exports.compareRoutePlansByScoreAdjustedDeliveredAmount = exports.compareRoutePlansByGasAdjustedDeliveredAmount = exports.SIGNATURE_PENALTY_PPM = exports.SIGNATURE_PENALTY_FLOOR_USD = exports.GAS_TOPUP_PENALTY_PPM = exports.GAS_TOPUP_PENALTY_FLOOR_USD = exports.hopOutputAmount = exports.composeRouteDelivery = void 0;
5
5
  /**
6
6
  * Framework-agnostic routing core for the Gibs bridge. Holds the raw numeric
7
7
  * primitives, the candidate shape, and candidate construction — none of which
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "buildDirectCrossingCandidate", { enumerable: tru
20
20
  var candidate_route_js_1 = require("./candidate-route.js");
21
21
  Object.defineProperty(exports, "buildRouteDataForCandidate", { enumerable: true, get: function () { return candidate_route_js_1.buildRouteDataForCandidate; } });
22
22
  Object.defineProperty(exports, "routeSignatureCount", { enumerable: true, get: function () { return candidate_route_js_1.routeSignatureCount; } });
23
+ Object.defineProperty(exports, "routeSignatureCountForAccount", { enumerable: true, get: function () { return candidate_route_js_1.routeSignatureCountForAccount; } });
23
24
  var composed_route_quality_js_1 = require("./composed-route-quality.js");
24
25
  Object.defineProperty(exports, "COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT", { enumerable: true, get: function () { return composed_route_quality_js_1.COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT; } });
25
26
  Object.defineProperty(exports, "COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT", { enumerable: true, get: function () { return composed_route_quality_js_1.COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT; } });
@@ -215,6 +215,8 @@ const groupEndsWithBridgeHop = (hops) => isBridgeCarrier(hops[hops.length - 1].e
215
215
  * section, in BOTH its shapes (the entry-chain swap and the onward
216
216
  * aggregator hop) — the section label names where the journey finishes
217
217
  * regardless of which service carries the last leg.
218
+ * - **"Swap on {X}"** — a group of the "Send to" shape whose first hop starts
219
+ * on the chain its last hop ends on: nothing is sent anywhere.
218
220
  * - **"Send to {X}"** — every other case: a non-final, non-bridge-ending
219
221
  * group. Matches `buildNearIntentsTwoStepRouteData`'s first section.
220
222
  *
@@ -236,7 +238,7 @@ const sectionLabel = (group, groupIndex, plan) => {
236
238
  if (isFinalGroup) {
237
239
  return `Complete at ${(0, chain_names_js_1.chainDisplayName)(destinationChainId)}`;
238
240
  }
239
- return `Send to ${(0, chain_names_js_1.chainDisplayName)(destinationChainId)}`;
241
+ return (0, near_intents_route_js_1.sendOrSwapLabel)(hops[0].edge.from.chainId, destinationChainId);
240
242
  };
241
243
  // ---------------------------------------------------------------------------
242
244
  // Hop-level rendering
@@ -291,7 +293,8 @@ const hopProviderName = (carrier) => {
291
293
  * `"Bridge to {toChainName}"` (matches `buildOutboundRouteData`'s onward
292
294
  * hop, which names the chain it finishes on even though an aggregator,
293
295
  * not the omnibridge, carries it);
294
- * - any other aggregator hop (something follows it) → `"Send to {toChainName}"`
296
+ * - any other aggregator hop (something follows it) → `"Send to {toChainName}"`,
297
+ * or `"Swap on {chainName}"` when the hop starts and ends on one chain
295
298
  * (matches `buildNearIntentsTwoStepRouteData`'s aggregator hop).
296
299
  *
297
300
  * @param hop - the hop to title
@@ -310,10 +313,10 @@ const hopTitle = (hop, hopIndex, plan, resolveToken) => {
310
313
  if (carrier === 'PulseX') {
311
314
  return `Swap to ${resolveToken(hop.edge.to).symbol}`;
312
315
  }
313
- const destinationChainName = (0, chain_names_js_1.chainDisplayName)(hop.edge.to.chainId);
314
- return isFinalHopOfPlan(hopIndex, plan)
315
- ? `Bridge to ${destinationChainName}`
316
- : `Send to ${destinationChainName}`;
316
+ if (isFinalHopOfPlan(hopIndex, plan)) {
317
+ return `Bridge to ${(0, chain_names_js_1.chainDisplayName)(hop.edge.to.chainId)}`;
318
+ }
319
+ return (0, near_intents_route_js_1.sendOrSwapLabel)(hop.edge.from.chainId, hop.edge.to.chainId);
317
320
  };
318
321
  /**
319
322
  * A hop's conversion subtitle.
@@ -30,6 +30,14 @@ export type Token = TokenMetadata & {
30
30
  * that carry no price (e.g. the dapp image lists).
31
31
  */
32
32
  priceUSD?: string | null;
33
+ /**
34
+ * LI.FI's `coinKey`: a short key naming the asset across chains (for example
35
+ * "USDC"). Present only on tokens parsed from a LI.FI list that carries one.
36
+ * LI.FI repeats some keys within one chain, so a key alone is not an identity.
37
+ */
38
+ coinKey?: string;
39
+ /** LI.FI's `verificationStatus` (for example "verified"). Present only on tokens parsed from a LI.FI list that carries one. */
40
+ verificationStatus?: string;
33
41
  };
34
42
  /** The token out for a given asset */
35
43
  export type TokenOut = Omit<Token, 'address'> & {
@@ -1025,6 +1025,9 @@ export const parseLifiTokens = (payload, chainId) => {
1025
1025
  // Preserve the inline spot price the LI.FI list carries — it is the USD
1026
1026
  // price source for aggregator chains (the dapp image lists carry none).
1027
1027
  priceUSD: typeof entry.priceUSD === 'string' ? entry.priceUSD : null,
1028
+ // Additive: kept only when LI.FI sends them, so a token without them has the same shape as before.
1029
+ ...(typeof entry.coinKey === 'string' ? { coinKey: entry.coinKey } : {}),
1030
+ ...(typeof entry.verificationStatus === 'string' ? { verificationStatus: entry.verificationStatus } : {}),
1028
1031
  });
1029
1032
  }
1030
1033
  return parsed;
@@ -1,5 +1,18 @@
1
1
  import type { DeferredCrossing, RouteCandidate } from './routing/candidate.js';
2
2
  import type { RouteData } from './routing/route-data.js';
3
+ /**
4
+ * The name of a step that starts on one chain and ends on another (or the same).
5
+ *
6
+ * A step whose two chains differ is "Send to {end chain}". A step that never
7
+ * leaves its chain is a swap, so it is "Swap on {chain}". "Send to Ethereum"
8
+ * on a swap that starts and ends on Ethereum names a crossing that never
9
+ * happens.
10
+ *
11
+ * @param startChainId - the chain the step starts on
12
+ * @param endChainId - the chain the step ends on
13
+ * @returns the step's English label
14
+ */
15
+ export declare const sendOrSwapLabel: (startChainId: number, endChainId: number) => string;
3
16
  /**
4
17
  * Builds the two-signature route for a candidate whose aggregator cannot
5
18
  * execute the omnibridge entry call.
@@ -53,6 +53,21 @@ const tokenNode = (token, chainId) => ({
53
53
  logoUri: imageLinks.tokenImageOrChainFallback(token),
54
54
  chainLogoUri: imageLinks.networkOrPseudo(chainId),
55
55
  });
56
+ /**
57
+ * The name of a step that starts on one chain and ends on another (or the same).
58
+ *
59
+ * A step whose two chains differ is "Send to {end chain}". A step that never
60
+ * leaves its chain is a swap, so it is "Swap on {chain}". "Send to Ethereum"
61
+ * on a swap that starts and ends on Ethereum names a crossing that never
62
+ * happens.
63
+ *
64
+ * @param startChainId - the chain the step starts on
65
+ * @param endChainId - the chain the step ends on
66
+ * @returns the step's English label
67
+ */
68
+ export const sendOrSwapLabel = (startChainId, endChainId) => startChainId === endChainId
69
+ ? `Swap on ${chainDisplayName(endChainId)}`
70
+ : `Send to ${chainDisplayName(endChainId)}`;
56
71
  /**
57
72
  * Parses one of the aggregator's own dollar figures.
58
73
  *
@@ -138,7 +153,7 @@ export const buildNearIntentsTwoStepRouteData = (candidate) => {
138
153
  // silently naming a different provider.
139
154
  providerName: providerById(candidate.quote.provider).label,
140
155
  providerKey: candidate.quote.provider,
141
- title: `Send to ${chainDisplayName(candidate.entryChainId)}`,
156
+ title: sendOrSwapLabel(originToken.chainId, candidate.entryChainId),
142
157
  conversion: `→ ${anchorToken.symbol}`,
143
158
  fromToken: originNode,
144
159
  toToken: anchorNode,
@@ -151,7 +166,7 @@ export const buildNearIntentsTwoStepRouteData = (candidate) => {
151
166
  // Named for where the funds END UP, and pointedly NOT "Bridge to
152
167
  // PulseChain": this signature does not reach PulseChain and a label that
153
168
  // said so would be the single-signature claim this route exists to avoid.
154
- label: `Send to ${chainDisplayName(candidate.entryChainId)}`,
169
+ label: sendOrSwapLabel(originToken.chainId, candidate.entryChainId),
155
170
  providerKey: candidate.quote.provider,
156
171
  tokenFlow: [originNode, anchorNode],
157
172
  // WHAT THE READER IS LEFT HOLDING between the two signatures, in their own
@@ -1,3 +1,4 @@
1
+ import { type CandidateExecutionInputs } from '../execution.js';
1
2
  import type { RouteCandidate } from './candidate.js';
2
3
  import type { RouteData } from './route-data.js';
3
4
  /**
@@ -37,7 +38,51 @@ export declare const buildRouteDataForCandidate: (candidate: RouteCandidate) =>
37
38
  * site, so a surface that warns about a long journey and a surface that ranks
38
39
  * routes cannot come to different counts.
39
40
  *
41
+ * THIS COUNT LEAVES OUT APPROVALS. A token that is not the chain's native coin
42
+ * needs an `approve` transaction before its first section when the allowance
43
+ * is too small, and that approval is a signature too. Raw route data does not
44
+ * say who the spender is or what the account has approved already, so this
45
+ * function cannot count it. Use {@link routeSignatureCountForAccount} for the
46
+ * number the wallet will really ask for.
47
+ *
40
48
  * @param route - the raw route data
41
- * @returns the number of signatures, one per section
49
+ * @returns the number of signatures, one per section, with no approvals
42
50
  */
43
51
  export declare const routeSignatureCount: (route: RouteData) => number;
52
+ /** What {@link routeSignatureCountForAccount} reports. */
53
+ export type RouteSignatureCount = {
54
+ /** Signatures for the sections themselves: one per planned section. */
55
+ readonly signatures: number;
56
+ /** Approval transactions the account must sign first, given what it has approved already. */
57
+ readonly approvals: number;
58
+ /** `signatures + approvals`: every wallet prompt the planned sections cause. */
59
+ readonly total: number;
60
+ };
61
+ /**
62
+ * How many wallet signatures a route asks one account for, approvals included.
63
+ *
64
+ * {@link routeSignatureCount} counts sections only, so a USDC route reads "1"
65
+ * and then asks for two prompts: approve, then send. This function plans the
66
+ * sections with {@link planCandidateExecution} and counts what that plan
67
+ * contains, so it uses the same allowance rule and the same spender the
68
+ * execution planner uses. A native input needs no approval. An allowance that
69
+ * already covers the amount needs none. A smaller, non-zero allowance counts
70
+ * the reset and the new approval, the two transactions the planner emits. The
71
+ * spender is whatever the planner picks: the omnibridge mediator, a swap
72
+ * venue's router (including routers passed in `swapVenues`), or the
73
+ * aggregator's approval address.
74
+ *
75
+ * Pass every section of the route to count the whole route. Pass fewer to
76
+ * count only the window you plan now; each section is counted against the
77
+ * allowance on chain today, so a later section that spends a token an earlier
78
+ * section delivers is counted as if the approval is still needed.
79
+ *
80
+ * An aggregator section needs its `aggregator` inputs, a quote and the request
81
+ * it answered, exactly as {@link planCandidateExecution} does. Without them
82
+ * this function throws, because the approval spender comes from that quote.
83
+ *
84
+ * @param inputs - the same inputs {@link planCandidateExecution} takes
85
+ * @returns the signatures, approvals, and their total
86
+ * @throws when a section cannot be planned; the message names the section and the reason
87
+ */
88
+ export declare const routeSignatureCountForAccount: (inputs: CandidateExecutionInputs) => Promise<RouteSignatureCount>;
@@ -1,4 +1,5 @@
1
1
  import { buildNearIntentsTwoStepRouteData } from '../near-intents-route.js';
2
+ import { planCandidateExecution } from '../execution.js';
2
3
  import { buildCandidateRouteData } from './producers.js';
3
4
  /**
4
5
  * One door in front of the two base route builders.
@@ -39,7 +40,50 @@ export const buildRouteDataForCandidate = (candidate) => candidate.deferredCross
39
40
  * site, so a surface that warns about a long journey and a surface that ranks
40
41
  * routes cannot come to different counts.
41
42
  *
43
+ * THIS COUNT LEAVES OUT APPROVALS. A token that is not the chain's native coin
44
+ * needs an `approve` transaction before its first section when the allowance
45
+ * is too small, and that approval is a signature too. Raw route data does not
46
+ * say who the spender is or what the account has approved already, so this
47
+ * function cannot count it. Use {@link routeSignatureCountForAccount} for the
48
+ * number the wallet will really ask for.
49
+ *
42
50
  * @param route - the raw route data
43
- * @returns the number of signatures, one per section
51
+ * @returns the number of signatures, one per section, with no approvals
44
52
  */
45
53
  export const routeSignatureCount = (route) => route.sections.length;
54
+ /**
55
+ * How many wallet signatures a route asks one account for, approvals included.
56
+ *
57
+ * {@link routeSignatureCount} counts sections only, so a USDC route reads "1"
58
+ * and then asks for two prompts: approve, then send. This function plans the
59
+ * sections with {@link planCandidateExecution} and counts what that plan
60
+ * contains, so it uses the same allowance rule and the same spender the
61
+ * execution planner uses. A native input needs no approval. An allowance that
62
+ * already covers the amount needs none. A smaller, non-zero allowance counts
63
+ * the reset and the new approval, the two transactions the planner emits. The
64
+ * spender is whatever the planner picks: the omnibridge mediator, a swap
65
+ * venue's router (including routers passed in `swapVenues`), or the
66
+ * aggregator's approval address.
67
+ *
68
+ * Pass every section of the route to count the whole route. Pass fewer to
69
+ * count only the window you plan now; each section is counted against the
70
+ * allowance on chain today, so a later section that spends a token an earlier
71
+ * section delivers is counted as if the approval is still needed.
72
+ *
73
+ * An aggregator section needs its `aggregator` inputs, a quote and the request
74
+ * it answered, exactly as {@link planCandidateExecution} does. Without them
75
+ * this function throws, because the approval spender comes from that quote.
76
+ *
77
+ * @param inputs - the same inputs {@link planCandidateExecution} takes
78
+ * @returns the signatures, approvals, and their total
79
+ * @throws when a section cannot be planned; the message names the section and the reason
80
+ */
81
+ export const routeSignatureCountForAccount = async (inputs) => {
82
+ const plan = await planCandidateExecution(inputs);
83
+ if (plan.kind === 'refused') {
84
+ throw new Error(`Cannot count signatures: section ${plan.sectionIndex} cannot be planned (${JSON.stringify(plan.refusal)}).`);
85
+ }
86
+ const approvals = plan.transactions.filter((transaction) => transaction.kind === 'approve').length;
87
+ const signatures = plan.transactions.length - approvals;
88
+ return { signatures, approvals, total: plan.transactions.length };
89
+ };
@@ -8,7 +8,7 @@ export { netDeliveredAmount, restateInDecimals, valueEquivalentAmount } from './
8
8
  export { omnibridgeProviderForEntryChain, pulseXConversionSwapPath } from './providers.js';
9
9
  export { buildDirectCrossingCandidate } from './candidate.js';
10
10
  export type { RouteCandidate, DeferredCrossing, DirectCrossingCandidateInputs } from './candidate.js';
11
- export { buildRouteDataForCandidate, routeSignatureCount } from './candidate-route.js';
11
+ export { buildRouteDataForCandidate, routeSignatureCount, routeSignatureCountForAccount, type RouteSignatureCount, } from './candidate-route.js';
12
12
  export { COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT, COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT, COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT, assessComposedRoute, composedRouteMayBeOffered, } from './composed-route-quality.js';
13
13
  export type { ComposedRouteVerdict } from './composed-route-quality.js';
14
14
  export { entryChainAnchorsForPair } from './entry-chain-anchor.js';
@@ -7,7 +7,7 @@
7
7
  export { netDeliveredAmount, restateInDecimals, valueEquivalentAmount } from './amounts.js';
8
8
  export { omnibridgeProviderForEntryChain, pulseXConversionSwapPath } from './providers.js';
9
9
  export { buildDirectCrossingCandidate } from './candidate.js';
10
- export { buildRouteDataForCandidate, routeSignatureCount } from './candidate-route.js';
10
+ export { buildRouteDataForCandidate, routeSignatureCount, routeSignatureCountForAccount, } from './candidate-route.js';
11
11
  export { COMPOSED_ROUTE_MAX_PRICE_IMPACT_PERCENT, COMPOSED_ROUTE_MAX_VALUE_GAIN_PERCENT, COMPOSED_ROUTE_MAX_VALUE_LOSS_PERCENT, assessComposedRoute, composedRouteMayBeOffered, } from './composed-route-quality.js';
12
12
  export { entryChainAnchorsForPair } from './entry-chain-anchor.js';
13
13
  export { usdValueOfAmount } from './usd-value.js';
@@ -2,7 +2,7 @@ import * as imageLinks from '../../image-links.js';
2
2
  import { chainDisplayName } from '../../chain-names.js';
3
3
  import { findProviderById } from '../../providers.js';
4
4
  import { WRAP_CARRIER_ID, carrierProviderKey, carrierStepKind } from '../carrier-provider-key.js';
5
- import { describeDeferredCrossing } from '../../near-intents-route.js';
5
+ import { describeDeferredCrossing, sendOrSwapLabel } from '../../near-intents-route.js';
6
6
  import { candidateDeliveredUsd, candidateOutsideFeesUsd, candidatePaidUsd, pulsechainChainId, providerDisplayName, } from '../producers.js';
7
7
  import { assetNodeKey, planComposedBasis, } from './graph.js';
8
8
  /**
@@ -179,6 +179,8 @@ const groupEndsWithBridgeHop = (hops) => isBridgeCarrier(hops[hops.length - 1].e
179
179
  * section, in BOTH its shapes (the entry-chain swap and the onward
180
180
  * aggregator hop) — the section label names where the journey finishes
181
181
  * regardless of which service carries the last leg.
182
+ * - **"Swap on {X}"** — a group of the "Send to" shape whose first hop starts
183
+ * on the chain its last hop ends on: nothing is sent anywhere.
182
184
  * - **"Send to {X}"** — every other case: a non-final, non-bridge-ending
183
185
  * group. Matches `buildNearIntentsTwoStepRouteData`'s first section.
184
186
  *
@@ -200,7 +202,7 @@ const sectionLabel = (group, groupIndex, plan) => {
200
202
  if (isFinalGroup) {
201
203
  return `Complete at ${chainDisplayName(destinationChainId)}`;
202
204
  }
203
- return `Send to ${chainDisplayName(destinationChainId)}`;
205
+ return sendOrSwapLabel(hops[0].edge.from.chainId, destinationChainId);
204
206
  };
205
207
  // ---------------------------------------------------------------------------
206
208
  // Hop-level rendering
@@ -255,7 +257,8 @@ const hopProviderName = (carrier) => {
255
257
  * `"Bridge to {toChainName}"` (matches `buildOutboundRouteData`'s onward
256
258
  * hop, which names the chain it finishes on even though an aggregator,
257
259
  * not the omnibridge, carries it);
258
- * - any other aggregator hop (something follows it) → `"Send to {toChainName}"`
260
+ * - any other aggregator hop (something follows it) → `"Send to {toChainName}"`,
261
+ * or `"Swap on {chainName}"` when the hop starts and ends on one chain
259
262
  * (matches `buildNearIntentsTwoStepRouteData`'s aggregator hop).
260
263
  *
261
264
  * @param hop - the hop to title
@@ -274,10 +277,10 @@ const hopTitle = (hop, hopIndex, plan, resolveToken) => {
274
277
  if (carrier === 'PulseX') {
275
278
  return `Swap to ${resolveToken(hop.edge.to).symbol}`;
276
279
  }
277
- const destinationChainName = chainDisplayName(hop.edge.to.chainId);
278
- return isFinalHopOfPlan(hopIndex, plan)
279
- ? `Bridge to ${destinationChainName}`
280
- : `Send to ${destinationChainName}`;
280
+ if (isFinalHopOfPlan(hopIndex, plan)) {
281
+ return `Bridge to ${chainDisplayName(hop.edge.to.chainId)}`;
282
+ }
283
+ return sendOrSwapLabel(hop.edge.from.chainId, hop.edge.to.chainId);
281
284
  };
282
285
  /**
283
286
  * A hop's conversion subtitle.
package/dist/types.d.ts CHANGED
@@ -30,6 +30,14 @@ export type Token = TokenMetadata & {
30
30
  * that carry no price (e.g. the dapp image lists).
31
31
  */
32
32
  priceUSD?: string | null;
33
+ /**
34
+ * LI.FI's `coinKey`: a short key naming the asset across chains (for example
35
+ * "USDC"). Present only on tokens parsed from a LI.FI list that carries one.
36
+ * LI.FI repeats some keys within one chain, so a key alone is not an identity.
37
+ */
38
+ coinKey?: string;
39
+ /** LI.FI's `verificationStatus` (for example "verified"). Present only on tokens parsed from a LI.FI list that carries one. */
40
+ verificationStatus?: string;
33
41
  };
34
42
  /** The token out for a given asset */
35
43
  export type TokenOut = Omit<Token, 'address'> & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gibs/bridge-sdk",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Build and price cross-chain transfers between PulseChain, Ethereum, and Binance Smart Chain.",
5
5
  "keywords": [
6
6
  "ethereum",
@@ -1007,8 +1007,8 @@
1007
1007
  "test:live:near-intents": "../../scripts/near-intents-live.sh"
1008
1008
  },
1009
1009
  "dependencies": {
1010
- "@gibs/abis": "^1.18.0",
1011
- "@gibs/common": "^1.18.0",
1010
+ "@gibs/abis": "^1.19.0",
1011
+ "@gibs/common": "^1.19.0",
1012
1012
  "@ton/core": "^0.63.1",
1013
1013
  "@ton/crypto": "^3.3.0"
1014
1014
  },