@lifi/composer-sdk 0.2.0-staging.4 → 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 (88) hide show
  1. package/README.md +81 -0
  2. package/dist/authoring/handles.cjs.map +1 -1
  3. package/dist/authoring/handles.d.cts +13 -6
  4. package/dist/authoring/handles.d.ts +13 -6
  5. package/dist/authoring/handles.js.map +1 -1
  6. package/dist/client.cjs +38 -6
  7. package/dist/client.cjs.map +1 -1
  8. package/dist/client.d.cts +25 -1
  9. package/dist/client.d.ts +25 -1
  10. package/dist/client.js +41 -6
  11. package/dist/client.js.map +1 -1
  12. package/dist/errors.cjs +4 -1
  13. package/dist/errors.cjs.map +1 -1
  14. package/dist/errors.js +6 -1
  15. package/dist/errors.js.map +1 -1
  16. package/dist/generated/config.generated.cjs.map +1 -1
  17. package/dist/generated/config.generated.d.cts +82 -7
  18. package/dist/generated/config.generated.d.ts +82 -7
  19. package/dist/generated/materialisers.generated.cjs +2 -8
  20. package/dist/generated/materialisers.generated.cjs.map +1 -1
  21. package/dist/generated/materialisers.generated.d.cts +1 -1
  22. package/dist/generated/materialisers.generated.d.ts +1 -1
  23. package/dist/generated/materialisers.generated.js +1 -6
  24. package/dist/generated/materialisers.generated.js.map +1 -1
  25. package/dist/generated/operations.generated.cjs +56 -31
  26. package/dist/generated/operations.generated.cjs.map +1 -1
  27. package/dist/generated/operations.generated.d.cts +204 -94
  28. package/dist/generated/operations.generated.d.ts +204 -94
  29. package/dist/generated/operations.generated.js +56 -31
  30. package/dist/generated/operations.generated.js.map +1 -1
  31. package/dist/index.cjs +8 -0
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +4 -3
  34. package/dist/index.d.ts +4 -3
  35. package/dist/index.js +8 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/{materialisers.generated-BfWJ-g7t.d.ts → materialisers.generated-BMkBQgWZ.d.ts} +3 -8
  38. package/dist/{materialisers.generated-DaJRwiRe.d.cts → materialisers.generated-BWq5oePO.d.cts} +3 -8
  39. package/dist/responseSchemas.cjs +110 -0
  40. package/dist/responseSchemas.cjs.map +1 -0
  41. package/dist/responseSchemas.d.cts +36 -0
  42. package/dist/responseSchemas.d.ts +36 -0
  43. package/dist/responseSchemas.js +74 -0
  44. package/dist/responseSchemas.js.map +1 -0
  45. package/dist/run/simulate.cjs +41 -0
  46. package/dist/run/simulate.cjs.map +1 -0
  47. package/dist/run/simulate.d.cts +59 -0
  48. package/dist/run/simulate.d.ts +59 -0
  49. package/dist/run/simulate.js +17 -0
  50. package/dist/run/simulate.js.map +1 -0
  51. package/dist/sdk.cjs +6 -1
  52. package/dist/sdk.cjs.map +1 -1
  53. package/dist/sdk.d.cts +13 -1
  54. package/dist/sdk.d.ts +13 -1
  55. package/dist/sdk.js +6 -1
  56. package/dist/sdk.js.map +1 -1
  57. package/package.json +7 -5
  58. package/src/authoring/handles.ts +19 -10
  59. package/src/client.ts +86 -11
  60. package/src/errors.ts +8 -10
  61. package/src/examples/aaveRepay.ts +55 -16
  62. package/src/examples/aaveRepayWithATokens.ts +41 -30
  63. package/src/examples/aaveRounding.ts +42 -0
  64. package/src/examples/aaveSetEMode.ts +0 -1
  65. package/src/examples/config.ts +1 -1
  66. package/src/examples/emitCustomEvent.ts +75 -0
  67. package/src/examples/invariantChecks.ts +96 -0
  68. package/src/examples/run.ts +20 -18
  69. package/src/examples/simulateCompiledSwap.ts +76 -0
  70. package/src/examples/simulateRawTransaction.ts +39 -0
  71. package/src/examples/untypedOpWithTypedRef.ts +26 -20
  72. package/src/generated/config.generated.ts +229 -6
  73. package/src/generated/materialisers.generated.ts +0 -12
  74. package/src/generated/operations.generated.ts +330 -177
  75. package/src/index.ts +19 -0
  76. package/src/responseSchemas.ts +143 -0
  77. package/src/run/simulate.ts +78 -0
  78. package/src/sdk.ts +20 -1
  79. package/src/examples/staged/aaveBorrow.ts +0 -104
  80. package/src/examples/staged/aaveDebtRebalanceExactOut.ts +0 -301
  81. package/src/examples/staged/aaveHealthFactorExplode.ts +0 -70
  82. package/src/examples/staged/aaveToMorphoDebtMigration.ts +0 -190
  83. package/src/examples/staged/aaveToMorphoDebtMigrationWithSwap.ts +0 -221
  84. package/src/examples/staged/aaveToMorphoDebtMigrationWithSwapExactOut.ts +0 -302
  85. package/src/examples/staged/flashloanRepay.ts +0 -138
  86. package/src/examples/staged/morphoBlueBorrow.ts +0 -89
  87. package/src/examples/staged/morphoBlueRepay.ts +0 -91
  88. package/src/examples/staged/registry.ts +0 -78
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
  /**
@@ -7,29 +7,44 @@ import { BASE_URL } from './config.js';
7
7
 
8
8
  // Aave v3 contracts on Ethereum mainnet.
9
9
  const AAVE_V3_POOL = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2';
10
+ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
10
11
  const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
12
+ // Aave v3 aEthUSDC receipt token and variable-debt WETH token.
13
+ const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';
14
+ const VDEBT_WETH = '0xeA51d7853EEFb32b6ee06b1C12E6dcCA88Be0fFE';
11
15
 
12
16
  export interface AaveRepayInput {
13
17
  readonly owner: Address;
14
- readonly amount: `${bigint}`;
18
+ /** USDC supplied as collateral so the proxy can borrow WETH (6 dp). */
19
+ readonly collateralAmount: `${bigint}`;
20
+ /** WETH borrowed against the collateral (18 dp). */
21
+ readonly borrowAmount: `${bigint}`;
22
+ /**
23
+ * WETH deposited to repay the debt (18 dp). Must exceed `borrowAmount`:
24
+ * Aave's scaled-balance rounding can leave the debt 1 wei above the
25
+ * borrowed amount, so the `max`-mode repay needs a little headroom.
26
+ */
27
+ readonly repayAmount: `${bigint}`;
15
28
  }
