@lifi/composer-sdk 0.5.1 → 0.6.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.
Files changed (62) hide show
  1. package/README.md +13 -3
  2. package/dist/authoring/FlowBuilderCore.cjs.map +1 -1
  3. package/dist/authoring/FlowBuilderCore.d.cts +11 -4
  4. package/dist/authoring/FlowBuilderCore.d.ts +11 -4
  5. package/dist/authoring/FlowBuilderCore.js.map +1 -1
  6. package/dist/authoring/handles.cjs.map +1 -1
  7. package/dist/authoring/handles.d.cts +2 -2
  8. package/dist/authoring/handles.d.ts +2 -2
  9. package/dist/authoring/handles.js.map +1 -1
  10. package/dist/authoring/raw.cjs +5 -3
  11. package/dist/authoring/raw.cjs.map +1 -1
  12. package/dist/authoring/raw.js +5 -3
  13. package/dist/authoring/raw.js.map +1 -1
  14. package/dist/client.cjs +41 -30
  15. package/dist/client.cjs.map +1 -1
  16. package/dist/client.d.cts +3 -3
  17. package/dist/client.d.ts +3 -3
  18. package/dist/client.js +41 -30
  19. package/dist/client.js.map +1 -1
  20. package/dist/generated/config.generated.cjs.map +1 -1
  21. package/dist/generated/config.generated.d.cts +46 -5
  22. package/dist/generated/config.generated.d.ts +46 -5
  23. package/dist/generated/operations.generated.cjs +4 -0
  24. package/dist/generated/operations.generated.cjs.map +1 -1
  25. package/dist/generated/operations.generated.d.cts +16 -2
  26. package/dist/generated/operations.generated.d.ts +16 -2
  27. package/dist/generated/operations.generated.js +4 -0
  28. package/dist/generated/operations.generated.js.map +1 -1
  29. package/dist/index.cjs +4 -1
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +3 -2
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.js +3 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/outputs.cjs +33 -0
  36. package/dist/outputs.cjs.map +1 -0
  37. package/dist/outputs.d.cts +10 -0
  38. package/dist/outputs.d.ts +10 -0
  39. package/dist/outputs.js +9 -0
  40. package/dist/outputs.js.map +1 -0
  41. package/dist/responseSchemas.cjs.map +1 -1
  42. package/dist/responseSchemas.d.cts +4 -4
  43. package/dist/responseSchemas.d.ts +4 -4
  44. package/dist/responseSchemas.js.map +1 -1
  45. package/dist/types.cjs.map +1 -1
  46. package/dist/types.d.cts +16 -5
  47. package/dist/types.d.ts +16 -5
  48. package/package.json +4 -3
  49. package/src/authoring/FlowBuilderCore.ts +12 -3
  50. package/src/authoring/handles.ts +2 -1
  51. package/src/authoring/raw.ts +4 -3
  52. package/src/client.ts +77 -45
  53. package/src/examples/flySwap.ts +3 -3
  54. package/src/examples/lifiZapAsync.ts +8 -7
  55. package/src/examples/swapAndZap.ts +7 -0
  56. package/src/examples/swapWithAllowRevert.ts +13 -3
  57. package/src/generated/config.generated.ts +49 -4
  58. package/src/generated/operations.generated.ts +25 -0
  59. package/src/index.ts +13 -2
  60. package/src/outputs.ts +16 -0
  61. package/src/responseSchemas.ts +4 -4
  62. package/src/types.ts +16 -4
@@ -8,12 +8,12 @@ import { SimulationRevert, SimulateResult } from '@lifi/compose-spec';
8
8
  declare const parseSimulateResult: (body: unknown) => SimulateResult | null;
9
9
  /**
10
10
  * The validated members of an HTTP 206 partial compile envelope. `data` is
11
- * verified to be an object but kept untyped — compose-spec owns its full shape
12
- * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows
13
- * it. `error` is fully validated.
11
+ * verified to be an object at runtime but left `unknown` here — compose-spec
12
+ * owns its full shape (`ComposeCompilePartialData`) as a hand-authored type, so
13
+ * the caller narrows it in one step. `error` is fully validated.
14
14
  */
