@xoxno/sdk-js 1.0.224 → 1.0.225

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 (46) hide show
  1. package/dist/esm/index.js +3 -0
  2. package/dist/esm/sdk/index.js +198 -0
  3. package/dist/esm/sdk/stellar/admin.js +385 -0
  4. package/dist/esm/sdk/stellar/auth-policy.js +310 -0
  5. package/dist/esm/sdk/stellar/cctp.js +23 -0
  6. package/dist/esm/sdk/stellar/contracts.js +49 -0
  7. package/dist/esm/sdk/stellar/events/decode.js +415 -0
  8. package/dist/esm/sdk/stellar/events/id.js +62 -0
  9. package/dist/esm/sdk/stellar/events/index.js +2 -0
  10. package/dist/esm/sdk/stellar/governance.js +403 -0
  11. package/dist/esm/sdk/stellar/index.js +25 -0
  12. package/dist/esm/sdk/stellar/lending-api-types.js +3 -0
  13. package/dist/esm/sdk/stellar/lending-read-types.js +1 -0
  14. package/dist/esm/sdk/stellar/lending-read.js +341 -0
  15. package/dist/esm/sdk/stellar/lending.js +358 -0
  16. package/dist/esm/sdk/stellar/math/account.js +256 -0
  17. package/dist/esm/sdk/stellar/math/index.js +11 -0
  18. package/dist/esm/sdk/stellar/math/limits.js +273 -0
  19. package/dist/esm/sdk/stellar/math/projection.js +130 -0
  20. package/dist/esm/sdk/stellar/math/rates.js +62 -0
  21. package/dist/esm/sdk/stellar/math/scaled.js +156 -0
  22. package/dist/esm/sdk/stellar/position-mode.js +11 -0
  23. package/dist/esm/sdk/stellar/prepare.js +47 -0
  24. package/dist/esm/sdk/stellar/quote.js +87 -0
  25. package/dist/esm/sdk/stellar/repay-swap.js +12 -0
  26. package/dist/esm/sdk/stellar/route-verify.js +285 -0
  27. package/dist/esm/sdk/stellar/scval-encode.js +464 -0
  28. package/dist/esm/sdk/stellar/swap-history.js +46 -0
  29. package/dist/esm/sdk/stellar/swap-index.js +1 -0
  30. package/dist/esm/sdk/stellar/swap.js +117 -0
  31. package/dist/esm/sdk/stellar/xdr-compat.js +17 -0
  32. package/dist/esm/sdk/stellar-schema.js +129 -0
  33. package/dist/esm/sdk/swagger.js +1957 -0
  34. package/dist/esm/sdk/types.js +1 -0
  35. package/dist/esm/sdk/utils.js +21 -0
  36. package/dist/esm/utils/api.js +169 -0
  37. package/dist/esm/utils/const.js +11 -0
  38. package/dist/esm/utils/errors.js +20 -0
  39. package/dist/esm/utils/guards.js +29 -0
  40. package/dist/esm/utils/helpers.js +20 -0
  41. package/dist/esm/utils/regex.js +14 -0
  42. package/dist/esm/utils/soroban-errors.js +149 -0
  43. package/dist/index.esm.js +1 -2
  44. package/dist/sdk/stellar/index.esm.js +1 -1
  45. package/package.json +2 -2
  46. package/dist/index.esm.js.LICENSE.txt +0 -8