16
29
 
17
30
  /**
18
- * Repay an Aave v3 variable-rate WETH debt by supplying WETH to the proxy.
19
- *
20
- * Aave internally caps the repay at `min(amount, outstandingDebt)`, so the
21
- * op returns any unspent WETH on the `residual` resource port. The
22
- * residual is swept back to the sender on completion.
31
+ * Supply USDC to Aave v3, borrow WETH against it, then repay the WETH borrow —
32
+ * a self-contained supply → borrow → repay round trip.
23
33
  *
24
34
  * Demonstrates:
25
- * - `aave.repay` with a resource input (the WETH used to repay).
26
- * - Binding `onBehalfOf` from the runtime context — the proxy repays its
27
- * own debt position.
28
- * - The `residual` port for surfacing any unused input back to the caller.
35
+ * - `lifi.zap` to supply USDC into Aave (USDC → aEthUSDC routing edge)
36
+ * - `aave.borrow` to mint variable WETH debt against the supplied collateral
37
+ * - `aave.repay` (mode: 'max') funded by a fresh WETH deposit, clearing the
38
+ * debt exactly so the collateral is freed
39
+ * - `sweepTo` to return all leftover proxy-held resources (the freed
40
+ * aEthUSDC collateral, the borrowed WETH, and the repay residual) to the
41
+ * sender
29
42
  */
