@lifi/composer-sdk 0.2.0-staging.5 → 0.2.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 (74) hide show
  1. package/README.md +81 -0
  2. package/dist/client.cjs +38 -6
  3. package/dist/client.cjs.map +1 -1
  4. package/dist/client.d.cts +25 -1
  5. package/dist/client.d.ts +25 -1
  6. package/dist/client.js +41 -6
  7. package/dist/client.js.map +1 -1
  8. package/dist/errors.cjs +4 -1
  9. package/dist/errors.cjs.map +1 -1
  10. package/dist/errors.js +6 -1
  11. package/dist/errors.js.map +1 -1
  12. package/dist/generated/config.generated.cjs.map +1 -1
  13. package/dist/generated/config.generated.d.cts +44 -7
  14. package/dist/generated/config.generated.d.ts +44 -7
  15. package/dist/generated/materialisers.generated.cjs +2 -8
  16. package/dist/generated/materialisers.generated.cjs.map +1 -1
  17. package/dist/generated/materialisers.generated.d.cts +1 -1
  18. package/dist/generated/materialisers.generated.d.ts +1 -1
  19. package/dist/generated/materialisers.generated.js +1 -6
  20. package/dist/generated/materialisers.generated.js.map +1 -1
  21. package/dist/generated/operations.generated.cjs +1 -30
  22. package/dist/generated/operations.generated.cjs.map +1 -1
  23. package/dist/generated/operations.generated.d.cts +2 -94
  24. package/dist/generated/operations.generated.d.ts +2 -94
  25. package/dist/generated/operations.generated.js +1 -30
  26. package/dist/generated/operations.generated.js.map +1 -1
  27. package/dist/index.cjs +8 -0
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +4 -3
  30. package/dist/index.d.ts +4 -3
  31. package/dist/index.js +8 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/{materialisers.generated-BfWJ-g7t.d.ts → materialisers.generated-BMkBQgWZ.d.ts} +3 -8
  34. package/dist/{materialisers.generated-DaJRwiRe.d.cts → materialisers.generated-BWq5oePO.d.cts} +3 -8
  35. package/dist/responseSchemas.cjs +110 -0
  36. package/dist/responseSchemas.cjs.map +1 -0
  37. package/dist/responseSchemas.d.cts +36 -0
  38. package/dist/responseSchemas.d.ts +36 -0
  39. package/dist/responseSchemas.js +74 -0
  40. package/dist/responseSchemas.js.map +1 -0
  41. package/dist/run/simulate.cjs +41 -0
  42. package/dist/run/simulate.cjs.map +1 -0
  43. package/dist/run/simulate.d.cts +59 -0
  44. package/dist/run/simulate.d.ts +59 -0
  45. package/dist/run/simulate.js +17 -0
  46. package/dist/run/simulate.js.map +1 -0
  47. package/dist/sdk.cjs +6 -1
  48. package/dist/sdk.cjs.map +1 -1
  49. package/dist/sdk.d.cts +13 -1
  50. package/dist/sdk.d.ts +13 -1
  51. package/dist/sdk.js +6 -1
  52. package/dist/sdk.js.map +1 -1
  53. package/package.json +7 -5
  54. package/src/client.ts +86 -11
  55. package/src/errors.ts +8 -10
  56. package/src/examples/simulateCompiledSwap.ts +76 -0
  57. package/src/examples/simulateRawTransaction.ts +39 -0
  58. package/src/generated/config.generated.ts +179 -6
  59. package/src/generated/materialisers.generated.ts +0 -12
  60. package/src/generated/operations.generated.ts +0 -177
  61. package/src/index.ts +19 -0
  62. package/src/responseSchemas.ts +143 -0
  63. package/src/run/simulate.ts +78 -0
  64. package/src/sdk.ts +20 -1
  65. package/src/examples/staged/aaveBorrow.ts +0 -128
  66. package/src/examples/staged/aaveDebtRebalanceExactOut.ts +0 -301
  67. package/src/examples/staged/aaveHealthFactorExplode.ts +0 -70
  68. package/src/examples/staged/aaveToMorphoDebtMigration.ts +0 -200
  69. package/src/examples/staged/aaveToMorphoDebtMigrationWithSwap.ts +0 -231
  70. package/src/examples/staged/aaveToMorphoDebtMigrationWithSwapExactOut.ts +0 -307
  71. package/src/examples/staged/flashloanRepay.ts +0 -142
  72. package/src/examples/staged/morphoBlueBorrow.ts +0 -93
  73. package/src/examples/staged/morphoBlueRepay.ts +0 -132
  74. package/src/examples/staged/registry.ts +0 -80