@@ -0,0 +1,47 @@
1
+ import { TransactionBuilder, rpc } from "@stellar/stellar-sdk";
2
+ import { STELLAR_NETWORK_PASSPHRASE } from "./contracts.js";
3
+ export function tagStellarInvokedContractError(contractId, error) {
4
+ const message = error instanceof Error ? error.message : String(error);
5
+ return new Error(`[xoxno-invoked:${contractId}] ${message}`);
6
+ }
7
+ /**
8
+ * Simulate a built Soroban envelope (footprint, auth, resource fee) and return
9
+ * prepared base64 XDR. Optionally tag failures with the invoked contract id so
10
+ * UIs can map `Error(Contract, #N)` codes to the right ABI.
11
+ * @param server - Host application's RPC server on the selected network.
12
+ * @param xdr - Unsigned builder output, not a signed transaction.
13
+ * @param opts - Pass `network` explicitly; it must match the RPC and wallet.
14
+ * @returns Prepared, unsigned base64 XDR for wallet signing.
15
+ * @remarks This helper does not sign, submit, or wait for confirmation. Existing
16
+ * calls without `network` retain a neutral hash domain because XDR contains no
17
+ * passphrase; always select the actual network when the wallet signs.
18
+ * @category Transaction preparation
19
+ */ export async function prepareStellarTxXdr(server, xdr, opts) {
20
+ const instructionLeeway = opts?.instructionLeeway ?? 20000000;
21
+ if (!Number.isInteger(instructionLeeway) || instructionLeeway < 0 || instructionLeeway > 0xffffffff) {
22
+ throw new Error('instructionLeeway must be an integer from 0 to 4294967295');
23
+ }
24
+ const tx = TransactionBuilder.fromXDR(xdr, opts?.network ? STELLAR_NETWORK_PASSPHRASE[opts.network] : '');
25
+ try {
26
+ const simulation = await server.simulateTransaction(tx, {
27
+ cpuInstructions: instructionLeeway
28
+ });
29
+ if (rpc.Api.isSimulationError(simulation)) throw new Error(simulation.error);
30
+ return rpc.assembleTransaction(tx, simulation).build().toXDR();
31
+ } catch (error) {
32
+ if (opts?.invokedContractId) {
33
+ throw tagStellarInvokedContractError(opts.invokedContractId, error);
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+ /**
39
+ * Prepare {@link BuiltStellarTx} before wallet signing.
40
+ * @param server - Host RPC server for the selected deployment.
41
+ * @param built - Unsigned output of any Stellar transaction builder.
42
+ * @param opts - Network and optional contract context for error mapping.
43
+ * @returns Prepared unsigned base64 XDR; sign this result, then submit it.
44
+ * @category Transaction preparation
45
+ */ export async function prepareStellarBuiltTx(server, built, opts) {
46
+ return prepareStellarTxXdr(server, built.xdr, opts);
47
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Typed client for the Stellar aggregator quote server
3
+ * (`GET /api/v1/quote`, `GET /api/v1/tokens`).
4
+ *
5
+ * The quote server is a standalone Rust service (deployed independently
6
+ * of the main XOXNO API), so this module hits its base URL directly
7
+ * rather than routing through the swagger-driven endpoints map.
8
+ *
9
+ * All response shapes are mirrored 1:1 by `@xoxno/types` DTOs
10
+ * (`StellarAggregatorQuoteResponseDto`, `StellarQuotePathDto`, etc.) —
11
+ * the wire shape is camelCase, the Soroban contract receives snake_case
12
+ * after the SDK encoder rewrites field names.
13
+ */ const buildUrl = (base, path, query)=>{
14
+ const url = new URL(path, base.endsWith('/') ? base : `${base}/`);
15
+ if (query) {
16
+ for (const [key, value] of Object.entries(query)){
17
+ if (value === undefined) continue;
18
+ url.searchParams.set(key, String(value));
19
+ }
20
+ }
21
+ return url.toString();
22
+ };
23
+ /**
24
+ * Fetch a route quote from the Stellar aggregator quote server.
25
+ *
26
+ * Forward mode: pass `amountIn` to compute the maximum reachable
27
+ * `amountOut`. Reverse mode: pass `amountOut` to compute the minimum
28
+ * `amountIn` that delivers at least that output.
29
+ *
30
+ * For lending strategies, pass the quote's `routeXdr` to the builder's
31
+ * `steps`. Amounts are token base-unit decimal strings. Requote after changes
32
+ * to amount, direction or slippage; preparation is the execution check.
33
+ *
34
+ * Passing `sender` (G...) together with `slippage` returns a direct-swap
35
+ * envelope under `transaction.envelopeXdr`. The router comes from the service's
36
+ * `/api/v1/config`; the `router` request field is ignored by the service and is
37
+ * no longer sent. Prepare the envelope before wallet signing unless
38
+ * `transaction.simulated` is true.
39
+ * @param request - Exactly one of amountIn or amountOut; optional referralId is forwarded unchanged.
40
+ * @param opts - Quote-server URL and optional fetch options supplied by the host.
41
+ * @returns Quote estimates and opaque route bytes; does not execute a swap.
42
+ * @category Strategy quotes
43
+ */ export async function getStellarAggregatorQuote(request, opts) {
44
+ if (!opts.baseUrl) {
45
+ throw new Error('Stellar quote baseUrl is required');
46
+ }
47
+ if (request.amountIn == null === (request.amountOut == null)) {
48
+ throw new Error('getStellarAggregatorQuote: exactly one of `amountIn` or `amountOut` must be provided');
49
+ }
50
+ const url = buildUrl(opts.baseUrl, 'api/v1/quote', {
51
+ from: request.from,
52
+ to: request.to,
53
+ amountIn: request.amountIn,
54
+ amountOut: request.amountOut,
55
+ maxHops: request.maxHops,
56
+ maxSplits: request.maxSplits,
57
+ slippage: request.slippage,
58
+ includePaths: request.includePaths,
59
+ sender: request.sender,
60
+ simulate: request.simulate,
61
+ referralId: request.referralId,
62
+ platform: request.platform,
63
+ fresh: request.fresh
64
+ });
65
+ const res = await fetch(url, opts.fetchOptions);
66
+ if (!res.ok) {
67
+ const body = await res.text().catch(()=>'');
68
+ throw new Error(`Stellar quote server responded ${res.status} ${res.statusText} for ${url} — ${body}`);
69
+ }
70
+ return await res.json();
71
+ }
72
+ /**
73
+ * List tokens currently indexed by the quote server. Useful to populate
74
+ * pickers or validate that a user-supplied token has on-chain liquidity.
75
+ * @category Strategy quotes
76
+ */ export async function getStellarQuoteTokens(opts) {
77
+ if (!opts.baseUrl) {
78
+ throw new Error('Stellar quote baseUrl is required');
79
+ }
80
+ const url = buildUrl(opts.baseUrl, 'api/v1/tokens');
81
+ const res = await fetch(url, opts.fetchOptions);
82
+ if (!res.ok) {
83
+ const body = await res.text().catch(()=>'');
84
+ throw new Error(`Stellar quote server responded ${res.status} ${res.statusText} for ${url} — ${body}`);
85
+ }
86
+ return await res.json();
87
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Empty strategy swap bytes for `repay_debt_with_collateral` when
3
+ * `collateral_token === debt_token`. The controller requires the swap payload
4
+ * to be genuinely empty in this case (`swap.is_empty()`) and reverts
5
+ * `InvalidPayments` if any route is supplied, even an unused placeholder.
6
+ *
7
+ * `token`/`collateralAmount` are accepted-but-ignored so existing callers of
8
+ * the old `(token, collateralAmount)` signature keep compiling; the swap
9
+ * payload is always empty regardless of their values.
10
+ */ export function buildSameTokenRepaySwapSteps(_token, _collateralAmount) {
11
+ return new Uint8Array(0);
12
+ }
@@ -0,0 +1,285 @@
1
+ function _define_property(obj, key, value) {
2
+ if (key in obj) {
3
+ Object.defineProperty(obj, key, {
4
+ value: value,
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true
8
+ });
9
+ } else {
10
+ obj[key] = value;
11
+ }
12
+ return obj;
13
+ }
14
+ /**
15
+ * Decoder and pre-signing verifier for the aggregator router's packed route
16
+ * (`routeXdr` / `swap_xdr`).
17
+ *
18
+ * The quote server is unauthenticated and returns the route as an opaque blob
19
+ * next to separate JSON display fields. The router enforces only the minimum
20
+ * that is ENCODED in the blob, so a caller must decode it and compare it with
21
+ * its own request before a wallet signs. The decoder mirrors
22
+ * `contracts/swap-aggregator/src/program.rs` (`Program::decode` + `validate`)
23
+ * and `execute::run` check for check; the encoder is `./scval-encode.ts`.
24
+ */ import { Address, Asset, scValToBigInt, StrKey, xdr } from "@stellar/stellar-sdk";
25
+ import { STELLAR_NETWORK_PASSPHRASE } from "./contracts.js";
26
+ import { xdrBytes, xdrField, xdrType } from "./xdr-compat.js";
27
+ import { asStellarStrategySwapBytes, MAX_AMOUNTS, MAX_ASSETS, MAX_OPS, MAX_WEIGHTS, MODE_ALL, MODE_FIXED_BASE, MODE_PPM_BASE, MODE_PREV, OP_BURN, OP_MINT, PPM_DENOMINATOR, STELLAR_PROGRAM_VERSION, STELLAR_SWAP_VENUES } from "./scval-encode.js";
28
+ const HEADER_LEN = 10;
29
+ const OP_LEN = 5;
30
+ const WEIGHT_LEN = 3;
31
+ const MAX_PROGRAM_BYTES = HEADER_LEN + OP_LEN * MAX_OPS + WEIGHT_LEN * MAX_WEIGHTS;
32
+ /**
33
+ * Cap on the serialized payload, checked before XDR parsing so a hostile
34
+ * response cannot force a proportional allocation. The largest legal payload
35
+ * (256 addresses, 126 amounts, 346 program bytes) is below 16 KiB.
36
+ */ export const STELLAR_ROUTE_MAX_BYTES = 16 * 1024;
37
+ /** Thrown when a route cannot be decoded or disagrees with the caller's request. */ export class StellarRouteVerificationError extends Error {
38
+ constructor(code, message){
39
+ super(`Stellar route verification failed [${code}]: ${message}`), _define_property(this, "code", void 0);
40
+ this.name = 'StellarRouteVerificationError';
41
+ this.code = code;
42
+ }
43
+ }
44
+ const fail = (code, message)=>{
45
+ throw new StellarRouteVerificationError(code, message);
46
+ };
47
+ const toBuffer = (route)=>{
48
+ const encoded = typeof route === 'string' ? route : route && typeof route === 'object' && 'routeXdr' in route ? route.routeXdr : undefined;
49
+ // Base64 expands 3 bytes to 4 characters; reject before decoding.
50
+ if (typeof encoded === 'string' && encoded.length > STELLAR_ROUTE_MAX_BYTES * 2) {
51
+ return fail('OVERSIZED', `encoded route has ${encoded.length} characters`);
52
+ }
53
+ try {
54
+ return xdrBytes(asStellarStrategySwapBytes(route));
55
+ } catch (error) {
56
+ return fail('MALFORMED', error instanceof Error ? error.message : String(error));
57
+ }
58
+ };
59
+ const readPayloadFields = (buf)=>{
60
+ let value;
61
+ try {
62
+ // `fromXDR` rejects truncated input and trailing bytes.
63
+ value = xdr.ScVal.fromXDR(buf);
64
+ } catch (error) {
65
+ return fail('MALFORMED', `payload is not one XDR ScVal: ${error instanceof Error ? error.message : String(error)}`);
66
+ }
67
+ if (xdrType(value) !== 'scvMap') {
68
+ return fail('MALFORMED', 'payload must be a StrategyPayload map');
69
+ }
70
+ const entries = xdrField(value, 'map') ?? [];
71
+ // A `#[contracttype]` struct is a map with exactly its fields, keys sorted.
72
+ const keys = entries.map((entry)=>xdrType(xdrField(entry, 'key')) === 'scvSymbol' ? xdrField(xdrField(entry, 'key'), 'sym').toString() : '');
73
+ if (keys.join(',') !== 'amounts,assets,ops') {
74
+ return fail('MALFORMED', 'payload fields must be exactly amounts, assets, ops');
75
+ }
76
+ const [amountsVal, assetsVal, opsVal] = entries.map((entry)=>xdrField(entry, 'val'));
77
+ if (xdrType(amountsVal) !== 'scvVec' || xdrType(assetsVal) !== 'scvVec' || xdrType(opsVal) !== 'scvBytes') {
78
+ return fail('MALFORMED', 'payload field has the wrong type');
79
+ }
80
+ return {
81
+ amounts: xdrField(amountsVal, 'vec') ?? [],
82
+ assets: xdrField(assetsVal, 'vec') ?? [],
83
+ ops: xdrBytes(opsVal)
84
+ };
85
+ };
86
+ /**
87
+ * Decode and structurally validate route bytes. Applies every check of the
88
+ * on-chain decoder, so a route that `Program::decode` rejects does not decode
89
+ * here. Venue behaviour (pools, liquidity, budget) is not checked.
90
+ * @throws StellarRouteVerificationError
91
+ */ export function decodeStellarRouteBytes(route) {
92
+ const buf = toBuffer(route);
93
+ if (buf.length === 0) fail('MALFORMED', 'route is empty');
94
+ if (buf.length > STELLAR_ROUTE_MAX_BYTES) {
95
+ fail('OVERSIZED', `route has ${buf.length} bytes, above ${STELLAR_ROUTE_MAX_BYTES}`);
96
+ }
97
+ const fields = readPayloadFields(buf);
98
+ const assetsLen = fields.assets.length;
99
+ const amountsLen = fields.amounts.length;
100
+ if (assetsLen === 0) fail('MALFORMED', 'asset registry is empty');
101
+ if (assetsLen > MAX_ASSETS || amountsLen > MAX_AMOUNTS) {
102
+ fail('OVERSIZED', `registries hold ${assetsLen} assets and ${amountsLen} amounts`);
103
+ }
104
+ const assets = fields.assets.map((value, i)=>{
105
+ if (xdrType(value) !== 'scvAddress') {
106
+ return fail('MALFORMED', `assets[${i}] is not an address`);
107
+ }
108
+ return Address.fromScAddress(xdrField(value, 'address')).toString();
109
+ });
110
+ const amounts = fields.amounts.map((value, i)=>{
111
+ if (xdrType(value) !== 'scvI128') {
112
+ return fail('MALFORMED', `amounts[${i}] is not an i128`);
113
+ }
114
+ return scValToBigInt(value);
115
+ });
116
+ const ops = fields.ops;
117
+ if (ops.length < HEADER_LEN) fail('MALFORMED', 'program header is truncated');
118
+ if (ops.length > MAX_PROGRAM_BYTES) {
119
+ fail('OVERSIZED', `program has ${ops.length} bytes, above ${MAX_PROGRAM_BYTES}`);
120
+ }
121
+ const byte = (at)=>{
122
+ const value = ops[at];
123
+ return value === undefined ? fail('MALFORMED', `program byte ${at} is missing`) : value;
124
+ };
125
+ const version = byte(0);
126
+ if (version !== STELLAR_PROGRAM_VERSION) {
127
+ fail('UNSUPPORTED_VERSION', `program version ${version}, expected ${STELLAR_PROGRAM_VERSION}`);
128
+ }
129
+ const opCount = byte(8);
130
+ const weightCount = byte(9);
131
+ if (opCount === 0) fail('INVALID_STRUCTURE', 'program has no instructions');
132
+ if (opCount > MAX_OPS || weightCount > MAX_WEIGHTS) {
133
+ fail('OVERSIZED', `program declares ${opCount} instructions and ${weightCount} weights`);
134
+ }
135
+ const weightsAt = HEADER_LEN + OP_LEN * opCount;
136
+ if (ops.length !== weightsAt + WEIGHT_LEN * weightCount) {
137
+ fail('MALFORMED', 'program length does not match its instruction and weight counts');
138
+ }
139
+ const tokenInIdx = byte(1);
140
+ const tokenOutIdx = byte(2);
141
+ const minOutIdx = byte(3);
142
+ if (tokenInIdx >= assetsLen || tokenOutIdx >= assetsLen || minOutIdx >= amountsLen) {
143
+ fail('MALFORMED', 'header index is outside its registry');
144
+ }
145
+ if (tokenInIdx === tokenOutIdx) fail('INVALID_STRUCTURE', 'token_in equals token_out');
146
+ const referralId = ops.readUInt32BE(4);
147
+ const weights = [];
148
+ for(let i = 0; i < weightCount; i += 1){
149
+ const ppm = ops.readUIntBE(weightsAt + WEIGHT_LEN * i, WEIGHT_LEN);
150
+ if (ppm === 0 || ppm > PPM_DENOMINATOR) {
151
+ fail('INVALID_STRUCTURE', `split weight ${ppm} outside 1..=${PPM_DENOMINATOR}`);
152
+ }
153
+ weights.push(ppm);
154
+ }
155
+ const asset = (idx)=>assets[idx] ?? fail('MALFORMED', `asset index ${idx} is outside the registry`);
156
+ const amount = (idx)=>amounts[idx] ?? fail('MALFORMED', `amount index ${idx} is outside the registry`);
157
+ const decoded = [];
158
+ // Registry index of the previous instruction's single output; a burn has none.
159
+ let producedIdx;
160
+ for(let i = 0; i < opCount; i += 1){
161
+ const at = HEADER_LEN + OP_LEN * i;
162
+ const opcode = byte(at);
163
+ const modeByte = byte(at + 1);
164
+ const idxA = byte(at + 2);
165
+ const idxB = byte(at + 3);
166
+ const idxC = byte(at + 4);
167
+ if (opcode > OP_MINT) fail('UNKNOWN_OPCODE', `instruction ${i} has opcode ${opcode}`);
168
+ let mode;
169
+ if (modeByte === MODE_ALL) {
170
+ mode = {
171
+ kind: 'all'
172
+ };
173
+ } else if (modeByte === MODE_PREV) {
174
+ // `Prev` needs a predecessor with one output, and that output must be
175
+ // this input. The contract compares registry INDICES, so do the same.
176
+ if (producedIdx === undefined || producedIdx !== idxB) {
177
+ fail('INVALID_STRUCTURE', `instruction ${i} does not chain onto its predecessor`);
178
+ }
179
+ mode = {
180
+ kind: 'prev'
181
+ };
182
+ } else if (modeByte >= MODE_PPM_BASE) {
183
+ const ppm = weights[modeByte - MODE_PPM_BASE];
184
+ if (ppm === undefined) fail('MALFORMED', `instruction ${i} weight index is out of range`);
185
+ mode = {
186
+ kind: 'ppm',
187
+ ppm: ppm
188
+ };
189
+ } else {
190
+ mode = {
191
+ kind: 'fixed',
192
+ amount: amount(modeByte - MODE_FIXED_BASE)
193
+ };
194
+ }
195
+ const pool = asset(idxA);
196
+ if (opcode === OP_BURN || opcode === OP_MINT) {
197
+ if (mode.kind !== 'all') fail('MALFORMED', `liquidity instruction ${i} must use mode All`);
198
+ if (idxC >= amountsLen) fail('MALFORMED', `instruction ${i} amount index is out of range`);
199
+ decoded.push({
200
+ kind: opcode === OP_BURN ? 'burn' : 'mint',
201
+ pool,
202
+ shareToken: asset(idxB),
203
+ amountIndex: idxC
204
+ });
205
+ producedIdx = opcode === OP_MINT ? idxB : undefined;
206
+ continue;
207
+ }
208
+ if (idxB === idxC) fail('INVALID_STRUCTURE', `instruction ${i} swaps a token for itself`);
209
+ decoded.push({
210
+ kind: 'swap',
211
+ venue: STELLAR_SWAP_VENUES[opcode],
212
+ mode,
213
+ pool,
214
+ tokenIn: asset(idxB),
215
+ tokenOut: asset(idxC)
216
+ });
217
+ producedIdx = idxC;
218
+ }
219
+ const totalMinOut = amount(minOutIdx);
220
+ // `execute::run` rejects a non-positive minimum.
221
+ if (totalMinOut <= 0n) fail('MIN_OUT_TOO_LOW', `encoded total_min_out is ${totalMinOut}`);
222
+ return {
223
+ version,
224
+ tokenIn: asset(tokenInIdx),
225
+ tokenOut: asset(tokenOutIdx),
226
+ totalMinOut,
227
+ referralId,
228
+ assets,
229
+ amounts,
230
+ ops: decoded,
231
+ weights
232
+ };
233
+ }
234
+ /**
235
+ * Contract id of a quote-server token id: `C…` as is, `XLM` / `native` and
236
+ * `CODE:GISSUER` through the Stellar Asset Contract id of `network`.
237
+ * @throws StellarRouteVerificationError when the id needs a network and none is given.
238
+ */ export function stellarTokenContractId(tokenId, network) {
239
+ if (StrKey.isValidContract(tokenId)) return tokenId;
240
+ if (!network) {
241
+ return fail('EXPECTATION_MISSING', `token ${tokenId} is not a contract id; pass \`network\` to derive its asset contract`);
242
+ }
243
+ try {
244
+ const sep = tokenId.indexOf(':');
245
+ const classic = tokenId === 'XLM' || tokenId === 'native' ? Asset.native() : new Asset(tokenId.slice(0, sep), tokenId.slice(sep + 1));
246
+ return classic.contractId(STELLAR_NETWORK_PASSPHRASE[network]);
247
+ } catch {
248
+ return fail('MALFORMED', `token id ${tokenId} is not a contract id or a classic asset`);
249
+ }
250
+ }
251
+ const toAmount = (value, label)=>{
252
+ if (typeof value === 'bigint') return value;
253
+ if (!/^\d+$/.test(value)) {
254
+ return fail('EXPECTATION_MISSING', `${label} must be a base-unit integer string`);
255
+ }
256
+ return BigInt(value);
257
+ };
258
+ /**
259
+ * Decode `route` and require it to match `expected`. Call before signing any
260
+ * transaction that carries route bytes; never sign when this throws.
261
+ * @returns The decoded route, for display or further checks.
262
+ * @throws StellarRouteVerificationError
263
+ */ export function verifyStellarRouteBytes(route, expected) {
264
+ const minOut = toAmount(expected.minOut, 'expected.minOut');
265
+ if (minOut <= 0n) fail('EXPECTATION_MISSING', 'expected.minOut must be positive');
266
+ const decoded = decodeStellarRouteBytes(route);
267
+ if (decoded.tokenIn !== expected.tokenIn) {
268
+ fail('TOKEN_IN_MISMATCH', `route spends ${decoded.tokenIn}, expected ${expected.tokenIn}`);
269
+ }
270
+ if (decoded.tokenOut !== expected.tokenOut) {
271
+ fail('TOKEN_OUT_MISMATCH', `route delivers ${decoded.tokenOut}, expected ${expected.tokenOut}`);
272
+ }
273
+ if (decoded.totalMinOut < minOut) {
274
+ fail('MIN_OUT_TOO_LOW', `route enforces a minimum of ${decoded.totalMinOut}, below the expected ${minOut}`);
275
+ }
276
+ if (expected.amountIn !== undefined) {
277
+ const amountIn = toAmount(expected.amountIn, 'expected.amountIn');
278
+ for (const op of decoded.ops){
279
+ if (op.kind === 'swap' && op.mode.kind === 'fixed' && op.tokenIn === decoded.tokenIn && op.mode.amount > amountIn) {
280
+ fail('AMOUNT_IN_EXCEEDED', `a leg spends ${op.mode.amount}, above the input ${amountIn}`);
281
+ }
282
+ }
283
+ }
284
+ return decoded;
285
+ }