@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.
- package/README.md +81 -0
- package/dist/authoring/handles.cjs.map +1 -1
- package/dist/authoring/handles.d.cts +13 -6
- package/dist/authoring/handles.d.ts +13 -6
- package/dist/authoring/handles.js.map +1 -1
- package/dist/client.cjs +38 -6
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +25 -1
- package/dist/client.d.ts +25 -1
- package/dist/client.js +41 -6
- package/dist/client.js.map +1 -1
- package/dist/errors.cjs +4 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +6 -1
- package/dist/errors.js.map +1 -1
- package/dist/generated/config.generated.cjs.map +1 -1
- package/dist/generated/config.generated.d.cts +82 -7
- package/dist/generated/config.generated.d.ts +82 -7
- package/dist/generated/materialisers.generated.cjs +2 -8
- package/dist/generated/materialisers.generated.cjs.map +1 -1
- package/dist/generated/materialisers.generated.d.cts +1 -1
- package/dist/generated/materialisers.generated.d.ts +1 -1
- package/dist/generated/materialisers.generated.js +1 -6
- package/dist/generated/materialisers.generated.js.map +1 -1
- package/dist/generated/operations.generated.cjs +56 -31
- package/dist/generated/operations.generated.cjs.map +1 -1
- package/dist/generated/operations.generated.d.cts +204 -94
- package/dist/generated/operations.generated.d.ts +204 -94
- package/dist/generated/operations.generated.js +56 -31
- package/dist/generated/operations.generated.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/{materialisers.generated-BfWJ-g7t.d.ts → materialisers.generated-BMkBQgWZ.d.ts} +3 -8
- package/dist/{materialisers.generated-DaJRwiRe.d.cts → materialisers.generated-BWq5oePO.d.cts} +3 -8
- package/dist/responseSchemas.cjs +110 -0
- package/dist/responseSchemas.cjs.map +1 -0
- package/dist/responseSchemas.d.cts +36 -0
- package/dist/responseSchemas.d.ts +36 -0
- package/dist/responseSchemas.js +74 -0
- package/dist/responseSchemas.js.map +1 -0
- package/dist/run/simulate.cjs +41 -0
- package/dist/run/simulate.cjs.map +1 -0
- package/dist/run/simulate.d.cts +59 -0
- package/dist/run/simulate.d.ts +59 -0
- package/dist/run/simulate.js +17 -0
- package/dist/run/simulate.js.map +1 -0
- package/dist/sdk.cjs +6 -1
- package/dist/sdk.cjs.map +1 -1
- package/dist/sdk.d.cts +13 -1
- package/dist/sdk.d.ts +13 -1
- package/dist/sdk.js +6 -1
- package/dist/sdk.js.map +1 -1
- package/package.json +7 -5
- package/src/authoring/handles.ts +19 -10
- package/src/client.ts +86 -11
- package/src/errors.ts +8 -10
- package/src/examples/aaveRepay.ts +55 -16
- package/src/examples/aaveRepayWithATokens.ts +41 -30
- package/src/examples/aaveRounding.ts +42 -0
- package/src/examples/aaveSetEMode.ts +0 -1
- package/src/examples/config.ts +1 -1
- package/src/examples/emitCustomEvent.ts +75 -0
- package/src/examples/invariantChecks.ts +96 -0
- package/src/examples/run.ts +20 -18
- package/src/examples/simulateCompiledSwap.ts +76 -0
- package/src/examples/simulateRawTransaction.ts +39 -0
- package/src/examples/untypedOpWithTypedRef.ts +26 -20
- package/src/generated/config.generated.ts +229 -6
- package/src/generated/materialisers.generated.ts +0 -12
- package/src/generated/operations.generated.ts +330 -177
- package/src/index.ts +19 -0
- package/src/responseSchemas.ts +143 -0
- package/src/run/simulate.ts +78 -0
- package/src/sdk.ts +20 -1
- package/src/examples/staged/aaveBorrow.ts +0 -104
- package/src/examples/staged/aaveDebtRebalanceExactOut.ts +0 -301
- package/src/examples/staged/aaveHealthFactorExplode.ts +0 -70
- package/src/examples/staged/aaveToMorphoDebtMigration.ts +0 -190
- package/src/examples/staged/aaveToMorphoDebtMigrationWithSwap.ts +0 -221
- package/src/examples/staged/aaveToMorphoDebtMigrationWithSwapExactOut.ts +0 -302
- package/src/examples/staged/flashloanRepay.ts +0 -138
- package/src/examples/staged/morphoBlueBorrow.ts +0 -89
- package/src/examples/staged/morphoBlueRepay.ts +0 -91
- 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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* - `
|
|
26
|
-
* -
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
85
|
+
assetIn: builder.inputs.repayIn,
|
|
49
86
|
onBehalfOf: builder.context.executionAddress,
|
|
50
87
|
},
|
|
51
88
|
config: {
|
|
52
89
|
pool: AAVE_V3_POOL,
|
|
53
|
-
mode: '
|
|
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
|
-
|
|
99
|
+
collateralIn: materialisers.directDeposit({ amount: collateralAmount }),
|
|
100
|
+
repayIn: materialisers.directDeposit({ amount: repayAmount }),
|
|
63
101
|
},
|
|
64
|
-
//
|
|
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
|
-
|
|
21
|
-
readonly
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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
|
-
* - `
|
|
34
|
-
* - `
|
|
35
|
-
* - `
|
|
36
|
-
* -
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
84
|
+
collateralIn: materialisers.directDeposit({ amount: collateralAmount }),
|
|
69
85
|
},
|
|
70
|
-
|
|
71
|
-
|
|
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}`;
|
package/src/examples/config.ts
CHANGED
|
@@ -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 = '
|
|
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
|
+
};
|