package/dist/sdk.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sdk.ts"],"sourcesContent":["import type {\n ComposeCompileRequest,\n ComposeCompileResult,\n InputSpec,\n} from '@lifi/compose-spec';\n\nimport {\n createFlowBuilderCore,\n type FlowBuilderCore,\n type FlowOptions,\n type TypedFlow,\n} from './authoring/FlowBuilderCore.js';\nimport { type ComposeClient, createComposeClient } from './client.js';\nimport {\n bindGeneratedOps,\n type GeneratedOps,\n} from './generated/operations.generated.js';\nimport { buildRun, type ComposeRunInput } from './run/inputs.js';\nimport type { InputSchema } from './types.js';\n\nexport type { ComposeRunInput };\n\n/**\n * A flow builder augmented with generated operation methods and a `compile` method\n * that submits the flow to the Compose backend for compilation.\n *\n * Created via {@link ComposeSdk.flow}.\n *\n * @typeParam T - The input schema describing the flow's required inputs.\n */\nexport type FlowBuilder<T extends InputSchema = InputSchema> =\n FlowBuilderCore<T> &\n GeneratedOps & {\n /**\n * Builds the flow document from the current builder state and submits it\n * to the Compose API for compilation.\n *\n * @param run - Runtime inputs, preconditions, and signer address.\n * @returns The compiled transaction calldata and metadata.\n * @throws {@link ComposeError} on network, validation, or server errors.\n *\n * @example\n * ```ts\n * const result = await builder.compile({\n * inputs: { token: materialisers.balanceOf({}) },\n * signer: '0xYourAddress...',\n * });\n * console.log(result.calldata);\n * ```\n */\n readonly compile: (\n run: ComposeRunInput<T>,\n ) => Promise<ComposeCompileResult>;\n };\n\n/**\n * Configuration for creating a Compose SDK instance.\n */\nexport interface ComposeSdkOptions {\n /** Base URL of the Compose API (e.g. `\"https://li.quest\"`). */\n readonly baseUrl: string;\n /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** Optional LI.FI API key for authenticated access. Sent as the `x-lifi-api-key` header on every request. */\n readonly apiKey?: string;\n}\n\n/**\n * The top-level Compose SDK interface.\n *\n * Provides methods to build flows, compile them into executable calldata, and\n * interact with the Compose API directly.\n *\n * @example\n * ```ts\n * import { createComposeSdk, resources, materialisers } from '@lifi/composer-sdk';\n *\n * const sdk = createComposeSdk({ baseUrl: 'https://li.quest' });\n *\n * const builder = sdk.flow(1, {\n * inputs: { usdc: resources.erc20('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 1) },\n * });\n *\n * builder.lifi.swap('swap1', {\n * bind: { amountIn: builder.inputs.usdc },\n * config: { resourceOut: resources.native(1) },\n * });\n *\n * const result = await builder.compile({\n * inputs: { usdc: materialisers.balanceOf({}) },\n * signer: '0xYourAddress...',\n * });\n * ```\n */\nexport interface ComposeSdk {\n /** Low-level HTTP client for the Compose API. */\n readonly client: ComposeClient;\n /**\n * Creates a new flow builder targeting the given chain.\n * @param chainId - The EVM chain ID (e.g. `1` for Ethereum mainnet).\n * @param options - Flow configuration including input declarations.\n * @returns A {@link FlowBuilder} with generated operation methods and a `compile` method.\n * @throws Error if any resource input's `chainId` doesn't match the flow's `chainId`.\n */\n readonly flow: <T extends InputSchema>(\n chainId: number,\n options: FlowOptions<T>,\n ) => FlowBuilder<T>;\n /**\n * Builds a raw compile request without sending it. Useful for inspecting the\n * request payload or submitting it through a custom transport.\n * @param flow - A built flow document (from `builder.build()`).\n * @param run - Runtime inputs, preconditions, and signer address.\n * @returns A {@link ComposeCompileRequest} ready to send to the Compose API.\n */\n readonly request: <T extends InputSchema>(\n flow: TypedFlow<T>,\n run: ComposeRunInput<T>,\n ) => ComposeCompileRequest;\n}\n\n/**\n * Creates a new Compose SDK instance.\n *\n * @param options - SDK configuration including the API base URL and optional fetch implementation.\n * @returns A {@link ComposeSdk} instance.\n *\n * @example\n * ```ts\n * const sdk = createComposeSdk({ baseUrl: 'https://li.quest' });\n * ```\n */\nexport const createComposeSdk = (options: ComposeSdkOptions): ComposeSdk => {\n const composeClient = createComposeClient(options);\n\n const request = <T extends InputSchema>(\n flowDoc: TypedFlow<T>,\n run: ComposeRunInput<T>,\n ): ComposeCompileRequest => ({\n flow: flowDoc,\n run: buildRun({\n inputs: run.inputs as Record<string, InputSpec>,\n preconditions: run.preconditions,\n signer: run.signer,\n assumptions: run.assumptions as Record<string, bigint> | undefined,\n referrer: run.referrer,\n integratorFeeBps: run.integratorFeeBps,\n maxPriceImpactBps: run.maxPriceImpactBps,\n sweepTo: run.sweepTo,\n simulationPolicy: run.simulationPolicy,\n checkOnChainAllowances: run.checkOnChainAllowances,\n }),\n });\n\n const flow = <T extends InputSchema>(\n chainId: number,\n opts: FlowOptions<T>,\n ): FlowBuilder<T> => {\n const core = createFlowBuilderCore(chainId, opts);\n const builder = Object.assign(core, bindGeneratedOps(core));\n\n const compile = async (\n run: ComposeRunInput<T>,\n ): Promise<ComposeCompileResult> => {\n const flowDoc = builder.build();\n const req = request(flowDoc, run);\n return composeClient.compile(req);\n };\n\n return Object.assign(builder, { compile });\n };\n\n return { client: composeClient, flow, request };\n};\n"],"mappings":"AAMA;AAAA,EACE;AAAA,OAIK;AACP,SAA6B,2BAA2B;AACxD;AAAA,EACE;AAAA,OAEK;AACP,SAAS,gBAAsC;AAmHxC,MAAM,mBAAmB,CAAC,YAA2C;AAC1E,QAAM,gBAAgB,oBAAoB,OAAO;AAEjD,QAAM,UAAU,CACd,SACA,SAC2B;AAAA,IAC3B,MAAM;AAAA,IACN,KAAK,SAAS;AAAA,MACZ,QAAQ,IAAI;AAAA,MACZ,eAAe,IAAI;AAAA,MACnB,QAAQ,IAAI;AAAA,MACZ,aAAa,IAAI;AAAA,MACjB,UAAU,IAAI;AAAA,MACd,kBAAkB,IAAI;AAAA,MACtB,mBAAmB,IAAI;AAAA,MACvB,SAAS,IAAI;AAAA,MACb,kBAAkB,IAAI;AAAA,MACtB,wBAAwB,IAAI;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,QAAM,OAAO,CACX,SACA,SACmB;AACnB,UAAM,OAAO,sBAAsB,SAAS,IAAI;AAChD,UAAM,UAAU,OAAO,OAAO,MAAM,iBAAiB,IAAI,CAAC;AAE1D,UAAM,UAAU,OACd,QACkC;AAClC,YAAM,UAAU,QAAQ,MAAM;AAC9B,YAAM,MAAM,QAAQ,SAAS,GAAG;AAChC,aAAO,cAAc,QAAQ,GAAG;AAAA,IAClC;AAEA,WAAO,OAAO,OAAO,SAAS,EAAE,QAAQ,CAAC;AAAA,EAC3C;AAEA,SAAO,EAAE,QAAQ,eAAe,MAAM,QAAQ;AAChD;","names":[]}
1
+ {"version":3,"sources":["../src/sdk.ts"],"sourcesContent":["import type {\n ComposeCompileRequest,\n ComposeCompileResult,\n InputSpec,\n SimulateRequest,\n SimulateResult,\n} from '@lifi/compose-spec';\n\nimport {\n createFlowBuilderCore,\n type FlowBuilderCore,\n type FlowOptions,\n type TypedFlow,\n} from './authoring/FlowBuilderCore.js';\nimport { type ComposeClient, createComposeClient } from './client.js';\nimport {\n bindGeneratedOps,\n type GeneratedOps,\n} from './generated/operations.generated.js';\nimport { buildRun, type ComposeRunInput } from './run/inputs.js';\nimport type { InputSchema } from './types.js';\n\nexport type { ComposeRunInput };\n\n/**\n * A flow builder augmented with generated operation methods and a `compile` method\n * that submits the flow to the Compose backend for compilation.\n *\n * Created via {@link ComposeSdk.flow}.\n *\n * @typeParam T - The input schema describing the flow's required inputs.\n */\nexport type FlowBuilder<T extends InputSchema = InputSchema> =\n FlowBuilderCore<T> &\n GeneratedOps & {\n /**\n * Builds the flow document from the current builder state and submits it\n * to the Compose API for compilation.\n *\n * @param run - Runtime inputs, preconditions, and signer address.\n * @returns The compiled transaction calldata and metadata.\n * @throws {@link ComposeError} on network, validation, or server errors.\n *\n * @example\n * ```ts\n * const result = await builder.compile({\n * inputs: { token: materialisers.balanceOf({}) },\n * signer: '0xYourAddress...',\n * });\n * console.log(result.calldata);\n * ```\n */\n readonly compile: (\n run: ComposeRunInput<T>,\n ) => Promise<ComposeCompileResult>;\n };\n\n/**\n * Configuration for creating a Compose SDK instance.\n */\nexport interface ComposeSdkOptions {\n /** Base URL of the Compose API (e.g. `\"https://li.quest\"`). */\n readonly baseUrl: string;\n /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */\n readonly fetch?: typeof globalThis.fetch;\n /** Optional LI.FI API key for authenticated access. Sent as the `x-lifi-api-key` header on every request. */\n readonly apiKey?: string;\n}\n\n/**\n * The top-level Compose SDK interface.\n *\n * Provides methods to build flows, compile them into executable calldata, and\n * interact with the Compose API directly.\n *\n * @example\n * ```ts\n * import { createComposeSdk, resources, materialisers } from '@lifi/composer-sdk';\n *\n * const sdk = createComposeSdk({ baseUrl: 'https://li.quest' });\n *\n * const builder = sdk.flow(1, {\n * inputs: { usdc: resources.erc20('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 1) },\n * });\n *\n * builder.lifi.swap('swap1', {\n * bind: { amountIn: builder.inputs.usdc },\n * config: { resourceOut: resources.native(1) },\n * });\n *\n * const result = await builder.compile({\n * inputs: { usdc: materialisers.balanceOf({}) },\n * signer: '0xYourAddress...',\n * });\n * ```\n */\nexport interface ComposeSdk {\n /** Low-level HTTP client for the Compose API. */\n readonly client: ComposeClient;\n /**\n * Creates a new flow builder targeting the given chain.\n * @param chainId - The EVM chain ID (e.g. `1` for Ethereum mainnet).\n * @param options - Flow configuration including input declarations.\n * @returns A {@link FlowBuilder} with generated operation methods and a `compile` method.\n * @throws Error if any resource input's `chainId` doesn't match the flow's `chainId`.\n */\n readonly flow: <T extends InputSchema>(\n chainId: number,\n options: FlowOptions<T>,\n ) => FlowBuilder<T>;\n /**\n * Builds a raw compile request without sending it. Useful for inspecting the\n * request payload or submitting it through a custom transport.\n * @param flow - A built flow document (from `builder.build()`).\n * @param run - Runtime inputs, preconditions, and signer address.\n * @returns A {@link ComposeCompileRequest} ready to send to the Compose API.\n */\n readonly request: <T extends InputSchema>(\n flow: TypedFlow<T>,\n run: ComposeRunInput<T>,\n ) => ComposeCompileRequest;\n /**\n * Simulates a raw, pre-encoded transaction. Thin pass-through to\n * {@link ComposeClient.simulate}; see it for the `ok` / `revert` / `error`\n * result discrimination and error behaviour. Pair with `buildSimulateRequest`\n * to assemble the request from a compile result.\n *\n * @param request - The raw transaction plus funding `requirements` and\n * `trackedBalances`.\n * @returns A {@link SimulateResult} (`ok` / `revert` / `error`).\n * @throws {@link ComposeError} on network, validation, or server errors.\n */\n readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;\n}\n\n/**\n * Creates a new Compose SDK instance.\n *\n * @param options - SDK configuration including the API base URL and optional fetch implementation.\n * @returns A {@link ComposeSdk} instance.\n *\n * @example\n * ```ts\n * const sdk = createComposeSdk({ baseUrl: 'https://li.quest' });\n * ```\n */\nexport const createComposeSdk = (options: ComposeSdkOptions): ComposeSdk => {\n const composeClient = createComposeClient(options);\n\n const request = <T extends InputSchema>(\n flowDoc: TypedFlow<T>,\n run: ComposeRunInput<T>,\n ): ComposeCompileRequest => ({\n flow: flowDoc,\n run: buildRun({\n inputs: run.inputs as Record<string, InputSpec>,\n preconditions: run.preconditions,\n signer: run.signer,\n assumptions: run.assumptions as Record<string, bigint> | undefined,\n referrer: run.referrer,\n integratorFeeBps: run.integratorFeeBps,\n maxPriceImpactBps: run.maxPriceImpactBps,\n sweepTo: run.sweepTo,\n simulationPolicy: run.simulationPolicy,\n checkOnChainAllowances: run.checkOnChainAllowances,\n }),\n });\n\n const flow = <T extends InputSchema>(\n chainId: number,\n opts: FlowOptions<T>,\n ): FlowBuilder<T> => {\n const core = createFlowBuilderCore(chainId, opts);\n const builder = Object.assign(core, bindGeneratedOps(core));\n\n const compile = async (\n run: ComposeRunInput<T>,\n ): Promise<ComposeCompileResult> => {\n const flowDoc = builder.build();\n const req = request(flowDoc, run);\n return composeClient.compile(req);\n };\n\n return Object.assign(builder, { compile });\n };\n\n return {\n client: composeClient,\n flow,\n request,\n simulate: composeClient.simulate,\n };\n};\n"],"mappings":"AAQA;AAAA,EACE;AAAA,OAIK;AACP,SAA6B,2BAA2B;AACxD;AAAA,EACE;AAAA,OAEK;AACP,SAAS,gBAAsC;AA+HxC,MAAM,mBAAmB,CAAC,YAA2C;AAC1E,QAAM,gBAAgB,oBAAoB,OAAO;AAEjD,QAAM,UAAU,CACd,SACA,SAC2B;AAAA,IAC3B,MAAM;AAAA,IACN,KAAK,SAAS;AAAA,MACZ,QAAQ,IAAI;AAAA,MACZ,eAAe,IAAI;AAAA,MACnB,QAAQ,IAAI;AAAA,MACZ,aAAa,IAAI;AAAA,MACjB,UAAU,IAAI;AAAA,MACd,kBAAkB,IAAI;AAAA,MACtB,mBAAmB,IAAI;AAAA,MACvB,SAAS,IAAI;AAAA,MACb,kBAAkB,IAAI;AAAA,MACtB,wBAAwB,IAAI;AAAA,IAC9B,CAAC;AAAA,EACH;AAEA,QAAM,OAAO,CACX,SACA,SACmB;AACnB,UAAM,OAAO,sBAAsB,SAAS,IAAI;AAChD,UAAM,UAAU,OAAO,OAAO,MAAM,iBAAiB,IAAI,CAAC;AAE1D,UAAM,UAAU,OACd,QACkC;AAClC,YAAM,UAAU,QAAQ,MAAM;AAC9B,YAAM,MAAM,QAAQ,SAAS,GAAG;AAChC,aAAO,cAAc,QAAQ,GAAG;AAAA,IAClC;AAEA,WAAO,OAAO,OAAO,SAAS,EAAE,QAAQ,CAAC;AAAA,EAC3C;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,UAAU,cAAc;AAAA,EAC1B;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifi/composer-sdk",
3
- "version": "0.2.0-staging.5",
3
+ "version": "0.2.0",
4
4
  "description": "Public Composer SDK for building and submitting flows",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "./package.json": "./package.json"
24
24
  },