15
15
  interface CompilePartialEnvelope {
16
- readonly data: Record<string, unknown>;
16
+ readonly data: unknown;
17
17
  readonly error: {
18
18
  readonly kind: string;
19
19
  readonly message: string;
@@ -8,12 +8,12 @@ import { SimulationRevert, SimulateResult } from '@lifi/compose-spec';
8
8
  declare const parseSimulateResult: (body: unknown) => SimulateResult | null;
9
9
  /**
10
10
  * The validated members of an HTTP 206 partial compile envelope. `data` is
11
- * verified to be an object but kept untyped — compose-spec owns its full shape
12
- * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows
13
- * it. `error` is fully validated.
11
+ * verified to be an object at runtime but left `unknown` here — compose-spec
12
+ * owns its full shape (`ComposeCompilePartialData`) as a hand-authored type, so
13
+ * the caller narrows it in one step. `error` is fully validated.
14
14
  */
15
15
  interface CompilePartialEnvelope {
16
- readonly data: Record<string, unknown>;
16
+ readonly data: unknown;
17
17
  readonly error: {
18
18
  readonly kind: string;
19
19
  readonly message: string;
@@ -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 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":[]}
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 at runtime but left `unknown` here — compose-spec\n * owns its full shape (`ComposeCompilePartialData`) as a hand-authored type, so\n * the caller narrows it in one step. `error` is fully validated.\n */\nexport interface CompilePartialEnvelope {\n readonly data: 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":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { LiteralBinding, Ref } from '@lifi/compose-spec';\nimport {\n InputSpec,\n MaterialiserInput,\n Resource,\n SolType,\n} from '@lifi/compose-spec';\n\n/**\n * A string that represents a non-negative integer (e.g. \"1000000000\").\n * Uses a template literal type so that only numeric-looking strings\n * are assignable at compile time.\n */\nexport type IntegerString = `${bigint}`;\n\n/**\n * Accepted input for integer-string config fields.\n * Callers may pass either a numeric string (\"1000000000\") or a native bigint\n * (1000000000n). Bigint values are stringified automatically during JSON\n * serialization via the SDK's `bigintReplacer`.\n */\nexport type IntegerStringInput = IntegerString | bigint;\n\n/**\n * A `0x`-prefixed hex string representing an Ethereum address.\n * Template literal type catches obviously wrong values at compile time.\n */\nexport type Address = `0x${string}`;\n\n/** The universe of output port kinds: Solidity scalar types plus `'resource'`. */\nexport type OutputKind = SolType | 'resource';\n\n/**\n * Maps an ABI parameter type string (from abitype's `ParseAbiItem`) to the\n * closest `OutputKind`. When the ABI type is one of the supported `SolType`\n * scalars, the mapping is exact; for all other Solidity types (e.g. tuples,\n * fixed-point decimals) it falls back to `SolType` — accepting any scalar\n * handle but not `'resource'` handles.\n *\n * The fallback excludes `'resource'` because every Solidity type is a scalar;\n * resource handles carry token-amount semantics that should only flow into\n * slots explicitly declared as resources.\n *\n * Extending `SolType` with new members automatically widens the exact-match\n * branch — no changes needed here.\n */\nexport type AbiTypeToOutputKind<T extends string> = T extends SolType\n ? T\n : SolType;\n\n/**\n * A typed `$ref` pointer carrying a phantom type parameter.\n * Extends `Ref` so it is accepted anywhere a plain `Ref` is.\n * The `__outputKind` field is never set at runtime — it exists only\n * for TypeScript's structural type checker to distinguish typed refs.\n */\nexport interface TypedRef<T extends OutputKind = OutputKind> extends Ref {\n readonly __outputKind?: T;\n}\n\n/**\n * A literal bind value carrying a phantom type parameter.\n *\n * The wire format accepts a constant in a bind slot as\n * `{ kind: <solType>, value: <string> }`, alongside `$ref` pointers. Use it\n * for a value that must be baked into the flow document rather than supplied\n * at run time — the destination of a flow the backend stores and re-runs\n * later (a `continuation.settle` node's `continuationFlow`) has no run to\n * take an input from.\n *\n * @typeParam T - The Solidity type of the literal (e.g. `'address'`).\n */\nexport interface TypedLiteral<\n T extends SolType = SolType,\n> extends LiteralBinding {\n readonly kind: T;\n readonly __outputKind?: T;\n}\n\n/**\n * An input declaration in a flow schema.\n * `Resource` values become resource inputs (token amounts);\n * `SolType` strings become handle inputs (scalars like addresses).\n */\nexport type InputDecl = Resource | SolType;\n\n/**\n * A record mapping input names to their declarations.\n * Used as the type parameter for generic flow builders and requests.\n */\nexport type InputSchema = Record<string, InputDecl>;\n\n/**\n * Maps an input declaration to the allowed runtime value type.\n * Resource inputs accept materialisers or literal amounts.\n * Handle inputs accept the scalar type matching their SolType.\n */\nexport type InputSpecOf<D> = D extends Resource\n ? MaterialiserInput | IntegerStringInput\n : D extends 'address'\n ? Address\n : D extends 'bool'\n ? boolean\n : D extends `${'u' | ''}int${string}`\n ? IntegerStringInput\n : D extends `bytes${string}`\n ? `0x${string}`\n : D extends 'string'\n ? string\n : InputSpec;\n\n/**\n * A guard applied to an operation, with the `port` field constrained\n * to a specific set of output port names.\n */\nexport interface TypedGuard<Port extends string = string> {\n readonly kind: string;\n readonly port: Port;\n readonly [key: string]: unknown;\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { LiteralBinding, Ref } from '@lifi/compose-spec';\nimport {\n InputSpec,\n MaterialiserInput,\n Resource,\n SolType,\n} from '@lifi/compose-spec';\n\n/**\n * A string that represents a non-negative integer (e.g. \"1000000000\").\n * Uses a template literal type so that only numeric-looking strings\n * are assignable at compile time.\n */\nexport type IntegerString = `${bigint}`;\n\n/**\n * Accepted input for integer-string config fields.\n * Callers may pass either a numeric string (\"1000000000\") or a native bigint\n * (1000000000n). Bigint values are stringified automatically during JSON\n * serialization via the SDK's `bigintReplacer`.\n */\nexport type IntegerStringInput = IntegerString | bigint;\n\n/**\n * A `0x`-prefixed hex string representing an Ethereum address.\n * Template literal type catches obviously wrong values at compile time.\n */\nexport type Address = `0x${string}`;\n\n/** The universe of output port kinds: Solidity scalar types plus `'resource'`. */\nexport type OutputKind = SolType | 'resource';\n\n/**\n * Maps an ABI parameter type string (from abitype's `ParseAbiItem`) to the\n * closest `OutputKind`. When the ABI type is one of the supported `SolType`\n * scalars, the mapping is exact; for all other Solidity types (e.g. tuples,\n * fixed-point decimals) it falls back to `SolType` — accepting any scalar\n * handle but not `'resource'` handles.\n *\n * The fallback excludes `'resource'` because every Solidity type is a scalar;\n * resource handles carry token-amount semantics that should only flow into\n * slots explicitly declared as resources.\n *\n * Extending `SolType` with new members automatically widens the exact-match\n * branch — no changes needed here.\n */\nexport type AbiTypeToOutputKind<T extends string> = T extends SolType\n ? T\n : SolType;\n\n/**\n * Phantom brand key for {@link TypedRef}.\n *\n * Declared, never assigned: it exists only so the type checker can tell a\n * typed ref from an arbitrary `{ $ref }` object. Keying it on a `unique\n * symbol` means no caller can produce one by writing an object literal.\n */\nexport declare const typedRefKind: unique symbol;\n\n/**\n * A typed `$ref` pointer carrying a phantom type parameter.\n *\n * On the wire, and at run time, this is exactly `{ $ref: string }` — the\n * brand is erased. The brand is *required* in the type so a plain `Ref` does\n * not satisfy `TypedRef` structurally, which is the contract `RefBindable`\n * advertises: an untyped `{ $ref }` never enters a typed bind slot. Build one\n * with `raw.ref<T>()`.\n */\nexport interface TypedRef<T extends OutputKind = OutputKind> extends Ref {\n readonly [typedRefKind]: T;\n}\n\n/**\n * A literal bind value carrying a phantom type parameter.\n *\n * The wire format accepts a constant in a bind slot as\n * `{ kind: <solType>, value: <string> }`, alongside `$ref` pointers. Use it\n * for a value that must be baked into the flow document rather than supplied\n * at run time — the destination of a flow the backend stores and re-runs\n * later (a `continuation.settle` node's `continuationFlow`) has no run to\n * take an input from.\n *\n * @typeParam T - The Solidity type of the literal (e.g. `'address'`).\n */\nexport interface TypedLiteral<\n T extends SolType = SolType,\n> extends LiteralBinding {\n readonly kind: T;\n readonly __outputKind?: T;\n}\n\n/**\n * An input declaration in a flow schema.\n * `Resource` values become resource inputs (token amounts);\n * `SolType` strings become handle inputs (scalars like addresses).\n */\nexport type InputDecl = Resource | SolType;\n\n/**\n * A record mapping input names to their declarations.\n * Used as the type parameter for generic flow builders and requests.\n */\nexport type InputSchema = Record<string, InputDecl>;\n\n/**\n * Maps an input declaration to the allowed runtime value type.\n * Resource inputs accept materialisers or literal amounts.\n * Handle inputs accept the scalar type matching their SolType.\n */\nexport type InputSpecOf<D> = D extends Resource\n ? MaterialiserInput | IntegerStringInput\n : D extends 'address'\n ? Address\n : D extends 'bool'\n ? boolean\n : D extends `${'u' | ''}int${string}`\n ? IntegerStringInput\n : D extends `bytes${string}`\n ? `0x${string}`\n : D extends 'string'\n ? string\n : InputSpec;\n\n/**\n * A guard applied to an operation, with the `port` field constrained\n * to a specific set of output port names.\n */\nexport interface TypedGuard<Port extends string = string> {\n readonly kind: string;\n readonly port: Port;\n readonly [key: string]: unknown;\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
package/dist/types.d.cts CHANGED
@@ -35,14 +35,25 @@ type OutputKind = SolType | 'resource';
35
35
  * branch — no changes needed here.
36
36
  */
37
37
  type AbiTypeToOutputKind<T extends string> = T extends SolType ? T : SolType;
38
+ /**
39
+ * Phantom brand key for {@link TypedRef}.
40
+ *
41
+ * Declared, never assigned: it exists only so the type checker can tell a
42
+ * typed ref from an arbitrary `{ $ref }` object. Keying it on a `unique
43
+ * symbol` means no caller can produce one by writing an object literal.
44
+ */
45
+ declare const typedRefKind: unique symbol;
38
46
  /**
39
47
  * A typed `$ref` pointer carrying a phantom type parameter.
40
- * Extends `Ref` so it is accepted anywhere a plain `Ref` is.
41
- * The `__outputKind` field is never set at runtime — it exists only
42
- * for TypeScript's structural type checker to distinguish typed refs.
48
+ *
49
+ * On the wire, and at run time, this is exactly `{ $ref: string }` — the
50
+ * brand is erased. The brand is *required* in the type so a plain `Ref` does
51
+ * not satisfy `TypedRef` structurally, which is the contract `RefBindable`
52
+ * advertises: an untyped `{ $ref }` never enters a typed bind slot. Build one
53
+ * with `raw.ref<T>()`.
43
54
  */
44
55
  interface TypedRef<T extends OutputKind = OutputKind> extends Ref {
45
- readonly __outputKind?: T;
56
+ readonly [typedRefKind]: T;
46
57
  }
47
58
  /**
48
59
  * A literal bind value carrying a phantom type parameter.
@@ -87,4 +98,4 @@ interface TypedGuard<Port extends string = string> {
87
98
  readonly [key: string]: unknown;
88
99
  }
89
100
 
90
- export type { AbiTypeToOutputKind, Address, InputDecl, InputSchema, InputSpecOf, IntegerString, IntegerStringInput, OutputKind, TypedGuard, TypedLiteral, TypedRef };
101
+ export { type AbiTypeToOutputKind, type Address, type InputDecl, type InputSchema, type InputSpecOf, type IntegerString, type IntegerStringInput, type OutputKind, type TypedGuard, type TypedLiteral, type TypedRef, typedRefKind };
package/dist/types.d.ts CHANGED
@@ -35,14 +35,25 @@ type OutputKind = SolType | 'resource';
35
35
  * branch — no changes needed here.
36
36
  */
37
37
  type AbiTypeToOutputKind<T extends string> = T extends SolType ? T : SolType;
38
+ /**
39
+ * Phantom brand key for {@link TypedRef}.
40
+ *
41
+ * Declared, never assigned: it exists only so the type checker can tell a
42
+ * typed ref from an arbitrary `{ $ref }` object. Keying it on a `unique
43
+ * symbol` means no caller can produce one by writing an object literal.
44
+ */
45
+ declare const typedRefKind: unique symbol;
38
46
  /**
39
47
  * A typed `$ref` pointer carrying a phantom type parameter.
40
- * Extends `Ref` so it is accepted anywhere a plain `Ref` is.
41
- * The `__outputKind` field is never set at runtime — it exists only
42
- * for TypeScript's structural type checker to distinguish typed refs.
48
+ *
49
+ * On the wire, and at run time, this is exactly `{ $ref: string }` — the
50
+ * brand is erased. The brand is *required* in the type so a plain `Ref` does
51
+ * not satisfy `TypedRef` structurally, which is the contract `RefBindable`
52
+ * advertises: an untyped `{ $ref }` never enters a typed bind slot. Build one
53
+ * with `raw.ref<T>()`.
43
54
  */
44
55
  interface TypedRef<T extends OutputKind = OutputKind> extends Ref {
45
- readonly __outputKind?: T;
56
+ readonly [typedRefKind]: T;
46
57
  }
47
58
  /**
48
59
  * A literal bind value carrying a phantom type parameter.
@@ -87,4 +98,4 @@ interface TypedGuard<Port extends string = string> {
87
98
  readonly [key: string]: unknown;
88
99
  }
89
100
 
90
- export type { AbiTypeToOutputKind, Address, InputDecl, InputSchema, InputSpecOf, IntegerString, IntegerStringInput, OutputKind, TypedGuard, TypedLiteral, TypedRef };
101
+ export { type AbiTypeToOutputKind, type Address, type InputDecl, type InputSchema, type InputSpecOf, type IntegerString, type IntegerStringInput, type OutputKind, type TypedGuard, type TypedLiteral, type TypedRef, typedRefKind };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/composer-sdk",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Public Composer SDK for building and submitting flows",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -34,8 +34,9 @@
34
34
  "pack:local": "yarn build && mkdir -p out && yarn pack --out 'out/%s-%v.tgz'",
35
35
  "generate": "tsx scripts/generate-sdk/index.ts",
36
36
  "generate:staged": "tsx scripts/generate-sdk/index.ts --staged",
37
- "test": "vitest run --config vitest.config.ts",
37
+ "test": "yarn check:types:test && vitest run --config vitest.config.ts",
38
38
  "check:types": "tsc --noEmit",
39
+ "check:types:test": "tsc -p tsconfig.test.json --noEmit",
39
40
  "check:sdk-drift": "vitest run --config vitest.drift.config.ts",
40
41
  "fix": "yarn fix:lint",
41
42
  "fix:lint": "eslint src --fix",
@@ -46,7 +47,7 @@
46
47
  "example:staged": "tsx src/examples/run.ts --staged"
47
48
  },
48
49
  "peerDependencies": {
49
- "@lifi/compose-spec": "0.5.1"
50
+ "@lifi/compose-spec": "0.6.1"
50
51
  },
51
52
  "dependencies": {
52
53
  "abitype": "^1.2.3",
@@ -4,6 +4,7 @@ import type {
4
4
  Call,
5
5
  Flow,
6
6
  FlowInput,
7
+ Ref,
7
8
  Resource,
8
9
  SolType,
9
10
  } from '@lifi/compose-spec';
@@ -39,13 +40,21 @@ export interface CallArgs {
39
40
 
40
41
  /**
41
42
  * Any value that can be bound to an operation parameter: an input handle, an
42
- * output handle from a previous operation, a typed `$ref` pointer, or a typed
43
- * literal.
43
+ * output handle from a previous operation, a typed `$ref` pointer, a typed
44
+ * literal, or a bare `{ $ref }` pointer.
45
+ *
46
+ * This is the bind type of {@link FlowBuilderInternal.call}, the entry point
47
+ * every generated operation lowers into — not a public authoring surface. A
48
+ * bare `Ref` belongs here because a generated op resolves its own typed binds
49
+ * to wire refs (`toBindRef` in `./signatureArgs.js`) before calling it. Typed
50
+ * operation slots use `Bindable<T>`, which rejects a bare `Ref`; see
51
+ * `RefBindable` in `./handles.js`.
44
52
  *
45
53
  * Use `raw.ref<T>()` to create a typed ref from a raw path string, and
46
54
  * `raw.literal<T>()` to bake in a constant.
47
55
  */
48
- export type AnyBindable = InputHandle | OutputHandle | TypedRef | TypedLiteral;
56
+ export type AnyBindable =
57
+ InputHandle | OutputHandle | TypedRef | TypedLiteral | Ref;
49
58
 
50
59
  /**
51
60
  * Provides references to runtime context values available inside a flow.
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  BindValue,
3
+ Ref,
3
4
  Resource,
4
5
  SolType,
5
6
  StaticSolType,
@@ -122,5 +123,5 @@ type LiteralBindable<K extends OutputKind> = K extends SolType
122
123
  * `$ref` pointers, refs and literals pass through as-is.
123
124
  */
124
125
  export const toBindValue = (
125
- bindable: InputHandle | OutputHandle | TypedRef | TypedLiteral,
126
+ bindable: InputHandle | OutputHandle | TypedRef | TypedLiteral | Ref,
126
127
  ): BindValue => ('_tag' in bindable ? handleToRef(bindable) : bindable);
@@ -35,9 +35,10 @@ import type { OutputKind, TypedLiteral, TypedRef } from '../types.js';
35
35
  * });
36
36
  * ```
37
37
  */
38
- export const ref = <T extends OutputKind>(path: string): TypedRef<T> => ({
39
- $ref: path,
40
- });
38
+ export const ref = <T extends OutputKind>(path: string): TypedRef<T> =>
39
+ // The brand is phantom, so the runtime value is the bare wire ref. This is
40
+ // the only place a `TypedRef` is minted; the cast is what the brand buys.
41
+ ({ $ref: path }) as TypedRef<T>;
41
42
 
42
43
  /**
43
44
  * Creates a typed literal for use in bind slots: a constant baked into the
package/src/client.ts CHANGED
@@ -123,11 +123,11 @@ export interface ComposeClient {
123
123
  * the server, not the shape of this contract.
124
124
  *
125
125
  * Only synchronous edges are considered unless the request sets
126
- * `allowAsyncRoutes: true`, so by default every produced resource comes back
127
- * `availability: 'now'` and a pair reachable only after settlement is a
126
+ * `allowAsyncRoutes: true`, so by default every terminal output comes back
127
+ * `delivery.when: 'now'` and a pair reachable only after settlement is a
128
128
  * `no_route_error`. Opting in adds the `lifi.zapAsync` fallback, which still
129
129
  * loses to a synchronous edge when the pair has one; read
130
- * `producedResources[*].availability` to see which representation you got.
130
+ * `outputs[*].delivery.when` to see which representation you got.
131
131
  *
132
132
  * Use {@link compile} for what the server does not author for you: several
133
133
  * chained operations, splits, explicit preconditions, or per-op guards.
@@ -200,16 +200,11 @@ const isNonNullObject = (v: unknown): v is Record<string, unknown> =>
200
200
  // Non-negative decimal integer string, as the server sends token amounts.
201
201
  const INTEGER_STRING = /^\d+$/;
202
202
 
203
- const decodeAmount = (
204
- value: unknown,
205
- key: string,
206
- field: 'amountOut' | 'amountOutMin',
207
- url: string,
208
- ): bigint => {
203
+ const decodeAmount = (value: unknown, path: string, url: string): bigint => {
209
204
  if (typeof value !== 'string' || !INTEGER_STRING.test(value)) {
210
205
  throw new ComposeError(
211
206
  'UNKNOWN_ERROR',
212
- `Malformed compose response: producedResources.${key}.simulated.${field} is ${
207
+ `Malformed compose response: ${path} is ${
213
208
  value === undefined ? 'absent' : 'not an integer string'
214
209
  }`,
215
210
  { url },
@@ -218,46 +213,83 @@ const decodeAmount = (
218
213
  return BigInt(value);
219
214
  };
220
215
 
221
- // `simulated.amountOut` / `.amountOutMin` are declared `bigint` on
222
- // `SimulatedAmounts` (the domain truth: `compose-core` produces real bigints)
223
- // but arrive as decimal strings on the wire (the backend stringifies them).
224
- // Decode at this seam so a consumer reading the declared type gets the value
225
- // the type promises. A resource without `simulated` is left untouched; a
226
- // `simulated` claiming a missing or non-numeric amount is a contract violation
227
- // and surfaces as a `ComposeError` rather than `NaN` / `BigInt(undefined)`.
228
- const decodeSimulatedAmounts = (data: unknown, url: string): void => {
229
- if (!isNonNullObject(data)) return;
230
- const resources = data.producedResources;
231
- if (!isNonNullObject(resources)) return;
232
- for (const key of Object.keys(resources)) {
233
- const resource = resources[key];
234
- if (!isNonNullObject(resource)) continue;
235
- const simulated = resource.simulated;
236
- if (simulated === undefined) continue;
237
- if (!isNonNullObject(simulated)) {
238
- throw new ComposeError(
239
- 'UNKNOWN_ERROR',
240
- `Malformed compose response: producedResources.${key}.simulated is not an object`,
241
- { url },
242
- );
243
- }
244
- simulated.amountOut = decodeAmount(
245
- simulated.amountOut,
246
- key,
247
- 'amountOut',
216
+ // The two records this seam reads out of a compile-family response body. Each
217
+ // is `unknown` because the seam is what establishes its shape: it validates
218
+ // every field before writing a decoded `bigint` back over the wire string.
219
+ interface WireAmountBody {
220
+ outputs?: unknown;
221
+ }
222
+
223
+ // One `Amount` as it arrives: each leaf is `unknown` until this seam has
224
+ // checked it, and is overwritten in place with the decoded `bigint`.
225
+ interface WireAmount {
226
+ estimate?: unknown;
227
+ minimum?: unknown;
228
+ }
229
+
230
+ const malformed = (path: string, what: string, url: string): ComposeError =>
231
+ new ComposeError(
232
+ 'UNKNOWN_ERROR',
233
+ `Malformed compose response: ${path} ${what}`,
234
+ { url },
235
+ );
236
+
237
+ // Decode one `Estimate` / `Minimum` leaf in place: `{ basis, value }` with
238
+ // `value` a decimal string on the wire and a `bigint` on `Amount`.
239
+ const decodeAmountLeaf = (
240
+ amount: WireAmount,
241
+ field: 'estimate' | 'minimum',
242
+ path: string,
243
+ url: string,
244
+ ): void => {
245
+ const leaf = amount[field];
246
+ if (leaf === undefined) return;
247
+ if (!isNonNullObject(leaf)) {
248
+ throw malformed(`${path}.${field}`, 'is not an object', url);
249
+ }
250
+ leaf.value = decodeAmount(leaf.value, `${path}.${field}.value`, url);
251
+ };
252
+
253
+ // `outputs` is REQUIRED on every compile-family body and is the record a
254
+ // caller reads amounts from, so its structure is checked here — the body is
255
+ // cast, not schema-parsed — before each `value` leaf becomes a `bigint`.
256
+ const decodeOutputs = (data: WireAmountBody, url: string): void => {
257
+ const outputs = data.outputs;
258
+ if (!isNonNullObject(outputs)) {
259
+ throw malformed(
260
+ 'outputs',
261
+ outputs === undefined ? 'is absent' : 'is not an object',
248
262
  url,
249
263
  );
250
- if (simulated.amountOutMin !== undefined) {
251
- simulated.amountOutMin = decodeAmount(
252
- simulated.amountOutMin,
253
- key,
254
- 'amountOutMin',
264
+ }
265
+ for (const key of Object.keys(outputs)) {
266
+ const entry = outputs[key];
267
+ if (!isNonNullObject(entry)) {
268
+ throw malformed(`outputs.${key}`, 'is not an object', url);
269
+ }
270
+ const amount = entry.amount;
271
+ if (!isNonNullObject(amount)) {
272
+ throw malformed(
273
+ `outputs.${key}.amount`,
274
+ amount === undefined ? 'is absent' : 'is not an object',
255
275
  url,
256
276
  );
257
277
  }
278
+ decodeAmountLeaf(amount, 'estimate', `outputs.${key}.amount`, url);
279
+ decodeAmountLeaf(amount, 'minimum', `outputs.${key}.amount`, url);
258
280
  }
259
281
  };
260
282
 
283
+ // THE decode seam for a compile-family response body: every amount the wire
284
+ // renders as a decimal string becomes the `bigint` its declared type promises.
285
+ // The amount record is handled here so the body is narrowed once — a
286
+ // non-object body is not this function's error to raise, since the enveloping
287
+ // parse already rejected it.
288
+ const decodeWireAmounts = (data: unknown, url: string): void => {
289
+ if (!isNonNullObject(data)) return;
290
+ decodeOutputs(data, url);
291
+ };
292
+
261
293
  const parseBody = async <T>(res: Response, url: string): Promise<T> => {
262
294
  const body = await res.json().catch((_) => null);
263
295
  if (!isNonNullObject(body) || !('data' in body)) {
@@ -273,7 +305,7 @@ const parseCompileSuccessBody = async (
273
305
  url: string,
274
306
  ): Promise<ComposeCompileResult> => {
275
307
  const data = await parseBody<ComposeCompileSuccessData>(res, url);
276
- decodeSimulatedAmounts(data, url);
308
+ decodeWireAmounts(data, url);
277
309
  return { ...data, status: 'success' as const };
278
310
  };
279
311
 
@@ -293,8 +325,8 @@ const parsePartialBody = async (
293
325
  // `data` is validated as an object by the schema; compose-spec owns its full
294
326
  // shape as a hand-authored type, so we narrow it here rather than re-declaring
295
327
  // that type as a schema. `error` is fully validated — no cast needed.
296
- const data = envelope.data as unknown as ComposeCompilePartialData;
297
- decodeSimulatedAmounts(data, url);
328
+ const data = envelope.data as ComposeCompilePartialData;
329
+ decodeWireAmounts(data, url);
298
330
  return { ...data, status: 'partial' as const, error: envelope.error };
299
331
  };
300
332
 
@@ -54,14 +54,14 @@ const WETH = '0x4200000000000000000000000000000000000006';
54
54
  * basis points, not percent.
55
55
  *
56
56
  * Widening it is safe but not free. The bottom of the band is what the compose
57
- * response publishes as `amountOutMin`, so a wider band advertises a weaker
57
+ * response publishes as the output's `amount.minimum`, so a wider band
58
58
  * minimum to whoever consumes your quote.
59
59
  *
60
60
  * ## Ignore the 50% slippage in the Fly calldata
61
61
  *
62
62
  * If you inspect the compiled transaction you will see a 50% slippage figure
63
63
  * where you configured 1%. That is deliberate and it is not your setting. All
64
- * Fly's own `slippage` parameter does is price its `amountOutMin`, which is an
64
+ * Fly's own `slippage` parameter does is price its minimum-out floor, which is an
65
65
  * absolute amount fixed at quote time and therefore cannot scale down when a
66
66
  * legitimately smaller input arrives. The op sends a high value so that floor
67
67
  * can never be the binding constraint, and enforces its own rate-based bound
@@ -137,7 +137,7 @@ export const buildFlySwapExample = (): {
137
137
  //
138
138
  // No slippage guard here, and none is accepted: `amountOut` declares
139
139
  // `providesMinimum`, so the op owns its own bound. Attaching one is a
140
- // `guard_error`. The published `amountOutMin` is the op's, derived from the
140
+ // `guard_error`. The published `amount.minimum` is the op's, derived from the
141
141
  // bottom of the input band.
142
142
  builder.fly.swap('swapAllToWeth', {
143
143
  bind: { amountIn: exitPt.amountOut },
@@ -21,15 +21,16 @@ export interface LifiZapAsyncInput {
21
21
  * - `lifi.zapAsync`, the future-availability sibling of `lifi.zap`
22
22
  * - Binding the required `recipient` input (the async op has no optional
23
23
  * recipient — the decl is non-optional, so `bind.recipient` is mandatory)
24
- * - Omitting `sweepTo`: `availability: 'future'` governs custody, so the
24
+ * - Omitting `sweepTo`: a future delivery governs custody, so the
25
25
  * output is delivered externally and never enters the execution VM — a
26
26
  * sweep would find nothing to move (custody, not amount reporting)
27
- * - Amount reporting is unaffected by availability: when the async lowering
28
- * binds a terminal handle the output still reports `simulated.amountOut`,
29
- * the protocol's in-transaction expected value (an estimate; the delivered
30
- * amount arrives after settlement). `simulated.amountOutMin` appears only
31
- * when a minimum source exists — for a future output that means a floor the
32
- * edge lowering itself reported, never a user guard
27
+ * - Amount reporting is unaffected by delivery timing: when the async lowering
28
+ * binds a terminal handle the output still reports an `amount.estimate`
29
+ * (`basis: 'simulated'`), the protocol's in-transaction expected value (the
30
+ * delivered amount arrives after settlement, when `delivery.when` is
31
+ * `'future'`). An `amount.minimum` appears only when a minimum source
32
+ * exists — for a future output that means a floor the edge lowering itself
33
+ * reported, never a user guard
33
34
  * - No output guards: the reported value is the initiating-transaction
34
35
  * estimate, not the asynchronously delivered amount a guard could enforce,
35
36
  * so `amountOut` is excluded from the generated `TypedGuard` union
@@ -20,6 +20,13 @@ const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';
20
20
  * - Chaining a swap into a zap by threading output handles
21
21
  * - The swap's amountOut feeds directly into the zap's amountIn
22
22
  * - Slippage guard on the zap output (swap output has providesMinimum)
23
+ *
24
+ * Reading the result of compiling this flow:
25
+ * - `result.outputs['swap.amountOut']` has `role: 'consumed'` (the zap consumes
26
+ * it); `consumedBy` is `'zap'` and `amount.minimum` is the swap floor the
27
+ * router enforces on the intermediate USDC.
28
+ * - `result.outputs['zap.amountOut']` has `role: 'terminal'`; its
29
+ * `amount.minimum` carries the guard floor set above.
23
30
  */
24
31
  export const buildSwapAndZapExample = (): {
25
32
  flow: Flow;
@@ -91,18 +91,28 @@ export const handleCompileResult = async (): Promise<void> => {
91
91
 
92
92
  if (result.status === 'success') {
93
93
  // Full success — transactionRequest includes gasLimit.
94
- const { transactionRequest, userProxy, producedResources } = result;
94
+ const { transactionRequest, userProxy, outputs } = result;
95
95
  void transactionRequest;
96
96
  void userProxy;
97
- void producedResources;
97
+ // Every linear resource output port appears in `outputs`, keyed
98
+ // `<nodeId>.<port>`. The swap's terminal output carries the amount.
99
+ const swapOut = outputs['swap.amountOut'];
100
+ void swapOut?.amount.estimate?.value; // simulated estimate on a 200
101
+ void swapOut?.amount.minimum?.value; // enforced floor, absent if none known
98
102
  return;
99
103
  }
100
104
 
101
105
  // Partial result — simulation reverted but a transaction is still available.
102
106
  // transactionRequest omits gasLimit; the caller must estimate gas themselves.
103
- const { transactionRequest, simulationRevert, error } = result;
107
+ const { transactionRequest, outputs, simulationRevert, error } = result;
104
108
  void transactionRequest;
105
109
 
110
+ // Nothing was measured, so the estimate is a quote and the minimum is
111
+ // op-sourced (or absent) rather than a guard floor.
112
+ const swapOut = outputs['swap.amountOut'];
113
+ void swapOut?.amount.estimate?.value; // quoted on a 206
114
+ void swapOut?.amount.minimum?.value; // op/declared floor, absent if none
115
+
106
116
  // Inspect the revert reason.
107
117
  void simulationRevert.code;
108
118
  void simulationRevert.rawErrorBytes;
@@ -60,6 +60,55 @@ export interface LifiSwapConfig {
60
60
  };
61
61
  }
62
62
 
63
+ /**
64
+ * a string starting with "0x"
65
+ */
66
+ export type StartsWith0X = string;
67
+ /**
68
+ * a number between 0 and 1
69
+ */
70
+ export type Between01 = number;
71
+
72
+ export interface LifiBridgeConfig {
73
+ resourceOut:
74
+ | {
75
+ kind: 'native';
76
+ chainId: Positive;
77
+ }
78
+ | {
79
+ kind: 'erc20';
80
+ token: Address;
81
+ chainId: Positive;
82
+ };
83
+ destination: StartsWith0X;
84
+ slippage?: Between01;
85
+ timing?: {
86
+ swapStepTimingStrategies?: {
87
+ strategy: 'minWaitTime';
88
+ minWaitTimeMs: number;
89
+ startingExpectedResults: number;
90
+ reduceEveryMs: number;
91
+ }[];
92
+ routeTimingStrategies?: {
93
+ strategy: 'minWaitTime';
94
+ minWaitTimeMs: number;
95
+ startingExpectedResults: number;
96
+ reduceEveryMs: number;
97
+ }[];
98
+ };
99
+ bridges?: {
100
+ allow?: MaxItems50;
101
+ deny?: MaxItems50;
102
+ prefer?: MaxItems50;
103
+ };
104
+ exchanges?: {
105
+ allow?: MaxItems50;
106
+ deny?: MaxItems50;
107
+ prefer?: MaxItems50;
108
+ };
109
+ order?: 'FASTEST' | 'CHEAPEST';
110
+ }
111
+
63
112
  export interface InvariantNumericConfig {
64
113
  op: 'gte' | 'gt' | 'lte' | 'lt' | 'eq' | 'neq';
65
114
  threshold: IntegerStringInput;
@@ -671,10 +720,6 @@ export type ContinuationAssertReadyConfig =
671
720
  comparator: 'gte' | 'eq' | 'lte';
672
721
  value: string;
673
722
  };
674
- /**
675
- * a string starting with "0x"
676
- */
677
- export type StartsWith0X = string;
678
723
 
679
724
  export interface LifiZapConfig {
680
725
  resourceOut: