@morpho-dev/router 0.0.11 → 0.0.13
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 +15 -0
- package/dist/drizzle/0000_add-offers-table.sql +37 -0
- package/dist/drizzle/0001_create_offer_status_relation.sql +10 -0
- package/dist/drizzle/0002_add_created_at_in_offer_status_relation.sql +3 -0
- package/dist/drizzle/0003_add-cursor-indices-to-offers.sql +6 -0
- package/dist/drizzle/0004_offer-start.sql +1 -0
- package/dist/drizzle/0005_rename-price-token-buy.sql +8 -0
- package/dist/drizzle/0006_rename-buy.sql +3 -0
- package/dist/drizzle/0007_rename-offering.sql +3 -0
- package/dist/drizzle/meta/0000_snapshot.json +344 -0
- package/dist/drizzle/meta/0001_snapshot.json +426 -0
- package/dist/drizzle/meta/0002_snapshot.json +439 -0
- package/dist/drizzle/meta/0003_snapshot.json +553 -0
- package/dist/drizzle/meta/0004_snapshot.json +559 -0
- package/dist/drizzle/meta/0005_snapshot.json +559 -0
- package/dist/drizzle/meta/0006_snapshot.json +559 -0
- package/dist/drizzle/meta/0007_snapshot.json +559 -0
- package/dist/drizzle/meta/_journal.json +62 -0
- package/dist/{index.d.cts → index.browser.d.cts} +34 -90
- package/dist/{index.d.ts → index.browser.d.ts} +34 -90
- package/dist/index.browser.js +1172 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +1154 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.node.d.cts +1342 -0
- package/dist/index.node.d.ts +1342 -0
- package/dist/{index.js → index.node.js} +2119 -4773
- package/dist/index.node.js.map +1 -0
- package/dist/{index.mjs → index.node.mjs} +2111 -4774
- package/dist/index.node.mjs.map +1 -0
- package/package.json +33 -11
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var mempool = require('@morpho-dev/mempool');
|
|
4
|
+
var chains$1 = require('viem/chains');
|
|
5
|
+
var v4 = require('zod/v4');
|
|
6
|
+
var zodOpenapi = require('zod-openapi');
|
|
7
|
+
var viem = require('viem');
|
|
8
|
+
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, key + "" , value);
|
|
16
|
+
|
|
17
|
+
// src/Chain.ts
|
|
18
|
+
var Chain_exports = {};
|
|
19
|
+
__export(Chain_exports, {
|
|
20
|
+
ChainId: () => ChainId,
|
|
21
|
+
chainIds: () => chainIds,
|
|
22
|
+
chainNames: () => chainNames,
|
|
23
|
+
chains: () => chains,
|
|
24
|
+
getChain: () => getChain
|
|
25
|
+
});
|
|
26
|
+
var chainNames = ["ethereum", "base"];
|
|
27
|
+
var ChainId = {
|
|
28
|
+
ETHEREUM: BigInt(chains$1.mainnet.id),
|
|
29
|
+
BASE: BigInt(chains$1.base.id)
|
|
30
|
+
};
|
|
31
|
+
var chainIds = new Set(Object.values(ChainId));
|
|
32
|
+
var chainNameLookup = new Map(Object.entries(ChainId).map(([key, value]) => [value, key]));
|
|
33
|
+
function getChain(chainId) {
|
|
34
|
+
const chainName = chainNameLookup.get(chainId)?.toLowerCase();
|
|
35
|
+
if (!chainName) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
return chains[chainName];
|
|
39
|
+
}
|
|
40
|
+
var chains = {
|
|
41
|
+
ethereum: {
|
|
42
|
+
...chains$1.mainnet,
|
|
43
|
+
id: ChainId.ETHEREUM,
|
|
44
|
+
name: "ethereum",
|
|
45
|
+
whitelistedAssets: new Set(
|
|
46
|
+
[
|
|
47
|
+
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
48
|
+
// USDC
|
|
49
|
+
"0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
|
50
|
+
// DAI
|
|
51
|
+
].map((address) => address.toLowerCase())
|
|
52
|
+
),
|
|
53
|
+
morpho: "0x0000000000000000000000000000000000000000"
|
|
54
|
+
},
|
|
55
|
+
base: {
|
|
56
|
+
...chains$1.base,
|
|
57
|
+
id: ChainId.BASE,
|
|
58
|
+
name: "base",
|
|
59
|
+
whitelistedAssets: new Set(
|
|
60
|
+
[
|
|
61
|
+
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
62
|
+
// USDC
|
|
63
|
+
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb"
|
|
64
|
+
// DAI
|
|
65
|
+
].map((address) => address.toLowerCase())
|
|
66
|
+
),
|
|
67
|
+
morpho: "0x0000000000000000000000000000000000000000"
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/core/Client.ts
|
|
72
|
+
var Client_exports = {};
|
|
73
|
+
__export(Client_exports, {
|
|
74
|
+
HttpForbiddenError: () => HttpForbiddenError,
|
|
75
|
+
HttpGetOffersFailedError: () => HttpGetOffersFailedError,
|
|
76
|
+
HttpRateLimitError: () => HttpRateLimitError,
|
|
77
|
+
HttpUnauthorizedError: () => HttpUnauthorizedError,
|
|
78
|
+
InvalidUrlError: () => InvalidUrlError,
|
|
79
|
+
connect: () => connect,
|
|
80
|
+
get: () => get,
|
|
81
|
+
match: () => match
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// src/RouterOffer.ts
|
|
85
|
+
var RouterOffer_exports = {};
|
|
86
|
+
__export(RouterOffer_exports, {
|
|
87
|
+
OfferStatusValues: () => OfferStatusValues
|
|
88
|
+
});
|
|
89
|
+
var OfferStatusValues = [
|
|
90
|
+
"valid",
|
|
91
|
+
"callback_not_supported",
|
|
92
|
+
"callback_error",
|
|
93
|
+
"unverified"
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// src/utils/batch.ts
|
|
97
|
+
function* batch(array, batchSize) {
|
|
98
|
+
for (let i = 0; i < array.length; i += batchSize) {
|
|
99
|
+
yield array.slice(i, i + batchSize);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/utils/cursor.ts
|
|
104
|
+
function validateCursor(cursor) {
|
|
105
|
+
if (!cursor || typeof cursor !== "object") {
|
|
106
|
+
throw new Error("Cursor must be an object");
|
|
107
|
+
}
|
|
108
|
+
const c = cursor;
|
|
109
|
+
if (!["rate", "maturity", "expiry", "amount"].includes(c.sort)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Invalid sort field: ${c.sort}. Must be one of: rate, maturity, expiry, amount`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (!["asc", "desc"].includes(c.dir)) {
|
|
115
|
+
throw new Error(`Invalid direction: ${c.dir}. Must be one of: asc, desc`);
|
|
116
|
+
}
|
|
117
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(c.hash)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Invalid hash format: ${c.hash}. Must be a 64-character hex string starting with 0x`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const validations = {
|
|
123
|
+
rate: {
|
|
124
|
+
field: "rate",
|
|
125
|
+
type: "string",
|
|
126
|
+
pattern: /^\d+$/,
|
|
127
|
+
error: "numeric string"
|
|
128
|
+
},
|
|
129
|
+
amount: {
|
|
130
|
+
field: "assets",
|
|
131
|
+
type: "string",
|
|
132
|
+
pattern: /^\d+$/,
|
|
133
|
+
error: "numeric string"
|
|
134
|
+
},
|
|
135
|
+
maturity: {
|
|
136
|
+
field: "maturity",
|
|
137
|
+
type: "number",
|
|
138
|
+
validator: (val) => val > 0,
|
|
139
|
+
error: "positive number"
|
|
140
|
+
},
|
|
141
|
+
expiry: {
|
|
142
|
+
field: "expiry",
|
|
143
|
+
type: "number",
|
|
144
|
+
validator: (val) => val > 0,
|
|
145
|
+
error: "positive number"
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const validation = validations[c.sort];
|
|
149
|
+
if (!validation) {
|
|
150
|
+
throw new Error(`Invalid sort field: ${c.sort}`);
|
|
151
|
+
}
|
|
152
|
+
const fieldValue = c[validation.field];
|
|
153
|
+
if (!fieldValue) {
|
|
154
|
+
throw new Error(`${c.sort} sort requires '${validation.field}' field to be present`);
|
|
155
|
+
}
|
|
156
|
+
if (typeof fieldValue !== validation.type) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`${c.sort} sort requires '${validation.field}' field of type ${validation.type}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (validation.pattern && !validation.pattern.test(fieldValue)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Invalid ${validation.field} format: ${fieldValue}. Must be a ${validation.error}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (validation.validator && !validation.validator(fieldValue)) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Invalid ${validation.field} value: ${fieldValue}. Must be a ${validation.error}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function encodeCursor(c) {
|
|
174
|
+
return Buffer.from(JSON.stringify(c)).toString("base64url");
|
|
175
|
+
}
|
|
176
|
+
function decodeCursor(token) {
|
|
177
|
+
if (!token) return null;
|
|
178
|
+
const decoded = JSON.parse(Buffer.from(token, "base64url").toString());
|
|
179
|
+
validateCursor(decoded);
|
|
180
|
+
return decoded;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/utils/wait.ts
|
|
184
|
+
async function wait(time) {
|
|
185
|
+
return new Promise((res) => setTimeout(res, time));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/utils/poll.ts
|
|
189
|
+
function poll(fn, { interval }) {
|
|
190
|
+
let active = true;
|
|
191
|
+
const unwatch = () => active = false;
|
|
192
|
+
const watch = async () => {
|
|
193
|
+
await wait(interval);
|
|
194
|
+
const poll2 = async () => {
|
|
195
|
+
if (!active) return;
|
|
196
|
+
await fn({ unpoll: unwatch });
|
|
197
|
+
await wait(interval);
|
|
198
|
+
poll2();
|
|
199
|
+
};
|
|
200
|
+
poll2();
|
|
201
|
+
};
|
|
202
|
+
watch();
|
|
203
|
+
return unwatch;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/core/apiSchema/requests.ts
|
|
207
|
+
var MAX_LIMIT = 100;
|
|
208
|
+
var DEFAULT_LIMIT = 20;
|
|
209
|
+
var MAX_LLTV = 100;
|
|
210
|
+
var MIN_LLTV = 0;
|
|
211
|
+
var GetOffersQueryParams = v4.z.object({
|
|
212
|
+
// Core filtering parameters
|
|
213
|
+
creators: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
|
|
214
|
+
message: "Creators must be comma-separated Ethereum addresses"
|
|
215
|
+
}).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
|
|
216
|
+
description: "Filter by multiple creator addresses (comma-separated)",
|
|
217
|
+
example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
|
218
|
+
}),
|
|
219
|
+
side: v4.z.enum(["buy", "sell"]).optional().meta({
|
|
220
|
+
description: "Filter by offer type: buy offers or sell offers",
|
|
221
|
+
example: "buy"
|
|
222
|
+
}),
|
|
223
|
+
chains: v4.z.string().regex(/^\d+(,\d+)*$/, {
|
|
224
|
+
message: "Chains must be comma-separated chain IDs"
|
|
225
|
+
}).transform((val) => val.split(",").map(Number)).optional().meta({
|
|
226
|
+
description: "Filter by multiple blockchain networks (comma-separated chain IDs)",
|
|
227
|
+
example: "1,137,10"
|
|
228
|
+
}),
|
|
229
|
+
loan_tokens: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
|
|
230
|
+
message: "Loan assets must be comma-separated Ethereum addresses"
|
|
231
|
+
}).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
|
|
232
|
+
description: "Filter by multiple loan assets (comma-separated)",
|
|
233
|
+
example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
|
234
|
+
}),
|
|
235
|
+
status: v4.z.string().regex(/^[a-zA-Z_]+(,[a-zA-Z_]+)*$/, {
|
|
236
|
+
message: "Status must be comma-separated status values"
|
|
237
|
+
}).transform((val) => val.split(",")).refine((statuses) => statuses.every((status) => OfferStatusValues.includes(status)), {
|
|
238
|
+
message: `Invalid status value. Must be one of: ${OfferStatusValues.join(", ")}`
|
|
239
|
+
}).optional().meta({
|
|
240
|
+
description: `Filter by multiple statuses (comma-separated). Valid values: ${OfferStatusValues.join(", ")}. By default, only offers with 'valid' status are returned.`,
|
|
241
|
+
example: "valid,callback_error"
|
|
242
|
+
}),
|
|
243
|
+
callback_addresses: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
|
|
244
|
+
message: "Callback addresses must be comma-separated Ethereum addresses"
|
|
245
|
+
}).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
|
|
246
|
+
description: "Filter by multiple callback addresses (comma-separated)",
|
|
247
|
+
example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
|
248
|
+
}),
|
|
249
|
+
// Asset range
|
|
250
|
+
min_amount: v4.z.bigint({ coerce: true }).positive({
|
|
251
|
+
message: "Min amount must be a positive number"
|
|
252
|
+
}).optional().meta({
|
|
253
|
+
description: "Minimum amount of assets in the offer",
|
|
254
|
+
example: "1000"
|
|
255
|
+
}),
|
|
256
|
+
max_amount: v4.z.bigint({ coerce: true }).positive({
|
|
257
|
+
message: "Max amount must be a positive number"
|
|
258
|
+
}).optional().meta({
|
|
259
|
+
description: "Maximum amount of assets in the offer",
|
|
260
|
+
example: "10000"
|
|
261
|
+
}),
|
|
262
|
+
// Rate range
|
|
263
|
+
min_rate: v4.z.bigint({ coerce: true }).positive({
|
|
264
|
+
message: "Min rate must be a positive number"
|
|
265
|
+
}).optional().meta({
|
|
266
|
+
description: "Minimum rate per asset (in wei)",
|
|
267
|
+
example: "500000000000000000"
|
|
268
|
+
}),
|
|
269
|
+
max_rate: v4.z.bigint({ coerce: true }).positive({
|
|
270
|
+
message: "Max rate must be a positive number"
|
|
271
|
+
}).optional().meta({
|
|
272
|
+
description: "Maximum rate per asset (in wei)",
|
|
273
|
+
example: "1500000000000000000"
|
|
274
|
+
}),
|
|
275
|
+
// Time range
|
|
276
|
+
min_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
|
|
277
|
+
description: "Minimum maturity timestamp (Unix timestamp in seconds)",
|
|
278
|
+
example: "1700000000"
|
|
279
|
+
}),
|
|
280
|
+
max_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
|
|
281
|
+
description: "Maximum maturity timestamp (Unix timestamp in seconds)",
|
|
282
|
+
example: "1800000000"
|
|
283
|
+
}),
|
|
284
|
+
min_expiry: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
|
|
285
|
+
description: "Minimum expiry timestamp (Unix timestamp in seconds)",
|
|
286
|
+
example: "1700000000"
|
|
287
|
+
}),
|
|
288
|
+
max_expiry: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
|
|
289
|
+
description: "Maximum expiry timestamp (Unix timestamp in seconds)",
|
|
290
|
+
example: "1800000000"
|
|
291
|
+
}),
|
|
292
|
+
// Collateral filtering
|
|
293
|
+
collateral_assets: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
|
|
294
|
+
message: "Collateral assets must be comma-separated Ethereum addresses"
|
|
295
|
+
}).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
|
|
296
|
+
description: "Filter by multiple collateral assets (comma-separated)",
|
|
297
|
+
example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
|
298
|
+
}),
|
|
299
|
+
collateral_oracles: v4.z.string().regex(/^0x[a-fA-F0-9]{40}(,0x[a-fA-F0-9]{40})*$/, {
|
|
300
|
+
message: "Collateral oracles must be comma-separated Ethereum addresses"
|
|
301
|
+
}).transform((val) => val.split(",").map((addr) => addr.toLowerCase())).optional().meta({
|
|
302
|
+
description: "Filter by multiple rate oracles (comma-separated)",
|
|
303
|
+
example: "0x1234567890123456789012345678901234567890,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
|
|
304
|
+
}),
|
|
305
|
+
collateral_tuple: v4.z.string().regex(
|
|
306
|
+
/^(0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)(#0x[a-fA-F0-9]{40}(:0x[a-fA-F0-9]{40})?(:[0-9]+(\.[0-9]+)?)?)*$/,
|
|
307
|
+
{
|
|
308
|
+
message: "Collateral tuple must be in format: asset:oracle:lltv#asset2:oracle2:lltv2. Oracle and lltv are optional. Asset must be 0x + 40 hex chars, oracle must be 0x + 40 hex chars, lltv must be a number (e.g., 80.5)."
|
|
309
|
+
}
|
|
310
|
+
).transform((val) => {
|
|
311
|
+
return val.split("#").map((tuple) => {
|
|
312
|
+
const parts = tuple.split(":");
|
|
313
|
+
if (parts.length === 0 || !parts[0]) {
|
|
314
|
+
throw new v4.z.ZodError([
|
|
315
|
+
{
|
|
316
|
+
code: "custom",
|
|
317
|
+
message: "Asset address is required for each collateral tuple",
|
|
318
|
+
path: ["collateral_tuple"],
|
|
319
|
+
input: val
|
|
320
|
+
}
|
|
321
|
+
]);
|
|
322
|
+
}
|
|
323
|
+
const asset = parts[0]?.toLowerCase();
|
|
324
|
+
const oracle = parts[1]?.toLowerCase();
|
|
325
|
+
const lltv = parts[2] ? parseFloat(parts[2]) : void 0;
|
|
326
|
+
if (lltv !== void 0 && (lltv < MIN_LLTV || lltv > MAX_LLTV)) {
|
|
327
|
+
throw new v4.z.ZodError([
|
|
328
|
+
{
|
|
329
|
+
code: "custom",
|
|
330
|
+
message: `LLTV must be between ${MIN_LLTV} and ${MAX_LLTV} (0-100%)`,
|
|
331
|
+
path: ["collateral_tuple"],
|
|
332
|
+
input: val
|
|
333
|
+
}
|
|
334
|
+
]);
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
asset,
|
|
338
|
+
oracle,
|
|
339
|
+
lltv
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
}).optional().meta({
|
|
343
|
+
description: "Filter by collateral combinations in format: asset:oracle:lltv#asset2:oracle2:lltv2. Oracle and lltv are optional. Use # to separate multiple combinations.",
|
|
344
|
+
example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:8000#0x9876543210987654321098765432109876543210::8000"
|
|
345
|
+
}),
|
|
346
|
+
min_lltv: v4.z.string().regex(/^\d+(\.\d+)?$/, {
|
|
347
|
+
message: "Min LLTV must be a valid number"
|
|
348
|
+
}).transform((val) => parseFloat(val)).pipe(v4.z.number().min(0).max(100)).optional().meta({
|
|
349
|
+
description: "Minimum Loan-to-Value ratio (LLTV) for collateral (percentage as decimal, e.g., 80.5 = 80.5%)",
|
|
350
|
+
example: "80.5"
|
|
351
|
+
}),
|
|
352
|
+
max_lltv: v4.z.string().regex(/^\d+(\.\d+)?$/, {
|
|
353
|
+
message: "Max LLTV must be a valid number"
|
|
354
|
+
}).transform((val) => parseFloat(val)).pipe(v4.z.number().min(0).max(100)).optional().meta({
|
|
355
|
+
description: "Maximum Loan-to-Value ratio (LLTV) for collateral (percentage as decimal, e.g., 95.5 = 95.5%)",
|
|
356
|
+
example: "95.5"
|
|
357
|
+
}),
|
|
358
|
+
// Sorting parameters
|
|
359
|
+
sort_by: v4.z.enum(["rate", "maturity", "expiry", "amount"]).optional().meta({
|
|
360
|
+
description: "Field to sort results by",
|
|
361
|
+
example: "rate"
|
|
362
|
+
}),
|
|
363
|
+
sort_order: v4.z.enum(["asc", "desc"]).optional().default("desc").meta({
|
|
364
|
+
description: "Sort direction: asc (ascending) or desc (descending, default)",
|
|
365
|
+
example: "desc"
|
|
366
|
+
}),
|
|
367
|
+
// Pagination
|
|
368
|
+
cursor: v4.z.string().optional().refine(
|
|
369
|
+
(val) => {
|
|
370
|
+
if (!val) return true;
|
|
371
|
+
try {
|
|
372
|
+
const decoded = decodeCursor(val);
|
|
373
|
+
return decoded !== null;
|
|
374
|
+
} catch (_error) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
|
|
380
|
+
}
|
|
381
|
+
).meta({
|
|
382
|
+
description: "Pagination cursor in base64url-encoded format",
|
|
383
|
+
example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
|
|
384
|
+
}),
|
|
385
|
+
limit: v4.z.string().regex(/^[1-9]\d*$/, {
|
|
386
|
+
message: "Limit must be a positive integer"
|
|
387
|
+
}).transform((val) => Number.parseInt(val, 10)).pipe(
|
|
388
|
+
v4.z.number().max(MAX_LIMIT, {
|
|
389
|
+
message: `Limit cannot exceed ${MAX_LIMIT}`
|
|
390
|
+
})
|
|
391
|
+
).optional().default(DEFAULT_LIMIT).meta({
|
|
392
|
+
description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
|
|
393
|
+
example: 10
|
|
394
|
+
})
|
|
395
|
+
}).refine(
|
|
396
|
+
(data) => data.min_maturity === void 0 || data.max_maturity === void 0 || data.max_maturity >= data.min_maturity,
|
|
397
|
+
{
|
|
398
|
+
message: "max_maturity must be greater than or equal to min_maturity",
|
|
399
|
+
path: ["max_maturity"]
|
|
400
|
+
}
|
|
401
|
+
).refine(
|
|
402
|
+
(data) => data.min_expiry === void 0 || data.max_expiry === void 0 || data.max_expiry >= data.min_expiry,
|
|
403
|
+
{
|
|
404
|
+
message: "max_expiry must be greater than or equal to min_expiry",
|
|
405
|
+
path: ["max_expiry"]
|
|
406
|
+
}
|
|
407
|
+
).refine(
|
|
408
|
+
(data) => data.min_amount === void 0 || data.max_amount === void 0 || data.max_amount >= data.min_amount,
|
|
409
|
+
{
|
|
410
|
+
message: "max_amount must be greater than or equal to min_amount",
|
|
411
|
+
path: ["max_amount"]
|
|
412
|
+
}
|
|
413
|
+
).refine(
|
|
414
|
+
(data) => data.min_rate === void 0 || data.max_rate === void 0 || data.max_rate >= data.min_rate,
|
|
415
|
+
{
|
|
416
|
+
message: "max_rate must be greater than or equal to min_rate",
|
|
417
|
+
path: ["max_rate"]
|
|
418
|
+
}
|
|
419
|
+
).refine(
|
|
420
|
+
(data) => data.min_lltv === void 0 || data.max_lltv === void 0 || data.max_lltv >= data.min_lltv,
|
|
421
|
+
{
|
|
422
|
+
message: "max_lltv must be greater than or equal to min_lltv",
|
|
423
|
+
path: ["max_lltv"]
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
var MatchOffersQueryParams = v4.z.object({
|
|
427
|
+
// Required parameters
|
|
428
|
+
side: v4.z.enum(["buy", "sell"]).meta({
|
|
429
|
+
description: "The desired side of the match: 'buy' if you want to buy, 'sell' if you want to sell. If your intent is to sell, buy offers will be returned, and vice versa.",
|
|
430
|
+
example: "buy"
|
|
431
|
+
}),
|
|
432
|
+
chain_id: v4.z.string().regex(/^\d+$/, {
|
|
433
|
+
message: "Chain ID must be a positive integer"
|
|
434
|
+
}).transform((val) => Number.parseInt(val, 10)).pipe(v4.z.number().positive()).meta({
|
|
435
|
+
description: "The blockchain network chain ID",
|
|
436
|
+
example: "1"
|
|
437
|
+
}),
|
|
438
|
+
// Optional parameters
|
|
439
|
+
rate: v4.z.bigint({ coerce: true }).positive({
|
|
440
|
+
message: "Rate must be a positive number"
|
|
441
|
+
}).optional().meta({
|
|
442
|
+
description: "Rate per asset (in wei) for matching offers",
|
|
443
|
+
example: "1000000000000000000"
|
|
444
|
+
}),
|
|
445
|
+
// Collateral filtering
|
|
446
|
+
collaterals: v4.z.string().regex(
|
|
447
|
+
/^(0x[a-fA-F0-9]{40}:0x[a-fA-F0-9]{40}:\d+)(#0x[a-fA-F0-9]{40}:0x[a-fA-F0-9]{40}:\d+)*$/,
|
|
448
|
+
{
|
|
449
|
+
message: "Collaterals must be in format: asset:oracle:lltv#asset2:oracle2:lltv2. All fields are required for each collateral."
|
|
450
|
+
}
|
|
451
|
+
).transform((val) => {
|
|
452
|
+
return val.split("#").map((collateral) => {
|
|
453
|
+
const parts = collateral.split(":");
|
|
454
|
+
if (parts.length !== 3) {
|
|
455
|
+
throw new v4.z.ZodError([
|
|
456
|
+
{
|
|
457
|
+
code: "custom",
|
|
458
|
+
message: "Each collateral must have exactly 3 parts: asset:oracle:lltv",
|
|
459
|
+
path: ["collaterals"],
|
|
460
|
+
input: val
|
|
461
|
+
}
|
|
462
|
+
]);
|
|
463
|
+
}
|
|
464
|
+
const [asset, oracle, lltvStr] = parts;
|
|
465
|
+
if (!asset || !oracle || !lltvStr) {
|
|
466
|
+
throw new v4.z.ZodError([
|
|
467
|
+
{
|
|
468
|
+
code: "custom",
|
|
469
|
+
message: "Asset, oracle, and lltv are all required for each collateral",
|
|
470
|
+
path: ["collaterals"],
|
|
471
|
+
input: val
|
|
472
|
+
}
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
const lltv = BigInt(lltvStr);
|
|
476
|
+
if (lltv <= 0n) {
|
|
477
|
+
throw new v4.z.ZodError([
|
|
478
|
+
{
|
|
479
|
+
code: "custom",
|
|
480
|
+
message: "LLTV must be a positive number",
|
|
481
|
+
path: ["collaterals"],
|
|
482
|
+
input: val
|
|
483
|
+
}
|
|
484
|
+
]);
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
asset: asset.toLowerCase(),
|
|
488
|
+
oracle: oracle.toLowerCase(),
|
|
489
|
+
lltv
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
}).optional().meta({
|
|
493
|
+
description: "Collateral requirements in format: asset:oracle:lltv#asset2:oracle2:lltv2. Use # to separate multiple collaterals.",
|
|
494
|
+
example: "0x1234567890123456789012345678901234567890:0xabcdefabcdefabcdefabcdefabcdefabcdefabcd:800000000000000000#0x9876543210987654321098765432109876543210:0xfedcbafedcbafedcbafedcbafedcbafedcbafedc:900000000000000000"
|
|
495
|
+
}),
|
|
496
|
+
// Maturity filtering
|
|
497
|
+
maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
|
|
498
|
+
description: "Exact maturity timestamp (Unix timestamp in seconds)",
|
|
499
|
+
example: "1700000000"
|
|
500
|
+
}),
|
|
501
|
+
min_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
|
|
502
|
+
description: "Minimum maturity timestamp (Unix timestamp in seconds, inclusive)",
|
|
503
|
+
example: "1700000000"
|
|
504
|
+
}),
|
|
505
|
+
max_maturity: v4.z.bigint({ coerce: true }).min(0n).optional().transform((val) => val === void 0 ? void 0 : Number(val)).meta({
|
|
506
|
+
description: "Maximum maturity timestamp (Unix timestamp in seconds, inclusive)",
|
|
507
|
+
example: "1800000000"
|
|
508
|
+
}),
|
|
509
|
+
// Asset and creator filtering
|
|
510
|
+
loan_token: v4.z.string().regex(/^0x[a-fA-F0-9]{40}$/, {
|
|
511
|
+
message: "Loan asset must be a valid Ethereum address"
|
|
512
|
+
}).transform((val) => val.toLowerCase()).optional().meta({
|
|
513
|
+
description: "The loan asset address to match against",
|
|
514
|
+
example: "0x1234567890123456789012345678901234567890"
|
|
515
|
+
}),
|
|
516
|
+
creator: v4.z.string().regex(/^0x[a-fA-F0-9]{40}$/, {
|
|
517
|
+
message: "Creator must be a valid Ethereum address"
|
|
518
|
+
}).transform((val) => val.toLowerCase()).optional().meta({
|
|
519
|
+
description: "Filter by a specific offer creator address",
|
|
520
|
+
example: "0x1234567890123456789012345678901234567890"
|
|
521
|
+
}),
|
|
522
|
+
// Status filtering
|
|
523
|
+
status: v4.z.string().regex(/^[a-zA-Z_]+(,[a-zA-Z_]+)*$/, {
|
|
524
|
+
message: "Status must be comma-separated status values"
|
|
525
|
+
}).transform((val) => val.split(",")).refine((statuses) => statuses.every((status) => OfferStatusValues.includes(status)), {
|
|
526
|
+
message: `Invalid status value. Must be one of: ${OfferStatusValues.join(", ")}`
|
|
527
|
+
}).optional().meta({
|
|
528
|
+
description: `Filter by multiple statuses (comma-separated). Valid values: ${OfferStatusValues.join(", ")}. By default, only offers with 'valid' status are returned.`,
|
|
529
|
+
example: "valid,callback_error"
|
|
530
|
+
}),
|
|
531
|
+
// Pagination
|
|
532
|
+
cursor: v4.z.string().optional().refine(
|
|
533
|
+
(val) => {
|
|
534
|
+
if (!val) return true;
|
|
535
|
+
try {
|
|
536
|
+
const decoded = decodeCursor(val);
|
|
537
|
+
return decoded !== null;
|
|
538
|
+
} catch (_error) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
message: "Invalid cursor format. Must be a valid base64url-encoded cursor object"
|
|
544
|
+
}
|
|
545
|
+
).meta({
|
|
546
|
+
description: "Pagination cursor in base64url-encoded format",
|
|
547
|
+
example: "eyJzb3J0IjoicHJpY2UiLCJkaXIiOiJkZXNjIiwicHJpY2UiOiIxMDAwMDAwMDAwMDAwMDAwMDAwIiwiaGFzaCI6IjB4ZGRmZDY4NTllM2UwODJkMTkzODlhMWFlYzFiZGFkN2U4ZDkyZDk2YjFhYTc5NDBkYTkxYTMxMjVkMzFlM2JlNWIifQ"
|
|
548
|
+
}),
|
|
549
|
+
limit: v4.z.string().regex(/^[1-9]\d*$/, {
|
|
550
|
+
message: "Limit must be a positive integer"
|
|
551
|
+
}).transform((val) => Number.parseInt(val, 10)).pipe(
|
|
552
|
+
v4.z.number().max(MAX_LIMIT, {
|
|
553
|
+
message: `Limit cannot exceed ${MAX_LIMIT}`
|
|
554
|
+
})
|
|
555
|
+
).optional().default(DEFAULT_LIMIT).meta({
|
|
556
|
+
description: `Limit maximum: ${MAX_LIMIT}. Default: ${DEFAULT_LIMIT}`,
|
|
557
|
+
example: 10
|
|
558
|
+
})
|
|
559
|
+
}).refine(
|
|
560
|
+
(data) => data.min_maturity === void 0 || data.max_maturity === void 0 || data.max_maturity >= data.min_maturity,
|
|
561
|
+
{
|
|
562
|
+
message: "max_maturity must be greater than or equal to min_maturity",
|
|
563
|
+
path: ["max_maturity"]
|
|
564
|
+
}
|
|
565
|
+
);
|
|
566
|
+
var schemas = {
|
|
567
|
+
get_offers: GetOffersQueryParams,
|
|
568
|
+
match_offers: MatchOffersQueryParams
|
|
569
|
+
};
|
|
570
|
+
function safeParse(action, query) {
|
|
571
|
+
return schemas[action].safeParse(query);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/core/apiSchema/openapi.ts
|
|
575
|
+
var successResponseSchema = v4.z.object({
|
|
576
|
+
status: v4.z.literal("success"),
|
|
577
|
+
cursor: v4.z.string().nullable(),
|
|
578
|
+
data: v4.z.array(v4.z.any()),
|
|
579
|
+
meta: v4.z.object({
|
|
580
|
+
timestamp: v4.z.string()
|
|
581
|
+
})
|
|
582
|
+
});
|
|
583
|
+
var errorResponseSchema = v4.z.object({
|
|
584
|
+
status: v4.z.literal("error"),
|
|
585
|
+
error: v4.z.object({
|
|
586
|
+
code: v4.z.string(),
|
|
587
|
+
message: v4.z.string(),
|
|
588
|
+
details: v4.z.any().optional()
|
|
589
|
+
}),
|
|
590
|
+
meta: v4.z.object({
|
|
591
|
+
timestamp: v4.z.string()
|
|
592
|
+
})
|
|
593
|
+
});
|
|
594
|
+
var paths = {
|
|
595
|
+
"/v1/offers": {
|
|
596
|
+
get: {
|
|
597
|
+
summary: "Get offers",
|
|
598
|
+
description: "Get all offers with optional filtering and pagination",
|
|
599
|
+
tags: ["Offers"],
|
|
600
|
+
requestParams: {
|
|
601
|
+
query: GetOffersQueryParams
|
|
602
|
+
},
|
|
603
|
+
responses: {
|
|
604
|
+
200: {
|
|
605
|
+
description: "Success",
|
|
606
|
+
content: {
|
|
607
|
+
"application/json": {
|
|
608
|
+
schema: successResponseSchema
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
400: {
|
|
613
|
+
description: "Bad Request",
|
|
614
|
+
content: {
|
|
615
|
+
"application/json": {
|
|
616
|
+
schema: errorResponseSchema
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
"/v1/match-offers": {
|
|
624
|
+
get: {
|
|
625
|
+
summary: "Match offers",
|
|
626
|
+
description: "Find offers that match specific criteria",
|
|
627
|
+
tags: ["Offers"],
|
|
628
|
+
requestParams: {
|
|
629
|
+
query: MatchOffersQueryParams
|
|
630
|
+
},
|
|
631
|
+
responses: {
|
|
632
|
+
200: {
|
|
633
|
+
description: "Success",
|
|
634
|
+
content: {
|
|
635
|
+
"application/json": {
|
|
636
|
+
schema: successResponseSchema
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
400: {
|
|
641
|
+
description: "Bad Request",
|
|
642
|
+
content: {
|
|
643
|
+
"application/json": {
|
|
644
|
+
schema: errorResponseSchema
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
zodOpenapi.createDocument({
|
|
653
|
+
openapi: "3.1.0",
|
|
654
|
+
info: {
|
|
655
|
+
title: "Router API",
|
|
656
|
+
version: "1.0.0",
|
|
657
|
+
description: "API for the Morpho Router"
|
|
658
|
+
},
|
|
659
|
+
tags: [
|
|
660
|
+
{
|
|
661
|
+
name: "Offers"
|
|
662
|
+
}
|
|
663
|
+
],
|
|
664
|
+
servers: [
|
|
665
|
+
{
|
|
666
|
+
url: "https://router.morpho.dev",
|
|
667
|
+
description: "Production server"
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
url: "http://localhost:8081",
|
|
671
|
+
description: "Local development server"
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
paths
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// src/core/Client.ts
|
|
678
|
+
function connect(opts) {
|
|
679
|
+
const u = new URL(opts?.url || "https://router.morpho.dev");
|
|
680
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
681
|
+
throw new InvalidUrlError(u.toString());
|
|
682
|
+
}
|
|
683
|
+
const headers = new Headers();
|
|
684
|
+
headers.set("Content-Type", "application/json");
|
|
685
|
+
opts?.apiKey !== void 0 ? headers.set("X-API-Key", opts.apiKey) : null;
|
|
686
|
+
const config = {
|
|
687
|
+
url: u,
|
|
688
|
+
headers
|
|
689
|
+
};
|
|
690
|
+
return {
|
|
691
|
+
...config,
|
|
692
|
+
get: (parameters) => get(config, parameters),
|
|
693
|
+
match: (parameters) => match(config, parameters)
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function get(config, parameters) {
|
|
697
|
+
const url = new URL(`${config.url.toString()}v1/offers`);
|
|
698
|
+
if (parameters.creators?.length) {
|
|
699
|
+
url.searchParams.set("creators", parameters.creators.join(","));
|
|
700
|
+
}
|
|
701
|
+
if (parameters.side) {
|
|
702
|
+
url.searchParams.set("side", parameters.side);
|
|
703
|
+
}
|
|
704
|
+
if (parameters.chains?.length) {
|
|
705
|
+
url.searchParams.set("chains", parameters.chains.join(","));
|
|
706
|
+
}
|
|
707
|
+
if (parameters.loanTokens?.length) {
|
|
708
|
+
url.searchParams.set("loan_tokens", parameters.loanTokens.join(","));
|
|
709
|
+
}
|
|
710
|
+
if (parameters.status?.length) {
|
|
711
|
+
url.searchParams.set("status", parameters.status.join(","));
|
|
712
|
+
}
|
|
713
|
+
if (parameters.callbackAddresses?.length) {
|
|
714
|
+
url.searchParams.set("callback_addresses", parameters.callbackAddresses.join(","));
|
|
715
|
+
}
|
|
716
|
+
if (parameters.minAmount !== void 0) {
|
|
717
|
+
url.searchParams.set("min_amount", parameters.minAmount.toString());
|
|
718
|
+
}
|
|
719
|
+
if (parameters.maxAmount !== void 0) {
|
|
720
|
+
url.searchParams.set("max_amount", parameters.maxAmount.toString());
|
|
721
|
+
}
|
|
722
|
+
if (parameters.minRate !== void 0) {
|
|
723
|
+
url.searchParams.set("min_rate", parameters.minRate.toString());
|
|
724
|
+
}
|
|
725
|
+
if (parameters.maxRate !== void 0) {
|
|
726
|
+
url.searchParams.set("max_rate", parameters.maxRate.toString());
|
|
727
|
+
}
|
|
728
|
+
if (parameters.minMaturity !== void 0) {
|
|
729
|
+
url.searchParams.set("min_maturity", parameters.minMaturity.toString());
|
|
730
|
+
}
|
|
731
|
+
if (parameters.maxMaturity !== void 0) {
|
|
732
|
+
url.searchParams.set("max_maturity", parameters.maxMaturity.toString());
|
|
733
|
+
}
|
|
734
|
+
if (parameters.minExpiry !== void 0) {
|
|
735
|
+
url.searchParams.set("min_expiry", parameters.minExpiry.toString());
|
|
736
|
+
}
|
|
737
|
+
if (parameters.maxExpiry !== void 0) {
|
|
738
|
+
url.searchParams.set("max_expiry", parameters.maxExpiry.toString());
|
|
739
|
+
}
|
|
740
|
+
if (parameters.collateralAssets?.length) {
|
|
741
|
+
url.searchParams.set("collateral_assets", parameters.collateralAssets.join(","));
|
|
742
|
+
}
|
|
743
|
+
if (parameters.collateralOracles?.length) {
|
|
744
|
+
url.searchParams.set("collateral_oracles", parameters.collateralOracles.join(","));
|
|
745
|
+
}
|
|
746
|
+
if (parameters.collateralTuple?.length) {
|
|
747
|
+
const tupleStr = parameters.collateralTuple.map(({ asset, oracle, lltv }) => {
|
|
748
|
+
let result = asset;
|
|
749
|
+
if (oracle) {
|
|
750
|
+
result += `:${oracle}`;
|
|
751
|
+
} else if (lltv !== void 0) {
|
|
752
|
+
result += `:`;
|
|
753
|
+
}
|
|
754
|
+
if (lltv !== void 0) result += `:${lltv}`;
|
|
755
|
+
return result;
|
|
756
|
+
}).join("#");
|
|
757
|
+
url.searchParams.set("collateral_tuple", tupleStr);
|
|
758
|
+
}
|
|
759
|
+
if (parameters.minLltv !== void 0) {
|
|
760
|
+
url.searchParams.set("min_lltv", parameters.minLltv.toString());
|
|
761
|
+
}
|
|
762
|
+
if (parameters.maxLltv !== void 0) {
|
|
763
|
+
url.searchParams.set("max_lltv", parameters.maxLltv.toString());
|
|
764
|
+
}
|
|
765
|
+
if (parameters.sortBy) {
|
|
766
|
+
url.searchParams.set("sort_by", parameters.sortBy);
|
|
767
|
+
}
|
|
768
|
+
if (parameters.sortOrder) {
|
|
769
|
+
url.searchParams.set("sort_order", parameters.sortOrder);
|
|
770
|
+
}
|
|
771
|
+
if (parameters.cursor) {
|
|
772
|
+
url.searchParams.set("cursor", parameters.cursor);
|
|
773
|
+
}
|
|
774
|
+
if (parameters.limit !== void 0) {
|
|
775
|
+
url.searchParams.set("limit", parameters.limit.toString());
|
|
776
|
+
}
|
|
777
|
+
const { cursor: returnedCursor, data: offers } = await getApi(config, url);
|
|
778
|
+
return {
|
|
779
|
+
cursor: returnedCursor,
|
|
780
|
+
offers: offers.map((offer) => {
|
|
781
|
+
const baseOffer = mempool.Offer.fromSnakeCase(offer);
|
|
782
|
+
return {
|
|
783
|
+
...baseOffer,
|
|
784
|
+
status: offer.status,
|
|
785
|
+
metadata: offer.metadata
|
|
786
|
+
};
|
|
787
|
+
})
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
async function match(config, parameters) {
|
|
791
|
+
const url = new URL(`${config.url.toString()}v1/match-offers`);
|
|
792
|
+
url.searchParams.set("side", parameters.side);
|
|
793
|
+
url.searchParams.set("chain_id", parameters.chainId.toString());
|
|
794
|
+
if (parameters.rate !== void 0) {
|
|
795
|
+
url.searchParams.set("rate", parameters.rate.toString());
|
|
796
|
+
}
|
|
797
|
+
if (parameters.collaterals?.length) {
|
|
798
|
+
const collateralsStr = parameters.collaterals.map(({ asset, oracle, lltv }) => `${asset}:${oracle}:${lltv}`).join("#");
|
|
799
|
+
url.searchParams.set("collaterals", collateralsStr);
|
|
800
|
+
}
|
|
801
|
+
if (parameters.maturity !== void 0) {
|
|
802
|
+
url.searchParams.set("maturity", parameters.maturity.toString());
|
|
803
|
+
}
|
|
804
|
+
if (parameters.minMaturity !== void 0) {
|
|
805
|
+
url.searchParams.set("min_maturity", parameters.minMaturity.toString());
|
|
806
|
+
}
|
|
807
|
+
if (parameters.maxMaturity !== void 0) {
|
|
808
|
+
url.searchParams.set("max_maturity", parameters.maxMaturity.toString());
|
|
809
|
+
}
|
|
810
|
+
if (parameters.loanToken) {
|
|
811
|
+
url.searchParams.set("loan_token", parameters.loanToken);
|
|
812
|
+
}
|
|
813
|
+
if (parameters.creator) {
|
|
814
|
+
url.searchParams.set("creator", parameters.creator);
|
|
815
|
+
}
|
|
816
|
+
if (parameters.status?.length) {
|
|
817
|
+
url.searchParams.set("status", parameters.status.join(","));
|
|
818
|
+
}
|
|
819
|
+
if (parameters.cursor) {
|
|
820
|
+
url.searchParams.set("cursor", parameters.cursor);
|
|
821
|
+
}
|
|
822
|
+
if (parameters.limit !== void 0) {
|
|
823
|
+
url.searchParams.set("limit", parameters.limit.toString());
|
|
824
|
+
}
|
|
825
|
+
const { cursor: returnedCursor, data: offers } = await getApi(config, url);
|
|
826
|
+
return {
|
|
827
|
+
cursor: returnedCursor,
|
|
828
|
+
offers: offers.map((offer) => {
|
|
829
|
+
const baseOffer = mempool.Offer.fromSnakeCase(offer);
|
|
830
|
+
return {
|
|
831
|
+
...baseOffer,
|
|
832
|
+
status: offer.status,
|
|
833
|
+
metadata: offer.metadata
|
|
834
|
+
};
|
|
835
|
+
})
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
async function getApi(config, url) {
|
|
839
|
+
const pathname = url.pathname;
|
|
840
|
+
let action;
|
|
841
|
+
switch (true) {
|
|
842
|
+
case pathname.includes("/v1/offers"):
|
|
843
|
+
action = "get_offers";
|
|
844
|
+
break;
|
|
845
|
+
case pathname.includes("/v1/match-offers"):
|
|
846
|
+
action = "match_offers";
|
|
847
|
+
break;
|
|
848
|
+
default:
|
|
849
|
+
throw new HttpGetOffersFailedError("Unknown endpoint", {
|
|
850
|
+
details: `Unsupported path: ${pathname}`
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
const schemaParseResult = safeParse(action, Object.fromEntries(url.searchParams));
|
|
854
|
+
if (!schemaParseResult.success) {
|
|
855
|
+
throw new HttpGetOffersFailedError(`Invalid URL parameters`, {
|
|
856
|
+
details: schemaParseResult.error.issues[0]?.message
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const response = await fetch(url.toString(), {
|
|
860
|
+
method: "GET",
|
|
861
|
+
headers: config.headers
|
|
862
|
+
});
|
|
863
|
+
if (!response.ok) {
|
|
864
|
+
switch (response.status) {
|
|
865
|
+
case 401:
|
|
866
|
+
throw new HttpUnauthorizedError();
|
|
867
|
+
case 403:
|
|
868
|
+
throw new HttpForbiddenError();
|
|
869
|
+
case 429:
|
|
870
|
+
throw new HttpRateLimitError();
|
|
871
|
+
}
|
|
872
|
+
throw new HttpGetOffersFailedError(`GET request returned ${response.status}`, {
|
|
873
|
+
details: await response.text()
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return response.json();
|
|
877
|
+
}
|
|
878
|
+
var InvalidUrlError = class extends mempool.Errors.BaseError {
|
|
879
|
+
constructor(url) {
|
|
880
|
+
super(`URL "${url}" is not http/https.`);
|
|
881
|
+
__publicField(this, "name", "Router.InvalidUrlError");
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
var HttpUnauthorizedError = class extends mempool.Errors.BaseError {
|
|
885
|
+
constructor() {
|
|
886
|
+
super("Unauthorized.", {
|
|
887
|
+
metaMessages: ["Ensure that an API key is provided."]
|
|
888
|
+
});
|
|
889
|
+
__publicField(this, "name", "Router.HttpUnauthorizedError");
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
var HttpForbiddenError = class extends mempool.Errors.BaseError {
|
|
893
|
+
constructor() {
|
|
894
|
+
super("Forbidden.", {
|
|
895
|
+
metaMessages: ["Ensure that the API key is valid."]
|
|
896
|
+
});
|
|
897
|
+
__publicField(this, "name", "Router.HttpForbiddenError");
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
var HttpRateLimitError = class extends mempool.Errors.BaseError {
|
|
901
|
+
constructor() {
|
|
902
|
+
super("Rate limit exceeded.", {
|
|
903
|
+
metaMessages: [
|
|
904
|
+
"The number of allowed requests has been exceeded. You must wait for the rate limit to reset."
|
|
905
|
+
]
|
|
906
|
+
});
|
|
907
|
+
__publicField(this, "name", "Router.HttpRateLimitError");
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
var HttpGetOffersFailedError = class extends mempool.Errors.BaseError {
|
|
911
|
+
constructor(message, { details } = {}) {
|
|
912
|
+
super(message, {
|
|
913
|
+
metaMessages: [details]
|
|
914
|
+
});
|
|
915
|
+
__publicField(this, "name", "Router.HttpGetOffersFailedError");
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/RouterEvent.ts
|
|
920
|
+
var RouterEvent_exports = {};
|
|
921
|
+
__export(RouterEvent_exports, {
|
|
922
|
+
buildId: () => buildId,
|
|
923
|
+
types: () => types
|
|
924
|
+
});
|
|
925
|
+
var types = ["offer_created", "offer_matched", "offer_validation"];
|
|
926
|
+
function buildId(event) {
|
|
927
|
+
switch (event.type) {
|
|
928
|
+
case "offer_created":
|
|
929
|
+
return `offer_created:${event.offer.hash.toLowerCase()}`;
|
|
930
|
+
case "offer_matched":
|
|
931
|
+
return `offer_matched:${event.offer.hash.toLowerCase()}`;
|
|
932
|
+
case "offer_validation":
|
|
933
|
+
return `offer_validation:${event.offer.hash.toLowerCase()}`;
|
|
934
|
+
default: {
|
|
935
|
+
throw new Error("Unhandled event type");
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// src/Validation.ts
|
|
941
|
+
var Validation_exports = {};
|
|
942
|
+
__export(Validation_exports, {
|
|
943
|
+
run: () => run
|
|
944
|
+
});
|
|
945
|
+
async function run(parameters) {
|
|
946
|
+
const { items, rules, ctx = {}, chunkSize } = parameters;
|
|
947
|
+
const issues = [];
|
|
948
|
+
let validItems = items.slice();
|
|
949
|
+
for (const rule of rules) {
|
|
950
|
+
if (validItems.length === 0) return { valid: [], issues };
|
|
951
|
+
const indicesToRemove = /* @__PURE__ */ new Set();
|
|
952
|
+
if (rule.kind === "single") {
|
|
953
|
+
for (let i = 0; i < validItems.length; i++) {
|
|
954
|
+
const item = validItems[i];
|
|
955
|
+
const issue = await rule.run(item, ctx);
|
|
956
|
+
if (issue) {
|
|
957
|
+
issues.push({ ...issue, ruleName: rule.name, item });
|
|
958
|
+
indicesToRemove.add(i);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
} else if (rule.kind === "batch") {
|
|
962
|
+
const exec = async (slice, offset) => {
|
|
963
|
+
const map = await rule.run(slice, ctx);
|
|
964
|
+
for (let i = 0; i < slice.length; i++) {
|
|
965
|
+
const issue = map.get(i);
|
|
966
|
+
if (issue !== void 0) {
|
|
967
|
+
issues.push({ ...issue, ruleName: rule.name, item: slice[i] });
|
|
968
|
+
indicesToRemove.add(offset + i);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
if (!chunkSize) await exec(validItems, 0);
|
|
973
|
+
else {
|
|
974
|
+
for (let i = 0; i < validItems.length; i += chunkSize) {
|
|
975
|
+
await exec(validItems.slice(i, i + chunkSize), i);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
validItems = validItems.filter((_, i) => !indicesToRemove.has(i));
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
valid: validItems,
|
|
983
|
+
issues
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/ValidationRule.ts
|
|
988
|
+
var ValidationRule_exports = {};
|
|
989
|
+
__export(ValidationRule_exports, {
|
|
990
|
+
batch: () => batch2,
|
|
991
|
+
morpho: () => morpho,
|
|
992
|
+
single: () => single
|
|
993
|
+
});
|
|
994
|
+
function single(name, run2) {
|
|
995
|
+
return { kind: "single", name, run: run2 };
|
|
996
|
+
}
|
|
997
|
+
function batch2(name, run2) {
|
|
998
|
+
return { kind: "batch", name, run: run2 };
|
|
999
|
+
}
|
|
1000
|
+
function morpho(parameters) {
|
|
1001
|
+
const { whitelistedChains } = parameters;
|
|
1002
|
+
const whitelistedChainIds = new Set(whitelistedChains.map((chain) => chain.id));
|
|
1003
|
+
const whitelistedLoanTokensPerChain = new Map(
|
|
1004
|
+
whitelistedChains.map((chain) => [
|
|
1005
|
+
chain.id,
|
|
1006
|
+
new Set(Array.from(chain.whitelistedAssets).map((a) => a.toLowerCase()))
|
|
1007
|
+
])
|
|
1008
|
+
);
|
|
1009
|
+
const morphoPerChain = new Map(
|
|
1010
|
+
whitelistedChains.map((chain) => [chain.id, chain.morpho.toLowerCase()])
|
|
1011
|
+
);
|
|
1012
|
+
const chainId = single("chain_id", (offer, _) => {
|
|
1013
|
+
if (!whitelistedChainIds.has(offer.chainId)) {
|
|
1014
|
+
return { message: `Chain ID ${offer.chainId} is not whitelisted` };
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
const loanToken = single("loan_token", (offer, _) => {
|
|
1018
|
+
if (!whitelistedLoanTokensPerChain.get(offer.chainId)?.has(offer.loanToken.toLowerCase())) {
|
|
1019
|
+
return {
|
|
1020
|
+
message: `Loan token ${offer.loanToken} is not whitelisted on chain ${offer.chainId}`
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
const expiry = single("expiry", (offer, _) => {
|
|
1025
|
+
if (offer.expiry < Math.floor(Date.now() / 1e3)) {
|
|
1026
|
+
return { message: "Expiry mismatch" };
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
const emptyCallback = single("empty_callback", (offer, _) => {
|
|
1030
|
+
if (offer.callback.data !== "0x") {
|
|
1031
|
+
return { message: "Callback data is not empty. Not supported yet." };
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
const sellOffersEmptyCallback = single(
|
|
1035
|
+
"sell_offers_empty_callback",
|
|
1036
|
+
(offer, _) => {
|
|
1037
|
+
if (!offer.buy && offer.callback.data === "0x") {
|
|
1038
|
+
return { message: "Sell offers with empty callback are not supported yet." };
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
const buyOffersEmptyCallback = batch2(
|
|
1043
|
+
"buy_offers_empty_callback",
|
|
1044
|
+
async (offers, { publicClients }) => {
|
|
1045
|
+
const issues = /* @__PURE__ */ new Map();
|
|
1046
|
+
const hashToIndex = /* @__PURE__ */ new Map();
|
|
1047
|
+
for (let i = 0; i < offers.length; i++) {
|
|
1048
|
+
const offer = offers[i];
|
|
1049
|
+
hashToIndex.set(offer.hash, i);
|
|
1050
|
+
}
|
|
1051
|
+
const { buyOffers, sellOffers: _sellOffers } = offers.reduce(
|
|
1052
|
+
(acc, offer) => {
|
|
1053
|
+
offer.buy ? acc.buyOffers.push(offer) : issues.set(hashToIndex.get(offer.hash), {
|
|
1054
|
+
message: "Onchain callback for sell offers is not supported yet."
|
|
1055
|
+
});
|
|
1056
|
+
return acc;
|
|
1057
|
+
},
|
|
1058
|
+
{ buyOffers: [], sellOffers: [] }
|
|
1059
|
+
);
|
|
1060
|
+
const buyOffersPerLoanAsset = /* @__PURE__ */ new Map();
|
|
1061
|
+
for (const offer of buyOffers) {
|
|
1062
|
+
const chainName = getChain(offer.chainId)?.name;
|
|
1063
|
+
const loanTokens = buyOffersPerLoanAsset.get(chainName) ?? /* @__PURE__ */ new Map();
|
|
1064
|
+
const offers2 = loanTokens.get(offer.loanToken.toLowerCase()) ?? [];
|
|
1065
|
+
offers2.push(offer);
|
|
1066
|
+
loanTokens.set(offer.loanToken.toLowerCase(), offers2);
|
|
1067
|
+
buyOffersPerLoanAsset.set(chainName, loanTokens);
|
|
1068
|
+
}
|
|
1069
|
+
await Promise.all(
|
|
1070
|
+
Array.from(buyOffersPerLoanAsset.entries()).map(async ([name, loanTokens]) => {
|
|
1071
|
+
const chainName = name;
|
|
1072
|
+
const publicClient = publicClients[chainName];
|
|
1073
|
+
const morpho2 = morphoPerChain.get(chains[chainName].id);
|
|
1074
|
+
if (!publicClient) {
|
|
1075
|
+
const offers2 = Array.from(loanTokens.values()).flat();
|
|
1076
|
+
for (const offer of offers2) {
|
|
1077
|
+
issues.set(hashToIndex.get(offer.hash), {
|
|
1078
|
+
message: `Public client for chain "${chainName}" is not available`
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const balances = /* @__PURE__ */ new Map();
|
|
1084
|
+
const allowances = /* @__PURE__ */ new Map();
|
|
1085
|
+
for (const [loanToken2, offers2] of loanTokens) {
|
|
1086
|
+
const data = await Promise.all(
|
|
1087
|
+
offers2.flatMap((offer) => [
|
|
1088
|
+
publicClient.readContract({
|
|
1089
|
+
address: loanToken2,
|
|
1090
|
+
abi: viem.parseAbi([
|
|
1091
|
+
"function balanceOf(address owner) view returns (uint256 balance)"
|
|
1092
|
+
]),
|
|
1093
|
+
functionName: "balanceOf",
|
|
1094
|
+
args: [offer.offering]
|
|
1095
|
+
}),
|
|
1096
|
+
publicClient.readContract({
|
|
1097
|
+
address: loanToken2,
|
|
1098
|
+
abi: viem.parseAbi([
|
|
1099
|
+
"function allowance(address owner, address spender) public view returns (uint256 remaining)"
|
|
1100
|
+
]),
|
|
1101
|
+
functionName: "allowance",
|
|
1102
|
+
args: [offer.offering, morpho2]
|
|
1103
|
+
})
|
|
1104
|
+
])
|
|
1105
|
+
);
|
|
1106
|
+
for (let i = 0; i < offers2.length; i++) {
|
|
1107
|
+
const user = offers2[i].offering.toLowerCase();
|
|
1108
|
+
const balance = data[i * 2] || 0n;
|
|
1109
|
+
const allowance = data[i * 2 + 1] || 0n;
|
|
1110
|
+
const userBalances = balances.get(user) ?? /* @__PURE__ */ new Map();
|
|
1111
|
+
userBalances.set(loanToken2.toLowerCase(), balance);
|
|
1112
|
+
const userAllowances = allowances.get(user) ?? /* @__PURE__ */ new Map();
|
|
1113
|
+
userAllowances.set(loanToken2.toLowerCase(), allowance);
|
|
1114
|
+
balances.set(user, userBalances);
|
|
1115
|
+
allowances.set(user, userAllowances);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
for (const offer of Array.from(loanTokens.values()).flat()) {
|
|
1119
|
+
const user = offer.offering.toLowerCase();
|
|
1120
|
+
const userBalances = balances.get(user);
|
|
1121
|
+
const balance = userBalances?.get(offer.loanToken.toLowerCase());
|
|
1122
|
+
if (balance < offer.assets) {
|
|
1123
|
+
issues.set(hashToIndex.get(offer.hash), {
|
|
1124
|
+
message: `Insufficient balance for ${offer.loanToken} on chain ${offer.chainId} (${balance.toString()} < ${offer.assets.toString()})`
|
|
1125
|
+
});
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const userAllowances = allowances.get(user);
|
|
1129
|
+
const allowance = userAllowances?.get(offer.loanToken.toLowerCase());
|
|
1130
|
+
if (allowance < offer.assets) {
|
|
1131
|
+
issues.set(hashToIndex.get(offer.hash), {
|
|
1132
|
+
message: `Insufficient allowance for ${offer.loanToken} on chain ${offer.chainId} (${allowance.toString()} < ${offer.assets.toString()})`
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
})
|
|
1137
|
+
);
|
|
1138
|
+
return issues;
|
|
1139
|
+
}
|
|
1140
|
+
);
|
|
1141
|
+
return [
|
|
1142
|
+
chainId,
|
|
1143
|
+
loanToken,
|
|
1144
|
+
expiry,
|
|
1145
|
+
// note: callback onchain check should be done last since it does not mean that the offer is forever invalid
|
|
1146
|
+
// integrators should be able to keep the offers that have an invalid callback onchain and be sure that is the only check that is not valid
|
|
1147
|
+
emptyCallback,
|
|
1148
|
+
sellOffersEmptyCallback,
|
|
1149
|
+
buyOffersEmptyCallback
|
|
1150
|
+
];
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
exports.Chain = Chain_exports;
|
|
1154
|
+
exports.Router = Client_exports;
|
|
1155
|
+
exports.RouterEvent = RouterEvent_exports;
|
|
1156
|
+
exports.RouterOffer = RouterOffer_exports;
|
|
1157
|
+
exports.Validation = Validation_exports;
|
|
1158
|
+
exports.ValidationRule = ValidationRule_exports;
|
|
1159
|
+
exports.batch = batch;
|
|
1160
|
+
exports.decodeCursor = decodeCursor;
|
|
1161
|
+
exports.encodeCursor = encodeCursor;
|
|
1162
|
+
exports.poll = poll;
|
|
1163
|
+
exports.validateCursor = validateCursor;
|
|
1164
|
+
exports.wait = wait;
|
|
1165
|
+
Object.keys(mempool).forEach(function (k) {
|
|
1166
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
1167
|
+
enumerable: true,
|
|
1168
|
+
get: function () { return mempool[k]; }
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
//# sourceMappingURL=index.browser.js.map
|
|
1172
|
+
//# sourceMappingURL=index.browser.js.map
|