25
25
  "engines": {
26
- "node": ">=18"
26
+ "node": ">=24"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
@@ -51,11 +51,12 @@
51
51
  "example:staged": "tsx src/examples/run.ts --staged"
52
52
  },
53
53
  "peerDependencies": {
54
- "@lifi/compose-spec": "0.2.0-staging.5"
54
+ "@lifi/compose-spec": "0.2.0"
55
55
  },
56
56
  "dependencies": {
57
57
  "abitype": "^1.2.3",
58
- "effect": "^3.21.0"
58
+ "effect": "^3.21.0",
59
+ "zod": "^4.1.11"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@eslint/compat": "^1.4.0",
@@ -84,6 +85,7 @@
84
85
  "!src/**/*.test.ts",
85
86
  "!src/**/*.mock.ts",
86
87
  "!src/**/*.typecheck.ts",
87
- "!dist/**/*.typecheck.*"
88
+ "!dist/**/*.typecheck.*",
89
+ "!src/examples/staged/**"
88
90
  ]
89
91
  }
package/src/client.ts CHANGED
@@ -4,10 +4,16 @@ import type {
4
4
  ComposeCompileResult,
5
5
  ComposeCompileSuccessData,
6
6
  ComposeManifest,
7
+ SimulateRequest,
8
+ SimulateResult,
7
9
  } from '@lifi/compose-spec';
