@lifi/composer-sdk 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +36 -4
  2. package/dist/authoring/raw.cjs +21 -8
  3. package/dist/authoring/raw.cjs.map +1 -1
  4. package/dist/authoring/raw.d.cts +21 -8
  5. package/dist/authoring/raw.d.ts +21 -8
  6. package/dist/authoring/raw.js +21 -8
  7. package/dist/authoring/raw.js.map +1 -1
  8. package/dist/authoring/signatureArgs.cjs +10 -3
  9. package/dist/authoring/signatureArgs.cjs.map +1 -1
  10. package/dist/authoring/signatureArgs.d.cts +23 -16
  11. package/dist/authoring/signatureArgs.d.ts +23 -16
  12. package/dist/authoring/signatureArgs.js +10 -3
  13. package/dist/authoring/signatureArgs.js.map +1 -1
  14. package/dist/client.cjs +11 -1
  15. package/dist/client.cjs.map +1 -1
  16. package/dist/client.js +11 -1
  17. package/dist/client.js.map +1 -1
  18. package/dist/errors.cjs +35 -13
  19. package/dist/errors.cjs.map +1 -1
  20. package/dist/errors.d.cts +19 -1
  21. package/dist/errors.d.ts +19 -1
  22. package/dist/errors.js +35 -14
  23. package/dist/errors.js.map +1 -1
  24. package/dist/generated/config.generated.d.cts +98 -2
  25. package/dist/generated/config.generated.d.ts +98 -2
  26. package/dist/generated/operations.generated.cjs +10 -0
  27. package/dist/generated/operations.generated.cjs.map +1 -1
  28. package/dist/generated/operations.generated.d.cts +57 -2
  29. package/dist/generated/operations.generated.d.ts +57 -2
  30. package/dist/generated/operations.generated.js +10 -0
  31. package/dist/generated/operations.generated.js.map +1 -1
  32. package/dist/index.cjs +1 -0
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +4 -4
  35. package/dist/index.d.ts +4 -4
  36. package/dist/index.js +2 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/outputs.cjs +3 -2
  39. package/dist/outputs.cjs.map +1 -1
  40. package/dist/outputs.d.cts +3 -2
  41. package/dist/outputs.d.ts +3 -2
  42. package/dist/outputs.js +3 -2
  43. package/dist/outputs.js.map +1 -1
  44. package/dist/responseSchemas.cjs.map +1 -1
  45. package/dist/responseSchemas.d.cts +2 -2
  46. package/dist/responseSchemas.d.ts +2 -2
  47. package/dist/responseSchemas.js.map +1 -1
  48. package/dist/run/inputs.cjs +0 -1
  49. package/dist/run/inputs.cjs.map +1 -1
  50. package/dist/run/inputs.d.cts +3 -11
  51. package/dist/run/inputs.d.ts +3 -11
  52. package/dist/run/inputs.js +0 -1
  53. package/dist/run/inputs.js.map +1 -1
  54. package/dist/sdk.cjs +0 -1
  55. package/dist/sdk.cjs.map +1 -1
  56. package/dist/sdk.js +0 -1
  57. package/dist/sdk.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/authoring/raw.ts +21 -8
  60. package/src/authoring/signatureArgs.ts +53 -23
  61. package/src/client.ts +24 -1
  62. package/src/errors.ts +48 -17
  63. package/src/examples/continuationBridgeSettle.ts +99 -0
  64. package/src/examples/continuationSettle.ts +113 -0
  65. package/src/examples/run.ts +8 -0
  66. package/src/generated/config.generated.ts +111 -2
  67. package/src/generated/operations.generated.ts +99 -1
  68. package/src/index.ts +6 -2
  69. package/src/outputs.ts +3 -2
  70. package/src/responseSchemas.ts +9 -5
  71. package/src/run/inputs.ts +3 -12
  72. package/src/sdk.ts +0 -1
  73. package/src/examples/swapWithFee.ts +0 -59
@@ -1 +1 @@
1
- {"version":3,"file":"signatureArgs.js","names":[],"sources":["../../src/authoring/signatureArgs.ts"],"sourcesContent":["import type { Ref } from '@lifi/compose-spec';\nimport { type ParseAbiItem, parseAbiItem } from 'abitype';\n\nimport type { AbiTypeToOutputKind, TypedGuard } from '../types.js';\n\nimport type { CallArgs } from './FlowBuilderCore.js';\nimport type { AnyRefBindable, OutputHandle, RefBindable } from './handles.js';\nimport { handleToRef } from './handles.js';\n\n/**\n * Type-level extraction of named bind keys from a function signature string.\n * When `TSig` is a string literal matching a Solidity function signature,\n * resolves to a record whose keys are the parameter names and values are\n * `RefBindable<kind>` where the kind is derived from each parameter's Solidity\n * type via `AbiTypeToOutputKind`. For recognised `SolType` parameters\n * (e.g. `uint256`, `address`, `uint128`) the bind slot is precisely typed;\n * for other Solidity types (e.g. tuples) the slot accepts any scalar handle.\n * Falls back to `Record<string, AnyRefBindable>` for non-literal strings.\n *\n * These slots are lowered into `config.args`, which is positional calldata\n * rather than a node `bind` record, so a literal is not accepted here: only\n * values that resolve to a `$ref`.\n */\nexport type SignatureBind<TSig extends string> = string extends TSig\n ? Record<string, AnyRefBindable>\n : ParseAbiItem<TSig> extends {\n type: 'function';\n inputs: infer I extends readonly { name: string; type: string }[];\n }\n ? {\n readonly [P in I[number] as P['name']]: RefBindable<\n AbiTypeToOutputKind<P['type']>\n >;\n }\n : Record<string, AnyRefBindable>;\n\n// ---------------------------------------------------------------------------\n// Return-type inference from function signatures\n// ---------------------------------------------------------------------------\n\n/**\n * Describes the output type(s) of a parsed function signature as a\n * human-readable string for inclusion in type-level error messages.\n */\ntype DescribeOutputs<O extends readonly { type: string }[]> =\n O extends readonly [{ type: infer T extends string }] ? T : 'multiple values';\n\n/**\n * A string-literal type that surfaces a clear error when the user provides a\n * function signature whose return type is not supported by core.call.\n * The message is visible in IDE tooltips and compiler diagnostics.\n */\ntype UnsupportedReturnTypeError<TLabel extends string, TSig extends string> =\n ParseAbiItem<TSig> extends {\n type: 'function';\n outputs: infer O extends readonly { type: string }[];\n }\n ? `[TypeError] ${TLabel}: return type '${DescribeOutputs<O>}' is not supported — only 'uint256' or void signatures are allowed`\n : `[TypeError] ${TLabel}: unable to parse return type from signature`;\n\n/**\n * Conditional output type for `core.call` based on the function signature's\n * return type:\n *\n * - `returns (uint256)` → `{ result: OutputHandle }`\n * - No returns clause → `{ result: undefined }`\n * - Anything else → `{ result: \"[TypeError] ...\" }` (compile-time error)\n *\n * When `TSig` is a non-literal `string` (e.g. a variable, not a string\n * constant), the type falls back to `{ result: OutputHandle | undefined }`\n * so that both void and uint256 call sites remain assignable.\n */\nexport type CoreCallResult<TSig extends string> = string extends TSig\n ? { readonly result: OutputHandle<'uint256'> | undefined }\n : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }\n ? { readonly result: undefined }\n : ParseAbiItem<TSig> extends {\n type: 'function';\n outputs: readonly [{ type: 'uint256' }];\n }\n ? { readonly result: OutputHandle<'uint256'> }\n : { readonly result: UnsupportedReturnTypeError<'core.call', TSig> };\n\n/**\n * Conditional output type for `core.staticCall`. staticCall always reads a\n * value, so void signatures are also rejected:\n *\n * - `returns (uint256)` → `{ result: OutputHandle }`\n * - No returns / other → `{ result: \"[TypeError] ...\" }` (compile-time error)\n */\nexport type StaticCallResult<TSig extends string> = string extends TSig\n ? { readonly result: OutputHandle<'uint256'> }\n : ParseAbiItem<TSig> extends {\n type: 'function';\n outputs: readonly [{ type: 'uint256' }];\n }\n ? { readonly result: OutputHandle<'uint256'> }\n : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }\n ? {\n readonly result: `[TypeError] core.staticCall: function has no return value — staticCall requires a 'returns (uint256)' signature`;\n }\n : {\n readonly result: UnsupportedReturnTypeError<'core.staticCall', TSig>;\n };\n\n/**\n * Parses a Solidity function signature and returns parameter names in order.\n * Delegates to abitype's runtime `parseAbiItem` — the same parser that powers\n * the compile-time `ParseAbiItem<TSig>` type — so runtime and type-level\n * parsing can never diverge.\n *\n * @throws if the signature is not a valid function signature or a parameter is unnamed\n */\nexport const parseFunctionParams = (functionSignature: string): string[] => {\n const parsed = parseAbiItem(functionSignature);\n if (parsed.type !== 'function') {\n throw new Error(\n `core.call: expected a function signature, got \"${parsed.type}\": \"${functionSignature}\"`,\n );\n }\n\n return parsed.inputs.map((input, index) => {\n if (!input.name) {\n throw new Error(\n `core.call: parameter at index ${index} in signature has no name. All parameters must be named for named binds to work.`,\n );\n }\n return input.name;\n });\n};\n\n/**\n * Converts a ref-resolving bindable to a `Ref` (`{ $ref: string }`).\n * Handles carry a `_tag` property and are converted via `handleToRef`;\n * `Ref` objects pass through as-is.\n */\nexport const toBindRef = (bindable: AnyRefBindable): Ref => {\n if ('_tag' in bindable) return handleToRef(bindable);\n return bindable;\n};\n\nexport interface BuildCallWireFormatInput {\n readonly resource?: AnyRefBindable;\n readonly bind: Record<string, AnyRefBindable>;\n readonly config: Record<string, unknown>;\n readonly guards?: readonly TypedGuard[];\n}\n\nexport interface CallWireResult {\n readonly op: 'core.call' | 'core.invoke';\n readonly bind: Record<string, Ref>;\n readonly config: Record<string, unknown>;\n readonly guards?: readonly TypedGuard[];\n}\n\n/**\n * Parses a function signature from `config`, validates bind keys against it,\n * and converts named binds to positional `$ref` args.\n */\nconst resolvePositionalArgs = (\n label: string,\n bind: Record<string, AnyRefBindable>,\n config: Record<string, unknown>,\n): Ref[] => {\n const functionSignature = config.functionSignature;\n if (typeof functionSignature !== 'string') {\n throw new Error(\n `${label}: config.functionSignature is required and must be a string`,\n );\n }\n\n const paramNames = parseFunctionParams(functionSignature);\n const paramSet = new Set(paramNames);\n const bindKeys = Object.keys(bind);\n const bindSet = new Set(bindKeys);\n\n const missing = paramNames.filter((n) => !bindSet.has(n));\n const extra = bindKeys.filter((k) => !paramSet.has(k));\n\n if (missing.length > 0 || extra.length > 0) {\n throw new Error(\n `${label}: bind keys [${bindKeys.join(\n ', ',\n )}] do not match signature parameters [${paramNames.join(\n ', ',\n )}]. Missing: ${missing.join(', ') || '(none)'}. Extra: ${\n extra.join(', ') || '(none)'\n }.`,\n );\n }\n\n return paramNames.map((name) => {\n const bindable = bind[name];\n if (bindable === undefined) {\n throw new Error(\n `${label}: bind value for parameter \"${name}\" is undefined`,\n );\n }\n return toBindRef(bindable);\n });\n};\n\n/**\n * Transforms the user-facing `resource` + named `bind` API into the wire\n * format that the backend expects:\n * - With resource: `core.call` with `bind: { input: <resourceRef> }`\n * - Without resource: `core.invoke` with `bind: {}`\n * - `config: { ...userConfig, args: [positional refs...] }`\n */\nexport const buildCallWireFormat = (\n args: BuildCallWireFormatInput,\n): CallWireResult => {\n const positionalArgs = resolvePositionalArgs(\n 'core.call',\n args.bind,\n args.config,\n );\n const hasResource = args.resource !== undefined;\n\n return {\n op: hasResource ? 'core.call' : 'core.invoke',\n bind: hasResource ? { input: toBindRef(args.resource) } : {},\n config: { ...args.config, args: positionalArgs },\n ...(args.guards && args.guards.length > 0 && { guards: args.guards }),\n };\n};\n\nexport interface BuildStaticCallWireFormatInput {\n readonly bind: Record<string, AnyRefBindable>;\n readonly config: Record<string, unknown>;\n readonly guards?: readonly TypedGuard[];\n}\n\n/**\n * Transforms the user-facing named `bind` API into the wire format that the\n * backend expects for `core.staticCall`. Same as `buildCallWireFormat` but\n * with no `resource` field — staticCall has no resource inputs.\n */\nexport const buildStaticCallWireFormat = (\n args: BuildStaticCallWireFormatInput,\n): CallArgs => {\n const positionalArgs = resolvePositionalArgs(\n 'core.staticCall',\n args.bind,\n args.config,\n );\n\n return {\n bind: {},\n config: { ...args.config, args: positionalArgs },\n ...(args.guards && args.guards.length > 0 && { guards: args.guards }),\n };\n};\n"],"mappings":";;;;;;;;;;;AAiHA,MAAa,uBAAuB,sBAAwC;CAC1E,MAAM,SAAS,aAAa,iBAAiB;CAC7C,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,MACR,kDAAkD,OAAO,KAAK,MAAM,kBAAkB,EACxF;CAGF,OAAO,OAAO,OAAO,KAAK,OAAO,UAAU;EACzC,IAAI,CAAC,MAAM,MACT,MAAM,IAAI,MACR,iCAAiC,MAAM,iFACzC;EAEF,OAAO,MAAM;CACf,CAAC;AACH;;;;;;AAOA,MAAa,aAAa,aAAkC;CAC1D,IAAI,UAAU,UAAU,OAAO,YAAY,QAAQ;CACnD,OAAO;AACT;;;;;AAoBA,MAAM,yBACJ,OACA,MACA,WACU;CACV,MAAM,oBAAoB,OAAO;CACjC,IAAI,OAAO,sBAAsB,UAC/B,MAAM,IAAI,MACR,GAAG,MAAM,4DACX;CAGF,MAAM,aAAa,oBAAoB,iBAAiB;CACxD,MAAM,WAAW,IAAI,IAAI,UAAU;CACnC,MAAM,WAAW,OAAO,KAAK,IAAI;CACjC,MAAM,UAAU,IAAI,IAAI,QAAQ;CAEhC,MAAM,UAAU,WAAW,QAAQ,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;CACxD,MAAM,QAAQ,SAAS,QAAQ,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;CAErD,IAAI,QAAQ,SAAS,KAAK,MAAM,SAAS,GACvC,MAAM,IAAI,MACR,GAAG,MAAM,eAAe,SAAS,KAC/B,IACF,EAAE,uCAAuC,WAAW,KAClD,IACF,EAAE,cAAc,QAAQ,KAAK,IAAI,KAAK,SAAS,WAC7C,MAAM,KAAK,IAAI,KAAK,SACrB,EACH;CAGF,OAAO,WAAW,KAAK,SAAS;EAC9B,MAAM,WAAW,KAAK;EACtB,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MACR,GAAG,MAAM,8BAA8B,KAAK,eAC9C;EAEF,OAAO,UAAU,QAAQ;CAC3B,CAAC;AACH;;;;;;;;AASA,MAAa,uBACX,SACmB;CACnB,MAAM,iBAAiB,sBACrB,aACA,KAAK,MACL,KAAK,MACP;CACA,MAAM,cAAc,KAAK,aAAa,KAAA;CAEtC,OAAO;EACL,IAAI,cAAc,cAAc;EAChC,MAAM,cAAc,EAAE,OAAO,UAAU,KAAK,QAAQ,EAAE,IAAI,CAAC;EAC3D,QAAQ;GAAE,GAAG,KAAK;GAAQ,MAAM;EAAe;EAC/C,GAAI,KAAK,UAAU,KAAK,OAAO,SAAS,KAAK,EAAE,QAAQ,KAAK,OAAO;CACrE;AACF;;;;;;AAaA,MAAa,6BACX,SACa;CACb,MAAM,iBAAiB,sBACrB,mBACA,KAAK,MACL,KAAK,MACP;CAEA,OAAO;EACL,MAAM,CAAC;EACP,QAAQ;GAAE,GAAG,KAAK;GAAQ,MAAM;EAAe;EAC/C,GAAI,KAAK,UAAU,KAAK,OAAO,SAAS,KAAK,EAAE,QAAQ,KAAK,OAAO;CACrE;AACF"}
1
+ {"version":3,"file":"signatureArgs.js","names":[],"sources":["../../src/authoring/signatureArgs.ts"],"sourcesContent":["import type { Ref, SolType } from '@lifi/compose-spec';\nimport { type ParseAbiItem, parseAbiItem } from 'abitype';\n\nimport type {\n AbiTypeToOutputKind,\n TypedGuard,\n TypedLiteral,\n} from '../types.js';\n\nimport type { CallArgs } from './FlowBuilderCore.js';\nimport type { AnyRefBindable, OutputHandle, RefBindable } from './handles.js';\nimport { handleToRef } from './handles.js';\n\n/**\n * Any value a signature-derived bind slot accepts, with no per-slot type\n * constraint: a ref-resolving bindable or a typed literal.\n */\ntype AnySignatureBindable = AnyRefBindable | TypedLiteral;\n\n// The literal arm of a signature-derived slot. A positional arg is encoded by\n// its parameter's ABI type, not by the literal's `kind`, so the kind must equal\n// that type exactly: the `bytes32` widening `Bindable<T>` applies would admit an\n// `address` literal the encoder then rejects. A parameter whose ABI type is no\n// `SolType` (a tuple, an array, `uint24`) has no literal kind to name, so its\n// slot stays ref-only.\ntype SignatureLiteral<TAbiType extends string> = TAbiType extends SolType\n ? TypedLiteral<TAbiType>\n : never;\n\n/**\n * Type-level extraction of named bind keys from a function signature string.\n * When `TSig` is a string literal matching a Solidity function signature,\n * resolves to a record whose keys are the parameter names and whose values are\n * `RefBindable<kind>`, where the kind is derived from each parameter's Solidity\n * type via `AbiTypeToOutputKind`, or a `TypedLiteral` (`raw.literal`) of that\n * exact Solidity type. For recognised `SolType` parameters (e.g. `uint256`,\n * `address`, `bytes`) the slot is precisely typed; other Solidity types (e.g.\n * tuples) accept any scalar handle and no literal.\n * Falls back to `Record<string, AnySignatureBindable>` for non-literal strings.\n *\n * These slots are lowered into `config.args`, positional calldata: a ref\n * becomes `{ $ref }` and a literal becomes its `value` string, which the\n * backend encodes against the parameter's type.\n */\nexport type SignatureBind<TSig extends string> = string extends TSig\n ? Record<string, AnySignatureBindable>\n : ParseAbiItem<TSig> extends {\n type: 'function';\n inputs: infer I extends readonly { name: string; type: string }[];\n }\n ? {\n readonly [P in I[number] as P['name']]:\n | RefBindable<AbiTypeToOutputKind<P['type']>>\n | SignatureLiteral<P['type']>;\n }\n : Record<string, AnySignatureBindable>;\n\n// ---------------------------------------------------------------------------\n// Return-type inference from function signatures\n// ---------------------------------------------------------------------------\n\n/**\n * Describes the output type(s) of a parsed function signature as a\n * human-readable string for inclusion in type-level error messages.\n */\ntype DescribeOutputs<O extends readonly { type: string }[]> =\n O extends readonly [{ type: infer T extends string }] ? T : 'multiple values';\n\n/**\n * A string-literal type that surfaces a clear error when the user provides a\n * function signature whose return type is not supported by core.call.\n * The message is visible in IDE tooltips and compiler diagnostics.\n */\ntype UnsupportedReturnTypeError<TLabel extends string, TSig extends string> =\n ParseAbiItem<TSig> extends {\n type: 'function';\n outputs: infer O extends readonly { type: string }[];\n }\n ? `[TypeError] ${TLabel}: return type '${DescribeOutputs<O>}' is not supported — only 'uint256' or void signatures are allowed`\n : `[TypeError] ${TLabel}: unable to parse return type from signature`;\n\n/**\n * Conditional output type for `core.call` based on the function signature's\n * return type:\n *\n * - `returns (uint256)` → `{ result: OutputHandle }`\n * - No returns clause → `{ result: undefined }`\n * - Anything else → `{ result: \"[TypeError] ...\" }` (compile-time error)\n *\n * When `TSig` is a non-literal `string` (e.g. a variable, not a string\n * constant), the type falls back to `{ result: OutputHandle | undefined }`\n * so that both void and uint256 call sites remain assignable.\n */\nexport type CoreCallResult<TSig extends string> = string extends TSig\n ? { readonly result: OutputHandle<'uint256'> | undefined }\n : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }\n ? { readonly result: undefined }\n : ParseAbiItem<TSig> extends {\n type: 'function';\n outputs: readonly [{ type: 'uint256' }];\n }\n ? { readonly result: OutputHandle<'uint256'> }\n : { readonly result: UnsupportedReturnTypeError<'core.call', TSig> };\n\n/**\n * Conditional output type for `core.staticCall`. staticCall always reads a\n * value, so void signatures are also rejected:\n *\n * - `returns (uint256)` → `{ result: OutputHandle }`\n * - No returns / other → `{ result: \"[TypeError] ...\" }` (compile-time error)\n */\nexport type StaticCallResult<TSig extends string> = string extends TSig\n ? { readonly result: OutputHandle<'uint256'> }\n : ParseAbiItem<TSig> extends {\n type: 'function';\n outputs: readonly [{ type: 'uint256' }];\n }\n ? { readonly result: OutputHandle<'uint256'> }\n : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }\n ? {\n readonly result: `[TypeError] core.staticCall: function has no return value — staticCall requires a 'returns (uint256)' signature`;\n }\n : {\n readonly result: UnsupportedReturnTypeError<'core.staticCall', TSig>;\n };\n\n/**\n * Parses a Solidity function signature and returns parameter names in order.\n * Delegates to abitype's runtime `parseAbiItem` — the same parser that powers\n * the compile-time `ParseAbiItem<TSig>` type — so runtime and type-level\n * parsing can never diverge.\n *\n * @throws if the signature is not a valid function signature or a parameter is unnamed\n */\nexport const parseFunctionParams = (functionSignature: string): string[] => {\n const parsed = parseAbiItem(functionSignature);\n if (parsed.type !== 'function') {\n throw new Error(\n `core.call: expected a function signature, got \"${parsed.type}\": \"${functionSignature}\"`,\n );\n }\n\n return parsed.inputs.map((input, index) => {\n if (!input.name) {\n throw new Error(\n `core.call: parameter at index ${index} in signature has no name. All parameters must be named for named binds to work.`,\n );\n }\n return input.name;\n });\n};\n\n/**\n * Converts a ref-resolving bindable to a `Ref` (`{ $ref: string }`).\n * Handles carry a `_tag` property and are converted via `handleToRef`;\n * `Ref` objects pass through as-is.\n */\nexport const toBindRef = (bindable: AnyRefBindable): Ref => {\n if ('_tag' in bindable) return handleToRef(bindable);\n return bindable;\n};\n\n/**\n * Converts one signature-derived bind to its positional `config.args` entry: a\n * ref-resolving bindable becomes `{ $ref }`; a literal becomes its `value`\n * verbatim, which the backend encodes against the parameter's ABI type (a\n * decimal or hex integer, a hex address or byte string, `'true'`/`'false'`).\n */\nconst toPositionalArg = (bindable: AnySignatureBindable): Ref | string =>\n 'kind' in bindable ? bindable.value : toBindRef(bindable);\n\nexport interface BuildCallWireFormatInput {\n readonly resource?: AnyRefBindable;\n readonly bind: Record<string, AnySignatureBindable>;\n readonly config: Record<string, unknown>;\n readonly guards?: readonly TypedGuard[];\n}\n\nexport interface CallWireResult {\n readonly op: 'core.call' | 'core.invoke';\n readonly bind: Record<string, Ref>;\n readonly config: Record<string, unknown>;\n readonly guards?: readonly TypedGuard[];\n}\n\n/**\n * Parses a function signature from `config`, validates bind keys against it,\n * and converts named binds to positional args.\n */\nconst resolvePositionalArgs = (\n label: string,\n bind: Record<string, AnySignatureBindable>,\n config: Record<string, unknown>,\n): (Ref | string)[] => {\n const functionSignature = config.functionSignature;\n if (typeof functionSignature !== 'string') {\n throw new Error(\n `${label}: config.functionSignature is required and must be a string`,\n );\n }\n\n const paramNames = parseFunctionParams(functionSignature);\n const paramSet = new Set(paramNames);\n const bindKeys = Object.keys(bind);\n const bindSet = new Set(bindKeys);\n\n const missing = paramNames.filter((n) => !bindSet.has(n));\n const extra = bindKeys.filter((k) => !paramSet.has(k));\n\n if (missing.length > 0 || extra.length > 0) {\n throw new Error(\n `${label}: bind keys [${bindKeys.join(\n ', ',\n )}] do not match signature parameters [${paramNames.join(\n ', ',\n )}]. Missing: ${missing.join(', ') || '(none)'}. Extra: ${\n extra.join(', ') || '(none)'\n }.`,\n );\n }\n\n return paramNames.map((name) => {\n const bindable = bind[name];\n if (bindable === undefined) {\n throw new Error(\n `${label}: bind value for parameter \"${name}\" is undefined`,\n );\n }\n return toPositionalArg(bindable);\n });\n};\n\n/**\n * Transforms the user-facing `resource` + named `bind` API into the wire\n * format that the backend expects:\n * - With resource: `core.call` with `bind: { input: <resourceRef> }`\n * - Without resource: `core.invoke` with `bind: {}`\n * - `config: { ...userConfig, args: [positional refs and literal values...] }`\n */\nexport const buildCallWireFormat = (\n args: BuildCallWireFormatInput,\n): CallWireResult => {\n const positionalArgs = resolvePositionalArgs(\n 'core.call',\n args.bind,\n args.config,\n );\n const hasResource = args.resource !== undefined;\n\n return {\n op: hasResource ? 'core.call' : 'core.invoke',\n bind: hasResource ? { input: toBindRef(args.resource) } : {},\n config: { ...args.config, args: positionalArgs },\n ...(args.guards && args.guards.length > 0 && { guards: args.guards }),\n };\n};\n\nexport interface BuildStaticCallWireFormatInput {\n readonly bind: Record<string, AnySignatureBindable>;\n readonly config: Record<string, unknown>;\n readonly guards?: readonly TypedGuard[];\n}\n\n/**\n * Transforms the user-facing named `bind` API into the wire format that the\n * backend expects for `core.staticCall`. Same as `buildCallWireFormat` but\n * with no `resource` field — staticCall has no resource inputs.\n */\nexport const buildStaticCallWireFormat = (\n args: BuildStaticCallWireFormatInput,\n): CallArgs => {\n const positionalArgs = resolvePositionalArgs(\n 'core.staticCall',\n args.bind,\n args.config,\n );\n\n return {\n bind: {},\n config: { ...args.config, args: positionalArgs },\n ...(args.guards && args.guards.length > 0 && { guards: args.guards }),\n };\n};\n"],"mappings":";;;;;;;;;;;AAsIA,MAAa,uBAAuB,sBAAwC;CAC1E,MAAM,SAAS,aAAa,iBAAiB;CAC7C,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,MACR,kDAAkD,OAAO,KAAK,MAAM,kBAAkB,EACxF;CAGF,OAAO,OAAO,OAAO,KAAK,OAAO,UAAU;EACzC,IAAI,CAAC,MAAM,MACT,MAAM,IAAI,MACR,iCAAiC,MAAM,iFACzC;EAEF,OAAO,MAAM;CACf,CAAC;AACH;;;;;;AAOA,MAAa,aAAa,aAAkC;CAC1D,IAAI,UAAU,UAAU,OAAO,YAAY,QAAQ;CACnD,OAAO;AACT;;;;;;;AAQA,MAAM,mBAAmB,aACvB,UAAU,WAAW,SAAS,QAAQ,UAAU,QAAQ;;;;;AAoB1D,MAAM,yBACJ,OACA,MACA,WACqB;CACrB,MAAM,oBAAoB,OAAO;CACjC,IAAI,OAAO,sBAAsB,UAC/B,MAAM,IAAI,MACR,GAAG,MAAM,4DACX;CAGF,MAAM,aAAa,oBAAoB,iBAAiB;CACxD,MAAM,WAAW,IAAI,IAAI,UAAU;CACnC,MAAM,WAAW,OAAO,KAAK,IAAI;CACjC,MAAM,UAAU,IAAI,IAAI,QAAQ;CAEhC,MAAM,UAAU,WAAW,QAAQ,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;CACxD,MAAM,QAAQ,SAAS,QAAQ,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;CAErD,IAAI,QAAQ,SAAS,KAAK,MAAM,SAAS,GACvC,MAAM,IAAI,MACR,GAAG,MAAM,eAAe,SAAS,KAC/B,IACF,EAAE,uCAAuC,WAAW,KAClD,IACF,EAAE,cAAc,QAAQ,KAAK,IAAI,KAAK,SAAS,WAC7C,MAAM,KAAK,IAAI,KAAK,SACrB,EACH;CAGF,OAAO,WAAW,KAAK,SAAS;EAC9B,MAAM,WAAW,KAAK;EACtB,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MACR,GAAG,MAAM,8BAA8B,KAAK,eAC9C;EAEF,OAAO,gBAAgB,QAAQ;CACjC,CAAC;AACH;;;;;;;;AASA,MAAa,uBACX,SACmB;CACnB,MAAM,iBAAiB,sBACrB,aACA,KAAK,MACL,KAAK,MACP;CACA,MAAM,cAAc,KAAK,aAAa,KAAA;CAEtC,OAAO;EACL,IAAI,cAAc,cAAc;EAChC,MAAM,cAAc,EAAE,OAAO,UAAU,KAAK,QAAQ,EAAE,IAAI,CAAC;EAC3D,QAAQ;GAAE,GAAG,KAAK;GAAQ,MAAM;EAAe;EAC/C,GAAI,KAAK,UAAU,KAAK,OAAO,SAAS,KAAK,EAAE,QAAQ,KAAK,OAAO;CACrE;AACF;;;;;;AAaA,MAAa,6BACX,SACa;CACb,MAAM,iBAAiB,sBACrB,mBACA,KAAK,MACL,KAAK,MACP;CAEA,OAAO;EACL,MAAM,CAAC;EACP,QAAQ;GAAE,GAAG,KAAK;GAAQ,MAAM;EAAe;EAC/C,GAAI,KAAK,UAAU,KAAK,OAAO,SAAS,KAAK,EAAE,QAAQ,KAAK,OAAO;CACrE;AACF"}
package/dist/client.cjs CHANGED
@@ -3,7 +3,7 @@ const require_responseSchemas = require("./responseSchemas.cjs");
3
3
  const require_errors = require("./errors.cjs");