30
43
  export const buildAaveRepay = ({
31
44
  owner,
32
- amount,
45
+ collateralAmount,
46
+ borrowAmount,
47
+ repayAmount,
33
48
  }: AaveRepayInput): {
34
49
  flow: Flow;
35
50
  request: ComposeCompileRequest;
@@ -39,18 +54,40 @@ export const buildAaveRepay = ({
39
54
  const builder = sdk.flow(1, {
40
55
  name: 'aave-repay-weth',
41
56
  inputs: {
42
- assetIn: resources.erc20(WETH, 1),
57
+ collateralIn: resources.erc20(USDC, 1),
58
+ repayIn: resources.erc20(WETH, 1),
59
+ },
60
+ });
61
+
62
+ // Supply USDC as collateral (USDC → aEthUSDC) so the proxy can borrow.
63
+ builder.lifi.zap('supply', {
64
+ bind: { amountIn: builder.inputs.collateralIn },
65
+ config: { resourceOut: resources.erc20(A_ETH_USDC, 1) },
66
+ });
67
+
68
+ // Borrow WETH against the collateral; the borrowed WETH stays on the
69
+ // proxy as a terminal resource and is swept back to the sender.
70
+ builder.aave.borrow('borrow', {
71
+ bind: {},
72
+ config: {
73
+ pool: AAVE_V3_POOL,
74
+ asset: WETH,
75
+ variableDebtToken: VDEBT_WETH,
76
+ amount: borrowAmount,
43
77
  },
44
78
  });
45
79
 
80
+ // Repay the just-opened WETH debt from the deposited WETH. mode 'max'
81
+ // passes type(uint256).max so Aave clamps to the outstanding debt exactly
82
+ // — required to clear the borrow flag and free the collateral for sweeping.
46
83
  builder.aave.repay('repay', {
47
84
  bind: {
48
- assetIn: builder.inputs.assetIn,
85
+ assetIn: builder.inputs.repayIn,
49
86
  onBehalfOf: builder.context.executionAddress,
50
87
  },
51
88
  config: {
52
89
  pool: AAVE_V3_POOL,
53
- mode: 'exact',
90
+ mode: 'max',
54
91
  },
55
92
  });
56
93
 
@@ -59,9 +96,11 @@ export const buildAaveRepay = ({
59
96
  const request = sdk.request(flow, {
60
97
  signer: owner,
61
98
  inputs: {
62
- assetIn: materialisers.directDeposit({ amount }),
99
+ collateralIn: materialisers.directDeposit({ amount: collateralAmount }),
100
+ repayIn: materialisers.directDeposit({ amount: repayAmount }),
63
101
  },
64
- // Any unspent WETH (residual) is swept back to the signer.
102
+ // Once the debt is cleared, sweep the freed aEthUSDC collateral, the
103
+ // borrowed WETH, and the repay residual back to the sender.
65
104
  sweepTo: builder.context.sender,
66
105
  });
67
106
 
@@ -1,11 +1,6 @@
1
1
  import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
2
2
 
3
- import {
4
- createComposeSdk,
5
- materialisers,
6
- preconditions,
7
- resources,
8
- } from '../index.js';
3
+ import { createComposeSdk, materialisers, resources } from '../index.js';
9
4
  import type { Address } from '../types.js';
10
5
 
11
6
  import { BASE_URL } from './config.js';
@@ -13,32 +8,33 @@ import { BASE_URL } from './config.js';
13
8
  // Aave v3 contracts on Ethereum mainnet.
14
9
  const AAVE_V3_POOL = '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2';
15
10
  const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
11
+ // Aave v3 aEthUSDC receipt token and variable-debt USDC token.
16
12
  const A_ETH_USDC = '0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c';
13
+ const VDEBT_USDC = '0x72E95b8931767C79bA4EeE721354d6E99a61D004';
17
14
 
18
15
  export interface AaveRepayWithATokensInput {
19
16
  readonly owner: Address;
20
- readonly proxyAddress: Address;
21
- readonly expectedATokenBalance: `${bigint}`;
17
+ /** USDC supplied as collateral, minting the aUSDC burned by the repay (6 dp). */
18
+ readonly collateralAmount: `${bigint}`;
19
+ /** USDC borrowed against the collateral, then repaid by this flow (6 dp). */
20
+ readonly borrowAmount: `${bigint}`;
22
21
  }
23
22
 
24
23
  /**
25
- * Repay an Aave v3 variable-rate USDC debt by burning aToken collateral
26
- * already held by the proxy (no fresh asset transfer required).
27
- *
28
- * The aTokens are sourced from the proxy's existing balance via the
29
- * `balanceOf` materialiser. The `erc20Balance` precondition fails fast
30
- * if the expected aTokens haven't landed on the proxy yet.
24
+ * Supply USDC to Aave v3, borrow USDC against it, then repay the debt by
25
+ * burning the supplied aUSDC directly — no fresh asset transfer required.
31
26
  *
32
27
  * Demonstrates:
33
- * - `aave.repayWithATokens` with the aToken as the resource input.
34
- * - `balanceOf` materialiser to consume proxy-held aTokens directly.
35
- * - `erc20Balance` precondition to guard against silent zero-value repays.
36
- * - The `residual` port returns any aTokens not burned (when debt < input).
28
+ * - `lifi.zap` to supply USDC into Aave (USDC → aEthUSDC routing edge)
29
+ * - `aave.borrow` to mint variable USDC debt against the supplied collateral
30
+ * - `aave.repayWithATokens` to clear the debt by burning the aUSDC collateral
31
+ * - `sweepTo` to return the borrowed USDC and the unburned aUSDC to the
32
+ * sender once the debt is cleared
37
33
  */
38
34
  export const buildAaveRepayWithATokens = ({
39
35
  owner,
40
- proxyAddress,
41
- expectedATokenBalance,
36
+ collateralAmount,
37
+ borrowAmount,
42
38
  }: AaveRepayWithATokensInput): {
43
39
  flow: Flow;
44
40
  request: ComposeCompileRequest;
@@ -48,12 +44,32 @@ export const buildAaveRepayWithATokens = ({
48
44
  const builder = sdk.flow(1, {
49
45
  name: 'aave-repay-with-atokens',
50
46
  inputs: {
51
- aTokenIn: resources.erc20(A_ETH_USDC, 1),
47
+ collateralIn: resources.erc20(USDC, 1),
48
+ },
49
+ });
50
+
51
+ // Supply USDC as collateral (USDC → aEthUSDC); the aUSDC is burned below.
52
+ const supplyOut = builder.lifi.zap('supply', {
53
+ bind: { amountIn: builder.inputs.collateralIn },
54
+ config: { resourceOut: resources.erc20(A_ETH_USDC, 1) },
55
+ });
56
+
57
+ // Borrow USDC against the supplied collateral to open a debt position.
58
+ builder.aave.borrow('borrow', {
59
+ bind: {},
60
+ config: {
61
+ pool: AAVE_V3_POOL,
62
+ asset: USDC,
63
+ variableDebtToken: VDEBT_USDC,
64
+ amount: borrowAmount,
52
65
  },
53
66
  });
54
67
 
68
+ // Repay the USDC debt by burning the aUSDC collateral directly. The aToken
69
+ // input exceeds the debt, so Aave clamps the payback to the outstanding
70
+ // debt exactly — clearing the borrow flag and freeing the rest for the sweep.
55
71
  builder.aave.repayWithATokens('repay-atokens', {
56
- bind: { aTokenIn: builder.inputs.aTokenIn },
72
+ bind: { aTokenIn: supplyOut.amountOut },
57
73
  config: {
58
74
  pool: AAVE_V3_POOL,
59
75
  asset: USDC,
@@ -65,15 +81,10 @@ export const buildAaveRepayWithATokens = ({
65
81
  const request = sdk.request(flow, {
66
82
  signer: owner,
67
83
  inputs: {
68
- aTokenIn: materialisers.balanceOf({ owner: proxyAddress }),
84
+ collateralIn: materialisers.directDeposit({ amount: collateralAmount }),
69
85
  },
70
- preconditions: [
71
- preconditions.erc20Balance({
72
- wallet: proxyAddress,
73
- token: A_ETH_USDC,
74
- balance: expectedATokenBalance,
75
- }),
76
- ],
86
+ // Once the debt is cleared, sweep the borrowed USDC and the unburned
87
+ // aUSDC collateral back to the sender.
77
88
  sweepTo: builder.context.sender,
78
89
  });
79
90
 
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Helpers for Aave v3's scaled-balance rounding.
3
+ *
4
+ * Aave v3 does not store balances directly. Every aToken / variable-debt
5
+ * balance is kept as scaled shares and converted on each access using the
6
+ * market's live index (RAY = 1e27 precision):
7
+ *
8
+ * - write: shares = amount.rayDiv(index) — round half-up to 1 ray-wei
9
+ * - read: balance = shares.rayMul(index) — round half-up to 1 wei
10
+ *
11
+ * The write+read round trip therefore drifts by up to ~index/RAY wei in
12
+ * either direction — about 1 wei while the index is below ~2e27, which
13
+ * holds for all current Aave v3 markets. Concretely, in the same block:
14
+ *
15
+ * - a debt opened with `borrow(x)` can read back as `x + 1` wei, and
16
+ * - collateral supplied with `supply(x)` can read back as `x - 1` wei.
17
+ *
18
+ * The drift direction depends on how the amount aligns with the index
19
+ * remainder, and the index accrues every block — so a simulation that
20
+ * passes with exact amounts at block N can still revert on-chain at block
21
+ * N+k. Any flow that settles an exact amount against a same-block Aave
22
+ * readback (flashloan legs, exact transfers) must budget the worst case:
23
+ *
24
+ * - `padForAaveDebtRounding` — for amounts that must *cover* a same-block
25
+ * debt readback, e.g. a flashloan principal funding a max-mode repay.
26
+ * - `trimForAaveWithdrawalRounding` — for amounts that must be *covered
27
+ * by* a same-block balance readback, e.g. a flashloan leg repaid from
28
+ * an Aave withdrawal.
29
+ *
30
+ * The over- or under-shoot wei surface as residual/dust and are swept.
31
+ */
32
+
33
+ /** Worst-case same-block readback drift for indices below ~2e27. */
34
+ export const AAVE_SAME_BLOCK_SKEW_WEI = 1n;
35
+
36
+ export const padForAaveDebtRounding = (amount: `${bigint}`): `${bigint}` =>
37
+ (BigInt(amount) + AAVE_SAME_BLOCK_SKEW_WEI).toString() as `${bigint}`;
38
+
39
+ export const trimForAaveWithdrawalRounding = (
40
+ amount: `${bigint}`,
41
+ ): `${bigint}` =>
42
+ (BigInt(amount) - AAVE_SAME_BLOCK_SKEW_WEI).toString() as `${bigint}`;
@@ -61,7 +61,6 @@ export const buildAaveSetEMode = ({
61
61
  const request = sdk.request(flow, {
62
62
  signer: owner,
63
63
  inputs: {},
64
- sweepTo: builder.context.sender,
65
64
  });
66
65
 
67
66
  return { flow, request };
@@ -6,4 +6,4 @@ export const API_KEY: string | undefined = process.env['LIFI_API_KEY'];
6
6
 
7
7
  export const OWNER = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';
8
8
  export const RECIPIENT = '0x1234567890abcdef1234567890abcdef12345678';
9
- export const PROXY = '0x2222222222222222222222222222222222222222';
9
+ export const PROXY = '0x0921afa7ab4895814f6130fd85aea549c345a109';
@@ -0,0 +1,75 @@
1
+ import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
2
+
3
+ import { createComposeSdk } from '../index.js';
4
+ import type { Address } from '../types.js';
5
+
6
+ import { BASE_URL } from './config.js';
7
+
8
+ // Example ERC-4626 vault address (Steakhouse USDC on Ethereum mainnet)
9
+ const VAULT = '0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB';
10
+
11
+ export interface EmitCustomEventInput {
12
+ readonly owner: Address;
13
+ }
14
+
15
+ /**
16
+ * Emit a custom on-chain event from a compose flow.
17
+ *
18
+ * Demonstrates:
19
+ * - core.emitEvent2 — surfacing an arbitrary `Rebalance(address,uint256)` event
20
+ * - Binding mixed-type handles (an `address` context ref and a `uint256` read)
21
+ * directly to the op's `bytes32` value ports — no `raw.ref` cast, because
22
+ * bytes32 ports are the raw 32-byte word type and accept any static-32-byte
23
+ * handle.
24
+ *
25
+ * The emitted log is a fixed VM event whose first data word is
26
+ * keccak256("Rebalance(address,uint256)") (a discriminator), followed by the
27
+ * execution address and the vault total — an indexer reads the discriminator
28
+ * to recognise the event and decodes the two values.
29
+ */
30
+ export const buildEmitCustomEvent = ({
31
+ owner,
32
+ }: EmitCustomEventInput): {
33
+ flow: Flow;
34
+ request: ComposeCompileRequest;
35
+ } => {
36
+ const sdk = createComposeSdk({ baseUrl: BASE_URL });
37
+
38
+ const builder = sdk.flow(1, {
39
+ name: 'emit-custom-event',
40
+ inputs: {},
41
+ });
42
+
43
+ // Read the vault's total assets at execution time — a fresh uint256 handle.
44
+ const vaultTotal = builder.core.staticCall('vault-total', {
45
+ bind: {},
46
+ config: {
47
+ target: VAULT,
48
+ functionSignature: 'function totalAssets() view returns (uint256)',
49
+ },
50
+ });
51
+
52
+ // Emit Rebalance(address,uint256): the executing address plus the vault total.
53
+ // value0 (address) and value1 (uint256) bind directly to the bytes32 ports.
54
+ builder.core.emitEvent2('log-rebalance', {
55
+ bind: {
56
+ value0: builder.context.executionAddress,
57
+ value1: vaultTotal.result,
58
+ },
59
+ config: {
60
+ signature: 'Rebalance(address,uint256)',
61
+ },
62
+ });
63
+
64
+ const flow = builder.build();
65
+
66
+ // No `sweepTo`: this flow only reads and emits a log — it moves no funds, so
67
+ // there are no proxy-held terminal resources to sweep. Specifying `sweepTo`
68
+ // here would fail compilation with "no ... terminal resources ... to sweep".
69
+ const request = sdk.request(flow, {
70
+ signer: owner,
71
+ inputs: {},
72
+ });
73
+
74
+ return { flow, request };
75
+ };
@@ -0,0 +1,96 @@
1
+ import type { ComposeCompileRequest, Flow } from '@lifi/compose-spec';
2
+
3
+ import { createComposeSdk, materialisers, resources } from '../index.js';
4
+ import type { Address } from '../types.js';
5
+
6
+ import { BASE_URL } from './config.js';
7
+
8
+ const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
9
+ const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
10
+
11
+ export interface InvariantChecksInput {
12
+ readonly owner: Address;
13
+ readonly spender: Address;
14
+ readonly amount: `${bigint}`;
15
+ readonly minAmountOut: `${bigint}`;
16
+ }
17
+
18
+ /**
19
+ * Swap WETH → USDC via LI.FI, then layer two of the newly-exposed invariant
20
+ * ops on the result.
21
+ *
22
+ * Demonstrates:
23
+ * - `invariant.gte` asserting the swap output is at least a constant threshold
24
+ * (`amountOut >= minAmountOut`) — the per-comparison shorthand for
25
+ * `invariant.numeric` with `{ op: 'gte' }`, so the comparison lives in the op
26
+ * name and the config is just `{ threshold }`.
27
+ * - `invariant.allowanceAtLeast` asserting the execution proxy still permits
28
+ * `spender` to move at least the swapped USDC on its behalf.
29
+ * - Binding a swap's `amountOut` resource handle into both a `uint256` numeric
30
+ * slot and a `resource` allowance slot.
31
+ *
32
+ * Both ops compile to on-chain `AssertRawInvariant` instructions; the run
33
+ * reverts if either assertion is violated.
34
+ */
35
+ export const buildInvariantChecks = ({
36
+ owner,
37
+ spender,
38
+ amount,
39
+ minAmountOut,
40
+ }: InvariantChecksInput): {
41
+ flow: Flow;
42
+ request: ComposeCompileRequest;
43
+ } => {
44
+ const sdk = createComposeSdk({ baseUrl: BASE_URL });
45
+
46
+ // One WETH resource input plus a scalar spender address.
47
+ const builder = sdk.flow(1, {
48
+ name: 'invariant-checks',
49
+ inputs: {
50
+ amountIn: resources.erc20(WETH, 1),
51
+ spender: 'address',
52
+ },
53
+ });
54
+
55
+ // Swap WETH → USDC via LI.FI.
56
+ const swapOutputs = builder.lifi.swap('swap', {
57
+ bind: { amountIn: builder.inputs.amountIn },
58
+ config: {
59
+ resourceOut: resources.erc20(USDC, 1),
60
+ slippage: 0.03,
61
+ },
62
+ });
63
+
64
+ // Assert the swap produced at least `minAmountOut` USDC. The threshold is a
65
+ // configured constant, so this is a numeric (not handle-vs-handle) compare;
66
+ // `invariant.gte` is the shorthand that bakes the comparison into the op id.
67
+ builder.invariant.gte('min-out', {
68
+ bind: { value: swapOutputs.amountOut },
69
+ config: { threshold: minAmountOut },
70
+ });
71
+
72
+ // Assert the execution proxy still grants `spender` an allowance of at least
73
+ // the swapped USDC amount.
74
+ builder.invariant.allowanceAtLeast('check-allowance', {
75
+ bind: {
76
+ minimumAmount: swapOutputs.amountOut,
77
+ owner: builder.context.executionAddress,
78
+ spender: builder.inputs.spender,
79
+ },
80
+ });
81
+
82
+ const flow = builder.build();
83
+
84
+ // directDeposit marks amountIn as a fixed amount of WETH pre-deposited into
85
+ // the VM; the spender address is supplied as a plain scalar input.
86
+ const request = sdk.request(flow, {
87
+ signer: owner,
88
+ inputs: {
89
+ amountIn: materialisers.directDeposit({ amount }),
90
+ spender,
91
+ },
92
+ sweepTo: builder.context.sender,
93
+ });
94
+
95
+ return { flow, request };
96
+ };