8
10
 
9
11
  import type { GetZapPacksOptions, ZapPackOverview } from './discovery.js';
10
12
  import { ComposeError, errorFromHttpResponse } from './errors.js';
13
+ import {
14
+ parseCompilePartialEnvelope,
15
+ parseSimulateResult,
16
+ } from './responseSchemas.js';
11
17
 
12
18
  // __SDK_VERSION__ is a compile-time constant injected by tsup (via `define` in tsup.config.ts)
13
19
  // and by vitest (via `define` in vitest.config.ts). Both read the version from package.json
@@ -78,6 +84,30 @@ export interface ComposeClient {
78
84
  readonly getZapPacks: (
79
85
  options?: GetZapPacksOptions,
80
86
  ) => Promise<readonly ZapPackOverview[]>;
87
+ /**
88
+ * Simulates a raw, pre-encoded transaction against `POST /simulate` and
89
+ * reports how the watched balances change and how much inner-call gas it
90
+ * burns.
91
+ *
92
+ * The result is a discriminated union on `status`:
93
+ * - `'ok'` — the simulation ran successfully; `deltas`/`gasUsed` are populated.
94
+ * - `'revert'` — the simulation ran but the transaction reverted on-chain. A
95
+ * revert is a *successful simulation*, not a transport error, so it is
96
+ * returned (HTTP 200) rather than thrown — mirroring how {@link compile}
97
+ * returns `status: 'partial'` on a simulated revert.
98
+ * - `'error'` — the request was well-formed but the simulation could not be
99
+ * set up or run (HTTP 422); `message` is intentionally generic.
100
+ *
101
+ * `bigint` amounts in the request (`value`, requirement `balance`/`allowance`)
102
+ * are serialised to decimal strings automatically.
103
+ *
104
+ * @param request - The raw transaction plus funding `requirements` and the
105
+ * `trackedBalances` to watch.
106
+ * @returns A {@link SimulateResult} (`ok` / `revert` / `error`).
107
+ * @throws {@link ComposeError} on network failures, HTTP 400 (malformed
108
+ * input), 401/403 (auth), 404, 429, and 5xx.
109
+ */
110
+ readonly simulate: (request: SimulateRequest) => Promise<SimulateResult>;
81
111
  }
