@lifi/composer-sdk 0.5.0 → 0.5.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/client.cjs +54 -31
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +9 -1
- package/dist/client.d.ts +9 -1
- package/dist/client.js +58 -31
- package/dist/client.js.map +1 -1
- package/dist/errors.cjs +42 -3
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +67 -4
- package/dist/errors.d.ts +67 -4
- package/dist/errors.js +29 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/responseSchemas.cjs +3 -0
- package/dist/responseSchemas.cjs.map +1 -1
- package/dist/responseSchemas.d.cts +3 -0
- package/dist/responseSchemas.d.ts +3 -0
- package/dist/responseSchemas.js +3 -0
- package/dist/responseSchemas.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +100 -34
- package/src/errors.ts +116 -3
- package/src/examples/run.ts +6 -9
- package/src/index.ts +8 -1
- package/src/responseSchemas.ts +6 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@lifi/composer-sdk` — TypeScript SDK for building and compiling\n * Compose flows into executable EVM calldata.\n *\n * Start by creating an SDK instance with {@link createComposeSdk}, then use\n * {@link ComposeSdk.flow} to build flows and compile them.\n *\n * @packageDocumentation\n */\n\nexport type {\n AbiTypeToOutputKind,\n Address,\n InputDecl,\n InputSchema,\n InputSpecOf,\n IntegerString,\n IntegerStringInput,\n OutputKind,\n TypedGuard,\n TypedRef,\n TypedLiteral,\n} from './types.js';\n\nexport type {\n ChainSummary,\n GetZapPacksOptions,\n ZapPackEdge,\n ZapPackOverview,\n} from './discovery.js';\nexport type {\n ComposeError,\n ComposeErrorCode,\n ComposePreparationOps,\n} from './errors.js';\nexport type {\n AppliedGuard,\n ApprovalEntry,\n Call,\n ComposeCompilePartialData,\n ComposeAction,\n ComposeFill,\n ComposeFee,\n ComposeUserProgramSimulation,\n ComposeCompileRequest,\n ComposeCompileResult,\n ComposeCompileSuccessData,\n ComposeErrorKind,\n ComposeRouteRequest,\n ComposeTransactionRequest,\n FailedPreparedOp,\n Flow,\n InputSpec,\n MaterialiserConfigOf,\n MaterialiserInput,\n Precondition,\n PreconditionConfigOf,\n PriceImpact,\n ProducedResource,\n Ref,\n Resource,\n RouteAmount,\n RouteAmountAll,\n RouteAmountExact,\n SimulatedAmounts,\n SimulationPolicy,\n SimulationRevert,\n SolType,\n SweepTo,\n SimulateRequest,\n SimulateResult,\n SimulateOkResult,\n SimulateRevertResult,\n SimulateRevertDecodeResult,\n SimulateSetupErrorResult,\n SimulateBalanceEntry,\n TrackedBalance,\n SlotFinderRequirement,\n Erc20BalanceRequirement,\n NativeBalanceRequirement,\n Erc20AllowanceRequirement,\n ContinuationIntent,\n ContinuationOutcome,\n Check,\n CheckComparator,\n Phase1ArtifactWire,\n ContinuationPlanWire,\n} from '@lifi/compose-spec';\nexport {\n SIMULATE_MAX_TRACKED_BALANCES,\n SIMULATE_MAX_REQUIREMENTS,\n} from '@lifi/compose-spec';\nexport {
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@lifi/composer-sdk` — TypeScript SDK for building and compiling\n * Compose flows into executable EVM calldata.\n *\n * Start by creating an SDK instance with {@link createComposeSdk}, then use\n * {@link ComposeSdk.flow} to build flows and compile them.\n *\n * @packageDocumentation\n */\n\nexport type {\n AbiTypeToOutputKind,\n Address,\n InputDecl,\n InputSchema,\n InputSpecOf,\n IntegerString,\n IntegerStringInput,\n OutputKind,\n TypedGuard,\n TypedRef,\n TypedLiteral,\n} from './types.js';\n\nexport type {\n ChainSummary,\n GetZapPacksOptions,\n ZapPackEdge,\n ZapPackOverview,\n} from './discovery.js';\nexport type {\n ComposeError,\n ComposeErrorCode,\n ComposeSdkOutdated,\n ComposeServerOutdated,\n ComposePreparationOps,\n} from './errors.js';\nexport type {\n AppliedGuard,\n ApprovalEntry,\n Call,\n ComposeCompilePartialData,\n ComposeAction,\n ComposeFill,\n ComposeFee,\n ComposeUserProgramSimulation,\n ComposeCompileRequest,\n ComposeCompileResult,\n ComposeCompileSuccessData,\n ComposeErrorKind,\n ComposeRouteRequest,\n ComposeTransactionRequest,\n FailedPreparedOp,\n Flow,\n InputSpec,\n MaterialiserConfigOf,\n MaterialiserInput,\n Precondition,\n PreconditionConfigOf,\n PriceImpact,\n ProducedResource,\n Ref,\n Resource,\n RouteAmount,\n RouteAmountAll,\n RouteAmountExact,\n SimulatedAmounts,\n SimulationPolicy,\n SimulationRevert,\n SolType,\n SweepTo,\n SimulateRequest,\n SimulateResult,\n SimulateOkResult,\n SimulateRevertResult,\n SimulateRevertDecodeResult,\n SimulateSetupErrorResult,\n SimulateBalanceEntry,\n TrackedBalance,\n SlotFinderRequirement,\n Erc20BalanceRequirement,\n NativeBalanceRequirement,\n Erc20AllowanceRequirement,\n ContinuationIntent,\n ContinuationOutcome,\n Check,\n CheckComparator,\n Phase1ArtifactWire,\n ContinuationPlanWire,\n} from '@lifi/compose-spec';\nexport {\n SIMULATE_MAX_TRACKED_BALANCES,\n SIMULATE_MAX_REQUIREMENTS,\n} from '@lifi/compose-spec';\nexport {\n isComposeError,\n isComposePreparationError,\n isComposeSdkOutdatedError,\n isComposeServerOutdatedError,\n} from './errors.js';\n\nexport type {\n Bindable,\n RefBindable,\n AnyRefBindable,\n InputHandle,\n ResourceInputHandle,\n OutputHandle,\n AnyHandle,\n} from './authoring/handles.js';\nexport { isInputHandle, isOutputHandle } from './authoring/handles.js';\nexport { ref } from './authoring/raw.js';\n\nexport type { AnyBindable } from './authoring/FlowBuilderCore.js';\nexport type { TypedFlow, FlowOptions } from './authoring/FlowBuilderCore.js';\n\nexport type { ComposeRunInput } from './run/inputs.js';\nexport { materialiser } from './run/inputs.js';\n\nexport { buildSimulateRequest } from './run/simulate.js';\nexport type { BuildSimulateRequestInput } from './run/simulate.js';\n\nexport { createComposeSdk } from './sdk.js';\nexport type { ComposeSdk, ComposeSdkOptions, FlowBuilder } from './sdk.js';\n\nimport * as guardsMod from './generated/guards.generated.js';\nimport * as materialisersMod from './generated/materialisers.generated.js';\nimport * as preconditionsMod from './generated/preconditions.generated.js';\nimport * as rawMod from './raw.js';\nimport * as resourcesMod from './resources.js';\nimport * as routeAmountMod from './routeAmount.js';\n\n/**\n * Guard factories for protecting operation outputs (e.g. slippage tolerance).\n * Pass guards to an operation's `guards` array.\n *\n * @example\n * ```ts\n * builder.lifi.swap('swap1', {\n * bind: { amountIn: builder.inputs.token },\n * config: { resourceOut: resources.native(1) },\n * guards: [guards.slippage({ port: 'amountOut', bps: 300 })],\n * });\n * ```\n */\nexport const guards: typeof guardsMod = guardsMod;\n/**\n * Materialiser factories that tell the runtime how to resolve token input\n * amounts on-chain (e.g. read a wallet balance, accept a direct deposit).\n *\n * @example\n * ```ts\n * await builder.compile({\n * inputs: { token: materialisers.balanceOf({}) },\n * signer: '0x...',\n * });\n * ```\n */\nexport const materialisers: typeof materialisersMod = materialisersMod;\n/**\n * Precondition factories for asserting on-chain state before a flow executes\n * (e.g. minimum token balances, ERC-20 allowances).\n *\n * @example\n * ```ts\n * await builder.compile({\n * inputs: { token: materialisers.balanceOf({}) },\n * preconditions: [\n * preconditions.erc20Balance({ wallet: '0x...', token: '0x...', balance: 1000n }),\n * ],\n * signer: '0x...',\n * });\n * ```\n */\nexport const preconditions: typeof preconditionsMod = preconditionsMod;\n/**\n * Resource declaration helpers for defining token inputs in a flow schema.\n * Use {@link resources.erc20} for ERC-20 tokens and {@link resources.native}\n * for the chain's native coin (ETH, MATIC, etc.).\n *\n * @example\n * ```ts\n * const builder = sdk.flow(1, {\n * inputs: {\n * usdc: resources.erc20('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 1),\n * eth: resources.native(1),\n * },\n * });\n * ```\n */\nexport const resources: typeof resourcesMod = resourcesMod;\n/**\n * Amount constructors for {@link ComposeSdk.route}. Use\n * {@link routeAmount.exact} to spend a fixed amount of the `fromToken`, and\n * {@link routeAmount.all} to spend the signer's whole balance.\n *\n * @example\n * ```ts\n * // USDC into the Aave v3 aEthUSDC position — one routing-catalog edge.\n * const result = await sdk.route({\n * chainId: 1,\n * fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',\n * toToken: '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c',\n * amount: routeAmount.exact(1_000_000n),\n * signer: '0xYourAddress...',\n * });\n * ```\n */\nexport const routeAmount: typeof routeAmountMod = routeAmountMod;\nexport * from './generated/config.generated.js';\n\n/**\n * Low-level escape hatch for constructing flow nodes, guards, and materialiser\n * inputs without the typed builder API. Prefer the generated operation methods\n * on {@link FlowBuilder} and the typed `guards`/`materialisers` namespaces\n * for most use cases.\n */\nexport const raw: typeof rawMod = rawMod;\n"],"mappings":"AA0FA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,SAAS,eAAe,sBAAsB;AAC9C,SAAS,WAAW;AAMpB,SAAS,oBAAoB;AAE7B,SAAS,4BAA4B;AAGrC,SAAS,wBAAwB;AAGjC,YAAY,eAAe;AAC3B,YAAY,sBAAsB;AAClC,YAAY,sBAAsB;AAClC,YAAY,YAAY;AACxB,YAAY,kBAAkB;AAC9B,YAAY,oBAAoB;AAezB,MAAM,SAA2B;AAajC,MAAM,gBAAyC;AAgB/C,MAAM,gBAAyC;AAgB/C,MAAM,YAAiC;AAkBvC,MAAM,cAAqC;AAClD,cAAc;AAQP,MAAM,MAAqB;","names":[]}
|
package/dist/responseSchemas.cjs
CHANGED
|
@@ -101,6 +101,9 @@ const serverErrorBodySchema = import_zod.default.object({
|
|
|
101
101
|
kind: import_zod.default.string().optional(),
|
|
102
102
|
message: import_zod.default.string().optional(),
|
|
103
103
|
path: import_zod.default.string().optional(),
|
|
104
|
+
sdkVersion: import_zod.default.string().optional(),
|
|
105
|
+
serverVersion: import_zod.default.string().optional(),
|
|
106
|
+
minimumSdkVersion: import_zod.default.string().optional(),
|
|
104
107
|
details: import_zod.default.custom().optional(),
|
|
105
108
|
failedOps: import_zod.default.array(failedPreparedOpSchema).readonly().optional(),
|
|
106
109
|
succeededOps: import_zod.default.array(import_zod.default.string()).readonly().optional()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/responseSchemas.ts"],"sourcesContent":["import type { SimulateResult, SimulationRevert } from '@lifi/compose-spec';\nimport z from 'zod';\n\n/**\n * Zod-backed parsers for Compose API response bodies.\n *\n * Server responses are untrusted input crossing into the SDK, so the shapes the\n * client reads back are validated here rather than asserted with `as` casts. The\n * schemas are module-private (the package builds with `--isolatedDeclarations`,\n * which forbids exporting un-annotated complex values); the public surface is\n * the typed `parse*` helpers, whose explicit return types double as the contract.\n *\n * The simulate union is the one response the SDK fully owns and parses itself\n * (it is un-enveloped, unlike `/compose`), so it gets a complete schema kept in\n * lockstep with the canonical `SimulateResult` in `@lifi/compose-spec` by the\n * compile-time assertion below — the same anchoring `@lifi/api-schemas` uses for\n * its server-side simulate schema, so the two cannot silently drift.\n */\n\nconst balanceEntrySchema = z.object({\n token: z.string(),\n owner: z.string(),\n amount: z.string(),\n});\n\nconst simulateOkSchema = z.object({\n status: z.literal('ok'),\n block: z.number(),\n timestamp: z.number(),\n balancesBefore: z.array(balanceEntrySchema),\n balancesAfter: z.array(balanceEntrySchema),\n deltas: z.array(balanceEntrySchema),\n gasUsed: z.string(),\n});\n\nconst simulateRevertDecodeResultSchema = z.object({\n errorCandidates: z\n .array(\n z.object({\n decodedErrorSignature: z.string(),\n decodedParams: z.array(z.string()),\n }),\n )\n .optional(),\n error: z.string().optional(),\n});\n\nconst simulateRevertSchema = z.object({\n status: z.literal('revert'),\n block: z.number(),\n timestamp: z.number(),\n revertReason: z.string().optional(),\n code: z.number().optional(),\n rawErrorBytes: z.string().optional(),\n decodeResult: simulateRevertDecodeResultSchema.optional(),\n});\n\nconst simulateSetupErrorSchema = z.object({\n status: z.literal('error'),\n message: z.string(),\n});\n\nconst simulateResultSchema = z.discriminatedUnion('status', [\n simulateOkSchema,\n simulateRevertSchema,\n simulateSetupErrorSchema,\n]);\n\n// The type the schema actually parses out. Kept private (referencing it from an\n// exported declaration would break `--isolatedDeclarations`) and pinned to the\n// canonical `SimulateResult` inside `parseSimulateResult` below.\ntype SimulateResultWire = z.infer<typeof simulateResultSchema>;\n\n/**\n * Parses a `POST /simulate` body into a {@link SimulateResult}. Returns `null`\n * when the body does not match the `ok`/`revert`/`error` union, letting the\n * caller raise a transport-level error.\n */\nexport const parseSimulateResult = (body: unknown): SimulateResult | null => {\n const parsed = simulateResultSchema.safeParse(body);\n if (!parsed.success) return null;\n // Pin the schema to the canonical contract in both directions, so it cannot\n // silently drift (mirroring the conformance assertion `@lifi/api-schemas` runs\n // on its server-side simulate schema). Assigning the inferred wire value to a\n // `SimulateResult` checks wire→spec; `satisfies SimulateResultWire` checks\n // spec→wire. A missing or mistyped field on either side fails to compile.\n const result: SimulateResult = parsed.data;\n return result satisfies SimulateResultWire;\n};\n\n/**\n * The validated members of an HTTP 206 partial compile envelope. `data` is\n * verified to be an object but kept untyped — compose-spec owns its full shape\n * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows\n * it. `error` is fully validated.\n */\nexport interface CompilePartialEnvelope {\n readonly data: Record<string, unknown>;\n readonly error: { readonly kind: string; readonly message: string };\n}\n\nconst compilePartialEnvelopeSchema = z.object({\n data: z.record(z.string(), z.unknown()),\n error: z.object({ kind: z.string(), message: z.string() }),\n});\n\n/** Parses an HTTP 206 partial compile envelope; `null` when it does not match. */\nexport const parseCompilePartialEnvelope = (\n body: unknown,\n): CompilePartialEnvelope | null => {\n const parsed = compilePartialEnvelopeSchema.safeParse(body);\n return parsed.success ? parsed.data : null;\n};\n\n/**\n * A single prepared-op failure inside a `preparation_error` envelope. `kind` is\n * kept as `string` here — the narrowing to `GenericComposeErrorKind` happens in\n * `errorFromHttpResponse`, where the value is lifted onto `ComposeError`.\n */\nexport interface ServerFailedPreparedOp {\n readonly callId: string;\n readonly op: string;\n readonly kind: string;\n readonly message: string;\n readonly path?: string;\n}\n\n/** Error envelope returned on non-2xx responses. */\nexport interface ServerErrorBody {\n readonly error?: {\n readonly kind?: string;\n readonly message?: string;\n readonly path?: string;\n readonly details?: SimulationRevert;\n readonly failedOps?: readonly ServerFailedPreparedOp[];\n readonly succeededOps?: readonly string[];\n };\n}\n\nconst failedPreparedOpSchema = z.object({\n callId: z.string(),\n op: z.string(),\n kind: z.string(),\n message: z.string(),\n path: z.string().optional(),\n});\n\n// `details` is passed through (`z.custom`) rather than parsed: the SDK does not\n// own the `SimulationRevert` schema and forwards the field verbatim onto\n// `ComposeError`.\n//\n// `failedOps`/`succeededOps` are optional here because every error kind shares\n// this envelope; they are only populated for `preparation_error` (422). Zod\n// strips undeclared keys, so omitting them would silently drop them.\nconst serverErrorBodySchema = z.object({\n error: z\n .object({\n kind: z.string().optional(),\n message: z.string().optional(),\n path: z.string().optional(),\n details: z.custom<SimulationRevert>().optional(),\n failedOps: z.array(failedPreparedOpSchema).readonly().optional(),\n succeededOps: z.array(z.string()).readonly().optional(),\n })\n .optional(),\n});\n\n/** Validates an already-parsed error envelope; `null` when it does not match. */\nexport const parseServerErrorBody = (json: unknown): ServerErrorBody | null => {\n const parsed = serverErrorBodySchema.safeParse(json);\n return parsed.success ? parsed.data : null;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,iBAAc;AAkBd,MAAM,qBAAqB,WAAAA,QAAE,OAAO;AAAA,EAClC,OAAO,WAAAA,QAAE,OAAO;AAAA,EAChB,OAAO,WAAAA,QAAE,OAAO;AAAA,EAChB,QAAQ,WAAAA,QAAE,OAAO;AACnB,CAAC;AAED,MAAM,mBAAmB,WAAAA,QAAE,OAAO;AAAA,EAChC,QAAQ,WAAAA,QAAE,QAAQ,IAAI;AAAA,EACtB,OAAO,WAAAA,QAAE,OAAO;AAAA,EAChB,WAAW,WAAAA,QAAE,OAAO;AAAA,EACpB,gBAAgB,WAAAA,QAAE,MAAM,kBAAkB;AAAA,EAC1C,eAAe,WAAAA,QAAE,MAAM,kBAAkB;AAAA,EACzC,QAAQ,WAAAA,QAAE,MAAM,kBAAkB;AAAA,EAClC,SAAS,WAAAA,QAAE,OAAO;AACpB,CAAC;AAED,MAAM,mCAAmC,WAAAA,QAAE,OAAO;AAAA,EAChD,iBAAiB,WAAAA,QACd;AAAA,IACC,WAAAA,QAAE,OAAO;AAAA,MACP,uBAAuB,WAAAA,QAAE,OAAO;AAAA,MAChC,eAAe,WAAAA,QAAE,MAAM,WAAAA,QAAE,OAAO,CAAC;AAAA,IACnC,CAAC;AAAA,EACH,EACC,SAAS;AAAA,EACZ,OAAO,WAAAA,QAAE,OAAO,EAAE,SAAS;AAC7B,CAAC;AAED,MAAM,uBAAuB,WAAAA,QAAE,OAAO;AAAA,EACpC,QAAQ,WAAAA,QAAE,QAAQ,QAAQ;AAAA,EAC1B,OAAO,WAAAA,QAAE,OAAO;AAAA,EAChB,WAAW,WAAAA,QAAE,OAAO;AAAA,EACpB,cAAc,WAAAA,QAAE,OAAO,EAAE,SAAS;AAAA,EAClC,MAAM,WAAAA,QAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,eAAe,WAAAA,QAAE,OAAO,EAAE,SAAS;AAAA,EACnC,cAAc,iCAAiC,SAAS;AAC1D,CAAC;AAED,MAAM,2BAA2B,WAAAA,QAAE,OAAO;AAAA,EACxC,QAAQ,WAAAA,QAAE,QAAQ,OAAO;AAAA,EACzB,SAAS,WAAAA,QAAE,OAAO;AACpB,CAAC;AAED,MAAM,uBAAuB,WAAAA,QAAE,mBAAmB,UAAU;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAYM,MAAM,sBAAsB,CAAC,SAAyC;AAC3E,QAAM,SAAS,qBAAqB,UAAU,IAAI;AAClD,MAAI,CAAC,OAAO,QAAS,QAAO;AAM5B,QAAM,SAAyB,OAAO;AACtC,SAAO;AACT;AAaA,MAAM,+BAA+B,WAAAA,QAAE,OAAO;AAAA,EAC5C,MAAM,WAAAA,QAAE,OAAO,WAAAA,QAAE,OAAO,GAAG,WAAAA,QAAE,QAAQ,CAAC;AAAA,EACtC,OAAO,WAAAA,QAAE,OAAO,EAAE,MAAM,WAAAA,QAAE,OAAO,GAAG,SAAS,WAAAA,QAAE,OAAO,EAAE,CAAC;AAC3D,CAAC;AAGM,MAAM,8BAA8B,CACzC,SACkC;AAClC,QAAM,SAAS,6BAA6B,UAAU,IAAI;AAC1D,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;
|
|
1
|
+
{"version":3,"sources":["../src/responseSchemas.ts"],"sourcesContent":["import type { SimulateResult, SimulationRevert } from '@lifi/compose-spec';\nimport z from 'zod';\n\n/**\n * Zod-backed parsers for Compose API response bodies.\n *\n * Server responses are untrusted input crossing into the SDK, so the shapes the\n * client reads back are validated here rather than asserted with `as` casts. The\n * schemas are module-private (the package builds with `--isolatedDeclarations`,\n * which forbids exporting un-annotated complex values); the public surface is\n * the typed `parse*` helpers, whose explicit return types double as the contract.\n *\n * The simulate union is the one response the SDK fully owns and parses itself\n * (it is un-enveloped, unlike `/compose`), so it gets a complete schema kept in\n * lockstep with the canonical `SimulateResult` in `@lifi/compose-spec` by the\n * compile-time assertion below — the same anchoring `@lifi/api-schemas` uses for\n * its server-side simulate schema, so the two cannot silently drift.\n */\n\nconst balanceEntrySchema = z.object({\n token: z.string(),\n owner: z.string(),\n amount: z.string(),\n});\n\nconst simulateOkSchema = z.object({\n status: z.literal('ok'),\n block: z.number(),\n timestamp: z.number(),\n balancesBefore: z.array(balanceEntrySchema),\n balancesAfter: z.array(balanceEntrySchema),\n deltas: z.array(balanceEntrySchema),\n gasUsed: z.string(),\n});\n\nconst simulateRevertDecodeResultSchema = z.object({\n errorCandidates: z\n .array(\n z.object({\n decodedErrorSignature: z.string(),\n decodedParams: z.array(z.string()),\n }),\n )\n .optional(),\n error: z.string().optional(),\n});\n\nconst simulateRevertSchema = z.object({\n status: z.literal('revert'),\n block: z.number(),\n timestamp: z.number(),\n revertReason: z.string().optional(),\n code: z.number().optional(),\n rawErrorBytes: z.string().optional(),\n decodeResult: simulateRevertDecodeResultSchema.optional(),\n});\n\nconst simulateSetupErrorSchema = z.object({\n status: z.literal('error'),\n message: z.string(),\n});\n\nconst simulateResultSchema = z.discriminatedUnion('status', [\n simulateOkSchema,\n simulateRevertSchema,\n simulateSetupErrorSchema,\n]);\n\n// The type the schema actually parses out. Kept private (referencing it from an\n// exported declaration would break `--isolatedDeclarations`) and pinned to the\n// canonical `SimulateResult` inside `parseSimulateResult` below.\ntype SimulateResultWire = z.infer<typeof simulateResultSchema>;\n\n/**\n * Parses a `POST /simulate` body into a {@link SimulateResult}. Returns `null`\n * when the body does not match the `ok`/`revert`/`error` union, letting the\n * caller raise a transport-level error.\n */\nexport const parseSimulateResult = (body: unknown): SimulateResult | null => {\n const parsed = simulateResultSchema.safeParse(body);\n if (!parsed.success) return null;\n // Pin the schema to the canonical contract in both directions, so it cannot\n // silently drift (mirroring the conformance assertion `@lifi/api-schemas` runs\n // on its server-side simulate schema). Assigning the inferred wire value to a\n // `SimulateResult` checks wire→spec; `satisfies SimulateResultWire` checks\n // spec→wire. A missing or mistyped field on either side fails to compile.\n const result: SimulateResult = parsed.data;\n return result satisfies SimulateResultWire;\n};\n\n/**\n * The validated members of an HTTP 206 partial compile envelope. `data` is\n * verified to be an object but kept untyped — compose-spec owns its full shape\n * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows\n * it. `error` is fully validated.\n */\nexport interface CompilePartialEnvelope {\n readonly data: Record<string, unknown>;\n readonly error: { readonly kind: string; readonly message: string };\n}\n\nconst compilePartialEnvelopeSchema = z.object({\n data: z.record(z.string(), z.unknown()),\n error: z.object({ kind: z.string(), message: z.string() }),\n});\n\n/** Parses an HTTP 206 partial compile envelope; `null` when it does not match. */\nexport const parseCompilePartialEnvelope = (\n body: unknown,\n): CompilePartialEnvelope | null => {\n const parsed = compilePartialEnvelopeSchema.safeParse(body);\n return parsed.success ? parsed.data : null;\n};\n\n/**\n * A single prepared-op failure inside a `preparation_error` envelope. `kind` is\n * kept as `string` here — the narrowing to `GenericComposeErrorKind` happens in\n * `errorFromHttpResponse`, where the value is lifted onto `ComposeError`.\n */\nexport interface ServerFailedPreparedOp {\n readonly callId: string;\n readonly op: string;\n readonly kind: string;\n readonly message: string;\n readonly path?: string;\n}\n\n/** Error envelope returned on non-2xx responses. */\nexport interface ServerErrorBody {\n readonly error?: {\n readonly kind?: string;\n readonly message?: string;\n readonly path?: string;\n readonly sdkVersion?: string;\n readonly serverVersion?: string;\n readonly minimumSdkVersion?: string;\n readonly details?: SimulationRevert;\n readonly failedOps?: readonly ServerFailedPreparedOp[];\n readonly succeededOps?: readonly string[];\n };\n}\n\nconst failedPreparedOpSchema = z.object({\n callId: z.string(),\n op: z.string(),\n kind: z.string(),\n message: z.string(),\n path: z.string().optional(),\n});\n\n// `details` is passed through (`z.custom`) rather than parsed: the SDK does not\n// own the `SimulationRevert` schema and forwards the field verbatim onto\n// `ComposeError`.\n//\n// `failedOps`/`succeededOps` are optional here because every error kind shares\n// this envelope; they are only populated for `preparation_error` (422). Zod\n// strips undeclared keys, so omitting them would silently drop them.\nconst serverErrorBodySchema = z.object({\n error: z\n .object({\n kind: z.string().optional(),\n message: z.string().optional(),\n path: z.string().optional(),\n sdkVersion: z.string().optional(),\n serverVersion: z.string().optional(),\n minimumSdkVersion: z.string().optional(),\n details: z.custom<SimulationRevert>().optional(),\n failedOps: z.array(failedPreparedOpSchema).readonly().optional(),\n succeededOps: z.array(z.string()).readonly().optional(),\n })\n .optional(),\n});\n\n/** Validates an already-parsed error envelope; `null` when it does not match. */\nexport const parseServerErrorBody = (json: unknown): ServerErrorBody | null => {\n const parsed = serverErrorBodySchema.safeParse(json);\n return parsed.success ? parsed.data : null;\n};\n"],"mappings":"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","names":["z"]}
|
|
@@ -39,6 +39,9 @@ interface ServerErrorBody {
|
|
|
39
39
|
readonly kind?: string;
|
|
40
40
|
readonly message?: string;
|
|
41
41
|
readonly path?: string;
|
|
42
|
+
readonly sdkVersion?: string;
|
|
43
|
+
readonly serverVersion?: string;
|
|
44
|
+
readonly minimumSdkVersion?: string;
|
|
42
45
|
readonly details?: SimulationRevert;
|
|
43
46
|
readonly failedOps?: readonly ServerFailedPreparedOp[];
|
|
44
47
|
readonly succeededOps?: readonly string[];
|
|
@@ -39,6 +39,9 @@ interface ServerErrorBody {
|
|
|
39
39
|
readonly kind?: string;
|
|
40
40
|
readonly message?: string;
|
|
41
41
|
readonly path?: string;
|
|
42
|
+
readonly sdkVersion?: string;
|
|
43
|
+
readonly serverVersion?: string;
|
|
44
|
+
readonly minimumSdkVersion?: string;
|
|
42
45
|
readonly details?: SimulationRevert;
|
|
43
46
|
readonly failedOps?: readonly ServerFailedPreparedOp[];
|
|
44
47
|
readonly succeededOps?: readonly string[];
|
package/dist/responseSchemas.js
CHANGED
|
@@ -66,6 +66,9 @@ const serverErrorBodySchema = z.object({
|
|
|
66
66
|
kind: z.string().optional(),
|
|
67
67
|
message: z.string().optional(),
|
|
68
68
|
path: z.string().optional(),
|
|
69
|
+
sdkVersion: z.string().optional(),
|
|
70
|
+
serverVersion: z.string().optional(),
|
|
71
|
+
minimumSdkVersion: z.string().optional(),
|
|
69
72
|
details: z.custom().optional(),
|
|
70
73
|
failedOps: z.array(failedPreparedOpSchema).readonly().optional(),
|
|
71
74
|
succeededOps: z.array(z.string()).readonly().optional()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/responseSchemas.ts"],"sourcesContent":["import type { SimulateResult, SimulationRevert } from '@lifi/compose-spec';\nimport z from 'zod';\n\n/**\n * Zod-backed parsers for Compose API response bodies.\n *\n * Server responses are untrusted input crossing into the SDK, so the shapes the\n * client reads back are validated here rather than asserted with `as` casts. The\n * schemas are module-private (the package builds with `--isolatedDeclarations`,\n * which forbids exporting un-annotated complex values); the public surface is\n * the typed `parse*` helpers, whose explicit return types double as the contract.\n *\n * The simulate union is the one response the SDK fully owns and parses itself\n * (it is un-enveloped, unlike `/compose`), so it gets a complete schema kept in\n * lockstep with the canonical `SimulateResult` in `@lifi/compose-spec` by the\n * compile-time assertion below — the same anchoring `@lifi/api-schemas` uses for\n * its server-side simulate schema, so the two cannot silently drift.\n */\n\nconst balanceEntrySchema = z.object({\n token: z.string(),\n owner: z.string(),\n amount: z.string(),\n});\n\nconst simulateOkSchema = z.object({\n status: z.literal('ok'),\n block: z.number(),\n timestamp: z.number(),\n balancesBefore: z.array(balanceEntrySchema),\n balancesAfter: z.array(balanceEntrySchema),\n deltas: z.array(balanceEntrySchema),\n gasUsed: z.string(),\n});\n\nconst simulateRevertDecodeResultSchema = z.object({\n errorCandidates: z\n .array(\n z.object({\n decodedErrorSignature: z.string(),\n decodedParams: z.array(z.string()),\n }),\n )\n .optional(),\n error: z.string().optional(),\n});\n\nconst simulateRevertSchema = z.object({\n status: z.literal('revert'),\n block: z.number(),\n timestamp: z.number(),\n revertReason: z.string().optional(),\n code: z.number().optional(),\n rawErrorBytes: z.string().optional(),\n decodeResult: simulateRevertDecodeResultSchema.optional(),\n});\n\nconst simulateSetupErrorSchema = z.object({\n status: z.literal('error'),\n message: z.string(),\n});\n\nconst simulateResultSchema = z.discriminatedUnion('status', [\n simulateOkSchema,\n simulateRevertSchema,\n simulateSetupErrorSchema,\n]);\n\n// The type the schema actually parses out. Kept private (referencing it from an\n// exported declaration would break `--isolatedDeclarations`) and pinned to the\n// canonical `SimulateResult` inside `parseSimulateResult` below.\ntype SimulateResultWire = z.infer<typeof simulateResultSchema>;\n\n/**\n * Parses a `POST /simulate` body into a {@link SimulateResult}. Returns `null`\n * when the body does not match the `ok`/`revert`/`error` union, letting the\n * caller raise a transport-level error.\n */\nexport const parseSimulateResult = (body: unknown): SimulateResult | null => {\n const parsed = simulateResultSchema.safeParse(body);\n if (!parsed.success) return null;\n // Pin the schema to the canonical contract in both directions, so it cannot\n // silently drift (mirroring the conformance assertion `@lifi/api-schemas` runs\n // on its server-side simulate schema). Assigning the inferred wire value to a\n // `SimulateResult` checks wire→spec; `satisfies SimulateResultWire` checks\n // spec→wire. A missing or mistyped field on either side fails to compile.\n const result: SimulateResult = parsed.data;\n return result satisfies SimulateResultWire;\n};\n\n/**\n * The validated members of an HTTP 206 partial compile envelope. `data` is\n * verified to be an object but kept untyped — compose-spec owns its full shape\n * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows\n * it. `error` is fully validated.\n */\nexport interface CompilePartialEnvelope {\n readonly data: Record<string, unknown>;\n readonly error: { readonly kind: string; readonly message: string };\n}\n\nconst compilePartialEnvelopeSchema = z.object({\n data: z.record(z.string(), z.unknown()),\n error: z.object({ kind: z.string(), message: z.string() }),\n});\n\n/** Parses an HTTP 206 partial compile envelope; `null` when it does not match. */\nexport const parseCompilePartialEnvelope = (\n body: unknown,\n): CompilePartialEnvelope | null => {\n const parsed = compilePartialEnvelopeSchema.safeParse(body);\n return parsed.success ? parsed.data : null;\n};\n\n/**\n * A single prepared-op failure inside a `preparation_error` envelope. `kind` is\n * kept as `string` here — the narrowing to `GenericComposeErrorKind` happens in\n * `errorFromHttpResponse`, where the value is lifted onto `ComposeError`.\n */\nexport interface ServerFailedPreparedOp {\n readonly callId: string;\n readonly op: string;\n readonly kind: string;\n readonly message: string;\n readonly path?: string;\n}\n\n/** Error envelope returned on non-2xx responses. */\nexport interface ServerErrorBody {\n readonly error?: {\n readonly kind?: string;\n readonly message?: string;\n readonly path?: string;\n readonly details?: SimulationRevert;\n readonly failedOps?: readonly ServerFailedPreparedOp[];\n readonly succeededOps?: readonly string[];\n };\n}\n\nconst failedPreparedOpSchema = z.object({\n callId: z.string(),\n op: z.string(),\n kind: z.string(),\n message: z.string(),\n path: z.string().optional(),\n});\n\n// `details` is passed through (`z.custom`) rather than parsed: the SDK does not\n// own the `SimulationRevert` schema and forwards the field verbatim onto\n// `ComposeError`.\n//\n// `failedOps`/`succeededOps` are optional here because every error kind shares\n// this envelope; they are only populated for `preparation_error` (422). Zod\n// strips undeclared keys, so omitting them would silently drop them.\nconst serverErrorBodySchema = z.object({\n error: z\n .object({\n kind: z.string().optional(),\n message: z.string().optional(),\n path: z.string().optional(),\n details: z.custom<SimulationRevert>().optional(),\n failedOps: z.array(failedPreparedOpSchema).readonly().optional(),\n succeededOps: z.array(z.string()).readonly().optional(),\n })\n .optional(),\n});\n\n/** Validates an already-parsed error envelope; `null` when it does not match. */\nexport const parseServerErrorBody = (json: unknown): ServerErrorBody | null => {\n const parsed = serverErrorBodySchema.safeParse(json);\n return parsed.success ? parsed.data : null;\n};\n"],"mappings":"AACA,OAAO,OAAO;AAkBd,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,OAAO,EAAE,OAAO;AAAA,EAChB,OAAO,EAAE,OAAO;AAAA,EAChB,QAAQ,EAAE,OAAO;AACnB,CAAC;AAED,MAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,QAAQ,EAAE,QAAQ,IAAI;AAAA,EACtB,OAAO,EAAE,OAAO;AAAA,EAChB,WAAW,EAAE,OAAO;AAAA,EACpB,gBAAgB,EAAE,MAAM,kBAAkB;AAAA,EAC1C,eAAe,EAAE,MAAM,kBAAkB;AAAA,EACzC,QAAQ,EAAE,MAAM,kBAAkB;AAAA,EAClC,SAAS,EAAE,OAAO;AACpB,CAAC;AAED,MAAM,mCAAmC,EAAE,OAAO;AAAA,EAChD,iBAAiB,EACd;AAAA,IACC,EAAE,OAAO;AAAA,MACP,uBAAuB,EAAE,OAAO;AAAA,MAChC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC;AAAA,IACnC,CAAC;AAAA,EACH,EACC,SAAS;AAAA,EACZ,OAAO,EAAE,OAAO,EAAE,SAAS;AAC7B,CAAC;AAED,MAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,QAAQ,EAAE,QAAQ,QAAQ;AAAA,EAC1B,OAAO,EAAE,OAAO;AAAA,EAChB,WAAW,EAAE,OAAO;AAAA,EACpB,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,EAClC,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,EACnC,cAAc,iCAAiC,SAAS;AAC1D,CAAC;AAED,MAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,QAAQ,EAAE,QAAQ,OAAO;AAAA,EACzB,SAAS,EAAE,OAAO;AACpB,CAAC;AAED,MAAM,uBAAuB,EAAE,mBAAmB,UAAU;AAAA,EAC1D;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAYM,MAAM,sBAAsB,CAAC,SAAyC;AAC3E,QAAM,SAAS,qBAAqB,UAAU,IAAI;AAClD,MAAI,CAAC,OAAO,QAAS,QAAO;AAM5B,QAAM,SAAyB,OAAO;AACtC,SAAO;AACT;AAaA,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EACtC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,CAAC;AAC3D,CAAC;AAGM,MAAM,8BAA8B,CACzC,SACkC;AAClC,QAAM,SAAS,6BAA6B,UAAU,IAAI;AAC1D,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;
|
|
1
|
+
{"version":3,"sources":["../src/responseSchemas.ts"],"sourcesContent":["import type { SimulateResult, SimulationRevert } from '@lifi/compose-spec';\nimport z from 'zod';\n\n/**\n * Zod-backed parsers for Compose API response bodies.\n *\n * Server responses are untrusted input crossing into the SDK, so the shapes the\n * client reads back are validated here rather than asserted with `as` casts. The\n * schemas are module-private (the package builds with `--isolatedDeclarations`,\n * which forbids exporting un-annotated complex values); the public surface is\n * the typed `parse*` helpers, whose explicit return types double as the contract.\n *\n * The simulate union is the one response the SDK fully owns and parses itself\n * (it is un-enveloped, unlike `/compose`), so it gets a complete schema kept in\n * lockstep with the canonical `SimulateResult` in `@lifi/compose-spec` by the\n * compile-time assertion below — the same anchoring `@lifi/api-schemas` uses for\n * its server-side simulate schema, so the two cannot silently drift.\n */\n\nconst balanceEntrySchema = z.object({\n token: z.string(),\n owner: z.string(),\n amount: z.string(),\n});\n\nconst simulateOkSchema = z.object({\n status: z.literal('ok'),\n block: z.number(),\n timestamp: z.number(),\n balancesBefore: z.array(balanceEntrySchema),\n balancesAfter: z.array(balanceEntrySchema),\n deltas: z.array(balanceEntrySchema),\n gasUsed: z.string(),\n});\n\nconst simulateRevertDecodeResultSchema = z.object({\n errorCandidates: z\n .array(\n z.object({\n decodedErrorSignature: z.string(),\n decodedParams: z.array(z.string()),\n }),\n )\n .optional(),\n error: z.string().optional(),\n});\n\nconst simulateRevertSchema = z.object({\n status: z.literal('revert'),\n block: z.number(),\n timestamp: z.number(),\n revertReason: z.string().optional(),\n code: z.number().optional(),\n rawErrorBytes: z.string().optional(),\n decodeResult: simulateRevertDecodeResultSchema.optional(),\n});\n\nconst simulateSetupErrorSchema = z.object({\n status: z.literal('error'),\n message: z.string(),\n});\n\nconst simulateResultSchema = z.discriminatedUnion('status', [\n simulateOkSchema,\n simulateRevertSchema,\n simulateSetupErrorSchema,\n]);\n\n// The type the schema actually parses out. Kept private (referencing it from an\n// exported declaration would break `--isolatedDeclarations`) and pinned to the\n// canonical `SimulateResult` inside `parseSimulateResult` below.\ntype SimulateResultWire = z.infer<typeof simulateResultSchema>;\n\n/**\n * Parses a `POST /simulate` body into a {@link SimulateResult}. Returns `null`\n * when the body does not match the `ok`/`revert`/`error` union, letting the\n * caller raise a transport-level error.\n */\nexport const parseSimulateResult = (body: unknown): SimulateResult | null => {\n const parsed = simulateResultSchema.safeParse(body);\n if (!parsed.success) return null;\n // Pin the schema to the canonical contract in both directions, so it cannot\n // silently drift (mirroring the conformance assertion `@lifi/api-schemas` runs\n // on its server-side simulate schema). Assigning the inferred wire value to a\n // `SimulateResult` checks wire→spec; `satisfies SimulateResultWire` checks\n // spec→wire. A missing or mistyped field on either side fails to compile.\n const result: SimulateResult = parsed.data;\n return result satisfies SimulateResultWire;\n};\n\n/**\n * The validated members of an HTTP 206 partial compile envelope. `data` is\n * verified to be an object but kept untyped — compose-spec owns its full shape\n * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows\n * it. `error` is fully validated.\n */\nexport interface CompilePartialEnvelope {\n readonly data: Record<string, unknown>;\n readonly error: { readonly kind: string; readonly message: string };\n}\n\nconst compilePartialEnvelopeSchema = z.object({\n data: z.record(z.string(), z.unknown()),\n error: z.object({ kind: z.string(), message: z.string() }),\n});\n\n/** Parses an HTTP 206 partial compile envelope; `null` when it does not match. */\nexport const parseCompilePartialEnvelope = (\n body: unknown,\n): CompilePartialEnvelope | null => {\n const parsed = compilePartialEnvelopeSchema.safeParse(body);\n return parsed.success ? parsed.data : null;\n};\n\n/**\n * A single prepared-op failure inside a `preparation_error` envelope. `kind` is\n * kept as `string` here — the narrowing to `GenericComposeErrorKind` happens in\n * `errorFromHttpResponse`, where the value is lifted onto `ComposeError`.\n */\nexport interface ServerFailedPreparedOp {\n readonly callId: string;\n readonly op: string;\n readonly kind: string;\n readonly message: string;\n readonly path?: string;\n}\n\n/** Error envelope returned on non-2xx responses. */\nexport interface ServerErrorBody {\n readonly error?: {\n readonly kind?: string;\n readonly message?: string;\n readonly path?: string;\n readonly sdkVersion?: string;\n readonly serverVersion?: string;\n readonly minimumSdkVersion?: string;\n readonly details?: SimulationRevert;\n readonly failedOps?: readonly ServerFailedPreparedOp[];\n readonly succeededOps?: readonly string[];\n };\n}\n\nconst failedPreparedOpSchema = z.object({\n callId: z.string(),\n op: z.string(),\n kind: z.string(),\n message: z.string(),\n path: z.string().optional(),\n});\n\n// `details` is passed through (`z.custom`) rather than parsed: the SDK does not\n// own the `SimulationRevert` schema and forwards the field verbatim onto\n// `ComposeError`.\n//\n// `failedOps`/`succeededOps` are optional here because every error kind shares\n// this envelope; they are only populated for `preparation_error` (422). Zod\n// strips undeclared keys, so omitting them would silently drop them.\nconst serverErrorBodySchema = z.object({\n error: z\n .object({\n kind: z.string().optional(),\n message: z.string().optional(),\n path: z.string().optional(),\n sdkVersion: z.string().optional(),\n serverVersion: z.string().optional(),\n minimumSdkVersion: z.string().optional(),\n details: z.custom<SimulationRevert>().optional(),\n failedOps: z.array(failedPreparedOpSchema).readonly().optional(),\n succeededOps: z.array(z.string()).readonly().optional(),\n })\n .optional(),\n});\n\n/** Validates an already-parsed error envelope; `null` when it does not match. */\nexport const parseServerErrorBody = (json: unknown): ServerErrorBody | null => {\n const parsed = serverErrorBodySchema.safeParse(json);\n return parsed.success ? parsed.data : null;\n};\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifi/composer-sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Public Composer SDK for building and submitting flows",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"example:staged": "tsx src/examples/run.ts --staged"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@lifi/compose-spec": "0.5.
|
|
49
|
+
"@lifi/compose-spec": "0.5.1"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"abitype": "^1.2.3",
|
package/src/client.ts
CHANGED
|
@@ -8,6 +8,12 @@ import type {
|
|
|
8
8
|
SimulateRequest,
|
|
9
9
|
SimulateResult,
|
|
10
10
|
} from '@lifi/compose-spec';
|
|
11
|
+
import {
|
|
12
|
+
checkCompatibility,
|
|
13
|
+
COMPOSE_VERSION_HEADER,
|
|
14
|
+
COMPOSER_SDK_VERSION_HEADER,
|
|
15
|
+
parseVersion,
|
|
16
|
+
} from '@lifi/compose-spec';
|
|
11
17
|
|
|
12
18
|
import type {
|
|
13
19
|
ChainSummary,
|
|
@@ -23,12 +29,14 @@ import {
|
|
|
23
29
|
// __SDK_VERSION__ is a compile-time constant injected by tsup (via `define` in tsup.config.ts)
|
|
24
30
|
// and by vitest (via `define` in vitest.config.ts). Both read the version from package.json
|
|
25
31
|
// at build/test time and replace this identifier with the literal string value.
|
|
26
|
-
// It is sent as the `
|
|
32
|
+
// It is sent as the `COMPOSER_SDK_VERSION_HEADER` request header so the server can identify the caller.
|
|
27
33
|
// Falls back to 'dev' when running via tsx without tsup substitution (e.g. the example harness).
|
|
28
34
|
declare const __SDK_VERSION__: string;
|
|
29
35
|
|
|
30
36
|
const SDK_VERSION: string =
|
|
31
37
|
typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';
|
|
38
|
+
// `undefined` for the `dev` sentinel, which disables the response version check.
|
|
39
|
+
const SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
42
|
* Configuration for creating a low-level Compose API client.
|
|
@@ -176,7 +184,14 @@ export interface ComposeClient {
|
|
|
176
184
|
readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;
|
|
177
185
|
}
|
|
178
186
|
|
|
179
|
-
|
|
187
|
+
/**
|
|
188
|
+
* `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.
|
|
189
|
+
*
|
|
190
|
+
* The compose wire carries token amounts as decimal strings, and `bigint` has
|
|
191
|
+
* no JSON representation, so every payload this client sends goes through it.
|
|
192
|
+
* Exported so the example harness prints a request exactly as it is sent.
|
|
193
|
+
*/
|
|
194
|
+
export const bigintReplacer = (_key: string, value: unknown): unknown =>
|
|
180
195
|
typeof value === 'bigint' ? value.toString() : value;
|
|
181
196
|
|
|
182
197
|
const isNonNullObject = (v: unknown): v is Record<string, unknown> =>
|
|
@@ -310,22 +325,92 @@ type ComposeTransport = {
|
|
|
310
325
|
readonly baseHeaders: Record<string, string>;
|
|
311
326
|
};
|
|
312
327
|
|
|
313
|
-
|
|
328
|
+
// A patch bump is additive and a minor bump is breaking, so a 200 from a
|
|
329
|
+
// backend behind this SDK cannot be trusted: the newer flow this SDK authored
|
|
330
|
+
// may have compiled under the server's older semantics without raising an
|
|
331
|
+
// error. This covers the deploy window where the SDK is published to npm ahead
|
|
332
|
+
// of the backend rollout. Only the compile-family POSTs are checked; discovery
|
|
333
|
+
// GETs stay reachable from a mismatched SDK, as on the server. An absent header
|
|
334
|
+
// (a pre-gate backend, a header-stripping proxy) is accepted, mirroring the
|
|
335
|
+
// server's pass-through of a compile that carries no version header.
|
|
336
|
+
const assertServerVersion = (res: Response, url: string): void => {
|
|
337
|
+
const serverVersion = res.headers.get(COMPOSE_VERSION_HEADER);
|
|
338
|
+
if (serverVersion === null || SDK_VERSION_TRIPLE === undefined) return;
|
|
339
|
+
const serverTriple = parseVersion(serverVersion);
|
|
340
|
+
if (serverTriple === undefined) return;
|
|
341
|
+
|
|
342
|
+
const compatibility = checkCompatibility(SDK_VERSION_TRIPLE, serverTriple);
|
|
343
|
+
if (compatibility === 'compatible') return;
|
|
344
|
+
|
|
345
|
+
const versions = { sdkVersion: SDK_VERSION, serverVersion };
|
|
346
|
+
if (compatibility === 'sdk_outdated') {
|
|
347
|
+
throw new ComposeError(
|
|
348
|
+
'VALIDATION_ERROR',
|
|
349
|
+
`composer-sdk ${SDK_VERSION} is older than the compose contract served at ${url} (${serverVersion}); upgrade @lifi/composer-sdk and @lifi/compose-spec`,
|
|
350
|
+
{
|
|
351
|
+
status: res.status,
|
|
352
|
+
url,
|
|
353
|
+
kind: 'sdk_outdated',
|
|
354
|
+
sdkOutdated: {
|
|
355
|
+
...versions,
|
|
356
|
+
minimumSdkVersion: `${serverTriple.major}.${serverTriple.minor}.0`,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
throw new ComposeError(
|
|
362
|
+
'VALIDATION_ERROR',
|
|
363
|
+
`composer-sdk ${SDK_VERSION} targets a newer compose contract than the server at ${url} serves (${serverVersion}); pin @lifi/composer-sdk and @lifi/compose-spec to ${serverVersion} or wait for the server rollout`,
|
|
364
|
+
{
|
|
365
|
+
status: res.status,
|
|
366
|
+
url,
|
|
367
|
+
kind: 'server_outdated',
|
|
368
|
+
serverOutdated: versions,
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
type SendInit =
|
|
374
|
+
| { readonly method: 'GET' }
|
|
375
|
+
| { readonly method: 'POST'; readonly body: unknown };
|
|
376
|
+
|
|
377
|
+
// GETs on this API carry no body; POSTs carry a JSON one with `bigint` amounts
|
|
378
|
+
// rendered as decimal strings.
|
|
379
|
+
const requestInit = (
|
|
380
|
+
baseHeaders: Record<string, string>,
|
|
381
|
+
init: SendInit,
|
|
382
|
+
): RequestInit =>
|
|
383
|
+
init.method === 'GET'
|
|
384
|
+
? { method: 'GET', headers: { ...baseHeaders } }
|
|
385
|
+
: {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
headers: { ...baseHeaders, 'Content-Type': 'application/json' },
|
|
388
|
+
body: JSON.stringify(init.body, bigintReplacer),
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// One fetch path for every route: maps transport failures to `NETWORK_ERROR`.
|
|
392
|
+
// The response version check is not here — only the compile-family POSTs gate
|
|
393
|
+
// on it (see `postCompile`); discovery GETs and `/simulate` are ungated.
|
|
394
|
+
const send = async (
|
|
314
395
|
{ fetchFn, baseHeaders }: ComposeTransport,
|
|
315
396
|
url: string,
|
|
316
|
-
|
|
317
|
-
): Promise<
|
|
318
|
-
let res: Response;
|
|
397
|
+
init: SendInit,
|
|
398
|
+
): Promise<Response> => {
|
|
319
399
|
try {
|
|
320
|
-
|
|
321
|
-
method: 'POST',
|
|
322
|
-
headers: { ...baseHeaders, 'Content-Type': 'application/json' },
|
|
323
|
-
body: JSON.stringify(request, bigintReplacer),
|
|
324
|
-
});
|
|
400
|
+
return await fetchFn(url, requestInit(baseHeaders, init));
|
|
325
401
|
} catch (err) {
|
|
326
402
|
const message = err instanceof Error ? err.message : String(err);
|
|
327
403
|
throw new ComposeError('NETWORK_ERROR', message, { cause: err });
|
|
328
404
|
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const postCompile = async (
|
|
408
|
+
transport: ComposeTransport,
|
|
409
|
+
url: string,
|
|
410
|
+
request: ComposeCompileRequest | ComposeRouteRequest,
|
|
411
|
+
): Promise<ComposeCompileResult> => {
|
|
412
|
+
const res = await send(transport, url, { method: 'POST', body: request });
|
|
413
|
+
assertServerVersion(res, url);
|
|
329
414
|
if (res.status === 206) {
|
|
330
415
|
return await parsePartialBody(res, url);
|
|
331
416
|
}
|
|
@@ -339,19 +424,10 @@ const postCompile = async (
|
|
|
339
424
|
// Every enveloped GET on this API behaves identically: no body, no query
|
|
340
425
|
// semantics of its own, `{ data }` on success, an error envelope otherwise.
|
|
341
426
|
const getJson = async <T>(
|
|
342
|
-
|
|
427
|
+
transport: ComposeTransport,
|
|
343
428
|
url: string,
|
|
344
429
|
): Promise<T> => {
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
res = await fetchFn(url, {
|
|
348
|
-
method: 'GET',
|
|
349
|
-
headers: { ...baseHeaders },
|
|
350
|
-
});
|
|
351
|
-
} catch (err) {
|
|
352
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
353
|
-
throw new ComposeError('NETWORK_ERROR', message, { cause: err });
|
|
354
|
-
}
|
|
430
|
+
const res = await send(transport, url, { method: 'GET' });
|
|
355
431
|
if (!res.ok) {
|
|
356
432
|
const body = await res.text();
|
|
357
433
|
throw errorFromHttpResponse(res.status, body, url);
|
|
@@ -386,7 +462,7 @@ export const createComposeClient = (
|
|
|
386
462
|
|
|
387
463
|
const baseHeaders: Record<string, string> = {
|
|
388
464
|
Accept: 'application/json',
|
|
389
|
-
|
|
465
|
+
[COMPOSER_SDK_VERSION_HEADER]: SDK_VERSION,
|
|
390
466
|
'x-lifi-api-key': trimmedApiKey,
|
|
391
467
|
};
|
|
392
468
|
const transport: ComposeTransport = { fetchFn, baseHeaders };
|
|
@@ -428,17 +504,7 @@ export const createComposeClient = (
|
|
|
428
504
|
request: SimulateRequest,
|
|
429
505
|
): Promise<SimulateResult> => {
|
|
430
506
|
const url = `${base}/simulate`;
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
res = await fetchFn(url, {
|
|
434
|
-
method: 'POST',
|
|
435
|
-
headers: { ...baseHeaders, 'Content-Type': 'application/json' },
|
|
436
|
-
body: JSON.stringify(request, bigintReplacer),
|
|
437
|
-
});
|
|
438
|
-
} catch (err) {
|
|
439
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
-
throw new ComposeError('NETWORK_ERROR', message, { cause: err });
|
|
441
|
-
}
|
|
507
|
+
const res = await send(transport, url, { method: 'POST', body: request });
|
|
442
508
|
// Only 200 (carries `ok`/`revert`) and 422 (carries the `error` member of
|
|
443
509
|
// the union) have a discriminated body. 422 is deliberately intercepted
|
|
444
510
|
// here (not thrown as VALIDATION_ERROR) so callers get one exhaustive
|
package/src/errors.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ComposeErrorKind,
|
|
3
|
+
ComposeRouteErrorKind,
|
|
3
4
|
FailedPreparedOp,
|
|
4
5
|
GenericComposeErrorKind,
|
|
5
6
|
SimulationRevert,
|
|
6
7
|
} from '@lifi/compose-spec';
|
|
8
|
+
import z from 'zod';
|
|
7
9
|
|
|
8
10
|
import {
|
|
9
11
|
parseServerErrorBody,
|
|
@@ -32,6 +34,24 @@ export type ComposeErrorCode =
|
|
|
32
34
|
| 'NOT_FOUND'
|
|
33
35
|
| 'UNKNOWN_ERROR';
|
|
34
36
|
|
|
37
|
+
/** Version details returned when the backend rejects an outdated SDK. */
|
|
38
|
+
export interface ComposeSdkOutdated {
|
|
39
|
+
readonly sdkVersion: string;
|
|
40
|
+
readonly serverVersion: string;
|
|
41
|
+
readonly minimumSdkVersion: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Version details attached when the SDK is ahead of the backend it reached.
|
|
46
|
+
* Raised by the backend's version gate, or by the SDK itself when the
|
|
47
|
+
* `x-lifi-composer-version` response header names an older contract than the
|
|
48
|
+
* one this SDK was built against.
|
|
49
|
+
*/
|
|
50
|
+
export interface ComposeServerOutdated {
|
|
51
|
+
readonly sdkVersion: string;
|
|
52
|
+
readonly serverVersion: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
35
55
|
/**
|
|
36
56
|
* Error class for all failures originating from the Compose SDK or API.
|
|
37
57
|
*
|
|
@@ -58,7 +78,7 @@ export class ComposeError extends Error {
|
|
|
58
78
|
/** The request URL that produced the error. */
|
|
59
79
|
readonly url?: string;
|
|
60
80
|
/** Server-provided error kind for finer-grained classification. */
|
|
61
|
-
readonly kind?: ComposeErrorKind;
|
|
81
|
+
readonly kind?: ComposeErrorKind | ComposeRouteErrorKind;
|
|
62
82
|
/** JSON-pointer path to the field that caused a validation error. */
|
|
63
83
|
readonly path?: string;
|
|
64
84
|
/**
|
|
@@ -78,6 +98,10 @@ export class ComposeError extends Error {
|
|
|
78
98
|
* `preparation_error` errors alongside {@link ComposeError.failedOps}.
|
|
79
99
|
*/
|
|
80
100
|
readonly succeededOps?: readonly string[];
|
|
101
|
+
/** Version details attached only to `sdk_outdated` errors. */
|
|
102
|
+
readonly sdkOutdated?: ComposeSdkOutdated;
|
|
103
|
+
/** Version details attached only to `server_outdated` errors. */
|
|
104
|
+
readonly serverOutdated?: ComposeServerOutdated;
|
|
81
105
|
|
|
82
106
|
constructor(
|
|
83
107
|
code: ComposeErrorCode,
|
|
@@ -86,11 +110,13 @@ export class ComposeError extends Error {
|
|
|
86
110
|
status?: number;
|
|
87
111
|
url?: string;
|
|
88
112
|
cause?: unknown;
|
|
89
|
-
kind?: ComposeErrorKind;
|
|
113
|
+
kind?: ComposeErrorKind | ComposeRouteErrorKind;
|
|
90
114
|
path?: string;
|
|
91
115
|
details?: SimulationRevert;
|
|
92
116
|
failedOps?: readonly FailedPreparedOp[];
|
|
93
117
|
succeededOps?: readonly string[];
|
|
118
|
+
sdkOutdated?: ComposeSdkOutdated;
|
|
119
|
+
serverOutdated?: ComposeServerOutdated;
|
|
94
120
|
},
|
|
95
121
|
) {
|
|
96
122
|
super(message, { cause: options?.cause });
|
|
@@ -102,6 +128,8 @@ export class ComposeError extends Error {
|
|
|
102
128
|
this.details = options?.details;
|
|
103
129
|
this.failedOps = options?.failedOps;
|
|
104
130
|
this.succeededOps = options?.succeededOps;
|
|
131
|
+
this.sdkOutdated = options?.sdkOutdated;
|
|
132
|
+
this.serverOutdated = options?.serverOutdated;
|
|
105
133
|
}
|
|
106
134
|
}
|
|
107
135
|
|
|
@@ -153,6 +181,69 @@ export const isComposePreparationError = (
|
|
|
153
181
|
Array.isArray(e.failedOps) &&
|
|
154
182
|
Array.isArray(e.succeededOps);
|
|
155
183
|
|
|
184
|
+
// `isComposeError` accepts a structurally-similar error by name, so an error
|
|
185
|
+
// raised by a *second* copy of this package in the dependency tree reaches the
|
|
186
|
+
// guard below without its `sdkOutdated` field being type-guaranteed. Parse it.
|
|
187
|
+
const sdkOutdatedSchema = z.object({
|
|
188
|
+
sdkVersion: z.string(),
|
|
189
|
+
serverVersion: z.string(),
|
|
190
|
+
minimumSdkVersion: z.string(),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Narrows an outdated-SDK rejection to its required version details.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* try {
|
|
199
|
+
* await builder.compile(run);
|
|
200
|
+
* } catch (e) {
|
|
201
|
+
* if (isComposeSdkOutdatedError(e)) {
|
|
202
|
+
* console.error(`Upgrade to at least ${e.sdkOutdated.minimumSdkVersion}`);
|
|
203
|
+
* return;
|
|
204
|
+
* }
|
|
205
|
+
* throw e;
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export const isComposeSdkOutdatedError = (
|
|
210
|
+
e: unknown,
|
|
211
|
+
): e is ComposeError & { readonly sdkOutdated: ComposeSdkOutdated } =>
|
|
212
|
+
isComposeError(e) &&
|
|
213
|
+
e.kind === 'sdk_outdated' &&
|
|
214
|
+
sdkOutdatedSchema.safeParse(e.sdkOutdated).success;
|
|
215
|
+
|
|
216
|
+
const serverOutdatedSchema = z.object({
|
|
217
|
+
sdkVersion: z.string(),
|
|
218
|
+
serverVersion: z.string(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Narrows a server-behind-SDK rejection to its required version details.
|
|
223
|
+
*
|
|
224
|
+
* This is the deploy-window error: the SDK on npm is ahead of the backend that
|
|
225
|
+
* answered. Pin the SDK to the server's `major.minor`, or wait for the rollout.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* try {
|
|
230
|
+
* await builder.compile(run);
|
|
231
|
+
* } catch (e) {
|
|
232
|
+
* if (isComposeServerOutdatedError(e)) {
|
|
233
|
+
* console.error(`Server serves ${e.serverOutdated.serverVersion}`);
|
|
234
|
+
* return;
|
|
235
|
+
* }
|
|
236
|
+
* throw e;
|
|
237
|
+
* }
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export const isComposeServerOutdatedError = (
|
|
241
|
+
e: unknown,
|
|
242
|
+
): e is ComposeError & { readonly serverOutdated: ComposeServerOutdated } =>
|
|
243
|
+
isComposeError(e) &&
|
|
244
|
+
e.kind === 'server_outdated' &&
|
|
245
|
+
serverOutdatedSchema.safeParse(e.serverOutdated).success;
|
|
246
|
+
|
|
156
247
|
const STATUS_TO_CODE: ReadonlyMap<number, ComposeErrorCode> = new Map<
|
|
157
248
|
number,
|
|
158
249
|
ComposeErrorCode
|
|
@@ -175,6 +266,24 @@ const tryParseErrorBody = (body: string): ServerErrorBody | null => {
|
|
|
175
266
|
return parseServerErrorBody(json);
|
|
176
267
|
};
|
|
177
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Extracts the version triple an `sdk_outdated` or `server_outdated` envelope
|
|
271
|
+
* carries. The slots reuse the same schemas the type guards narrow with. Zod
|
|
272
|
+
* strips keys the schema does not declare, so the parsed value is exactly the
|
|
273
|
+
* triple; a partial payload fails the parse and yields `undefined` rather than
|
|
274
|
+
* a half-populated object.
|
|
275
|
+
*/
|
|
276
|
+
const readVersionSlots = (serverError: ServerErrorBody['error']) => ({
|
|
277
|
+
sdkOutdated:
|
|
278
|
+
serverError?.kind === 'sdk_outdated'
|
|
279
|
+
? sdkOutdatedSchema.safeParse(serverError).data
|
|
280
|
+
: undefined,
|
|
281
|
+
serverOutdated:
|
|
282
|
+
serverError?.kind === 'server_outdated'
|
|
283
|
+
? serverOutdatedSchema.safeParse(serverError).data
|
|
284
|
+
: undefined,
|
|
285
|
+
});
|
|
286
|
+
|
|
178
287
|
/**
|
|
179
288
|
* Constructs a {@link ComposeError} from an HTTP error response, extracting
|
|
180
289
|
* structured error details from the response body when available.
|
|
@@ -191,6 +300,7 @@ export const errorFromHttpResponse = (
|
|
|
191
300
|
): ComposeError => {
|
|
192
301
|
const parsed = tryParseErrorBody(body);
|
|
193
302
|
const serverError = parsed?.error;
|
|
303
|
+
const { sdkOutdated, serverOutdated } = readVersionSlots(serverError);
|
|
194
304
|
|
|
195
305
|
return new ComposeError(
|
|
196
306
|
STATUS_TO_CODE.get(status) ??
|
|
@@ -199,7 +309,8 @@ export const errorFromHttpResponse = (
|
|
|
199
309
|
{
|
|
200
310
|
status,
|
|
201
311
|
url,
|
|
202
|
-
kind: serverError?.kind as
|
|
312
|
+
kind: serverError?.kind as
|
|
313
|
+
ComposeErrorKind | ComposeRouteErrorKind | undefined,
|
|
203
314
|
path: serverError?.path,
|
|
204
315
|
details: serverError?.details,
|
|
205
316
|
// The wire schema types `kind` as a bare string; the server only ever
|
|
@@ -209,6 +320,8 @@ export const errorFromHttpResponse = (
|
|
|
209
320
|
kind: op.kind as GenericComposeErrorKind,
|
|
210
321
|
})),
|
|
211
322
|
succeededOps: serverError?.succeededOps,
|
|
323
|
+
sdkOutdated,
|
|
324
|
+
serverOutdated,
|
|
212
325
|
},
|
|
213
326
|
);
|
|
214
327
|
};
|
package/src/examples/run.ts
CHANGED
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
ComposeRouteRequest,
|
|
44
44
|
} from '@lifi/compose-spec';
|
|
45
45
|
|
|
46
|
+
import { bigintReplacer } from '../client.js';
|
|
46
47
|
import { type ComposeSdk, createComposeSdk } from '../sdk.js';
|
|
47
48
|
|
|
48
49
|
import { buildAaveRepay } from './aaveRepay.js';
|
|
@@ -76,6 +77,9 @@ import { buildSwapWithOutputValidation } from './swapWithOutputValidation.js';
|
|
|
76
77
|
import { buildPartialTransfer, buildTransferTokens } from './transferTokens.js';
|
|
77
78
|
import { buildUntypedOpWithTypedRef } from './untypedOpWithTypedRef.js';
|
|
78
79
|
|
|
80
|
+
const stringifyJson = (value: unknown) =>
|
|
81
|
+
JSON.stringify(value, bigintReplacer, 2);
|
|
82
|
+
|
|
79
83
|
const EXAMPLES: Record<string, () => ComposeCompileRequest> = {
|
|
80
84
|
'aave-repay': () =>
|
|
81
85
|
buildAaveRepay({
|
|
@@ -233,14 +237,7 @@ const run = async () => {
|
|
|
233
237
|
const request = routeBuilder ? routeBuilder() : builder!();
|
|
234
238
|
|
|
235
239
|
console.log('--- Request ---');
|
|
236
|
-
console.log(
|
|
237
|
-
JSON.stringify(
|
|
238
|
-
request,
|
|
239
|
-
(_key, value: unknown) =>
|
|
240
|
-
typeof value === 'bigint' ? value.toString() : value,
|
|
241
|
-
2,
|
|
242
|
-
),
|
|
243
|
-
);
|
|
240
|
+
console.log(stringifyJson(request));
|
|
244
241
|
|
|
245
242
|
console.log(`\n--- Compiling against ${BASE_URL} ---`);
|
|
246
243
|
const result = routeBuilder
|
|
@@ -248,7 +245,7 @@ const run = async () => {
|
|
|
248
245
|
: await sdk.client.compile(request as ComposeCompileRequest);
|
|
249
246
|
|
|
250
247
|
console.log('\n--- Result ---');
|
|
251
|
-
console.log(
|
|
248
|
+
console.log(stringifyJson(result));
|
|
252
249
|
};
|
|
253
250
|
|
|
254
251
|
run().catch((err: unknown) => {
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,8 @@ export type {
|
|
|
31
31
|
export type {
|
|
32
32
|
ComposeError,
|
|
33
33
|
ComposeErrorCode,
|
|
34
|
+
ComposeSdkOutdated,
|
|
35
|
+
ComposeServerOutdated,
|
|
34
36
|
ComposePreparationOps,
|
|
35
37
|
} from './errors.js';
|
|
36
38
|
export type {
|
|
@@ -90,7 +92,12 @@ export {
|
|
|
90
92
|
SIMULATE_MAX_TRACKED_BALANCES,
|
|
91
93
|
SIMULATE_MAX_REQUIREMENTS,
|
|
92
94
|
} from '@lifi/compose-spec';
|
|
93
|
-
export {
|
|
95
|
+
export {
|
|
96
|
+
isComposeError,
|
|
97
|
+
isComposePreparationError,
|
|
98
|
+
isComposeSdkOutdatedError,
|
|
99
|
+
isComposeServerOutdatedError,
|
|
100
|
+
} from './errors.js';
|
|
94
101
|
|
|
95
102
|
export type {
|
|
96
103
|
Bindable,
|