4
4
  let _lifi_compose_spec = require("@lifi/compose-spec");
5
5
  //#region src/client.ts
6
- const SDK_VERSION = "0.10.0";
6
+ const SDK_VERSION = "0.12.0";
7
7
  const SDK_VERSION_TRIPLE = (0, _lifi_compose_spec.parseVersion)(SDK_VERSION);
8
8
  /**
9
9
  * `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.
@@ -38,9 +38,19 @@ const decodeOutputs = (data, url) => {
38
38
  decodeAmountLeaf(amount, "minimum", `outputs.${key}.amount`, url);
39
39
  }
40
40
  };
41
+ const decodeEpilogue = (data, url) => {
42
+ const epilogue = data.epilogue;
43
+ if (epilogue === void 0) return;
44
+ if (!isNonNullObject(epilogue) || !Array.isArray(epilogue.forwards)) throw malformed("epilogue", "is not an object with forwards", url);
45
+ epilogue.forwards.forEach((forward, index) => {
46
+ if (!isNonNullObject(forward)) throw malformed(`epilogue.forwards[${index}]`, "is not an object", url);
47
+ forward.amount = decodeAmount(forward.amount, `epilogue.forwards[${index}].amount`, url);
48
+ });
49
+ };
41
50
  const decodeWireAmounts = (data, url) => {
42
51
  if (!isNonNullObject(data)) return;
43
52
  decodeOutputs(data, url);
53
+ decodeEpilogue(data, url);
44
54
  };
45
55
  const parseBody = async (res, url) => {
46
56
  const body = await res.json().catch((_) => null);
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","names":["parseVersion","ComposeError","parseCompilePartialEnvelope","parseSimulateResult","COMPOSE_VERSION_HEADER","checkCompatibility","errorFromHttpResponse","COMPOSER_SDK_VERSION_HEADER"],"sources":["../src/client.ts"],"sourcesContent":["import type {\n ComposeCompilePartialData,\n ComposeCompileRequest,\n ComposeCompileResult,\n ComposeCompileSuccessData,\n ComposeManifest,\n ComposeRouteRequest,\n SimulateRequest,\n SimulateResult,\n} from '@lifi/compose-spec';\nimport {\n checkCompatibility,\n COMPOSE_VERSION_HEADER,\n COMPOSER_SDK_VERSION_HEADER,\n parseVersion,\n} from '@lifi/compose-spec';\n\nimport type {\n ChainSummary,\n GetZapPacksOptions,\n ZapPackOverview,\n} from './discovery.js';\nimport { ComposeError, errorFromHttpResponse } from './errors.js';\nimport {\n parseCompilePartialEnvelope,\n parseSimulateResult,\n} from './responseSchemas.js';\n\n// __SDK_VERSION__ is a compile-time constant injected by tsdown (via `define` in tsdown.config.ts)\n// and by vitest (via `define` in vitest.config.ts). Both read the version from package.json\n// at build/test time and replace this identifier with the literal string value.\n// It is sent as the `COMPOSER_SDK_VERSION_HEADER` request header so the server can identify the caller.\n// Falls back to 'dev' when running via tsx without tsdown substitution (e.g. the example harness).\ndeclare const __SDK_VERSION__: string;\n\nconst SDK_VERSION: string =\n typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';\n// `undefined` for the `dev` sentinel, which disables the response version check.\nconst SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);\n\n/**\n * Configuration for creating a low-level Compose API client.\n */\nexport interface ComposeClientOptions {\n /** Base URL of the Compose API. */\n readonly baseUrl: string;\n /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** LI.FI API key, sent as the `x-lifi-api-key` header on every request. Required — the Compose API rejects unauthenticated requests. */\n readonly apiKey: string;\n}\n\n/**\n * Low-level HTTP client for the Compose API.\n *\n * Handles request serialization, SDK version headers, and error mapping.\n * Prefer using {@link ComposeSdk} for the full builder experience. Use this\n * directly when you need to decouple request building from submission — e.g.\n * build via `sdk.request()` then submit via `client.compile()` with custom\n * retry logic or request inspection.\n */\nexport interface ComposeClient {\n /**\n * Fetches the server's operation manifest describing all supported operations,\n * guards, materialisers, and preconditions.\n * @returns The manifest document.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly getManifest: () => Promise<ComposeManifest>;\n /**\n * Fetches the chains the Compose API supports, ascending by `chainId`.\n *\n * A chain is listed once the server confirms the VM contract is deployed on\n * it — the same condition {@link compile}, {@link route} and\n * {@link simulate} validate against. A chain absent from this list is\n * rejected outright by those endpoints.\n *\n * The converse does not hold: a listed chain can still have no routing edge\n * for a given token pair, which {@link route} reports as `no_route_error`.\n * Use {@link getZapPacks} to enumerate the pairs that do route.\n *\n * Each entry carries a display `name` alongside its `chainId`. The name is a\n * deployment-maintained label — render it, but key on `chainId`.\n *\n * The list tracks the server's chain registry, which refreshes periodically.\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @returns The supported chains, ascending by `chainId`.\n * @throws {@link ComposeError} on network or server errors.\n */\n readonly getChains: () => Promise<readonly ChainSummary[]>;\n /**\n * Submits a compile request and returns the result.\n *\n * When the caller passes `simulationPolicy: 'allow-revert'` and the transaction\n * reverts in simulation, the server responds with HTTP 206 and the SDK returns a\n * partial result (`status: 'partial'`) instead of throwing. The partial result\n * includes the transaction (without `gasLimit`) and revert diagnostics.\n *\n * @param request - The full compile request including flow and run inputs.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly compile: (\n request: ComposeCompileRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Compiles a token pair into a submit-ready transaction via\n * `POST /compose/route`.\n *\n * A convenience path that skips flow authoring: the server builds a one-node\n * zap flow with a `directDeposit` input from the `fromToken` / `toToken`\n * pair and runs it through the same pipeline as {@link compile}. The\n * response is therefore the same discriminated result — HTTP 206 under\n * `simulationPolicy: 'allow-revert'` yields `status: 'partial'` rather than\n * throwing, exactly as it does for {@link compile}.\n *\n * The server plans a short chain of catalog edges for the pair (a position\n * entry or exit, a wrap or unwrap, a mint or burn), optionally opened by a\n * `lifi.swap`. {@link getZapPacks} lists the single edges; the pairs the\n * planner connects are their closure up to the hop budget, which\n * {@link buildReachability} computes locally. A pair the planner cannot\n * connect is rejected with `kind: 'no_route_error'`.\n *\n * Only synchronous edges are considered unless the request sets\n * `allowAsyncRoutes: true`, so by default every terminal output comes back\n * `delivery.when: 'now'` and a pair reachable only after settlement is a\n * `no_route_error`. Opting in adds the `lifi.zapAsync` fallback, which still\n * loses to a synchronous edge when the pair has one; read\n * `outputs[*].delivery.when` to see which representation you got.\n *\n * Use {@link compile} for what the server does not author for you: several\n * chained operations, splits, explicit preconditions, or per-op guards.\n *\n * `bigint` amounts in `amount` are serialised to decimal strings\n * automatically.\n *\n * @param request - The from/to token pair, amount, signer, and route options.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors —\n * including `NOT_FOUND` (HTTP 404) when the planner cannot connect the pair.\n */\n readonly route: (\n request: ComposeRouteRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Fetches the available routing edges grouped by protocol.\n *\n * The edge catalog is dynamic — it reflects the current state of the\n * backend's routing snapshot (protocols, chains, token blacklists).\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @param options - Optional filter to restrict results to specific protocols.\n * @returns An array of {@link ZapPackOverview} objects, one per protocol.\n * @throws {@link ComposeError} on network or server errors (503 when the\n * routing catalog is not yet initialized).\n */\n readonly getZapPacks: (\n options?: GetZapPacksOptions,\n ) => Promise<readonly ZapPackOverview[]>;\n /**\n * Simulates a raw, pre-encoded transaction against `POST /simulate` and\n * reports how the watched balances change and how much inner-call gas it\n * burns.\n *\n * The result is a discriminated union on `status`:\n * - `'ok'` — the simulation ran successfully; `deltas`/`gasUsed` are populated.\n * - `'revert'` — the simulation ran but the transaction reverted on-chain. A\n * revert is a *successful simulation*, not a transport error, so it is\n * returned (HTTP 200) rather than thrown — mirroring how {@link compile}\n * returns `status: 'partial'` on a simulated revert.\n * - `'error'` — the request was well-formed but the simulation could not be\n * set up or run (HTTP 422); `message` is intentionally generic.\n *\n * `bigint` amounts in the request (`value`, requirement `balance`/`allowance`)\n * are serialised to decimal strings automatically.\n *\n * @param request - The raw transaction plus funding `requirements` and the\n * `trackedBalances` to watch.\n * @returns A {@link SimulateResult} (`ok` / `revert` / `error`).\n * @throws {@link ComposeError} on network failures, HTTP 400 (malformed\n * input), 401/403 (auth), 404, 429, and 5xx.\n */\n readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;\n}\n\n/**\n * `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.\n *\n * The compose wire carries token amounts as decimal strings, and `bigint` has\n * no JSON representation, so every payload this client sends goes through it.\n * Exported so the example harness prints a request exactly as it is sent.\n */\nexport const bigintReplacer = (_key: string, value: unknown): unknown =>\n typeof value === 'bigint' ? value.toString() : value;\n\nconst isNonNullObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null;\n\n// Non-negative decimal integer string, as the server sends token amounts.\nconst INTEGER_STRING = /^\\d+$/;\n\nconst decodeAmount = (value: unknown, path: string, url: string): bigint => {\n if (typeof value !== 'string' || !INTEGER_STRING.test(value)) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} is ${\n value === undefined ? 'absent' : 'not an integer string'\n }`,\n { url },\n );\n }\n return BigInt(value);\n};\n\n// The two records this seam reads out of a compile-family response body. Each\n// is `unknown` because the seam is what establishes its shape: it validates\n// every field before writing a decoded `bigint` back over the wire string.\ninterface WireAmountBody {\n outputs?: unknown;\n}\n\n// One `Amount` as it arrives: each leaf is `unknown` until this seam has\n// checked it, and is overwritten in place with the decoded `bigint`.\ninterface WireAmount {\n estimate?: unknown;\n minimum?: unknown;\n}\n\nconst malformed = (path: string, what: string, url: string): ComposeError =>\n new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} ${what}`,\n { url },\n );\n\n// Decode one `Estimate` / `Minimum` leaf in place: `{ basis, value }` with\n// `value` a decimal string on the wire and a `bigint` on `Amount`.\nconst decodeAmountLeaf = (\n amount: WireAmount,\n field: 'estimate' | 'minimum',\n path: string,\n url: string,\n): void => {\n const leaf = amount[field];\n if (leaf === undefined) return;\n if (!isNonNullObject(leaf)) {\n throw malformed(`${path}.${field}`, 'is not an object', url);\n }\n leaf.value = decodeAmount(leaf.value, `${path}.${field}.value`, url);\n};\n\n// `outputs` is REQUIRED on every compile-family body and is the record a\n// caller reads amounts from, so its structure is checked here — the body is\n// cast, not schema-parsed — before each `value` leaf becomes a `bigint`.\nconst decodeOutputs = (data: WireAmountBody, url: string): void => {\n const outputs = data.outputs;\n if (!isNonNullObject(outputs)) {\n throw malformed(\n 'outputs',\n outputs === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n for (const key of Object.keys(outputs)) {\n const entry = outputs[key];\n if (!isNonNullObject(entry)) {\n throw malformed(`outputs.${key}`, 'is not an object', url);\n }\n const amount = entry.amount;\n if (!isNonNullObject(amount)) {\n throw malformed(\n `outputs.${key}.amount`,\n amount === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n decodeAmountLeaf(amount, 'estimate', `outputs.${key}.amount`, url);\n decodeAmountLeaf(amount, 'minimum', `outputs.${key}.amount`, url);\n }\n};\n\n// THE decode seam for a compile-family response body: every amount the wire\n// renders as a decimal string becomes the `bigint` its declared type promises.\n// The amount record is handled here so the body is narrowed once — a\n// non-object body is not this function's error to raise, since the enveloping\n// parse already rejected it.\nconst decodeWireAmounts = (data: unknown, url: string): void => {\n if (!isNonNullObject(data)) return;\n decodeOutputs(data, url);\n};\n\nconst parseBody = async <T>(res: Response, url: string): Promise<T> => {\n const body = await res.json().catch((_) => null);\n if (!isNonNullObject(body) || !('data' in body)) {\n throw new ComposeError('UNKNOWN_ERROR', 'Unexpected response format', {\n url,\n });\n }\n return body.data as T;\n};\n\nconst parseCompileSuccessBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const data = await parseBody<ComposeCompileSuccessData>(res, url);\n decodeWireAmounts(data, url);\n return { ...data, status: 'success' as const };\n};\n\nconst parsePartialBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const body = await res.json().catch((_) => null);\n const envelope = parseCompilePartialEnvelope(body);\n if (envelope === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected partial response format',\n { url },\n );\n }\n // `data` is validated as an object by the schema; compose-spec owns its full\n // shape as a hand-authored type, so we narrow it here rather than re-declaring\n // that type as a schema. `error` is fully validated — no cast needed.\n const data = envelope.data as ComposeCompilePartialData;\n decodeWireAmounts(data, url);\n return { ...data, status: 'partial' as const, error: envelope.error };\n};\n\n// `/simulate` is un-enveloped: the discriminated body (`{ status, ... }`) is at\n// the top level, NOT wrapped in `{ data }` like `/compose`. So this reads the\n// body directly and validates it against the simulate union rather than reusing\n// `parseBody`.\nconst parseSimulateBody = async (\n res: Response,\n url: string,\n): Promise<SimulateResult> => {\n const body = await res.json().catch((_) => null);\n const result = parseSimulateResult(body);\n if (result === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected simulate response format',\n { url },\n );\n }\n return result;\n};\n// `POST /compose` and `POST /compose/route` differ only in path and request\n// shape: both return the enveloped success data on 200 and the partial\n// envelope on 206, so they share one transport.\ntype ComposeTransport = {\n readonly fetchFn: typeof fetch;\n readonly baseHeaders: Record<string, string>;\n};\n\n// A patch bump is additive and a minor bump is breaking, so a 200 from a\n// backend behind this SDK cannot be trusted: the newer flow this SDK authored\n// may have compiled under the server's older semantics without raising an\n// error. This covers the deploy window where the SDK is published to npm ahead\n// of the backend rollout. Only the compile-family POSTs are checked; discovery\n// GETs stay reachable from a mismatched SDK, as on the server. An absent header\n// (a pre-gate backend, a header-stripping proxy) is accepted, mirroring the\n// server's pass-through of a compile that carries no version header.\nconst assertServerVersion = (res: Response, url: string): void => {\n const serverVersion = res.headers.get(COMPOSE_VERSION_HEADER);\n if (serverVersion === null || SDK_VERSION_TRIPLE === undefined) return;\n const serverTriple = parseVersion(serverVersion);\n if (serverTriple === undefined) return;\n\n const compatibility = checkCompatibility(SDK_VERSION_TRIPLE, serverTriple);\n if (compatibility === 'compatible') return;\n\n const versions = { sdkVersion: SDK_VERSION, serverVersion };\n if (compatibility === 'sdk_outdated') {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `composer-sdk ${SDK_VERSION} is older than the compose contract served at ${url} (${serverVersion}); upgrade @lifi/composer-sdk and @lifi/compose-spec`,\n {\n status: res.status,\n url,\n kind: 'sdk_outdated',\n sdkOutdated: {\n ...versions,\n minimumSdkVersion: `${serverTriple.major}.${serverTriple.minor}.0`,\n },\n },\n );\n }\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `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`,\n {\n status: res.status,\n url,\n kind: 'server_outdated',\n serverOutdated: versions,\n },\n );\n};\n\ntype SendInit =\n | { readonly method: 'GET' }\n | { readonly method: 'POST'; readonly body: unknown };\n\n// GETs on this API carry no body; POSTs carry a JSON one with `bigint` amounts\n// rendered as decimal strings.\nconst requestInit = (\n baseHeaders: Record<string, string>,\n init: SendInit,\n): RequestInit =>\n init.method === 'GET'\n ? { method: 'GET', headers: { ...baseHeaders } }\n : {\n method: 'POST',\n headers: { ...baseHeaders, 'Content-Type': 'application/json' },\n body: JSON.stringify(init.body, bigintReplacer),\n };\n\n// One fetch path for every route: maps transport failures to `NETWORK_ERROR`.\n// The response version check is not here — only the compile-family POSTs gate\n// on it (see `postCompile`); discovery GETs and `/simulate` are ungated.\nconst send = async (\n { fetchFn, baseHeaders }: ComposeTransport,\n url: string,\n init: SendInit,\n): Promise<Response> => {\n try {\n return await fetchFn(url, requestInit(baseHeaders, init));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new ComposeError('NETWORK_ERROR', message, { cause: err });\n }\n};\n\nconst postCompile = async (\n transport: ComposeTransport,\n url: string,\n request: ComposeCompileRequest | ComposeRouteRequest,\n): Promise<ComposeCompileResult> => {\n const res = await send(transport, url, { method: 'POST', body: request });\n assertServerVersion(res, url);\n if (res.status === 206) {\n return await parsePartialBody(res, url);\n }\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseCompileSuccessBody(res, url);\n};\n\n// Every enveloped GET on this API behaves identically: no body, no query\n// semantics of its own, `{ data }` on success, an error envelope otherwise.\nconst getJson = async <T>(\n transport: ComposeTransport,\n url: string,\n): Promise<T> => {\n const res = await send(transport, url, { method: 'GET' });\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseBody<T>(res, url);\n};\n\n/**\n * Creates a low-level Compose API client.\n *\n * @param options - Client configuration including the API base URL.\n * @returns A {@link ComposeClient} instance.\n */\nexport const createComposeClient = (\n options: ComposeClientOptions,\n): ComposeClient => {\n if (!options.baseUrl || !/^https?:\\/\\//i.test(options.baseUrl)) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `Invalid baseUrl: expected an HTTP(S) URL, got \"${options.baseUrl}\"`,\n );\n }\n const trimmedApiKey = options.apiKey?.trim() || undefined;\n if (!trimmedApiKey) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n 'apiKey is required: pass a LI.FI API key to createComposeSdk().',\n );\n }\n const fetchFn = options.fetch ?? globalThis.fetch;\n const base = options.baseUrl.replace(/\\/$/, '');\n\n const baseHeaders: Record<string, string> = {\n Accept: 'application/json',\n [COMPOSER_SDK_VERSION_HEADER]: SDK_VERSION,\n 'x-lifi-api-key': trimmedApiKey,\n };\n const transport: ComposeTransport = { fetchFn, baseHeaders };\n\n const getManifest = async (): Promise<ComposeManifest> =>\n getJson<ComposeManifest>(transport, `${base}/compose/manifest`);\n\n const getChains = async (): Promise<readonly ChainSummary[]> =>\n getJson<readonly ChainSummary[]>(transport, `${base}/chains`);\n\n const compile = async (\n request: ComposeCompileRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose`, request);\n\n const route = async (\n request: ComposeRouteRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose/route`, request);\n\n const getZapPacks = async (\n options?: GetZapPacksOptions,\n ): Promise<readonly ZapPackOverview[]> => {\n const params = new URLSearchParams();\n if (options?.protocols !== undefined) {\n // Backend expects a single comma-separated value, not repeated keys.\n const raw = options.protocols;\n const list = typeof raw === 'string' ? raw : raw.join(',');\n params.set('protocols', list);\n }\n const qs = params.toString();\n return getJson<readonly ZapPackOverview[]>(\n transport,\n `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`,\n );\n };\n\n const simulate = async (\n request: SimulateRequest,\n ): Promise<SimulateResult> => {\n const url = `${base}/simulate`;\n const res = await send(transport, url, { method: 'POST', body: request });\n // Only 200 (carries `ok`/`revert`) and 422 (carries the `error` member of\n // the union) have a discriminated body. 422 is deliberately intercepted\n // here (not thrown as VALIDATION_ERROR) so callers get one exhaustive\n // `switch (result.status)`. Every other status is a transport error and is\n // thrown — including HTTP 400 (malformed input, no `status` body) and any\n // unexpected 2xx.\n if (res.status === 200 || res.status === 422) {\n return await parseSimulateBody(res, url);\n }\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n };\n\n return { getManifest, getChains, compile, route, getZapPacks, simulate };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","names":["parseVersion","ComposeError","parseCompilePartialEnvelope","parseSimulateResult","COMPOSE_VERSION_HEADER","checkCompatibility","errorFromHttpResponse","COMPOSER_SDK_VERSION_HEADER"],"sources":["../src/client.ts"],"sourcesContent":["import type {\n ComposeCompilePartialData,\n ComposeCompileRequest,\n ComposeCompileResult,\n ComposeCompileSuccessData,\n ComposeManifest,\n ComposeRouteRequest,\n SimulateRequest,\n SimulateResult,\n} from '@lifi/compose-spec';\nimport {\n checkCompatibility,\n COMPOSE_VERSION_HEADER,\n COMPOSER_SDK_VERSION_HEADER,\n parseVersion,\n} from '@lifi/compose-spec';\n\nimport type {\n ChainSummary,\n GetZapPacksOptions,\n ZapPackOverview,\n} from './discovery.js';\nimport { ComposeError, errorFromHttpResponse } from './errors.js';\nimport {\n parseCompilePartialEnvelope,\n parseSimulateResult,\n} from './responseSchemas.js';\n\n// __SDK_VERSION__ is a compile-time constant injected by tsdown (via `define` in tsdown.config.ts)\n// and by vitest (via `define` in vitest.config.ts). Both read the version from package.json\n// at build/test time and replace this identifier with the literal string value.\n// It is sent as the `COMPOSER_SDK_VERSION_HEADER` request header so the server can identify the caller.\n// Falls back to 'dev' when running via tsx without tsdown substitution (e.g. the example harness).\ndeclare const __SDK_VERSION__: string;\n\nconst SDK_VERSION: string =\n typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';\n// `undefined` for the `dev` sentinel, which disables the response version check.\nconst SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);\n\n/**\n * Configuration for creating a low-level Compose API client.\n */\nexport interface ComposeClientOptions {\n /** Base URL of the Compose API. */\n readonly baseUrl: string;\n /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** LI.FI API key, sent as the `x-lifi-api-key` header on every request. Required — the Compose API rejects unauthenticated requests. */\n readonly apiKey: string;\n}\n\n/**\n * Low-level HTTP client for the Compose API.\n *\n * Handles request serialization, SDK version headers, and error mapping.\n * Prefer using {@link ComposeSdk} for the full builder experience. Use this\n * directly when you need to decouple request building from submission — e.g.\n * build via `sdk.request()` then submit via `client.compile()` with custom\n * retry logic or request inspection.\n */\nexport interface ComposeClient {\n /**\n * Fetches the server's operation manifest describing all supported operations,\n * guards, materialisers, and preconditions.\n * @returns The manifest document.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly getManifest: () => Promise<ComposeManifest>;\n /**\n * Fetches the chains the Compose API supports, ascending by `chainId`.\n *\n * A chain is listed once the server confirms the VM contract is deployed on\n * it — the same condition {@link compile}, {@link route} and\n * {@link simulate} validate against. A chain absent from this list is\n * rejected outright by those endpoints.\n *\n * The converse does not hold: a listed chain can still have no routing edge\n * for a given token pair, which {@link route} reports as `no_route_error`.\n * Use {@link getZapPacks} to enumerate the pairs that do route.\n *\n * Each entry carries a display `name` alongside its `chainId`. The name is a\n * deployment-maintained label — render it, but key on `chainId`.\n *\n * The list tracks the server's chain registry, which refreshes periodically.\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @returns The supported chains, ascending by `chainId`.\n * @throws {@link ComposeError} on network or server errors.\n */\n readonly getChains: () => Promise<readonly ChainSummary[]>;\n /**\n * Submits a compile request and returns the result.\n *\n * When the caller passes `simulationPolicy: 'allow-revert'` and the transaction\n * reverts in simulation, the server responds with HTTP 206 and the SDK returns a\n * partial result (`status: 'partial'`) instead of throwing. The partial result\n * includes the transaction (without `gasLimit`) and revert diagnostics.\n *\n * @param request - The full compile request including flow and run inputs.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly compile: (\n request: ComposeCompileRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Compiles a token pair into a submit-ready transaction via\n * `POST /compose/route`.\n *\n * A convenience path that skips flow authoring: the server builds a one-node\n * zap flow with a `directDeposit` input from the `fromToken` / `toToken`\n * pair and runs it through the same pipeline as {@link compile}. The\n * response is therefore the same discriminated result — HTTP 206 under\n * `simulationPolicy: 'allow-revert'` yields `status: 'partial'` rather than\n * throwing, exactly as it does for {@link compile}.\n *\n * The server plans a short chain of catalog edges for the pair (a position\n * entry or exit, a wrap or unwrap, a mint or burn), optionally opened by a\n * `lifi.swap`. {@link getZapPacks} lists the single edges; the pairs the\n * planner connects are their closure up to the hop budget, which\n * {@link buildReachability} computes locally. A pair the planner cannot\n * connect is rejected with `kind: 'no_route_error'`.\n *\n * Only synchronous edges are considered unless the request sets\n * `allowAsyncRoutes: true`, so by default every terminal output comes back\n * `delivery.when: 'now'` and a pair reachable only after settlement is a\n * `no_route_error`. Opting in adds the `lifi.zapAsync` fallback, which still\n * loses to a synchronous edge when the pair has one; read\n * `outputs[*].delivery.when` to see which representation you got.\n *\n * Use {@link compile} for what the server does not author for you: several\n * chained operations, splits, explicit preconditions, or per-op guards.\n *\n * `bigint` amounts in `amount` are serialised to decimal strings\n * automatically.\n *\n * @param request - The from/to token pair, amount, signer, and route options.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors —\n * including `NOT_FOUND` (HTTP 404) when the planner cannot connect the pair.\n */\n readonly route: (\n request: ComposeRouteRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Fetches the available routing edges grouped by protocol.\n *\n * The edge catalog is dynamic — it reflects the current state of the\n * backend's routing snapshot (protocols, chains, token blacklists).\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @param options - Optional filter to restrict results to specific protocols.\n * @returns An array of {@link ZapPackOverview} objects, one per protocol.\n * @throws {@link ComposeError} on network or server errors (503 when the\n * routing catalog is not yet initialized).\n */\n readonly getZapPacks: (\n options?: GetZapPacksOptions,\n ) => Promise<readonly ZapPackOverview[]>;\n /**\n * Simulates a raw, pre-encoded transaction against `POST /simulate` and\n * reports how the watched balances change and how much inner-call gas it\n * burns.\n *\n * The result is a discriminated union on `status`:\n * - `'ok'` — the simulation ran successfully; `deltas`/`gasUsed` are populated.\n * - `'revert'` — the simulation ran but the transaction reverted on-chain. A\n * revert is a *successful simulation*, not a transport error, so it is\n * returned (HTTP 200) rather than thrown — mirroring how {@link compile}\n * returns `status: 'partial'` on a simulated revert.\n * - `'error'` — the request was well-formed but the simulation could not be\n * set up or run (HTTP 422); `message` is intentionally generic.\n *\n * `bigint` amounts in the request (`value`, requirement `balance`/`allowance`)\n * are serialised to decimal strings automatically.\n *\n * @param request - The raw transaction plus funding `requirements` and the\n * `trackedBalances` to watch.\n * @returns A {@link SimulateResult} (`ok` / `revert` / `error`).\n * @throws {@link ComposeError} on network failures, HTTP 400 (malformed\n * input), 401/403 (auth), 404, 429, and 5xx.\n */\n readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;\n}\n\n/**\n * `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.\n *\n * The compose wire carries token amounts as decimal strings, and `bigint` has\n * no JSON representation, so every payload this client sends goes through it.\n * Exported so the example harness prints a request exactly as it is sent.\n */\nexport const bigintReplacer = (_key: string, value: unknown): unknown =>\n typeof value === 'bigint' ? value.toString() : value;\n\nconst isNonNullObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null;\n\n// Non-negative decimal integer string, as the server sends token amounts.\nconst INTEGER_STRING = /^\\d+$/;\n\nconst decodeAmount = (value: unknown, path: string, url: string): bigint => {\n if (typeof value !== 'string' || !INTEGER_STRING.test(value)) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} is ${\n value === undefined ? 'absent' : 'not an integer string'\n }`,\n { url },\n );\n }\n return BigInt(value);\n};\n\n// The two records this seam reads out of a compile-family response body. Each\n// is `unknown` because the seam is what establishes its shape: it validates\n// every field before writing a decoded `bigint` back over the wire string.\ninterface WireAmountBody {\n outputs?: unknown;\n epilogue?: unknown;\n}\n\n// One `Amount` as it arrives: each leaf is `unknown` until this seam has\n// checked it, and is overwritten in place with the decoded `bigint`.\ninterface WireAmount {\n estimate?: unknown;\n minimum?: unknown;\n}\n\nconst malformed = (path: string, what: string, url: string): ComposeError =>\n new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} ${what}`,\n { url },\n );\n\n// Decode one `Estimate` / `Minimum` leaf in place: `{ basis, value }` with\n// `value` a decimal string on the wire and a `bigint` on `Amount`.\nconst decodeAmountLeaf = (\n amount: WireAmount,\n field: 'estimate' | 'minimum',\n path: string,\n url: string,\n): void => {\n const leaf = amount[field];\n if (leaf === undefined) return;\n if (!isNonNullObject(leaf)) {\n throw malformed(`${path}.${field}`, 'is not an object', url);\n }\n leaf.value = decodeAmount(leaf.value, `${path}.${field}.value`, url);\n};\n\n// `outputs` is REQUIRED on every compile-family body and is the record a\n// caller reads amounts from, so its structure is checked here — the body is\n// cast, not schema-parsed — before each `value` leaf becomes a `bigint`.\nconst decodeOutputs = (data: WireAmountBody, url: string): void => {\n const outputs = data.outputs;\n if (!isNonNullObject(outputs)) {\n throw malformed(\n 'outputs',\n outputs === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n for (const key of Object.keys(outputs)) {\n const entry = outputs[key];\n if (!isNonNullObject(entry)) {\n throw malformed(`outputs.${key}`, 'is not an object', url);\n }\n const amount = entry.amount;\n if (!isNonNullObject(amount)) {\n throw malformed(\n `outputs.${key}.amount`,\n amount === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n decodeAmountLeaf(amount, 'estimate', `outputs.${key}.amount`, url);\n decodeAmountLeaf(amount, 'minimum', `outputs.${key}.amount`, url);\n }\n};\n\n// `epilogue` is optional (absent without an epilogue, and on every partial);\n// when present, each named forward's `amount` is a decimal string on the wire.\nconst decodeEpilogue = (data: WireAmountBody, url: string): void => {\n const epilogue = data.epilogue;\n if (epilogue === undefined) return;\n if (!isNonNullObject(epilogue) || !Array.isArray(epilogue.forwards)) {\n throw malformed('epilogue', 'is not an object with forwards', url);\n }\n const forwards: readonly unknown[] = epilogue.forwards;\n forwards.forEach((forward, index) => {\n if (!isNonNullObject(forward)) {\n throw malformed(`epilogue.forwards[${index}]`, 'is not an object', url);\n }\n forward.amount = decodeAmount(\n forward.amount,\n `epilogue.forwards[${index}].amount`,\n url,\n );\n });\n};\n\n// THE decode seam for a compile-family response body: every amount the wire\n// renders as a decimal string becomes the `bigint` its declared type promises.\n// The amount records are handled here so the body is narrowed once — a\n// non-object body is not this function's error to raise, since the enveloping\n// parse already rejected it.\nconst decodeWireAmounts = (data: unknown, url: string): void => {\n if (!isNonNullObject(data)) return;\n decodeOutputs(data, url);\n decodeEpilogue(data, url);\n};\n\nconst parseBody = async <T>(res: Response, url: string): Promise<T> => {\n const body = await res.json().catch((_) => null);\n if (!isNonNullObject(body) || !('data' in body)) {\n throw new ComposeError('UNKNOWN_ERROR', 'Unexpected response format', {\n url,\n });\n }\n return body.data as T;\n};\n\nconst parseCompileSuccessBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const data = await parseBody<ComposeCompileSuccessData>(res, url);\n decodeWireAmounts(data, url);\n return { ...data, status: 'success' as const };\n};\n\nconst parsePartialBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const body = await res.json().catch((_) => null);\n const envelope = parseCompilePartialEnvelope(body);\n if (envelope === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected partial response format',\n { url },\n );\n }\n // `data` is validated as an object by the schema; compose-spec owns its full\n // shape as a hand-authored type, so we narrow it here rather than re-declaring\n // that type as a schema. `error` is fully validated — no cast needed.\n const data = envelope.data as ComposeCompilePartialData;\n decodeWireAmounts(data, url);\n return { ...data, status: 'partial' as const, error: envelope.error };\n};\n\n// `/simulate` is un-enveloped: the discriminated body (`{ status, ... }`) is at\n// the top level, NOT wrapped in `{ data }` like `/compose`. So this reads the\n// body directly and validates it against the simulate union rather than reusing\n// `parseBody`.\nconst parseSimulateBody = async (\n res: Response,\n url: string,\n): Promise<SimulateResult> => {\n const body = await res.json().catch((_) => null);\n const result = parseSimulateResult(body);\n if (result === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected simulate response format',\n { url },\n );\n }\n return result;\n};\n// `POST /compose` and `POST /compose/route` differ only in path and request\n// shape: both return the enveloped success data on 200 and the partial\n// envelope on 206, so they share one transport.\ntype ComposeTransport = {\n readonly fetchFn: typeof fetch;\n readonly baseHeaders: Record<string, string>;\n};\n\n// A patch bump is additive and a minor bump is breaking, so a 200 from a\n// backend behind this SDK cannot be trusted: the newer flow this SDK authored\n// may have compiled under the server's older semantics without raising an\n// error. This covers the deploy window where the SDK is published to npm ahead\n// of the backend rollout. Only the compile-family POSTs are checked; discovery\n// GETs stay reachable from a mismatched SDK, as on the server. An absent header\n// (a pre-gate backend, a header-stripping proxy) is accepted, mirroring the\n// server's pass-through of a compile that carries no version header.\nconst assertServerVersion = (res: Response, url: string): void => {\n const serverVersion = res.headers.get(COMPOSE_VERSION_HEADER);\n if (serverVersion === null || SDK_VERSION_TRIPLE === undefined) return;\n const serverTriple = parseVersion(serverVersion);\n if (serverTriple === undefined) return;\n\n const compatibility = checkCompatibility(SDK_VERSION_TRIPLE, serverTriple);\n if (compatibility === 'compatible') return;\n\n const versions = { sdkVersion: SDK_VERSION, serverVersion };\n if (compatibility === 'sdk_outdated') {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `composer-sdk ${SDK_VERSION} is older than the compose contract served at ${url} (${serverVersion}); upgrade @lifi/composer-sdk and @lifi/compose-spec`,\n {\n status: res.status,\n url,\n kind: 'sdk_outdated',\n sdkOutdated: {\n ...versions,\n minimumSdkVersion: `${serverTriple.major}.${serverTriple.minor}.0`,\n },\n },\n );\n }\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `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`,\n {\n status: res.status,\n url,\n kind: 'server_outdated',\n serverOutdated: versions,\n },\n );\n};\n\ntype SendInit =\n | { readonly method: 'GET' }\n | { readonly method: 'POST'; readonly body: unknown };\n\n// GETs on this API carry no body; POSTs carry a JSON one with `bigint` amounts\n// rendered as decimal strings.\nconst requestInit = (\n baseHeaders: Record<string, string>,\n init: SendInit,\n): RequestInit =>\n init.method === 'GET'\n ? { method: 'GET', headers: { ...baseHeaders } }\n : {\n method: 'POST',\n headers: { ...baseHeaders, 'Content-Type': 'application/json' },\n body: JSON.stringify(init.body, bigintReplacer),\n };\n\n// One fetch path for every route: maps transport failures to `NETWORK_ERROR`.\n// The response version check is not here — only the compile-family POSTs gate\n// on it (see `postCompile`); discovery GETs and `/simulate` are ungated.\nconst send = async (\n { fetchFn, baseHeaders }: ComposeTransport,\n url: string,\n init: SendInit,\n): Promise<Response> => {\n try {\n return await fetchFn(url, requestInit(baseHeaders, init));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new ComposeError('NETWORK_ERROR', message, { cause: err });\n }\n};\n\nconst postCompile = async (\n transport: ComposeTransport,\n url: string,\n request: ComposeCompileRequest | ComposeRouteRequest,\n): Promise<ComposeCompileResult> => {\n const res = await send(transport, url, { method: 'POST', body: request });\n assertServerVersion(res, url);\n if (res.status === 206) {\n return await parsePartialBody(res, url);\n }\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseCompileSuccessBody(res, url);\n};\n\n// Every enveloped GET on this API behaves identically: no body, no query\n// semantics of its own, `{ data }` on success, an error envelope otherwise.\nconst getJson = async <T>(\n transport: ComposeTransport,\n url: string,\n): Promise<T> => {\n const res = await send(transport, url, { method: 'GET' });\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseBody<T>(res, url);\n};\n\n/**\n * Creates a low-level Compose API client.\n *\n * @param options - Client configuration including the API base URL.\n * @returns A {@link ComposeClient} instance.\n */\nexport const createComposeClient = (\n options: ComposeClientOptions,\n): ComposeClient => {\n if (!options.baseUrl || !/^https?:\\/\\//i.test(options.baseUrl)) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `Invalid baseUrl: expected an HTTP(S) URL, got \"${options.baseUrl}\"`,\n );\n }\n const trimmedApiKey = options.apiKey?.trim() || undefined;\n if (!trimmedApiKey) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n 'apiKey is required: pass a LI.FI API key to createComposeSdk().',\n );\n }\n const fetchFn = options.fetch ?? globalThis.fetch;\n const base = options.baseUrl.replace(/\\/$/, '');\n\n const baseHeaders: Record<string, string> = {\n Accept: 'application/json',\n [COMPOSER_SDK_VERSION_HEADER]: SDK_VERSION,\n 'x-lifi-api-key': trimmedApiKey,\n };\n const transport: ComposeTransport = { fetchFn, baseHeaders };\n\n const getManifest = async (): Promise<ComposeManifest> =>\n getJson<ComposeManifest>(transport, `${base}/compose/manifest`);\n\n const getChains = async (): Promise<readonly ChainSummary[]> =>\n getJson<readonly ChainSummary[]>(transport, `${base}/chains`);\n\n const compile = async (\n request: ComposeCompileRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose`, request);\n\n const route = async (\n request: ComposeRouteRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose/route`, request);\n\n const getZapPacks = async (\n options?: GetZapPacksOptions,\n ): Promise<readonly ZapPackOverview[]> => {\n const params = new URLSearchParams();\n if (options?.protocols !== undefined) {\n // Backend expects a single comma-separated value, not repeated keys.\n const raw = options.protocols;\n const list = typeof raw === 'string' ? raw : raw.join(',');\n params.set('protocols', list);\n }\n const qs = params.toString();\n return getJson<readonly ZapPackOverview[]>(\n transport,\n `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`,\n );\n };\n\n const simulate = async (\n request: SimulateRequest,\n ): Promise<SimulateResult> => {\n const url = `${base}/simulate`;\n const res = await send(transport, url, { method: 'POST', body: request });\n // Only 200 (carries `ok`/`revert`) and 422 (carries the `error` member of\n // the union) have a discriminated body. 422 is deliberately intercepted\n // here (not thrown as VALIDATION_ERROR) so callers get one exhaustive\n // `switch (result.status)`. Every other status is a transport error and is\n // thrown — including HTTP 400 (malformed input, no `status` body) and any\n // unexpected 2xx.\n if (res.status === 200 || res.status === 422) {\n return await parseSimulateBody(res, url);\n }\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n };\n\n return { getManifest, getChains, compile, route, getZapPacks, simulate };\n};\n"],"mappings":"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"}
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@ import { parseCompilePartialEnvelope, parseSimulateResult } from "./responseSche
2
2
  import { ComposeError, errorFromHttpResponse } from "./errors.js";
3
3
  import { COMPOSER_SDK_VERSION_HEADER, COMPOSE_VERSION_HEADER, checkCompatibility, parseVersion } from "@lifi/compose-spec";
4
4
  //#region src/client.ts
5
- const SDK_VERSION = "0.10.0";
5
+ const SDK_VERSION = "0.12.0";
6
6
  const SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);
7
7
  /**
8
8
  * `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.
@@ -37,9 +37,19 @@ const decodeOutputs = (data, url) => {
37
37
  decodeAmountLeaf(amount, "minimum", `outputs.${key}.amount`, url);
38
38
  }
39
39
  };
40
+ const decodeEpilogue = (data, url) => {
41
+ const epilogue = data.epilogue;
42
+ if (epilogue === void 0) return;
43
+ if (!isNonNullObject(epilogue) || !Array.isArray(epilogue.forwards)) throw malformed("epilogue", "is not an object with forwards", url);
44
+ epilogue.forwards.forEach((forward, index) => {
45
+ if (!isNonNullObject(forward)) throw malformed(`epilogue.forwards[${index}]`, "is not an object", url);
46
+ forward.amount = decodeAmount(forward.amount, `epilogue.forwards[${index}].amount`, url);
47
+ });
48
+ };
40
49
  const decodeWireAmounts = (data, url) => {
41
50
  if (!isNonNullObject(data)) return;
42
51
  decodeOutputs(data, url);
52
+ decodeEpilogue(data, url);
43
53
  };
44
54
  const parseBody = async (res, url) => {
45
55
  const body = await res.json().catch((_) => null);
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n ComposeCompilePartialData,\n ComposeCompileRequest,\n ComposeCompileResult,\n ComposeCompileSuccessData,\n ComposeManifest,\n ComposeRouteRequest,\n SimulateRequest,\n SimulateResult,\n} from '@lifi/compose-spec';\nimport {\n checkCompatibility,\n COMPOSE_VERSION_HEADER,\n COMPOSER_SDK_VERSION_HEADER,\n parseVersion,\n} from '@lifi/compose-spec';\n\nimport type {\n ChainSummary,\n GetZapPacksOptions,\n ZapPackOverview,\n} from './discovery.js';\nimport { ComposeError, errorFromHttpResponse } from './errors.js';\nimport {\n parseCompilePartialEnvelope,\n parseSimulateResult,\n} from './responseSchemas.js';\n\n// __SDK_VERSION__ is a compile-time constant injected by tsdown (via `define` in tsdown.config.ts)\n// and by vitest (via `define` in vitest.config.ts). Both read the version from package.json\n// at build/test time and replace this identifier with the literal string value.\n// It is sent as the `COMPOSER_SDK_VERSION_HEADER` request header so the server can identify the caller.\n// Falls back to 'dev' when running via tsx without tsdown substitution (e.g. the example harness).\ndeclare const __SDK_VERSION__: string;\n\nconst SDK_VERSION: string =\n typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';\n// `undefined` for the `dev` sentinel, which disables the response version check.\nconst SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);\n\n/**\n * Configuration for creating a low-level Compose API client.\n */\nexport interface ComposeClientOptions {\n /** Base URL of the Compose API. */\n readonly baseUrl: string;\n /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** LI.FI API key, sent as the `x-lifi-api-key` header on every request. Required — the Compose API rejects unauthenticated requests. */\n readonly apiKey: string;\n}\n\n/**\n * Low-level HTTP client for the Compose API.\n *\n * Handles request serialization, SDK version headers, and error mapping.\n * Prefer using {@link ComposeSdk} for the full builder experience. Use this\n * directly when you need to decouple request building from submission — e.g.\n * build via `sdk.request()` then submit via `client.compile()` with custom\n * retry logic or request inspection.\n */\nexport interface ComposeClient {\n /**\n * Fetches the server's operation manifest describing all supported operations,\n * guards, materialisers, and preconditions.\n * @returns The manifest document.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly getManifest: () => Promise<ComposeManifest>;\n /**\n * Fetches the chains the Compose API supports, ascending by `chainId`.\n *\n * A chain is listed once the server confirms the VM contract is deployed on\n * it — the same condition {@link compile}, {@link route} and\n * {@link simulate} validate against. A chain absent from this list is\n * rejected outright by those endpoints.\n *\n * The converse does not hold: a listed chain can still have no routing edge\n * for a given token pair, which {@link route} reports as `no_route_error`.\n * Use {@link getZapPacks} to enumerate the pairs that do route.\n *\n * Each entry carries a display `name` alongside its `chainId`. The name is a\n * deployment-maintained label — render it, but key on `chainId`.\n *\n * The list tracks the server's chain registry, which refreshes periodically.\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @returns The supported chains, ascending by `chainId`.\n * @throws {@link ComposeError} on network or server errors.\n */\n readonly getChains: () => Promise<readonly ChainSummary[]>;\n /**\n * Submits a compile request and returns the result.\n *\n * When the caller passes `simulationPolicy: 'allow-revert'` and the transaction\n * reverts in simulation, the server responds with HTTP 206 and the SDK returns a\n * partial result (`status: 'partial'`) instead of throwing. The partial result\n * includes the transaction (without `gasLimit`) and revert diagnostics.\n *\n * @param request - The full compile request including flow and run inputs.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly compile: (\n request: ComposeCompileRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Compiles a token pair into a submit-ready transaction via\n * `POST /compose/route`.\n *\n * A convenience path that skips flow authoring: the server builds a one-node\n * zap flow with a `directDeposit` input from the `fromToken` / `toToken`\n * pair and runs it through the same pipeline as {@link compile}. The\n * response is therefore the same discriminated result — HTTP 206 under\n * `simulationPolicy: 'allow-revert'` yields `status: 'partial'` rather than\n * throwing, exactly as it does for {@link compile}.\n *\n * The server plans a short chain of catalog edges for the pair (a position\n * entry or exit, a wrap or unwrap, a mint or burn), optionally opened by a\n * `lifi.swap`. {@link getZapPacks} lists the single edges; the pairs the\n * planner connects are their closure up to the hop budget, which\n * {@link buildReachability} computes locally. A pair the planner cannot\n * connect is rejected with `kind: 'no_route_error'`.\n *\n * Only synchronous edges are considered unless the request sets\n * `allowAsyncRoutes: true`, so by default every terminal output comes back\n * `delivery.when: 'now'` and a pair reachable only after settlement is a\n * `no_route_error`. Opting in adds the `lifi.zapAsync` fallback, which still\n * loses to a synchronous edge when the pair has one; read\n * `outputs[*].delivery.when` to see which representation you got.\n *\n * Use {@link compile} for what the server does not author for you: several\n * chained operations, splits, explicit preconditions, or per-op guards.\n *\n * `bigint` amounts in `amount` are serialised to decimal strings\n * automatically.\n *\n * @param request - The from/to token pair, amount, signer, and route options.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors —\n * including `NOT_FOUND` (HTTP 404) when the planner cannot connect the pair.\n */\n readonly route: (\n request: ComposeRouteRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Fetches the available routing edges grouped by protocol.\n *\n * The edge catalog is dynamic — it reflects the current state of the\n * backend's routing snapshot (protocols, chains, token blacklists).\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @param options - Optional filter to restrict results to specific protocols.\n * @returns An array of {@link ZapPackOverview} objects, one per protocol.\n * @throws {@link ComposeError} on network or server errors (503 when the\n * routing catalog is not yet initialized).\n */\n readonly getZapPacks: (\n options?: GetZapPacksOptions,\n ) => Promise<readonly ZapPackOverview[]>;\n /**\n * Simulates a raw, pre-encoded transaction against `POST /simulate` and\n * reports how the watched balances change and how much inner-call gas it\n * burns.\n *\n * The result is a discriminated union on `status`:\n * - `'ok'` — the simulation ran successfully; `deltas`/`gasUsed` are populated.\n * - `'revert'` — the simulation ran but the transaction reverted on-chain. A\n * revert is a *successful simulation*, not a transport error, so it is\n * returned (HTTP 200) rather than thrown — mirroring how {@link compile}\n * returns `status: 'partial'` on a simulated revert.\n * - `'error'` — the request was well-formed but the simulation could not be\n * set up or run (HTTP 422); `message` is intentionally generic.\n *\n * `bigint` amounts in the request (`value`, requirement `balance`/`allowance`)\n * are serialised to decimal strings automatically.\n *\n * @param request - The raw transaction plus funding `requirements` and the\n * `trackedBalances` to watch.\n * @returns A {@link SimulateResult} (`ok` / `revert` / `error`).\n * @throws {@link ComposeError} on network failures, HTTP 400 (malformed\n * input), 401/403 (auth), 404, 429, and 5xx.\n */\n readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;\n}\n\n/**\n * `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.\n *\n * The compose wire carries token amounts as decimal strings, and `bigint` has\n * no JSON representation, so every payload this client sends goes through it.\n * Exported so the example harness prints a request exactly as it is sent.\n */\nexport const bigintReplacer = (_key: string, value: unknown): unknown =>\n typeof value === 'bigint' ? value.toString() : value;\n\nconst isNonNullObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null;\n\n// Non-negative decimal integer string, as the server sends token amounts.\nconst INTEGER_STRING = /^\\d+$/;\n\nconst decodeAmount = (value: unknown, path: string, url: string): bigint => {\n if (typeof value !== 'string' || !INTEGER_STRING.test(value)) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} is ${\n value === undefined ? 'absent' : 'not an integer string'\n }`,\n { url },\n );\n }\n return BigInt(value);\n};\n\n// The two records this seam reads out of a compile-family response body. Each\n// is `unknown` because the seam is what establishes its shape: it validates\n// every field before writing a decoded `bigint` back over the wire string.\ninterface WireAmountBody {\n outputs?: unknown;\n}\n\n// One `Amount` as it arrives: each leaf is `unknown` until this seam has\n// checked it, and is overwritten in place with the decoded `bigint`.\ninterface WireAmount {\n estimate?: unknown;\n minimum?: unknown;\n}\n\nconst malformed = (path: string, what: string, url: string): ComposeError =>\n new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} ${what}`,\n { url },\n );\n\n// Decode one `Estimate` / `Minimum` leaf in place: `{ basis, value }` with\n// `value` a decimal string on the wire and a `bigint` on `Amount`.\nconst decodeAmountLeaf = (\n amount: WireAmount,\n field: 'estimate' | 'minimum',\n path: string,\n url: string,\n): void => {\n const leaf = amount[field];\n if (leaf === undefined) return;\n if (!isNonNullObject(leaf)) {\n throw malformed(`${path}.${field}`, 'is not an object', url);\n }\n leaf.value = decodeAmount(leaf.value, `${path}.${field}.value`, url);\n};\n\n// `outputs` is REQUIRED on every compile-family body and is the record a\n// caller reads amounts from, so its structure is checked here — the body is\n// cast, not schema-parsed — before each `value` leaf becomes a `bigint`.\nconst decodeOutputs = (data: WireAmountBody, url: string): void => {\n const outputs = data.outputs;\n if (!isNonNullObject(outputs)) {\n throw malformed(\n 'outputs',\n outputs === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n for (const key of Object.keys(outputs)) {\n const entry = outputs[key];\n if (!isNonNullObject(entry)) {\n throw malformed(`outputs.${key}`, 'is not an object', url);\n }\n const amount = entry.amount;\n if (!isNonNullObject(amount)) {\n throw malformed(\n `outputs.${key}.amount`,\n amount === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n decodeAmountLeaf(amount, 'estimate', `outputs.${key}.amount`, url);\n decodeAmountLeaf(amount, 'minimum', `outputs.${key}.amount`, url);\n }\n};\n\n// THE decode seam for a compile-family response body: every amount the wire\n// renders as a decimal string becomes the `bigint` its declared type promises.\n// The amount record is handled here so the body is narrowed once — a\n// non-object body is not this function's error to raise, since the enveloping\n// parse already rejected it.\nconst decodeWireAmounts = (data: unknown, url: string): void => {\n if (!isNonNullObject(data)) return;\n decodeOutputs(data, url);\n};\n\nconst parseBody = async <T>(res: Response, url: string): Promise<T> => {\n const body = await res.json().catch((_) => null);\n if (!isNonNullObject(body) || !('data' in body)) {\n throw new ComposeError('UNKNOWN_ERROR', 'Unexpected response format', {\n url,\n });\n }\n return body.data as T;\n};\n\nconst parseCompileSuccessBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const data = await parseBody<ComposeCompileSuccessData>(res, url);\n decodeWireAmounts(data, url);\n return { ...data, status: 'success' as const };\n};\n\nconst parsePartialBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const body = await res.json().catch((_) => null);\n const envelope = parseCompilePartialEnvelope(body);\n if (envelope === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected partial response format',\n { url },\n );\n }\n // `data` is validated as an object by the schema; compose-spec owns its full\n // shape as a hand-authored type, so we narrow it here rather than re-declaring\n // that type as a schema. `error` is fully validated — no cast needed.\n const data = envelope.data as ComposeCompilePartialData;\n decodeWireAmounts(data, url);\n return { ...data, status: 'partial' as const, error: envelope.error };\n};\n\n// `/simulate` is un-enveloped: the discriminated body (`{ status, ... }`) is at\n// the top level, NOT wrapped in `{ data }` like `/compose`. So this reads the\n// body directly and validates it against the simulate union rather than reusing\n// `parseBody`.\nconst parseSimulateBody = async (\n res: Response,\n url: string,\n): Promise<SimulateResult> => {\n const body = await res.json().catch((_) => null);\n const result = parseSimulateResult(body);\n if (result === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected simulate response format',\n { url },\n );\n }\n return result;\n};\n// `POST /compose` and `POST /compose/route` differ only in path and request\n// shape: both return the enveloped success data on 200 and the partial\n// envelope on 206, so they share one transport.\ntype ComposeTransport = {\n readonly fetchFn: typeof fetch;\n readonly baseHeaders: Record<string, string>;\n};\n\n// A patch bump is additive and a minor bump is breaking, so a 200 from a\n// backend behind this SDK cannot be trusted: the newer flow this SDK authored\n// may have compiled under the server's older semantics without raising an\n// error. This covers the deploy window where the SDK is published to npm ahead\n// of the backend rollout. Only the compile-family POSTs are checked; discovery\n// GETs stay reachable from a mismatched SDK, as on the server. An absent header\n// (a pre-gate backend, a header-stripping proxy) is accepted, mirroring the\n// server's pass-through of a compile that carries no version header.\nconst assertServerVersion = (res: Response, url: string): void => {\n const serverVersion = res.headers.get(COMPOSE_VERSION_HEADER);\n if (serverVersion === null || SDK_VERSION_TRIPLE === undefined) return;\n const serverTriple = parseVersion(serverVersion);\n if (serverTriple === undefined) return;\n\n const compatibility = checkCompatibility(SDK_VERSION_TRIPLE, serverTriple);\n if (compatibility === 'compatible') return;\n\n const versions = { sdkVersion: SDK_VERSION, serverVersion };\n if (compatibility === 'sdk_outdated') {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `composer-sdk ${SDK_VERSION} is older than the compose contract served at ${url} (${serverVersion}); upgrade @lifi/composer-sdk and @lifi/compose-spec`,\n {\n status: res.status,\n url,\n kind: 'sdk_outdated',\n sdkOutdated: {\n ...versions,\n minimumSdkVersion: `${serverTriple.major}.${serverTriple.minor}.0`,\n },\n },\n );\n }\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `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`,\n {\n status: res.status,\n url,\n kind: 'server_outdated',\n serverOutdated: versions,\n },\n );\n};\n\ntype SendInit =\n | { readonly method: 'GET' }\n | { readonly method: 'POST'; readonly body: unknown };\n\n// GETs on this API carry no body; POSTs carry a JSON one with `bigint` amounts\n// rendered as decimal strings.\nconst requestInit = (\n baseHeaders: Record<string, string>,\n init: SendInit,\n): RequestInit =>\n init.method === 'GET'\n ? { method: 'GET', headers: { ...baseHeaders } }\n : {\n method: 'POST',\n headers: { ...baseHeaders, 'Content-Type': 'application/json' },\n body: JSON.stringify(init.body, bigintReplacer),\n };\n\n// One fetch path for every route: maps transport failures to `NETWORK_ERROR`.\n// The response version check is not here — only the compile-family POSTs gate\n// on it (see `postCompile`); discovery GETs and `/simulate` are ungated.\nconst send = async (\n { fetchFn, baseHeaders }: ComposeTransport,\n url: string,\n init: SendInit,\n): Promise<Response> => {\n try {\n return await fetchFn(url, requestInit(baseHeaders, init));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new ComposeError('NETWORK_ERROR', message, { cause: err });\n }\n};\n\nconst postCompile = async (\n transport: ComposeTransport,\n url: string,\n request: ComposeCompileRequest | ComposeRouteRequest,\n): Promise<ComposeCompileResult> => {\n const res = await send(transport, url, { method: 'POST', body: request });\n assertServerVersion(res, url);\n if (res.status === 206) {\n return await parsePartialBody(res, url);\n }\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseCompileSuccessBody(res, url);\n};\n\n// Every enveloped GET on this API behaves identically: no body, no query\n// semantics of its own, `{ data }` on success, an error envelope otherwise.\nconst getJson = async <T>(\n transport: ComposeTransport,\n url: string,\n): Promise<T> => {\n const res = await send(transport, url, { method: 'GET' });\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseBody<T>(res, url);\n};\n\n/**\n * Creates a low-level Compose API client.\n *\n * @param options - Client configuration including the API base URL.\n * @returns A {@link ComposeClient} instance.\n */\nexport const createComposeClient = (\n options: ComposeClientOptions,\n): ComposeClient => {\n if (!options.baseUrl || !/^https?:\\/\\//i.test(options.baseUrl)) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `Invalid baseUrl: expected an HTTP(S) URL, got \"${options.baseUrl}\"`,\n );\n }\n const trimmedApiKey = options.apiKey?.trim() || undefined;\n if (!trimmedApiKey) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n 'apiKey is required: pass a LI.FI API key to createComposeSdk().',\n );\n }\n const fetchFn = options.fetch ?? globalThis.fetch;\n const base = options.baseUrl.replace(/\\/$/, '');\n\n const baseHeaders: Record<string, string> = {\n Accept: 'application/json',\n [COMPOSER_SDK_VERSION_HEADER]: SDK_VERSION,\n 'x-lifi-api-key': trimmedApiKey,\n };\n const transport: ComposeTransport = { fetchFn, baseHeaders };\n\n const getManifest = async (): Promise<ComposeManifest> =>\n getJson<ComposeManifest>(transport, `${base}/compose/manifest`);\n\n const getChains = async (): Promise<readonly ChainSummary[]> =>\n getJson<readonly ChainSummary[]>(transport, `${base}/chains`);\n\n const compile = async (\n request: ComposeCompileRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose`, request);\n\n const route = async (\n request: ComposeRouteRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose/route`, request);\n\n const getZapPacks = async (\n options?: GetZapPacksOptions,\n ): Promise<readonly ZapPackOverview[]> => {\n const params = new URLSearchParams();\n if (options?.protocols !== undefined) {\n // Backend expects a single comma-separated value, not repeated keys.\n const raw = options.protocols;\n const list = typeof raw === 'string' ? raw : raw.join(',');\n params.set('protocols', list);\n }\n const qs = params.toString();\n return getJson<readonly ZapPackOverview[]>(\n transport,\n `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`,\n );\n };\n\n const simulate = async (\n request: SimulateRequest,\n ): Promise<SimulateResult> => {\n const url = `${base}/simulate`;\n const res = await send(transport, url, { method: 'POST', body: request });\n // Only 200 (carries `ok`/`revert`) and 422 (carries the `error` member of\n // the union) have a discriminated body. 422 is deliberately intercepted\n // here (not thrown as VALIDATION_ERROR) so callers get one exhaustive\n // `switch (result.status)`. Every other status is a transport error and is\n // thrown — including HTTP 400 (malformed input, no `status` body) and any\n // unexpected 2xx.\n if (res.status === 200 || res.status === 422) {\n return await parseSimulateBody(res, url);\n }\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n };\n\n return { getManifest, getChains, compile, route, getZapPacks, simulate };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type {\n ComposeCompilePartialData,\n ComposeCompileRequest,\n ComposeCompileResult,\n ComposeCompileSuccessData,\n ComposeManifest,\n ComposeRouteRequest,\n SimulateRequest,\n SimulateResult,\n} from '@lifi/compose-spec';\nimport {\n checkCompatibility,\n COMPOSE_VERSION_HEADER,\n COMPOSER_SDK_VERSION_HEADER,\n parseVersion,\n} from '@lifi/compose-spec';\n\nimport type {\n ChainSummary,\n GetZapPacksOptions,\n ZapPackOverview,\n} from './discovery.js';\nimport { ComposeError, errorFromHttpResponse } from './errors.js';\nimport {\n parseCompilePartialEnvelope,\n parseSimulateResult,\n} from './responseSchemas.js';\n\n// __SDK_VERSION__ is a compile-time constant injected by tsdown (via `define` in tsdown.config.ts)\n// and by vitest (via `define` in vitest.config.ts). Both read the version from package.json\n// at build/test time and replace this identifier with the literal string value.\n// It is sent as the `COMPOSER_SDK_VERSION_HEADER` request header so the server can identify the caller.\n// Falls back to 'dev' when running via tsx without tsdown substitution (e.g. the example harness).\ndeclare const __SDK_VERSION__: string;\n\nconst SDK_VERSION: string =\n typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';\n// `undefined` for the `dev` sentinel, which disables the response version check.\nconst SDK_VERSION_TRIPLE = parseVersion(SDK_VERSION);\n\n/**\n * Configuration for creating a low-level Compose API client.\n */\nexport interface ComposeClientOptions {\n /** Base URL of the Compose API. */\n readonly baseUrl: string;\n /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** LI.FI API key, sent as the `x-lifi-api-key` header on every request. Required — the Compose API rejects unauthenticated requests. */\n readonly apiKey: string;\n}\n\n/**\n * Low-level HTTP client for the Compose API.\n *\n * Handles request serialization, SDK version headers, and error mapping.\n * Prefer using {@link ComposeSdk} for the full builder experience. Use this\n * directly when you need to decouple request building from submission — e.g.\n * build via `sdk.request()` then submit via `client.compile()` with custom\n * retry logic or request inspection.\n */\nexport interface ComposeClient {\n /**\n * Fetches the server's operation manifest describing all supported operations,\n * guards, materialisers, and preconditions.\n * @returns The manifest document.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly getManifest: () => Promise<ComposeManifest>;\n /**\n * Fetches the chains the Compose API supports, ascending by `chainId`.\n *\n * A chain is listed once the server confirms the VM contract is deployed on\n * it — the same condition {@link compile}, {@link route} and\n * {@link simulate} validate against. A chain absent from this list is\n * rejected outright by those endpoints.\n *\n * The converse does not hold: a listed chain can still have no routing edge\n * for a given token pair, which {@link route} reports as `no_route_error`.\n * Use {@link getZapPacks} to enumerate the pairs that do route.\n *\n * Each entry carries a display `name` alongside its `chainId`. The name is a\n * deployment-maintained label — render it, but key on `chainId`.\n *\n * The list tracks the server's chain registry, which refreshes periodically.\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @returns The supported chains, ascending by `chainId`.\n * @throws {@link ComposeError} on network or server errors.\n */\n readonly getChains: () => Promise<readonly ChainSummary[]>;\n /**\n * Submits a compile request and returns the result.\n *\n * When the caller passes `simulationPolicy: 'allow-revert'` and the transaction\n * reverts in simulation, the server responds with HTTP 206 and the SDK returns a\n * partial result (`status: 'partial'`) instead of throwing. The partial result\n * includes the transaction (without `gasLimit`) and revert diagnostics.\n *\n * @param request - The full compile request including flow and run inputs.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly compile: (\n request: ComposeCompileRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Compiles a token pair into a submit-ready transaction via\n * `POST /compose/route`.\n *\n * A convenience path that skips flow authoring: the server builds a one-node\n * zap flow with a `directDeposit` input from the `fromToken` / `toToken`\n * pair and runs it through the same pipeline as {@link compile}. The\n * response is therefore the same discriminated result — HTTP 206 under\n * `simulationPolicy: 'allow-revert'` yields `status: 'partial'` rather than\n * throwing, exactly as it does for {@link compile}.\n *\n * The server plans a short chain of catalog edges for the pair (a position\n * entry or exit, a wrap or unwrap, a mint or burn), optionally opened by a\n * `lifi.swap`. {@link getZapPacks} lists the single edges; the pairs the\n * planner connects are their closure up to the hop budget, which\n * {@link buildReachability} computes locally. A pair the planner cannot\n * connect is rejected with `kind: 'no_route_error'`.\n *\n * Only synchronous edges are considered unless the request sets\n * `allowAsyncRoutes: true`, so by default every terminal output comes back\n * `delivery.when: 'now'` and a pair reachable only after settlement is a\n * `no_route_error`. Opting in adds the `lifi.zapAsync` fallback, which still\n * loses to a synchronous edge when the pair has one; read\n * `outputs[*].delivery.when` to see which representation you got.\n *\n * Use {@link compile} for what the server does not author for you: several\n * chained operations, splits, explicit preconditions, or per-op guards.\n *\n * `bigint` amounts in `amount` are serialised to decimal strings\n * automatically.\n *\n * @param request - The from/to token pair, amount, signer, and route options.\n * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.\n * @throws {@link ComposeError} on network, validation, or server errors —\n * including `NOT_FOUND` (HTTP 404) when the planner cannot connect the pair.\n */\n readonly route: (\n request: ComposeRouteRequest,\n ) => Promise<ComposeCompileResult>;\n /**\n * Fetches the available routing edges grouped by protocol.\n *\n * The edge catalog is dynamic — it reflects the current state of the\n * backend's routing snapshot (protocols, chains, token blacklists).\n * Results are not cached by the SDK; callers should cache as appropriate.\n *\n * @param options - Optional filter to restrict results to specific protocols.\n * @returns An array of {@link ZapPackOverview} objects, one per protocol.\n * @throws {@link ComposeError} on network or server errors (503 when the\n * routing catalog is not yet initialized).\n */\n readonly getZapPacks: (\n options?: GetZapPacksOptions,\n ) => Promise<readonly ZapPackOverview[]>;\n /**\n * Simulates a raw, pre-encoded transaction against `POST /simulate` and\n * reports how the watched balances change and how much inner-call gas it\n * burns.\n *\n * The result is a discriminated union on `status`:\n * - `'ok'` — the simulation ran successfully; `deltas`/`gasUsed` are populated.\n * - `'revert'` — the simulation ran but the transaction reverted on-chain. A\n * revert is a *successful simulation*, not a transport error, so it is\n * returned (HTTP 200) rather than thrown — mirroring how {@link compile}\n * returns `status: 'partial'` on a simulated revert.\n * - `'error'` — the request was well-formed but the simulation could not be\n * set up or run (HTTP 422); `message` is intentionally generic.\n *\n * `bigint` amounts in the request (`value`, requirement `balance`/`allowance`)\n * are serialised to decimal strings automatically.\n *\n * @param request - The raw transaction plus funding `requirements` and the\n * `trackedBalances` to watch.\n * @returns A {@link SimulateResult} (`ok` / `revert` / `error`).\n * @throws {@link ComposeError} on network failures, HTTP 400 (malformed\n * input), 401/403 (auth), 404, 429, and 5xx.\n */\n readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;\n}\n\n/**\n * `JSON.stringify` replacer that renders `bigint` amounts as decimal strings.\n *\n * The compose wire carries token amounts as decimal strings, and `bigint` has\n * no JSON representation, so every payload this client sends goes through it.\n * Exported so the example harness prints a request exactly as it is sent.\n */\nexport const bigintReplacer = (_key: string, value: unknown): unknown =>\n typeof value === 'bigint' ? value.toString() : value;\n\nconst isNonNullObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null;\n\n// Non-negative decimal integer string, as the server sends token amounts.\nconst INTEGER_STRING = /^\\d+$/;\n\nconst decodeAmount = (value: unknown, path: string, url: string): bigint => {\n if (typeof value !== 'string' || !INTEGER_STRING.test(value)) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} is ${\n value === undefined ? 'absent' : 'not an integer string'\n }`,\n { url },\n );\n }\n return BigInt(value);\n};\n\n// The two records this seam reads out of a compile-family response body. Each\n// is `unknown` because the seam is what establishes its shape: it validates\n// every field before writing a decoded `bigint` back over the wire string.\ninterface WireAmountBody {\n outputs?: unknown;\n epilogue?: unknown;\n}\n\n// One `Amount` as it arrives: each leaf is `unknown` until this seam has\n// checked it, and is overwritten in place with the decoded `bigint`.\ninterface WireAmount {\n estimate?: unknown;\n minimum?: unknown;\n}\n\nconst malformed = (path: string, what: string, url: string): ComposeError =>\n new ComposeError(\n 'UNKNOWN_ERROR',\n `Malformed compose response: ${path} ${what}`,\n { url },\n );\n\n// Decode one `Estimate` / `Minimum` leaf in place: `{ basis, value }` with\n// `value` a decimal string on the wire and a `bigint` on `Amount`.\nconst decodeAmountLeaf = (\n amount: WireAmount,\n field: 'estimate' | 'minimum',\n path: string,\n url: string,\n): void => {\n const leaf = amount[field];\n if (leaf === undefined) return;\n if (!isNonNullObject(leaf)) {\n throw malformed(`${path}.${field}`, 'is not an object', url);\n }\n leaf.value = decodeAmount(leaf.value, `${path}.${field}.value`, url);\n};\n\n// `outputs` is REQUIRED on every compile-family body and is the record a\n// caller reads amounts from, so its structure is checked here — the body is\n// cast, not schema-parsed — before each `value` leaf becomes a `bigint`.\nconst decodeOutputs = (data: WireAmountBody, url: string): void => {\n const outputs = data.outputs;\n if (!isNonNullObject(outputs)) {\n throw malformed(\n 'outputs',\n outputs === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n for (const key of Object.keys(outputs)) {\n const entry = outputs[key];\n if (!isNonNullObject(entry)) {\n throw malformed(`outputs.${key}`, 'is not an object', url);\n }\n const amount = entry.amount;\n if (!isNonNullObject(amount)) {\n throw malformed(\n `outputs.${key}.amount`,\n amount === undefined ? 'is absent' : 'is not an object',\n url,\n );\n }\n decodeAmountLeaf(amount, 'estimate', `outputs.${key}.amount`, url);\n decodeAmountLeaf(amount, 'minimum', `outputs.${key}.amount`, url);\n }\n};\n\n// `epilogue` is optional (absent without an epilogue, and on every partial);\n// when present, each named forward's `amount` is a decimal string on the wire.\nconst decodeEpilogue = (data: WireAmountBody, url: string): void => {\n const epilogue = data.epilogue;\n if (epilogue === undefined) return;\n if (!isNonNullObject(epilogue) || !Array.isArray(epilogue.forwards)) {\n throw malformed('epilogue', 'is not an object with forwards', url);\n }\n const forwards: readonly unknown[] = epilogue.forwards;\n forwards.forEach((forward, index) => {\n if (!isNonNullObject(forward)) {\n throw malformed(`epilogue.forwards[${index}]`, 'is not an object', url);\n }\n forward.amount = decodeAmount(\n forward.amount,\n `epilogue.forwards[${index}].amount`,\n url,\n );\n });\n};\n\n// THE decode seam for a compile-family response body: every amount the wire\n// renders as a decimal string becomes the `bigint` its declared type promises.\n// The amount records are handled here so the body is narrowed once — a\n// non-object body is not this function's error to raise, since the enveloping\n// parse already rejected it.\nconst decodeWireAmounts = (data: unknown, url: string): void => {\n if (!isNonNullObject(data)) return;\n decodeOutputs(data, url);\n decodeEpilogue(data, url);\n};\n\nconst parseBody = async <T>(res: Response, url: string): Promise<T> => {\n const body = await res.json().catch((_) => null);\n if (!isNonNullObject(body) || !('data' in body)) {\n throw new ComposeError('UNKNOWN_ERROR', 'Unexpected response format', {\n url,\n });\n }\n return body.data as T;\n};\n\nconst parseCompileSuccessBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const data = await parseBody<ComposeCompileSuccessData>(res, url);\n decodeWireAmounts(data, url);\n return { ...data, status: 'success' as const };\n};\n\nconst parsePartialBody = async (\n res: Response,\n url: string,\n): Promise<ComposeCompileResult> => {\n const body = await res.json().catch((_) => null);\n const envelope = parseCompilePartialEnvelope(body);\n if (envelope === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected partial response format',\n { url },\n );\n }\n // `data` is validated as an object by the schema; compose-spec owns its full\n // shape as a hand-authored type, so we narrow it here rather than re-declaring\n // that type as a schema. `error` is fully validated — no cast needed.\n const data = envelope.data as ComposeCompilePartialData;\n decodeWireAmounts(data, url);\n return { ...data, status: 'partial' as const, error: envelope.error };\n};\n\n// `/simulate` is un-enveloped: the discriminated body (`{ status, ... }`) is at\n// the top level, NOT wrapped in `{ data }` like `/compose`. So this reads the\n// body directly and validates it against the simulate union rather than reusing\n// `parseBody`.\nconst parseSimulateBody = async (\n res: Response,\n url: string,\n): Promise<SimulateResult> => {\n const body = await res.json().catch((_) => null);\n const result = parseSimulateResult(body);\n if (result === null) {\n throw new ComposeError(\n 'UNKNOWN_ERROR',\n 'Unexpected simulate response format',\n { url },\n );\n }\n return result;\n};\n// `POST /compose` and `POST /compose/route` differ only in path and request\n// shape: both return the enveloped success data on 200 and the partial\n// envelope on 206, so they share one transport.\ntype ComposeTransport = {\n readonly fetchFn: typeof fetch;\n readonly baseHeaders: Record<string, string>;\n};\n\n// A patch bump is additive and a minor bump is breaking, so a 200 from a\n// backend behind this SDK cannot be trusted: the newer flow this SDK authored\n// may have compiled under the server's older semantics without raising an\n// error. This covers the deploy window where the SDK is published to npm ahead\n// of the backend rollout. Only the compile-family POSTs are checked; discovery\n// GETs stay reachable from a mismatched SDK, as on the server. An absent header\n// (a pre-gate backend, a header-stripping proxy) is accepted, mirroring the\n// server's pass-through of a compile that carries no version header.\nconst assertServerVersion = (res: Response, url: string): void => {\n const serverVersion = res.headers.get(COMPOSE_VERSION_HEADER);\n if (serverVersion === null || SDK_VERSION_TRIPLE === undefined) return;\n const serverTriple = parseVersion(serverVersion);\n if (serverTriple === undefined) return;\n\n const compatibility = checkCompatibility(SDK_VERSION_TRIPLE, serverTriple);\n if (compatibility === 'compatible') return;\n\n const versions = { sdkVersion: SDK_VERSION, serverVersion };\n if (compatibility === 'sdk_outdated') {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `composer-sdk ${SDK_VERSION} is older than the compose contract served at ${url} (${serverVersion}); upgrade @lifi/composer-sdk and @lifi/compose-spec`,\n {\n status: res.status,\n url,\n kind: 'sdk_outdated',\n sdkOutdated: {\n ...versions,\n minimumSdkVersion: `${serverTriple.major}.${serverTriple.minor}.0`,\n },\n },\n );\n }\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `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`,\n {\n status: res.status,\n url,\n kind: 'server_outdated',\n serverOutdated: versions,\n },\n );\n};\n\ntype SendInit =\n | { readonly method: 'GET' }\n | { readonly method: 'POST'; readonly body: unknown };\n\n// GETs on this API carry no body; POSTs carry a JSON one with `bigint` amounts\n// rendered as decimal strings.\nconst requestInit = (\n baseHeaders: Record<string, string>,\n init: SendInit,\n): RequestInit =>\n init.method === 'GET'\n ? { method: 'GET', headers: { ...baseHeaders } }\n : {\n method: 'POST',\n headers: { ...baseHeaders, 'Content-Type': 'application/json' },\n body: JSON.stringify(init.body, bigintReplacer),\n };\n\n// One fetch path for every route: maps transport failures to `NETWORK_ERROR`.\n// The response version check is not here — only the compile-family POSTs gate\n// on it (see `postCompile`); discovery GETs and `/simulate` are ungated.\nconst send = async (\n { fetchFn, baseHeaders }: ComposeTransport,\n url: string,\n init: SendInit,\n): Promise<Response> => {\n try {\n return await fetchFn(url, requestInit(baseHeaders, init));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new ComposeError('NETWORK_ERROR', message, { cause: err });\n }\n};\n\nconst postCompile = async (\n transport: ComposeTransport,\n url: string,\n request: ComposeCompileRequest | ComposeRouteRequest,\n): Promise<ComposeCompileResult> => {\n const res = await send(transport, url, { method: 'POST', body: request });\n assertServerVersion(res, url);\n if (res.status === 206) {\n return await parsePartialBody(res, url);\n }\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseCompileSuccessBody(res, url);\n};\n\n// Every enveloped GET on this API behaves identically: no body, no query\n// semantics of its own, `{ data }` on success, an error envelope otherwise.\nconst getJson = async <T>(\n transport: ComposeTransport,\n url: string,\n): Promise<T> => {\n const res = await send(transport, url, { method: 'GET' });\n if (!res.ok) {\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n }\n return await parseBody<T>(res, url);\n};\n\n/**\n * Creates a low-level Compose API client.\n *\n * @param options - Client configuration including the API base URL.\n * @returns A {@link ComposeClient} instance.\n */\nexport const createComposeClient = (\n options: ComposeClientOptions,\n): ComposeClient => {\n if (!options.baseUrl || !/^https?:\\/\\//i.test(options.baseUrl)) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n `Invalid baseUrl: expected an HTTP(S) URL, got \"${options.baseUrl}\"`,\n );\n }\n const trimmedApiKey = options.apiKey?.trim() || undefined;\n if (!trimmedApiKey) {\n throw new ComposeError(\n 'VALIDATION_ERROR',\n 'apiKey is required: pass a LI.FI API key to createComposeSdk().',\n );\n }\n const fetchFn = options.fetch ?? globalThis.fetch;\n const base = options.baseUrl.replace(/\\/$/, '');\n\n const baseHeaders: Record<string, string> = {\n Accept: 'application/json',\n [COMPOSER_SDK_VERSION_HEADER]: SDK_VERSION,\n 'x-lifi-api-key': trimmedApiKey,\n };\n const transport: ComposeTransport = { fetchFn, baseHeaders };\n\n const getManifest = async (): Promise<ComposeManifest> =>\n getJson<ComposeManifest>(transport, `${base}/compose/manifest`);\n\n const getChains = async (): Promise<readonly ChainSummary[]> =>\n getJson<readonly ChainSummary[]>(transport, `${base}/chains`);\n\n const compile = async (\n request: ComposeCompileRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose`, request);\n\n const route = async (\n request: ComposeRouteRequest,\n ): Promise<ComposeCompileResult> =>\n postCompile(transport, `${base}/compose/route`, request);\n\n const getZapPacks = async (\n options?: GetZapPacksOptions,\n ): Promise<readonly ZapPackOverview[]> => {\n const params = new URLSearchParams();\n if (options?.protocols !== undefined) {\n // Backend expects a single comma-separated value, not repeated keys.\n const raw = options.protocols;\n const list = typeof raw === 'string' ? raw : raw.join(',');\n params.set('protocols', list);\n }\n const qs = params.toString();\n return getJson<readonly ZapPackOverview[]>(\n transport,\n `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`,\n );\n };\n\n const simulate = async (\n request: SimulateRequest,\n ): Promise<SimulateResult> => {\n const url = `${base}/simulate`;\n const res = await send(transport, url, { method: 'POST', body: request });\n // Only 200 (carries `ok`/`revert`) and 422 (carries the `error` member of\n // the union) have a discriminated body. 422 is deliberately intercepted\n // here (not thrown as VALIDATION_ERROR) so callers get one exhaustive\n // `switch (result.status)`. Every other status is a transport error and is\n // thrown — including HTTP 400 (malformed input, no `status` body) and any\n // unexpected 2xx.\n if (res.status === 200 || res.status === 422) {\n return await parseSimulateBody(res, url);\n }\n const body = await res.text();\n throw errorFromHttpResponse(res.status, body, url);\n };\n\n return { getManifest, getChains, compile, route, getZapPacks, simulate };\n};\n"],"mappings":"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"}
package/dist/errors.cjs CHANGED
@@ -55,6 +55,12 @@ var ComposeError = class extends Error {
55
55
  */
56
56
  details;
57
57
  /**
58
+ * The delivery tranche's reason, first quote, and abandoned escrow,
59
+ * attached to `delivery_error` errors. Narrow with
60
+ * {@link isComposeDeliveryError}.
61
+ */
62
+ delivery;
63
+ /**
58
64
  * The prepared ops that failed, attached to `preparation_error` errors.
59
65
  * Each entry carries the `callId` of the failing node so callers can drop
60
66
  * the unroutable legs and resubmit a smaller flow.
@@ -79,6 +85,7 @@ var ComposeError = class extends Error {
79
85
  this.port = options?.port;
80
86
  this.fix = options?.fix;
81
87
  this.details = options?.details;
88
+ this.delivery = options?.delivery;
82
89
  this.failedOps = options?.failedOps;
83
90
  this.succeededOps = options?.succeededOps;
84
91
  this.sdkOutdated = options?.sdkOutdated;
@@ -113,6 +120,15 @@ const isComposeError = (e) => e instanceof ComposeError || e instanceof Error &&
113
120
  * ```
114
121
  */
115
122
  const isComposePreparationError = (e) => isComposeError(e) && e.kind === "preparation_error" && Array.isArray(e.failedOps) && Array.isArray(e.succeededOps);
123
+ /**
124
+ * Type guard for `delivery_error` failures (HTTP 422): a bridge delivering into
125
+ * a settle escrow no longer fits it. Narrows {@link ComposeError.delivery} to
126
+ * present. Recompile: each compile quotes and registers afresh.
127
+ *
128
+ * @param e - The value to check.
129
+ * @returns `true` if `e` is a `ComposeError` carrying delivery details.
130
+ */
131
+ const isComposeDeliveryError = (e) => isComposeError(e) && e.kind === "delivery_error" && e.delivery !== void 0;
116
132
  const sdkOutdatedSchema = zod.default.object({
117
133
  sdkVersion: zod.default.string(),
118
134
  serverVersion: zod.default.string(),
@@ -178,15 +194,22 @@ const tryParseErrorBody = (body) => {
178
194
  };
179
195
  /**
180
196
  * Extracts the version triple an `sdk_outdated` or `server_outdated` envelope
181
- * carries. The slots reuse the same schemas the type guards narrow with. Zod
182
- * strips keys the schema does not declare, so the parsed value is exactly the
183
- * triple; a partial payload fails the parse and yields `undefined` rather than
184
- * a half-populated object.
197
+ * carries, and routes the wire `details` by kind. The version slots reuse the
198
+ * same schemas the type guards narrow with. Zod strips keys the schema does
199
+ * not declare, so the parsed value is exactly the triple; a partial payload
200
+ * fails the parse and yields `undefined` rather than a half-populated object.
201
+ * A `delivery_error`'s `details` land in `delivery`; every other kind's are
202
+ * forwarded verbatim as `details`.
185
203
  */
186
- const readVersionSlots = (serverError) => ({
187
- sdkOutdated: serverError?.kind === "sdk_outdated" ? sdkOutdatedSchema.safeParse(serverError).data : void 0,
188
- serverOutdated: serverError?.kind === "server_outdated" ? serverOutdatedSchema.safeParse(serverError).data : void 0
189
- });
204
+ const readKindSlots = (serverError) => {
205
+ const delivery = serverError?.kind === "delivery_error";
206
+ return {
207
+ sdkOutdated: serverError?.kind === "sdk_outdated" ? sdkOutdatedSchema.safeParse(serverError).data : void 0,
208
+ serverOutdated: serverError?.kind === "server_outdated" ? serverOutdatedSchema.safeParse(serverError).data : void 0,
209
+ details: delivery ? void 0 : serverError?.details,
210
+ delivery: delivery ? serverError.details : void 0
211
+ };
212
+ };
190
213
  /**
191
214
  * Constructs a {@link ComposeError} from an HTTP error response, extracting
192
215
  * structured error details from the response body when available.
@@ -198,7 +221,7 @@ const readVersionSlots = (serverError) => ({
198
221
  */
199
222
  const errorFromHttpResponse = (status, body, url) => {
200
223
  const serverError = tryParseErrorBody(body)?.error;
201
- const { sdkOutdated, serverOutdated } = readVersionSlots(serverError);
224
+ const slots = readKindSlots(serverError);
202
225
  return new ComposeError(STATUS_TO_CODE.get(status) ?? (status >= 500 ? "SERVER_ERROR" : "UNKNOWN_ERROR"), (serverError?.message ?? body) || `HTTP ${status}`, {
203
226
  status,
204
227
  url,
@@ -206,19 +229,18 @@ const errorFromHttpResponse = (status, body, url) => {
206
229
  path: serverError?.path,
207
230
  port: serverError?.port,
208
231
  fix: serverError?.fix,
209
- details: serverError?.details,
232
+ ...slots,
210
233
  failedOps: serverError?.failedOps?.map((op) => ({
211
234
  ...op,
212
235
  kind: op.kind
213
236
  })),
214
- succeededOps: serverError?.succeededOps,
215
- sdkOutdated,
216
- serverOutdated
237
+ succeededOps: serverError?.succeededOps
217
238
  });
218
239
  };
219
240
  //#endregion
220
241
  exports.ComposeError = ComposeError;
221
242
  exports.errorFromHttpResponse = errorFromHttpResponse;
243
+ exports.isComposeDeliveryError = isComposeDeliveryError;
222
244
  exports.isComposeError = isComposeError;
223
245
  exports.isComposePreparationError = isComposePreparationError;
224
246
  exports.isComposeSdkOutdatedError = isComposeSdkOutdatedError;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.cjs","names":["z","parseServerErrorBody"],"sources":["../src/errors.ts"],"sourcesContent":["import type {\n ComposeErrorKind,\n ComposeRouteErrorKind,\n FailedPreparedOp,\n GenericComposeErrorKind,\n SimulationRevert,\n} from '@lifi/compose-spec';\nimport z from 'zod';\n\nimport {\n parseServerErrorBody,\n type ServerErrorBody,\n} from './responseSchemas.js';\n\n/**\n * Machine-readable error codes returned by the SDK.\n *\n * - `NETWORK_ERROR` — The HTTP request failed (DNS, timeout, connection refused).\n * - `VALIDATION_ERROR` — The server rejected the request (HTTP 400/422).\n * - `UNAUTHENTICATED` — The request lacks valid authentication credentials (HTTP 401).\n * - `FORBIDDEN` — The server understood the request but refuses to authorise it (HTTP 403).\n * - `SERVER_ERROR` — The server returned a 5xx status.\n * - `RATE_LIMITED` — The server returned HTTP 429.\n * - `NOT_FOUND` — The requested resource does not exist (HTTP 404).\n * - `UNKNOWN_ERROR` — An unexpected error that doesn't fit other categories.\n */\nexport type ComposeErrorCode =\n | 'NETWORK_ERROR'\n | 'VALIDATION_ERROR'\n | 'UNAUTHENTICATED'\n | 'FORBIDDEN'\n | 'SERVER_ERROR'\n | 'RATE_LIMITED'\n | 'NOT_FOUND'\n | 'UNKNOWN_ERROR';\n\n/** Version details returned when the backend rejects an outdated SDK. */\nexport interface ComposeSdkOutdated {\n readonly sdkVersion: string;\n readonly serverVersion: string;\n readonly minimumSdkVersion: string;\n}\n\n/**\n * Version details attached when the SDK is ahead of the backend it reached.\n * Raised by the backend's version gate, or by the SDK itself when the\n * `x-lifi-composer-version` response header names an older contract than the\n * one this SDK was built against.\n */\nexport interface ComposeServerOutdated {\n readonly sdkVersion: string;\n readonly serverVersion: string;\n}\n\n/**\n * Error class for all failures originating from the Compose SDK or API.\n *\n * Includes structured metadata beyond the error message to support\n * programmatic error handling.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposeError(e) && e.code === 'VALIDATION_ERROR') {\n * console.error('Invalid request:', e.message, e.path);\n * }\n * }\n * ```\n */\nexport class ComposeError extends Error {\n override readonly name = 'ComposeError';\n /** Machine-readable error category. */\n readonly code: ComposeErrorCode;\n /** HTTP status code, when the error originated from an HTTP response. */\n readonly status?: number;\n /** The request URL that produced the error. */\n readonly url?: string;\n /** Server-provided error kind for finer-grained classification. */\n readonly kind?: ComposeErrorKind | ComposeRouteErrorKind;\n /**\n * Where a validation error points: the node id for a rejection of an\n * authored node, or a JSON-pointer path for a rejection of the request shape.\n */\n readonly path?: string;\n /**\n * The input port of {@link ComposeError.path}'s node whose binding caused the\n * error. Set when the rejection is about what a port binds rather than about\n * the node's config.\n */\n readonly port?: string;\n /**\n * The one edit that clears the error, written for whoever authored the flow.\n * Set on `continuation.settle` authoring rejections; may be absent on other\n * `validation_error`s.\n */\n readonly fix?: string;\n /**\n * Simulation revert diagnostics attached to `simulation_revert` errors.\n * Contains the raw error bytes and decoded error candidates when the\n * backend can parse the revert reason.\n */\n readonly details?: SimulationRevert;\n /**\n * The prepared ops that failed, attached to `preparation_error` errors.\n * Each entry carries the `callId` of the failing node so callers can drop\n * the unroutable legs and resubmit a smaller flow.\n */\n readonly failedOps?: readonly FailedPreparedOp[];\n /**\n * The `callId`s of the prepared ops that succeeded, attached to\n * `preparation_error` errors alongside {@link ComposeError.failedOps}.\n */\n readonly succeededOps?: readonly string[];\n /** Version details attached only to `sdk_outdated` errors. */\n readonly sdkOutdated?: ComposeSdkOutdated;\n /** Version details attached only to `server_outdated` errors. */\n readonly serverOutdated?: ComposeServerOutdated;\n\n constructor(\n code: ComposeErrorCode,\n message: string,\n options?: {\n status?: number;\n url?: string;\n cause?: unknown;\n kind?: ComposeErrorKind | ComposeRouteErrorKind;\n path?: string;\n port?: string;\n fix?: string;\n details?: SimulationRevert;\n failedOps?: readonly FailedPreparedOp[];\n succeededOps?: readonly string[];\n sdkOutdated?: ComposeSdkOutdated;\n serverOutdated?: ComposeServerOutdated;\n },\n ) {\n super(message, { cause: options?.cause });\n this.code = code;\n this.status = options?.status;\n this.url = options?.url;\n this.kind = options?.kind;\n this.path = options?.path;\n this.port = options?.port;\n this.fix = options?.fix;\n this.details = options?.details;\n this.failedOps = options?.failedOps;\n this.succeededOps = options?.succeededOps;\n this.sdkOutdated = options?.sdkOutdated;\n this.serverOutdated = options?.serverOutdated;\n }\n}\n\n/**\n * The per-op preparation diagnostics carried by `preparation_error` responses,\n * with both arrays known to be present. Produced by narrowing with\n * {@link isComposePreparationError}.\n */\nexport interface ComposePreparationOps {\n readonly failedOps: readonly FailedPreparedOp[];\n readonly succeededOps: readonly string[];\n}\n\n/**\n * Type guard that narrows an unknown error to {@link ComposeError}.\n * @param e - The value to check.\n * @returns `true` if `e` is an instance of `ComposeError`.\n */\nexport const isComposeError = (e: unknown): e is ComposeError =>\n e instanceof ComposeError ||\n (e instanceof Error && e.name === 'ComposeError' && 'code' in e);\n\n/**\n * Type guard for `preparation_error` failures (HTTP 422): a mixed basket where\n * some prepared ops failed and others succeeded. Narrows both\n * {@link ComposeError.failedOps} and {@link ComposeError.succeededOps} to\n * present, so callers can read them without a cast or a truthiness check.\n *\n * @param e - The value to check.\n * @returns `true` if `e` is a `ComposeError` carrying per-op preparation diagnostics.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposePreparationError(e)) {\n * console.error('Failed ops:', e.failedOps);\n * console.error('Succeeded ops:', e.succeededOps);\n * }\n * }\n * ```\n */\nexport const isComposePreparationError = (\n e: unknown,\n): e is ComposeError & ComposePreparationOps =>\n isComposeError(e) &&\n e.kind === 'preparation_error' &&\n Array.isArray(e.failedOps) &&\n Array.isArray(e.succeededOps);\n\n// `isComposeError` accepts a structurally-similar error by name, so an error\n// raised by a *second* copy of this package in the dependency tree reaches the\n// guard below without its `sdkOutdated` field being type-guaranteed. Parse it.\nconst sdkOutdatedSchema = z.object({\n sdkVersion: z.string(),\n serverVersion: z.string(),\n minimumSdkVersion: z.string(),\n});\n\n/**\n * Narrows an outdated-SDK rejection to its required version details.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposeSdkOutdatedError(e)) {\n * console.error(`Upgrade to at least ${e.sdkOutdated.minimumSdkVersion}`);\n * return;\n * }\n * throw e;\n * }\n * ```\n */\nexport const isComposeSdkOutdatedError = (\n e: unknown,\n): e is ComposeError & { readonly sdkOutdated: ComposeSdkOutdated } =>\n isComposeError(e) &&\n e.kind === 'sdk_outdated' &&\n sdkOutdatedSchema.safeParse(e.sdkOutdated).success;\n\nconst serverOutdatedSchema = z.object({\n sdkVersion: z.string(),\n serverVersion: z.string(),\n});\n\n/**\n * Narrows a server-behind-SDK rejection to its required version details.\n *\n * This is the deploy-window error: the SDK on npm is ahead of the backend that\n * answered. Pin the SDK to the server's `major.minor`, or wait for the rollout.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposeServerOutdatedError(e)) {\n * console.error(`Server serves ${e.serverOutdated.serverVersion}`);\n * return;\n * }\n * throw e;\n * }\n * ```\n */\nexport const isComposeServerOutdatedError = (\n e: unknown,\n): e is ComposeError & { readonly serverOutdated: ComposeServerOutdated } =>\n isComposeError(e) &&\n e.kind === 'server_outdated' &&\n serverOutdatedSchema.safeParse(e.serverOutdated).success;\n\nconst STATUS_TO_CODE: ReadonlyMap<number, ComposeErrorCode> = new Map<\n number,\n ComposeErrorCode\n>([\n [400, 'VALIDATION_ERROR'],\n [401, 'UNAUTHENTICATED'],\n [403, 'FORBIDDEN'],\n [404, 'NOT_FOUND'],\n [422, 'VALIDATION_ERROR'],\n [429, 'RATE_LIMITED'],\n]);\n\nconst tryParseErrorBody = (body: string): ServerErrorBody | null => {\n let json: unknown;\n try {\n json = JSON.parse(body);\n } catch {\n return null;\n }\n return parseServerErrorBody(json);\n};\n\n/**\n * Extracts the version triple an `sdk_outdated` or `server_outdated` envelope\n * carries. The slots reuse the same schemas the type guards narrow with. Zod\n * strips keys the schema does not declare, so the parsed value is exactly the\n * triple; a partial payload fails the parse and yields `undefined` rather than\n * a half-populated object.\n */\nconst readVersionSlots = (serverError: ServerErrorBody['error']) => ({\n sdkOutdated:\n serverError?.kind === 'sdk_outdated'\n ? sdkOutdatedSchema.safeParse(serverError).data\n : undefined,\n serverOutdated:\n serverError?.kind === 'server_outdated'\n ? serverOutdatedSchema.safeParse(serverError).data\n : undefined,\n});\n\n/**\n * Constructs a {@link ComposeError} from an HTTP error response, extracting\n * structured error details from the response body when available.\n *\n * @param status - The HTTP status code.\n * @param body - The raw response body text.\n * @param url - The request URL that produced the error.\n * @returns A `ComposeError` with the appropriate error code and metadata.\n */\nexport const errorFromHttpResponse = (\n status: number,\n body: string,\n url: string,\n): ComposeError => {\n const parsed = tryParseErrorBody(body);\n const serverError = parsed?.error;\n const { sdkOutdated, serverOutdated } = readVersionSlots(serverError);\n\n return new ComposeError(\n STATUS_TO_CODE.get(status) ??\n (status >= 500 ? 'SERVER_ERROR' : 'UNKNOWN_ERROR'),\n (serverError?.message ?? body) || `HTTP ${status}`,\n {\n status,\n url,\n kind: serverError?.kind as\n ComposeErrorKind | ComposeRouteErrorKind | undefined,\n path: serverError?.path,\n port: serverError?.port,\n fix: serverError?.fix,\n details: serverError?.details,\n // The wire schema types `kind` as a bare string; the server only ever\n // reports generic kinds for individual ops.\n failedOps: serverError?.failedOps?.map((op) => ({\n ...op,\n kind: op.kind as GenericComposeErrorKind,\n })),\n succeededOps: serverError?.succeededOps,\n sdkOutdated,\n serverOutdated,\n },\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuEA,IAAa,eAAb,cAAkC,MAAM;CACtC,OAAyB;;CAEzB;;CAEA;;CAEA;;CAEA;;;;;CAKA;;;;;;CAMA;;;;;;CAMA;;;;;;CAMA;;;;;;CAMA;;;;;CAKA;;CAEA;;CAEA;CAEA,YACE,MACA,SACA,SAcA;EACA,MAAM,SAAS,EAAE,OAAO,SAAS,MAAM,CAAC;EACxC,KAAK,OAAO;EACZ,KAAK,SAAS,SAAS;EACvB,KAAK,MAAM,SAAS;EACpB,KAAK,OAAO,SAAS;EACrB,KAAK,OAAO,SAAS;EACrB,KAAK,OAAO,SAAS;EACrB,KAAK,MAAM,SAAS;EACpB,KAAK,UAAU,SAAS;EACxB,KAAK,YAAY,SAAS;EAC1B,KAAK,eAAe,SAAS;EAC7B,KAAK,cAAc,SAAS;EAC5B,KAAK,iBAAiB,SAAS;CACjC;AACF;;;;;;AAiBA,MAAa,kBAAkB,MAC7B,aAAa,gBACZ,aAAa,SAAS,EAAE,SAAS,kBAAkB,UAAU;;;;;;;;;;;;;;;;;;;;;;AAuBhE,MAAa,6BACX,MAEA,eAAe,CAAC,KAChB,EAAE,SAAS,uBACX,MAAM,QAAQ,EAAE,SAAS,KACzB,MAAM,QAAQ,EAAE,YAAY;AAK9B,MAAM,oBAAoBA,IAAAA,QAAE,OAAO;CACjC,YAAYA,IAAAA,QAAE,OAAO;CACrB,eAAeA,IAAAA,QAAE,OAAO;CACxB,mBAAmBA,IAAAA,QAAE,OAAO;AAC9B,CAAC;;;;;;;;;;;;;;;;;AAkBD,MAAa,6BACX,MAEA,eAAe,CAAC,KAChB,EAAE,SAAS,kBACX,kBAAkB,UAAU,EAAE,WAAW,CAAC,CAAC;AAE7C,MAAM,uBAAuBA,IAAAA,QAAE,OAAO;CACpC,YAAYA,IAAAA,QAAE,OAAO;CACrB,eAAeA,IAAAA,QAAE,OAAO;AAC1B,CAAC;;;;;;;;;;;;;;;;;;;;AAqBD,MAAa,gCACX,MAEA,eAAe,CAAC,KAChB,EAAE,SAAS,qBACX,qBAAqB,UAAU,EAAE,cAAc,CAAC,CAAC;AAEnD,MAAM,iCAAwD,IAAI,IAGhE;CACA,CAAC,KAAK,kBAAkB;CACxB,CAAC,KAAK,iBAAiB;CACvB,CAAC,KAAK,WAAW;CACjB,CAAC,KAAK,WAAW;CACjB,CAAC,KAAK,kBAAkB;CACxB,CAAC,KAAK,cAAc;AACtB,CAAC;AAED,MAAM,qBAAqB,SAAyC;CAClE,IAAI;CACJ,IAAI;EACF,OAAO,KAAK,MAAM,IAAI;CACxB,QAAQ;EACN,OAAO;CACT;CACA,OAAOC,wBAAAA,qBAAqB,IAAI;AAClC;;;;;;;;AASA,MAAM,oBAAoB,iBAA2C;CACnE,aACE,aAAa,SAAS,iBAClB,kBAAkB,UAAU,WAAW,CAAC,CAAC,OACzC,KAAA;CACN,gBACE,aAAa,SAAS,oBAClB,qBAAqB,UAAU,WAAW,CAAC,CAAC,OAC5C,KAAA;AACR;;;;;;;;;;AAWA,MAAa,yBACX,QACA,MACA,QACiB;CAEjB,MAAM,cADS,kBAAkB,IACR,CAAC,EAAE;CAC5B,MAAM,EAAE,aAAa,mBAAmB,iBAAiB,WAAW;CAEpE,OAAO,IAAI,aACT,eAAe,IAAI,MAAM,MACtB,UAAU,MAAM,iBAAiB,mBACnC,aAAa,WAAW,SAAS,QAAQ,UAC1C;EACE;EACA;EACA,MAAM,aAAa;EAEnB,MAAM,aAAa;EACnB,MAAM,aAAa;EACnB,KAAK,aAAa;EAClB,SAAS,aAAa;EAGtB,WAAW,aAAa,WAAW,KAAK,QAAQ;GAC9C,GAAG;GACH,MAAM,GAAG;EACX,EAAE;EACF,cAAc,aAAa;EAC3B;EACA;CACF,CACF;AACF"}
1
+ {"version":3,"file":"errors.cjs","names":["z","parseServerErrorBody"],"sources":["../src/errors.ts"],"sourcesContent":["import type {\n ComposeErrorKind,\n ComposeRouteErrorKind,\n DeliveryErrorDetails,\n FailedPreparedOp,\n GenericComposeErrorKind,\n SimulationRevert,\n} from '@lifi/compose-spec';\nimport z from 'zod';\n\nimport {\n parseServerErrorBody,\n type ServerErrorBody,\n} from './responseSchemas.js';\n\n/**\n * Machine-readable error codes returned by the SDK.\n *\n * - `NETWORK_ERROR` — The HTTP request failed (DNS, timeout, connection refused).\n * - `VALIDATION_ERROR` — The server rejected the request (HTTP 400/422).\n * - `UNAUTHENTICATED` — The request lacks valid authentication credentials (HTTP 401).\n * - `FORBIDDEN` — The server understood the request but refuses to authorise it (HTTP 403).\n * - `SERVER_ERROR` — The server returned a 5xx status.\n * - `RATE_LIMITED` — The server returned HTTP 429.\n * - `NOT_FOUND` — The requested resource does not exist (HTTP 404).\n * - `UNKNOWN_ERROR` — An unexpected error that doesn't fit other categories.\n */\nexport type ComposeErrorCode =\n | 'NETWORK_ERROR'\n | 'VALIDATION_ERROR'\n | 'UNAUTHENTICATED'\n | 'FORBIDDEN'\n | 'SERVER_ERROR'\n | 'RATE_LIMITED'\n | 'NOT_FOUND'\n | 'UNKNOWN_ERROR';\n\n/** Version details returned when the backend rejects an outdated SDK. */\nexport interface ComposeSdkOutdated {\n readonly sdkVersion: string;\n readonly serverVersion: string;\n readonly minimumSdkVersion: string;\n}\n\n/**\n * Version details attached when the SDK is ahead of the backend it reached.\n * Raised by the backend's version gate, or by the SDK itself when the\n * `x-lifi-composer-version` response header names an older contract than the\n * one this SDK was built against.\n */\nexport interface ComposeServerOutdated {\n readonly sdkVersion: string;\n readonly serverVersion: string;\n}\n\n/**\n * Error class for all failures originating from the Compose SDK or API.\n *\n * Includes structured metadata beyond the error message to support\n * programmatic error handling.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposeError(e) && e.code === 'VALIDATION_ERROR') {\n * console.error('Invalid request:', e.message, e.path);\n * }\n * }\n * ```\n */\nexport class ComposeError extends Error {\n override readonly name = 'ComposeError';\n /** Machine-readable error category. */\n readonly code: ComposeErrorCode;\n /** HTTP status code, when the error originated from an HTTP response. */\n readonly status?: number;\n /** The request URL that produced the error. */\n readonly url?: string;\n /** Server-provided error kind for finer-grained classification. */\n readonly kind?: ComposeErrorKind | ComposeRouteErrorKind;\n /**\n * Where a validation error points: the node id for a rejection of an\n * authored node, or a JSON-pointer path for a rejection of the request shape.\n */\n readonly path?: string;\n /**\n * The input port of {@link ComposeError.path}'s node whose binding caused the\n * error. Set when the rejection is about what a port binds rather than about\n * the node's config.\n */\n readonly port?: string;\n /**\n * The one edit that clears the error, written for whoever authored the flow.\n * Set on `continuation.settle` authoring rejections; may be absent on other\n * `validation_error`s.\n */\n readonly fix?: string;\n /**\n * Simulation revert diagnostics attached to `simulation_revert` errors.\n * Contains the raw error bytes and decoded error candidates when the\n * backend can parse the revert reason.\n */\n readonly details?: SimulationRevert;\n /**\n * The delivery tranche's reason, first quote, and abandoned escrow,\n * attached to `delivery_error` errors. Narrow with\n * {@link isComposeDeliveryError}.\n */\n readonly delivery?: DeliveryErrorDetails;\n /**\n * The prepared ops that failed, attached to `preparation_error` errors.\n * Each entry carries the `callId` of the failing node so callers can drop\n * the unroutable legs and resubmit a smaller flow.\n */\n readonly failedOps?: readonly FailedPreparedOp[];\n /**\n * The `callId`s of the prepared ops that succeeded, attached to\n * `preparation_error` errors alongside {@link ComposeError.failedOps}.\n */\n readonly succeededOps?: readonly string[];\n /** Version details attached only to `sdk_outdated` errors. */\n readonly sdkOutdated?: ComposeSdkOutdated;\n /** Version details attached only to `server_outdated` errors. */\n readonly serverOutdated?: ComposeServerOutdated;\n\n constructor(\n code: ComposeErrorCode,\n message: string,\n options?: {\n status?: number;\n url?: string;\n cause?: unknown;\n kind?: ComposeErrorKind | ComposeRouteErrorKind;\n path?: string;\n port?: string;\n fix?: string;\n details?: SimulationRevert;\n delivery?: DeliveryErrorDetails;\n failedOps?: readonly FailedPreparedOp[];\n succeededOps?: readonly string[];\n sdkOutdated?: ComposeSdkOutdated;\n serverOutdated?: ComposeServerOutdated;\n },\n ) {\n super(message, { cause: options?.cause });\n this.code = code;\n this.status = options?.status;\n this.url = options?.url;\n this.kind = options?.kind;\n this.path = options?.path;\n this.port = options?.port;\n this.fix = options?.fix;\n this.details = options?.details;\n this.delivery = options?.delivery;\n this.failedOps = options?.failedOps;\n this.succeededOps = options?.succeededOps;\n this.sdkOutdated = options?.sdkOutdated;\n this.serverOutdated = options?.serverOutdated;\n }\n}\n\n/**\n * The per-op preparation diagnostics carried by `preparation_error` responses,\n * with both arrays known to be present. Produced by narrowing with\n * {@link isComposePreparationError}.\n */\nexport interface ComposePreparationOps {\n readonly failedOps: readonly FailedPreparedOp[];\n readonly succeededOps: readonly string[];\n}\n\n/**\n * Type guard that narrows an unknown error to {@link ComposeError}.\n * @param e - The value to check.\n * @returns `true` if `e` is an instance of `ComposeError`.\n */\nexport const isComposeError = (e: unknown): e is ComposeError =>\n e instanceof ComposeError ||\n (e instanceof Error && e.name === 'ComposeError' && 'code' in e);\n\n/**\n * Type guard for `preparation_error` failures (HTTP 422): a mixed basket where\n * some prepared ops failed and others succeeded. Narrows both\n * {@link ComposeError.failedOps} and {@link ComposeError.succeededOps} to\n * present, so callers can read them without a cast or a truthiness check.\n *\n * @param e - The value to check.\n * @returns `true` if `e` is a `ComposeError` carrying per-op preparation diagnostics.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposePreparationError(e)) {\n * console.error('Failed ops:', e.failedOps);\n * console.error('Succeeded ops:', e.succeededOps);\n * }\n * }\n * ```\n */\nexport const isComposePreparationError = (\n e: unknown,\n): e is ComposeError & ComposePreparationOps =>\n isComposeError(e) &&\n e.kind === 'preparation_error' &&\n Array.isArray(e.failedOps) &&\n Array.isArray(e.succeededOps);\n\n/**\n * Type guard for `delivery_error` failures (HTTP 422): a bridge delivering into\n * a settle escrow no longer fits it. Narrows {@link ComposeError.delivery} to\n * present. Recompile: each compile quotes and registers afresh.\n *\n * @param e - The value to check.\n * @returns `true` if `e` is a `ComposeError` carrying delivery details.\n */\nexport const isComposeDeliveryError = (\n e: unknown,\n): e is ComposeError & { readonly delivery: DeliveryErrorDetails } =>\n isComposeError(e) && e.kind === 'delivery_error' && e.delivery !== undefined;\n\n// `isComposeError` accepts a structurally-similar error by name, so an error\n// raised by a *second* copy of this package in the dependency tree reaches the\n// guard below without its `sdkOutdated` field being type-guaranteed. Parse it.\nconst sdkOutdatedSchema = z.object({\n sdkVersion: z.string(),\n serverVersion: z.string(),\n minimumSdkVersion: z.string(),\n});\n\n/**\n * Narrows an outdated-SDK rejection to its required version details.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposeSdkOutdatedError(e)) {\n * console.error(`Upgrade to at least ${e.sdkOutdated.minimumSdkVersion}`);\n * return;\n * }\n * throw e;\n * }\n * ```\n */\nexport const isComposeSdkOutdatedError = (\n e: unknown,\n): e is ComposeError & { readonly sdkOutdated: ComposeSdkOutdated } =>\n isComposeError(e) &&\n e.kind === 'sdk_outdated' &&\n sdkOutdatedSchema.safeParse(e.sdkOutdated).success;\n\nconst serverOutdatedSchema = z.object({\n sdkVersion: z.string(),\n serverVersion: z.string(),\n});\n\n/**\n * Narrows a server-behind-SDK rejection to its required version details.\n *\n * This is the deploy-window error: the SDK on npm is ahead of the backend that\n * answered. Pin the SDK to the server's `major.minor`, or wait for the rollout.\n *\n * @example\n * ```ts\n * try {\n * await builder.compile(run);\n * } catch (e) {\n * if (isComposeServerOutdatedError(e)) {\n * console.error(`Server serves ${e.serverOutdated.serverVersion}`);\n * return;\n * }\n * throw e;\n * }\n * ```\n */\nexport const isComposeServerOutdatedError = (\n e: unknown,\n): e is ComposeError & { readonly serverOutdated: ComposeServerOutdated } =>\n isComposeError(e) &&\n e.kind === 'server_outdated' &&\n serverOutdatedSchema.safeParse(e.serverOutdated).success;\n\nconst STATUS_TO_CODE: ReadonlyMap<number, ComposeErrorCode> = new Map<\n number,\n ComposeErrorCode\n>([\n [400, 'VALIDATION_ERROR'],\n [401, 'UNAUTHENTICATED'],\n [403, 'FORBIDDEN'],\n [404, 'NOT_FOUND'],\n [422, 'VALIDATION_ERROR'],\n [429, 'RATE_LIMITED'],\n]);\n\nconst tryParseErrorBody = (body: string): ServerErrorBody | null => {\n let json: unknown;\n try {\n json = JSON.parse(body);\n } catch {\n return null;\n }\n return parseServerErrorBody(json);\n};\n\n/**\n * Extracts the version triple an `sdk_outdated` or `server_outdated` envelope\n * carries, and routes the wire `details` by kind. The version slots reuse the\n * same schemas the type guards narrow with. Zod strips keys the schema does\n * not declare, so the parsed value is exactly the triple; a partial payload\n * fails the parse and yields `undefined` rather than a half-populated object.\n * A `delivery_error`'s `details` land in `delivery`; every other kind's are\n * forwarded verbatim as `details`.\n */\nconst readKindSlots = (serverError: ServerErrorBody['error']) => {\n const delivery = serverError?.kind === 'delivery_error';\n return {\n sdkOutdated:\n serverError?.kind === 'sdk_outdated'\n ? sdkOutdatedSchema.safeParse(serverError).data\n : undefined,\n serverOutdated:\n serverError?.kind === 'server_outdated'\n ? serverOutdatedSchema.safeParse(serverError).data\n : undefined,\n details: delivery\n ? undefined\n : (serverError?.details as SimulationRevert | undefined),\n delivery: delivery\n ? (serverError.details as DeliveryErrorDetails | undefined)\n : undefined,\n };\n};\n\n/**\n * Constructs a {@link ComposeError} from an HTTP error response, extracting\n * structured error details from the response body when available.\n *\n * @param status - The HTTP status code.\n * @param body - The raw response body text.\n * @param url - The request URL that produced the error.\n * @returns A `ComposeError` with the appropriate error code and metadata.\n */\nexport const errorFromHttpResponse = (\n status: number,\n body: string,\n url: string,\n): ComposeError => {\n const parsed = tryParseErrorBody(body);\n const serverError = parsed?.error;\n const slots = readKindSlots(serverError);\n\n return new ComposeError(\n STATUS_TO_CODE.get(status) ??\n (status >= 500 ? 'SERVER_ERROR' : 'UNKNOWN_ERROR'),\n (serverError?.message ?? body) || `HTTP ${status}`,\n {\n status,\n url,\n kind: serverError?.kind as\n ComposeErrorKind | ComposeRouteErrorKind | undefined,\n path: serverError?.path,\n port: serverError?.port,\n fix: serverError?.fix,\n ...slots,\n // The wire schema types `kind` as a bare string; the server only ever\n // reports generic kinds for individual ops.\n failedOps: serverError?.failedOps?.map((op) => ({\n ...op,\n kind: op.kind as GenericComposeErrorKind,\n })),\n succeededOps: serverError?.succeededOps,\n },\n );\n};\n"],"mappings":"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"}