82
112
 
83
113
  const bigintReplacer = (_key: string, value: unknown): unknown =>
@@ -109,22 +139,39 @@ const parsePartialBody = async (
109
139
  url: string,
110
140
  ): Promise<ComposeCompileResult> => {
111
141
  const body = await res.json().catch((_) => null);
112
- if (
113
- !isNonNullObject(body) ||
114
- !('data' in body) ||
115
- !isNonNullObject(body.data) ||
116
- !('error' in body) ||
117
- !isNonNullObject(body.error)
118
- ) {
142
+ const envelope = parseCompilePartialEnvelope(body);
143
+ if (envelope === null) {
119
144
  throw new ComposeError(
120
145
  'UNKNOWN_ERROR',
121
146
  'Unexpected partial response format',
122
147
  { url },
123
148
  );
124
149
  }
125
- const data = body.data as unknown as ComposeCompilePartialData;
126
- const error = body.error as unknown as { kind: string; message: string };
127
- return { ...data, status: 'partial' as const, error };
150
+ // `data` is validated as an object by the schema; compose-spec owns its full
151
+ // shape as a hand-authored type, so we narrow it here rather than re-declaring
152
+ // that type as a schema. `error` is fully validated — no cast needed.
153
+ const data = envelope.data as unknown as ComposeCompilePartialData;
154
+ return { ...data, status: 'partial' as const, error: envelope.error };
155
+ };
156
+
157
+ // `/simulate` is un-enveloped: the discriminated body (`{ status, ... }`) is at
158
+ // the top level, NOT wrapped in `{ data }` like `/compose`. So this reads the
159
+ // body directly and validates it against the simulate union rather than reusing
160
+ // `parseBody`.
161
+ const parseSimulateBody = async (
162
+ res: Response,
163
+ url: string,
164
+ ): Promise<SimulateResult> => {
165
+ const body = await res.json().catch((_) => null);
166
+ const result = parseSimulateResult(body);
167
+ if (result === null) {
168
+ throw new ComposeError(
169
+ 'UNKNOWN_ERROR',
170
+ 'Unexpected simulate response format',
171
+ { url },
172
+ );
173
+ }
174
+ return result;
128
175
  };
129
176
 
130
177
  /**
@@ -226,5 +273,33 @@ export const createComposeClient = (
226
273
  return await parseBody<readonly ZapPackOverview[]>(res, url);
227
274
  };
228
275
 
229
- return { getManifest, compile, getZapPacks };
276
+ const simulate = async (
277
+ request: SimulateRequest,
278
+ ): Promise<SimulateResult> => {
279
+ const url = `${base}/simulate`;
280
+ let res: Response;
281
+ try {
282
+ res = await fetchFn(url, {
283
+ method: 'POST',
284
+ headers: { ...baseHeaders, 'Content-Type': 'application/json' },
285
+ body: JSON.stringify(request, bigintReplacer),
286
+ });
287
+ } catch (err) {
288
+ const message = err instanceof Error ? err.message : String(err);
289
+ throw new ComposeError('NETWORK_ERROR', message, { cause: err });
290
+ }
291
+ // Only 200 (carries `ok`/`revert`) and 422 (carries the `error` member of
292
+ // the union) have a discriminated body. 422 is deliberately intercepted
293
+ // here (not thrown as VALIDATION_ERROR) so callers get one exhaustive
294
+ // `switch (result.status)`. Every other status is a transport error and is
295
+ // thrown — including HTTP 400 (malformed input, no `status` body) and any
296
+ // unexpected 2xx.
297
+ if (res.status === 200 || res.status === 422) {
298
+ return await parseSimulateBody(res, url);
299
+ }
300
+ const body = await res.text();
301
+ throw errorFromHttpResponse(res.status, body, url);
302
+ };
303
+
304
+ return { getManifest, compile, getZapPacks, simulate };
230
305
  };
package/src/errors.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import type { ComposeErrorKind, SimulationRevert } from '@lifi/compose-spec';
2
2
 
3
+ import {
4
+ parseServerErrorBody,
5
+ type ServerErrorBody,
6
+ } from './responseSchemas.js';
7
+
3
8
  /**
4
9
  * Machine-readable error codes returned by the SDK.
5
10
  *
@@ -101,21 +106,14 @@ const STATUS_TO_CODE: ReadonlyMap<number, ComposeErrorCode> = new Map<
101
106
  [429, 'RATE_LIMITED'],
102
107
  ]);
103
108
 
104
- interface ServerErrorBody {
105
- readonly error?: {
106
- readonly kind?: string;
107
- readonly message?: string;
108
- readonly path?: string;
109
- readonly details?: SimulationRevert;
110
- };
111
- }
112
-
113
109
  const tryParseErrorBody = (body: string): ServerErrorBody | null => {
110
+ let json: unknown;
114
111
  try {
115
- return JSON.parse(body) as ServerErrorBody;
112
+ json = JSON.parse(body);
116
113
  } catch {
117
114
  return null;
118
115
  }
116
+ return parseServerErrorBody(json);
119
117
  };
120
118
 
121
119
  /**
@@ -0,0 +1,76 @@
1
+ import type { SimulateResult } from '@lifi/compose-spec';
2
+
3
+ import { buildSimulateRequest, materialisers, resources } from '../index.js';
4
+ import type { ComposeSdk } from '../index.js';
5
+
6
+ import { OWNER } from './config.js';
7
+
8
+ const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
9
+ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
10
+
11
+ // 1 WETH (18 decimals).
12
+ const ONE_WETH = 1_000_000_000_000_000_000n;
13
+
14
+ /**
15
+ * End-to-end `/simulate` usage: compile a WETH→USDC swap, then simulate the
16
+ * compiled transaction to see how the signer's balances move and how much
17
+ * inner-call gas it burns — without broadcasting anything on-chain.
18
+ *
19
+ * Demonstrates the compile → simulate arc:
20
+ * - `buildSimulateRequest` lifts a `ComposeCompileResult` into a
21
+ * `SimulateRequest`, supplying the fields a compiled flow cannot carry: the
22
+ * target `chainId`, the `signer` (`from`), the funding `requirements`, and the
23
+ * `trackedBalances` to watch.
24
+ * - `sdk.simulate` runs it and returns a discriminated `ok | revert | error`
25
+ * result. A `revert` is a *successful simulation* whose execution reverted, so
26
+ * it is returned (not thrown); only transport/auth/5xx failures throw.
27
+ *
28
+ * Unlike the pure-builder examples, this performs network I/O through the
29
+ * injected `sdk` (one `/compose` call, one `/simulate` call). Pass a live
30
+ * `createComposeSdk({ baseUrl })` to run it for real, or a mock-backed SDK in
31
+ * tests.
32
+ */
33
+ export const simulateCompiledSwap = async (
34
+ sdk: ComposeSdk,
35
+ ): Promise<SimulateResult> => {
36
+ // Author and compile a single WETH → USDC swap on Ethereum mainnet.
37
+ const builder = sdk.flow(1, {
38
+ name: 'swap-weth-to-usdc',
39
+ inputs: { amountIn: resources.erc20(WETH, 1) },
40
+ });
41
+
42
+ builder.lifi.swap('swap', {
43
+ bind: { amountIn: builder.inputs.amountIn },
44
+ config: { resourceOut: resources.erc20(USDC, 1), slippage: 0.03 },
45
+ });
46
+
47
+ const compiled = await builder.compile({
48
+ signer: OWNER,
49
+ inputs: {
50
+ amountIn: materialisers.directDeposit({ amount: ONE_WETH }),
51
+ },
52
+ sweepTo: builder.context.sender,
53
+ });
54
+
55
+ // Lift the compiled transaction into a simulation request. The chain, signer,
56
+ // funding, and watched balances are NOT inferable from a compiled flow, so
57
+ // they are supplied here. `to`/`data`/`value` come from the compile result.
58
+ const request = buildSimulateRequest({
59
+ result: compiled,
60
+ chainId: 1,
61
+ signer: OWNER,
62
+ // Seed the signer with the 1 WETH the swap deposits, so the inner call has
63
+ // funds to pull. `bigint` amounts are serialised to decimal strings.
64
+ requirements: [
65
+ { type: 'Erc20Balance', wallet: OWNER, token: WETH, balance: ONE_WETH },
66
+ ],
67
+ // Watch the signer's WETH (spent) and USDC (received) balances; the response
68
+ // `deltas` come back ordered to match.
69
+ trackedBalances: [
70
+ { token: WETH, owner: OWNER },
71
+ { token: USDC, owner: OWNER },
72
+ ],
73
+ });
74
+
75
+ return sdk.simulate(request);
76
+ };
@@ -0,0 +1,39 @@
1
+ import type { SimulateRequest } from '@lifi/compose-spec';
2
+
3
+ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
4
+ const SENDER = '0x1111111111111111111111111111111111111111';
5
+ const RECIPIENT = '0x2222222222222222222222222222222222222222';
6
+
7
+ /**
8
+ * Build a representative `POST /simulate` request: a USDC transfer of 1 USDC
9
+ * (1_000_000 base units at 6 decimals) from `SENDER` to `RECIPIENT` on chain 1.
10
+ *
11
+ * Demonstrates:
12
+ * - A pre-encoded `transfer(address,uint256)` calldata literal (`data`)
13
+ * - Funding the sender with an `Erc20Balance` requirement so the transfer can
14
+ * succeed in simulation (`bigint` amounts are accepted and serialised)
15
+ * - Watching both the sender's and recipient's USDC balance via `trackedBalances`
16
+ *
17
+ * Pure: returns a plain {@link SimulateRequest}, performs no network I/O. Pass
18
+ * it to `sdk.client.simulate(...)` / `sdk.simulate(...)` to run it.
19
+ */
20
+ export const buildSimulateRawTransactionExample = (): SimulateRequest => ({
21
+ chainId: 1,
22
+ from: SENDER,
23
+ to: USDC,
24
+ // transfer(RECIPIENT, 1_000_000)
25
+ data: '0xa9059cbb0000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000000000000000000f4240',
26
+ value: 0n,
27
+ requirements: [
28
+ {
29
+ type: 'Erc20Balance',
30
+ wallet: SENDER,
31
+ token: USDC,
32
+ balance: 1_000_000n,
33
+ },
34
+ ],
35
+ trackedBalances: [
36
+ { token: USDC, owner: SENDER },
37
+ { token: USDC, owner: RECIPIENT },
38
+ ],
39
+ });
@@ -126,23 +126,139 @@ export interface CoreEmitEvent4Config {
126
126
  signature: string;
127
127
  }
128
128
 
129
+ /**
130
+ * a string at most 65536 character(s) long
131
+ */
132
+ export type MaxLength65536 = string;
133
+
129
134
  export interface CoreCallConfig {
130
135
  target: Address;
131
136
  functionSignature: string;
132
137
  callType?: 'Call' | 'ValueCall';
133
- args?: Unknown[];
138
+ args?: (
139
+ | (
140
+ | MaxLength65536
141
+ | number
142
+ | boolean
143
+ | {
144
+ $ref: string;
145
+ }
146
+ )
147
+ | (
148
+ | (
149
+ | MaxLength65536
150
+ | number
151
+ | boolean
152
+ | {
153
+ $ref: string;
154
+ }
155
+ )
156
+ | (
157
+ | (
158
+ | MaxLength65536
159
+ | number
160
+ | boolean
161
+ | {
162
+ $ref: string;
163
+ }
164
+ )
165
+ | (
166
+ | (
167
+ | MaxLength65536
168
+ | number
169
+ | boolean
170
+ | {
171
+ $ref: string;
172
+ }
173
+ )
174
+ | (
175
+ | (
176
+ | MaxLength65536
177
+ | number
178
+ | boolean
179
+ | {
180
+ $ref: string;
181
+ }
182
+ )
183
+ | (
184
+ | MaxLength65536
185
+ | number
186
+ | boolean
187
+ | {
188
+ $ref: string;
189
+ }
190
+ )[]
191
+ )[]
192
+ )[]
193
+ )[]
194
+ )[]
195
+ )[];
134
196
  approvals?: {
135
197
  spender: Address;
136
198
  }[];
137
199
  }
138
- export interface Unknown {
139
- [k: string]: unknown;
140
- }
141
200
 
142
201
  export interface CoreStaticCallConfig {
143
202
  target: Address;
144
203
  functionSignature: string;
145
- args?: Unknown[];
204
+ args?: (
205
+ | (
206
+ | MaxLength65536
207
+ | number
208
+ | boolean
209
+ | {
210
+ $ref: string;
211
+ }
212
+ )
213
+ | (
214
+ | (
215
+ | MaxLength65536
216
+ | number
217
+ | boolean
218
+ | {
219
+ $ref: string;
220
+ }
221
+ )
222
+ | (
223
+ | (
224
+ | MaxLength65536
225
+ | number
226
+ | boolean
227
+ | {
228
+ $ref: string;
229
+ }
230
+ )
231
+ | (
232
+ | (
233
+ | MaxLength65536
234
+ | number
235
+ | boolean
236
+ | {
237
+ $ref: string;
238
+ }
239
+ )
240
+ | (
241
+ | (
242
+ | MaxLength65536
243
+ | number
244
+ | boolean
245
+ | {
246
+ $ref: string;
247
+ }
248
+ )
249
+ | (
250
+ | MaxLength65536
251
+ | number
252
+ | boolean
253
+ | {
254
+ $ref: string;
255
+ }
256
+ )[]
257
+ )[]
258
+ )[]
259
+ )[]
260
+ )[]
261
+ )[];
146
262
  }
147
263
 
148
264
  export interface CoreRawCallConfig {
@@ -156,7 +272,64 @@ export interface CoreInvokeConfig {
156
272
  target: Address;
157
273
  functionSignature: string;
158
274
  callType?: 'Call' | 'ValueCall';
159
- args?: Unknown[];
275
+ args?: (
276
+ | (
277
+ | MaxLength65536
278
+ | number
279
+ | boolean
280
+ | {
281
+ $ref: string;
282
+ }
283
+ )
284
+ | (
285
+ | (
286
+ | MaxLength65536
287
+ | number
288
+ | boolean
289
+ | {
290
+ $ref: string;
291
+ }
292
+ )
293
+ | (
294
+ | (
295
+ | MaxLength65536
296
+ | number
297
+ | boolean
298
+ | {
299
+ $ref: string;
300
+ }
301
+ )
302
+ | (
303
+ | (
304
+ | MaxLength65536
305
+ | number
306
+ | boolean
307
+ | {
308
+ $ref: string;
309
+ }
310
+ )
311
+ | (
312
+ | (
313
+ | MaxLength65536
314
+ | number
315
+ | boolean
316
+ | {
317
+ $ref: string;
318
+ }
319
+ )
320
+ | (
321
+ | MaxLength65536
322
+ | number
323
+ | boolean
324
+ | {
325
+ $ref: string;
326
+ }
327
+ )[]
328
+ )[]
329
+ )[]
330
+ )[]
331
+ )[]
332
+ )[];
160
333
  }
161
334
 
162
335
  export interface CoreAsResourceConfig {
@@ -10,7 +10,6 @@ import type {
10
10
  BalanceOfMaterialiserConfig as BalanceOfMaterialiserConfigSchema,
11
11
  CallMaterialiserConfig as CallMaterialiserConfigSchema,
12
12
  DirectDepositMaterialiserConfig as DirectDepositMaterialiserConfigSchema,
13
- FlashloanMaterialiserConfig as FlashloanMaterialiserConfigSchema,
14
13
  } from './config.generated.js';
15
14
 
16
15
  export type BalanceOfMaterialiserConfig =
@@ -43,14 +42,3 @@ export const call = (config: CallMaterialiserConfig): MaterialiserInput => ({
43
42
  kind: 'call',
44
43
  ...config,
45
44
  });
46
-
47
- export type FlashloanMaterialiserConfig =
48
- MaterialiserConfigOf<FlashloanMaterialiserConfigSchema>;
49
-
50
- /** Borrow tokens from a flashloan provider; amount becomes a register populated by the proxy at callback time — accepts: resource */
51
- export const flashloan = (
52
- config: FlashloanMaterialiserConfig,
53
- ): MaterialiserInput => ({
54
- kind: 'flashloan',
55
- ...config,
56
- });