@gibs/quotes 1.13.0 → 1.13.1
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/dist/search/enumerate.js +7 -1
- package/dist/search/serve.d.ts +77 -23
- package/dist/search/serve.js +45 -20
- package/dist/search/waves.js +6 -4
- package/dist/types.d.ts +2 -0
- package/package.json +2 -2
package/dist/search/enumerate.js
CHANGED
|
@@ -93,7 +93,13 @@ const resolveHopOutcome = (options) => {
|
|
|
93
93
|
return {
|
|
94
94
|
kind: 'delivered',
|
|
95
95
|
deliveredAmount: exactResult.deliveredAmount,
|
|
96
|
-
outcome:
|
|
96
|
+
outcome: exactResult.priceImpactPercent === undefined
|
|
97
|
+
? { kind: 'quote', deliveredAmount: exactResult.deliveredAmount }
|
|
98
|
+
: {
|
|
99
|
+
kind: 'quote',
|
|
100
|
+
deliveredAmount: exactResult.deliveredAmount,
|
|
101
|
+
priceImpactPercent: exactResult.priceImpactPercent,
|
|
102
|
+
},
|
|
97
103
|
};
|
|
98
104
|
}
|
|
99
105
|
if (carrier?.estimate !== undefined) {
|
package/dist/search/serve.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComposedRouteVerdict, type PlannedHop, type RoutePlan } from '@gibs/bridge-sdk/routing';
|
|
1
|
+
import { type ComposedRouteVerdict, type GasCostFor, type HopCarrier, type PlannedHop, type RoutePlan } from '@gibs/bridge-sdk/routing';
|
|
2
2
|
/**
|
|
3
3
|
* Decides whether a priced {@link RoutePlan} may be served to a reader as a
|
|
4
4
|
* route they can sign — the boundary between the route-search engine
|
|
@@ -79,15 +79,6 @@ export type ServableWithheldReason = {
|
|
|
79
79
|
readonly kind: 'too-many-signatures';
|
|
80
80
|
readonly signatureCount: number;
|
|
81
81
|
readonly maxSignatures: number;
|
|
82
|
-
} | {
|
|
83
|
-
/**
|
|
84
|
-
* A section other than the first starts on a chain the reader holds no
|
|
85
|
-
* native gas on, and the hop landing there does not deliver native
|
|
86
|
-
* coin either — so the reader would have nothing to pay that section's
|
|
87
|
-
* own signature with.
|
|
88
|
-
*/
|
|
89
|
-
readonly kind: 'gasless-middle-chain';
|
|
90
|
-
readonly chainId: number;
|
|
91
82
|
} | {
|
|
92
83
|
/**
|
|
93
84
|
* The reader named a recipient other than their own wallet for a
|
|
@@ -103,6 +94,42 @@ export type ServableWithheldReason = {
|
|
|
103
94
|
readonly kind: 'quality';
|
|
104
95
|
readonly verdict: ComposedRouteVerdict | null;
|
|
105
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* One gas top-up a served plan needs: a section other than the first starts
|
|
99
|
+
* on a chain where the reader holds no native gas, and the previous section
|
|
100
|
+
* does not land native coin there. The reader must get gas on that chain
|
|
101
|
+
* before they can sign the section.
|
|
102
|
+
*
|
|
103
|
+
* A PART OF THE ROUTE, NEVER A REASON TO WITHHOLD IT. The owner: "if the user
|
|
104
|
+
* needs gas, then they should get it" — a gas top-up is a scored part of the
|
|
105
|
+
* route. The ranking already charges {@link estimatedCostAmount} (as the
|
|
106
|
+
* `refuelAmount` of the same `GasCostFor`); this record is what a host shows
|
|
107
|
+
* the reader so the charge is not invisible.
|
|
108
|
+
*
|
|
109
|
+
* NO PROVIDER CARRIES THE TOP-UP TODAY. No descriptor in `@gibs/bridge-sdk`'s
|
|
110
|
+
* provider registry declares a refuel or native-drop capability, so the top-up
|
|
111
|
+
* is a requirement the host presents, not a hop inside the plan.
|
|
112
|
+
*/
|
|
113
|
+
export type GasTopUp = {
|
|
114
|
+
/** 0-based index into `plan.signatures` of the section that needs the gas. */
|
|
115
|
+
readonly sectionIndex: number;
|
|
116
|
+
/** The chain the reader needs native gas on. */
|
|
117
|
+
readonly chainId: number;
|
|
118
|
+
/** The carrier the reader signs with on that chain — the section's first hop. */
|
|
119
|
+
readonly carrier: HopCarrier;
|
|
120
|
+
/**
|
|
121
|
+
* The native coin the section's signature burns, in that chain's native
|
|
122
|
+
* base units — the least the top-up must land. Null when the host passed no
|
|
123
|
+
* {@link ServableContext.gasCostFor}, or its quote carries no native figure.
|
|
124
|
+
*/
|
|
125
|
+
readonly nativeAmountNeeded: bigint | null;
|
|
126
|
+
/**
|
|
127
|
+
* What getting that gas costs the reader, in the plan's destination token's
|
|
128
|
+
* base units — the same `refuelAmount` the ranking subtracts. Null when the
|
|
129
|
+
* host passed no {@link ServableContext.gasCostFor}.
|
|
130
|
+
*/
|
|
131
|
+
readonly estimatedCostAmount: bigint | null;
|
|
132
|
+
};
|
|
106
133
|
/** The verdict {@link servableVerdict} returns. */
|
|
107
134
|
export type ServableVerdict = {
|
|
108
135
|
readonly kind: 'servable';
|
|
@@ -110,6 +137,12 @@ export type ServableVerdict = {
|
|
|
110
137
|
readonly firstSectionRequest: FirstSectionRequest;
|
|
111
138
|
/** Who signs the first section. */
|
|
112
139
|
readonly leg: PlanFirstLegKind;
|
|
140
|
+
/**
|
|
141
|
+
* Every gas top-up the plan needs, in section order — empty when the
|
|
142
|
+
* reader can pay gas for every section. A host must show each one
|
|
143
|
+
* with the route; see {@link GasTopUp}.
|
|
144
|
+
*/
|
|
145
|
+
readonly gasTopUps: readonly GasTopUp[];
|
|
113
146
|
} | {
|
|
114
147
|
readonly kind: 'withheld';
|
|
115
148
|
readonly reason: ServableWithheldReason;
|
|
@@ -133,19 +166,38 @@ export type ServableContext = {
|
|
|
133
166
|
readonly customRecipientSectionIndexes: ReadonlySet<number>;
|
|
134
167
|
/**
|
|
135
168
|
* The composed-route quality verdict for this plan (`assessComposedRoute`,
|
|
136
|
-
* `@gibs/bridge-sdk/routing`).
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
169
|
+
* `@gibs/bridge-sdk/routing`). A plan with more than one signature is
|
|
170
|
+
* always gated: `null` or omitted means "not priced yet," which withholds
|
|
171
|
+
* it exactly as a `'rejected'` verdict would. A single-signature plan
|
|
172
|
+
* strands the reader nowhere between signatures, so it is gated only when
|
|
173
|
+
* the host passes a verdict (a swap that loses real value is still a bad
|
|
174
|
+
* offer); `null` or omitted, the gate does not apply to it.
|
|
141
175
|
*/
|
|
142
176
|
readonly qualityVerdict?: ComposedRouteVerdict | null;
|
|
143
177
|
/** Overrides the signature-count ceiling. Defaults to {@link MAX_SERVABLE_SIGNATURES}. */
|
|
144
178
|
readonly maxSignatures?: number;
|
|
179
|
+
/**
|
|
180
|
+
* The SAME gas model the search ranked with (`@gibs/bridge-sdk/routing`'s
|
|
181
|
+
* `GasCostFor`). It prices each {@link GasTopUp}. Omitted, a top-up is
|
|
182
|
+
* still reported, with its amounts null.
|
|
183
|
+
*/
|
|
184
|
+
readonly gasCostFor?: GasCostFor;
|
|
145
185
|
};
|
|
186
|
+
/**
|
|
187
|
+
* Lists the gas top-ups `plan` needs — see {@link GasTopUp}. A section needs
|
|
188
|
+
* one when it is not the first, its chain is in `gaslessChainIds`, and the
|
|
189
|
+
* previous section does not land native coin there
|
|
190
|
+
* (`sectionIsFundedByLanding`).
|
|
191
|
+
*
|
|
192
|
+
* @param plan - the plan to inspect
|
|
193
|
+
* @param context - the reader's gasless chains, and optionally the gas model
|
|
194
|
+
* that prices each top-up
|
|
195
|
+
* @returns every top-up, in section order; empty when none is needed
|
|
196
|
+
*/
|
|
197
|
+
export declare const planGasTopUps: (plan: RoutePlan, context: Pick<ServableContext, "gaslessChainIds" | "gasCostFor">) => readonly GasTopUp[];
|
|
146
198
|
/**
|
|
147
199
|
* Decides whether `plan` may be served to a reader as a route they can sign
|
|
148
|
-
* right now, against the
|
|
200
|
+
* right now, against the four reasons a route can be held back:
|
|
149
201
|
*
|
|
150
202
|
* 1. **`unconfirmed`** — some hop still rests on an estimate, not a live
|
|
151
203
|
* quote (`planIsConfirmed`).
|
|
@@ -153,16 +205,18 @@ export type ServableContext = {
|
|
|
153
205
|
* {@link ServableContext.maxSignatures} allows.
|
|
154
206
|
* 3. **`middle-recipient-not-reader`** — a non-last section would land
|
|
155
207
|
* somewhere other than the reader's own wallet.
|
|
156
|
-
* 4. **`
|
|
157
|
-
* reader cannot pay gas on, and nothing lands native coin there to cover
|
|
158
|
-
* it.
|
|
159
|
-
* 5. **`quality`** — a composed, multi-signature plan fails the value-loss
|
|
208
|
+
* 4. **`quality`** — a composed, multi-signature plan fails the value-loss
|
|
160
209
|
* gate (`assessComposedRoute`).
|
|
161
210
|
*
|
|
162
211
|
* Checked in that order; the first failing check is the reason returned. A
|
|
163
|
-
* plan that clears all
|
|
164
|
-
* hops
|
|
165
|
-
* go build the
|
|
212
|
+
* plan that clears all four is `'servable'`, carrying the first section's
|
|
213
|
+
* hops, the first-hop signer kind ({@link planFirstLegKind}), and every gas
|
|
214
|
+
* top-up the reader needs ({@link planGasTopUps}) so a host can go build the
|
|
215
|
+
* real signing request and show what the route asks of the reader.
|
|
216
|
+
*
|
|
217
|
+
* A GASLESS MIDDLE CHAIN NEVER WITHHOLDS A PLAN. It used to; the owner ruled
|
|
218
|
+
* that "a gas top-up is a scored part of the route, never a reason to
|
|
219
|
+
* withhold." The plan is served with a {@link GasTopUp} instead.
|
|
166
220
|
*
|
|
167
221
|
* THROWS, RATHER THAN RETURNING A VERDICT, FOR A MALFORMED PLAN — an empty
|
|
168
222
|
* plan, or one that fails {@link sectionHandoverAgrees}. Both name a defect
|
package/dist/search/serve.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { assetNodeKey, composedRouteMayBeOffered, isRateLimitedCarrier, planIsConfirmed, } from '@gibs/bridge-sdk/routing';
|
|
1
|
+
import { assetNodeKey, composedRouteMayBeOffered, isRateLimitedCarrier, planIsConfirmed, sectionIsFundedByLanding, } from '@gibs/bridge-sdk/routing';
|
|
3
2
|
/**
|
|
4
3
|
* Reads {@link PlanFirstLegKind} off a plan's FIRST hop only.
|
|
5
4
|
*
|
|
@@ -61,9 +60,40 @@ export const sectionHandoverAgrees = (plan) => {
|
|
|
61
60
|
};
|
|
62
61
|
/** Owner decision, 2026-09-23: "minimize the steps, but if you have to go through 3 ... that is what you have to do." */
|
|
63
62
|
export const MAX_SERVABLE_SIGNATURES = 3;
|
|
63
|
+
/**
|
|
64
|
+
* Lists the gas top-ups `plan` needs — see {@link GasTopUp}. A section needs
|
|
65
|
+
* one when it is not the first, its chain is in `gaslessChainIds`, and the
|
|
66
|
+
* previous section does not land native coin there
|
|
67
|
+
* (`sectionIsFundedByLanding`).
|
|
68
|
+
*
|
|
69
|
+
* @param plan - the plan to inspect
|
|
70
|
+
* @param context - the reader's gasless chains, and optionally the gas model
|
|
71
|
+
* that prices each top-up
|
|
72
|
+
* @returns every top-up, in section order; empty when none is needed
|
|
73
|
+
*/
|
|
74
|
+
export const planGasTopUps = (plan, context) => plan.signatures.flatMap((group, sectionIndex) => {
|
|
75
|
+
const firstHop = plan.hops[group[0] ?? -1];
|
|
76
|
+
if (sectionIndex === 0 || firstHop === undefined)
|
|
77
|
+
return [];
|
|
78
|
+
const { chainId } = firstHop.edge.from;
|
|
79
|
+
if (!context.gaslessChainIds.has(chainId))
|
|
80
|
+
return [];
|
|
81
|
+
if (sectionIsFundedByLanding(plan, sectionIndex))
|
|
82
|
+
return [];
|
|
83
|
+
const quote = context.gasCostFor?.(chainId, firstHop.edge.carrier);
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
sectionIndex,
|
|
87
|
+
chainId,
|
|
88
|
+
carrier: firstHop.edge.carrier,
|
|
89
|
+
nativeAmountNeeded: quote?.nativeGasAmount ?? null,
|
|
90
|
+
estimatedCostAmount: quote?.refuelAmount ?? null,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
});
|
|
64
94
|
/**
|
|
65
95
|
* Decides whether `plan` may be served to a reader as a route they can sign
|
|
66
|
-
* right now, against the
|
|
96
|
+
* right now, against the four reasons a route can be held back:
|
|
67
97
|
*
|
|
68
98
|
* 1. **`unconfirmed`** — some hop still rests on an estimate, not a live
|
|
69
99
|
* quote (`planIsConfirmed`).
|
|
@@ -71,16 +101,18 @@ export const MAX_SERVABLE_SIGNATURES = 3;
|
|
|
71
101
|
* {@link ServableContext.maxSignatures} allows.
|
|
72
102
|
* 3. **`middle-recipient-not-reader`** — a non-last section would land
|
|
73
103
|
* somewhere other than the reader's own wallet.
|
|
74
|
-
* 4. **`
|
|
75
|
-
* reader cannot pay gas on, and nothing lands native coin there to cover
|
|
76
|
-
* it.
|
|
77
|
-
* 5. **`quality`** — a composed, multi-signature plan fails the value-loss
|
|
104
|
+
* 4. **`quality`** — a composed, multi-signature plan fails the value-loss
|
|
78
105
|
* gate (`assessComposedRoute`).
|
|
79
106
|
*
|
|
80
107
|
* Checked in that order; the first failing check is the reason returned. A
|
|
81
|
-
* plan that clears all
|
|
82
|
-
* hops
|
|
83
|
-
* go build the
|
|
108
|
+
* plan that clears all four is `'servable'`, carrying the first section's
|
|
109
|
+
* hops, the first-hop signer kind ({@link planFirstLegKind}), and every gas
|
|
110
|
+
* top-up the reader needs ({@link planGasTopUps}) so a host can go build the
|
|
111
|
+
* real signing request and show what the route asks of the reader.
|
|
112
|
+
*
|
|
113
|
+
* A GASLESS MIDDLE CHAIN NEVER WITHHOLDS A PLAN. It used to; the owner ruled
|
|
114
|
+
* that "a gas top-up is a scored part of the route, never a reason to
|
|
115
|
+
* withhold." The plan is served with a {@link GasTopUp} instead.
|
|
84
116
|
*
|
|
85
117
|
* THROWS, RATHER THAN RETURNING A VERDICT, FOR A MALFORMED PLAN — an empty
|
|
86
118
|
* plan, or one that fails {@link sectionHandoverAgrees}. Both name a defect
|
|
@@ -116,16 +148,8 @@ export const servableVerdict = (plan, context) => {
|
|
|
116
148
|
return { kind: 'withheld', reason: { kind: 'middle-recipient-not-reader', sectionIndex } };
|
|
117
149
|
}
|
|
118
150
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const landingHop = plan.hops[previousGroup[previousGroup.length - 1]];
|
|
122
|
-
const landingNode = landingHop.edge.to;
|
|
123
|
-
const landsNativeCoin = isNativeAsset(landingNode.address, landingNode.ecosystem);
|
|
124
|
-
if (!landsNativeCoin && context.gaslessChainIds.has(landingNode.chainId)) {
|
|
125
|
-
return { kind: 'withheld', reason: { kind: 'gasless-middle-chain', chainId: landingNode.chainId } };
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (plan.signatures.length > 1) {
|
|
151
|
+
const gateApplies = plan.signatures.length > 1 || (context.qualityVerdict ?? null) !== null;
|
|
152
|
+
if (gateApplies) {
|
|
129
153
|
const verdict = context.qualityVerdict ?? null;
|
|
130
154
|
const passesQualityGate = verdict !== null && composedRouteMayBeOffered(verdict);
|
|
131
155
|
if (!passesQualityGate) {
|
|
@@ -138,5 +162,6 @@ export const servableVerdict = (plan, context) => {
|
|
|
138
162
|
kind: 'servable',
|
|
139
163
|
firstSectionRequest: { hops: firstSectionHops, inputAmount: context.amount },
|
|
140
164
|
leg: planFirstLegKind(plan),
|
|
165
|
+
gasTopUps: planGasTopUps(plan, context),
|
|
141
166
|
};
|
|
142
167
|
};
|
package/dist/search/waves.js
CHANGED
|
@@ -330,13 +330,15 @@ const recomposePath = (path, startAmount, askedAmountForHop, cache) => {
|
|
|
330
330
|
// ever deliver. The difference stays in the reader's wallet unspent.
|
|
331
331
|
runningAmount = hop.edge.fusesNext && minimumDeliveredAmount !== null ? minimumDeliveredAmount : deliveredAmount;
|
|
332
332
|
const refusalReason = refusalReasonByHopIndex[index] ?? null;
|
|
333
|
+
const measuredImpact = hop.outcome.priceImpactPercent;
|
|
334
|
+
const impactField = measuredImpact === undefined ? {} : { priceImpactPercent: measuredImpact };
|
|
333
335
|
const outcome = figureHop.figure.kind === 'quote'
|
|
334
336
|
? minimumDeliveredAmount === null
|
|
335
|
-
? { kind: 'quote', deliveredAmount }
|
|
336
|
-
: { kind: 'quote', deliveredAmount, minimumDeliveredAmount }
|
|
337
|
+
? { kind: 'quote', deliveredAmount, ...impactField }
|
|
338
|
+
: { kind: 'quote', deliveredAmount, minimumDeliveredAmount, ...impactField }
|
|
337
339
|
: refusalReason === null
|
|
338
|
-
? { kind: 'estimate', deliveredAmount, basis: figureHop.figure.basis }
|
|
339
|
-
: { kind: 'estimate', deliveredAmount, basis: figureHop.figure.basis, refusalReason };
|
|
340
|
+
? { kind: 'estimate', deliveredAmount, basis: figureHop.figure.basis, ...impactField }
|
|
341
|
+
: { kind: 'estimate', deliveredAmount, basis: figureHop.figure.basis, refusalReason, ...impactField };
|
|
340
342
|
updatedHops.push({ ...hop, outcome });
|
|
341
343
|
}
|
|
342
344
|
return { ...path, hops: updatedHops };
|
package/dist/types.d.ts
CHANGED
|
@@ -129,6 +129,8 @@ export type ExactHopResult = {
|
|
|
129
129
|
readonly outcome: 'delivered';
|
|
130
130
|
/** What this edge delivers for exactly the requested input amount, in `to`'s base units. */
|
|
131
131
|
readonly deliveredAmount: bigint;
|
|
132
|
+
/** How far this edge's executed rate falls below its marginal rate, in percent; absent when the carrier cannot measure it. */
|
|
133
|
+
readonly priceImpactPercent?: number;
|
|
132
134
|
} | {
|
|
133
135
|
readonly outcome: 'refused';
|
|
134
136
|
/** Why this carrier's own data says the amount cannot cross. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gibs/quotes",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"description": "Isomorphic core for asking cross-chain bridge aggregators (LI.FI, Relay, NEAR Intents) for display and execution quotes — the wire contract, refusal reading, and the shared guards a host wraps every upstream call in. Runs unchanged in a browser or in Node.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ethereum",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"typecheck": "npx tsc -p tsconfig.test.json"
|
|
123
123
|
},
|
|
124
124
|
"dependencies": {
|
|
125
|
-
"@gibs/bridge-sdk": "^1.13.
|
|
125
|
+
"@gibs/bridge-sdk": "^1.13.1",
|
|
126
126
|
"zod": "^3.25.76"
|
|
127
127
|
},
|
|
128
128
|
"peerDependencies": {
|