@ember-finance/sdk 2.12.0-ccip.0 → 2.13.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/dist/src/abis/index.d.ts +1 -3
- package/dist/src/abis/index.js +1 -4
- package/dist/src/api/v2/models/evm-contracts.d.ts +0 -9
- package/dist/src/api/v2/models/evm-oftadapter.d.ts +0 -11
- package/dist/src/api/v2/models/evm-oftadapter.js +1 -4
- package/dist/src/api/v2/models/evm-oftpeer.d.ts +0 -12
- package/dist/src/api/v2/models/index.d.ts +0 -3
- package/dist/src/api/v2/models/index.js +0 -3
- package/dist/src/api/v2/models/vault-parking-lot-info.d.ts +6 -0
- package/dist/src/evm-vaults/interfaces/index.d.ts +0 -39
- package/dist/src/evm-vaults/on-chain-calls/tx-builder.d.ts +1 -15
- package/dist/src/evm-vaults/on-chain-calls/tx-builder.js +0 -36
- package/dist/src/evm-vaults/on-chain-calls/user.d.ts +1 -33
- package/dist/src/evm-vaults/on-chain-calls/user.js +0 -61
- package/dist/src/evm-vaults/on-chain-calls/vault-reader.d.ts +1 -14
- package/dist/src/evm-vaults/on-chain-calls/vault-reader.js +0 -30
- package/dist/src/evm-vaults/utils/deployment-parser.d.ts +1 -27
- package/dist/src/evm-vaults/utils/deployment-parser.js +0 -37
- package/dist/src/evm-vaults/utils/index.d.ts +0 -1
- package/dist/src/evm-vaults/utils/index.js +0 -1
- package/dist/src/placements/adapters/alphalend.d.ts +1 -0
- package/dist/src/placements/adapters/alphalend.js +58 -0
- package/dist/src/placements/adapters/bluefin-spot.d.ts +1 -0
- package/dist/src/placements/adapters/bluefin-spot.js +78 -0
- package/dist/src/placements/adapters/current.d.ts +1 -0
- package/dist/src/placements/adapters/current.js +70 -0
- package/dist/src/placements/adapters/evm-protocols.d.ts +1 -0
- package/dist/src/placements/adapters/evm-protocols.js +8 -0
- package/dist/src/placements/adapters/factory.d.ts +15 -0
- package/dist/src/placements/adapters/factory.js +62 -0
- package/dist/src/placements/adapters/index.d.ts +14 -0
- package/dist/src/placements/adapters/index.js +25 -0
- package/dist/src/placements/adapters/navi.d.ts +1 -0
- package/dist/src/placements/adapters/navi.js +53 -0
- package/dist/src/placements/adapters/pendle.d.ts +9 -0
- package/dist/src/placements/adapters/pendle.js +108 -0
- package/dist/src/placements/adapters/royco.d.ts +2 -0
- package/dist/src/placements/adapters/royco.js +49 -0
- package/dist/src/placements/adapters/suilend.d.ts +1 -0
- package/dist/src/placements/adapters/suilend.js +57 -0
- package/dist/src/placements/batch.d.ts +70 -0
- package/dist/src/placements/batch.js +197 -0
- package/dist/src/placements/client.d.ts +17 -0
- package/dist/src/placements/client.js +194 -0
- package/dist/src/placements/evm.d.ts +29 -0
- package/dist/src/placements/evm.js +280 -0
- package/dist/src/placements/index.d.ts +9 -0
- package/dist/src/placements/index.js +9 -0
- package/dist/src/placements/pointers.d.ts +24 -0
- package/dist/src/placements/pointers.js +126 -0
- package/dist/src/placements/registry.d.ts +11 -0
- package/dist/src/placements/registry.js +19 -0
- package/dist/src/placements/sui.d.ts +47 -0
- package/dist/src/placements/sui.js +302 -0
- package/dist/src/placements/types.d.ts +147 -0
- package/dist/src/placements/types.js +1 -0
- package/dist/src/placements/vaults.d.ts +15 -0
- package/dist/src/placements/vaults.js +97 -0
- package/package.json +6 -1
- package/dist/src/abis/CcipRouter.json +0 -58
- package/dist/src/api/v2/models/evm-ccip-lane.d.ts +0 -73
- package/dist/src/api/v2/models/evm-ccip-lane.js +0 -14
- package/dist/src/api/v2/models/evm-ccip-rate-limits.d.ts +0 -36
- package/dist/src/api/v2/models/evm-ccip-rate-limits.js +0 -14
- package/dist/src/api/v2/models/evm-ccip-token-pool.d.ts +0 -122
- package/dist/src/api/v2/models/evm-ccip-token-pool.js +0 -17
- package/dist/src/evm-vaults/utils/ccip.d.ts +0 -23
- package/dist/src/evm-vaults/utils/ccip.js +0 -32
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { Batcher, ChunkTooLargeError, TransientError, isAbortError, isTransientStatus, retryAfterMs } from "./batch.js";
|
|
2
|
+
const GRAPHQL_PAGE_SIZE = 50;
|
|
3
|
+
/**
|
|
4
|
+
* Canonical form for a Sui coin type: `0x` stripped from the address segment,
|
|
5
|
+
* lowercased and left-padded to 64 hex chars.
|
|
6
|
+
*
|
|
7
|
+
* Object JSON returns unprefixed padded types ("0000..02::sui::SUI") while
|
|
8
|
+
* config and RPC use the short prefixed form ("0x2::sui::SUI"). Comparing the
|
|
9
|
+
* two without normalising silently never matches.
|
|
10
|
+
*
|
|
11
|
+
* Only the address is folded. It is hex, so case is not significant there, but
|
|
12
|
+
* the module and struct names are Move identifiers and two that differ only in
|
|
13
|
+
* case are two different types.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeCoinType(coinType) {
|
|
16
|
+
const parts = coinType.trim().split("::");
|
|
17
|
+
if (parts.length < 2)
|
|
18
|
+
return coinType.trim().toLowerCase();
|
|
19
|
+
const address = parts[0].toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
20
|
+
return [address, ...parts.slice(1)].join("::");
|
|
21
|
+
}
|
|
22
|
+
export function coinTypesMatch(a, b) {
|
|
23
|
+
return normalizeCoinType(a) === normalizeCoinType(b);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A Sui address as GraphQL wants a dynamic-field key: base64 of its BCS
|
|
27
|
+
* encoding, which for an address is simply its 32 raw bytes.
|
|
28
|
+
*/
|
|
29
|
+
export function bcsAddress(address) {
|
|
30
|
+
// Hand-rolled rather than via Buffer, because this runs in the browser.
|
|
31
|
+
const hex = address.trim().replace(/^0x/, "").toLowerCase().padStart(64, "0");
|
|
32
|
+
if (!/^[0-9a-f]{64}$/.test(hex))
|
|
33
|
+
throw new Error(`not a sui address: ${address}`);
|
|
34
|
+
let binary = "";
|
|
35
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
36
|
+
binary += String.fromCharCode(Number.parseInt(hex.slice(i, i + 2), 16));
|
|
37
|
+
}
|
|
38
|
+
return btoa(binary);
|
|
39
|
+
}
|
|
40
|
+
/** Type arguments of a Move type, e.g. `Pool<A, B>` -> ["A", "B"]. */
|
|
41
|
+
export function typeArguments(type) {
|
|
42
|
+
const open = type.indexOf("<");
|
|
43
|
+
if (open === -1 || !type.endsWith(">"))
|
|
44
|
+
return [];
|
|
45
|
+
const inner = type.slice(open + 1, -1);
|
|
46
|
+
const args = [];
|
|
47
|
+
let depth = 0;
|
|
48
|
+
let current = "";
|
|
49
|
+
for (const ch of inner) {
|
|
50
|
+
if (ch === "<")
|
|
51
|
+
depth++;
|
|
52
|
+
if (ch === ">")
|
|
53
|
+
depth--;
|
|
54
|
+
if (ch === "," && depth === 0) {
|
|
55
|
+
args.push(current.trim());
|
|
56
|
+
current = "";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
current += ch;
|
|
60
|
+
}
|
|
61
|
+
if (current.trim())
|
|
62
|
+
args.push(current.trim());
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
/** The object id a Move table's entries actually hang off. */
|
|
66
|
+
export function tableId(field) {
|
|
67
|
+
// Protocols wrap tables to different depths — `{ id }`, `{ table: { id } }`,
|
|
68
|
+
// `{ table: { table: { id } } }` — and the outer id resolves to nothing, so
|
|
69
|
+
// reading it returns an empty field list rather than an error.
|
|
70
|
+
const nested = field;
|
|
71
|
+
return nested?.table?.table?.id ?? nested?.table?.id ?? nested?.id;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Some protocols wrap fixed-point numbers as `{ value }` scaled by 1e18.
|
|
75
|
+
* Returns base units, truncated — these feed balances, not accounting.
|
|
76
|
+
*/
|
|
77
|
+
export function decimalToBaseUnits(field) {
|
|
78
|
+
const raw = field?.value ?? field;
|
|
79
|
+
if (raw === undefined || raw === null)
|
|
80
|
+
return 0n;
|
|
81
|
+
return BigInt(String(raw)) / 1000000000000000000n;
|
|
82
|
+
}
|
|
83
|
+
export function toBigInt(field) {
|
|
84
|
+
if (field === undefined || field === null)
|
|
85
|
+
return 0n;
|
|
86
|
+
return BigInt(String(field));
|
|
87
|
+
}
|
|
88
|
+
export const DEFAULT_SUI_GRAPHQL_ENDPOINT = "https://graphql.mainnet.sui.io/graphql";
|
|
89
|
+
const OBJECTS_ALIAS = "objects";
|
|
90
|
+
const OBJECT_SELECTION = "{ address asMoveObject { contents { type { repr } json } } }";
|
|
91
|
+
const OBJECT_KEY_WEIGHT = 0.25;
|
|
92
|
+
const OBJECT_KEY_BYTES = 90;
|
|
93
|
+
const SUI_ADDRESS = /^(0x)?[0-9a-fA-F]{1,64}$/;
|
|
94
|
+
function normalizeAddress(address) {
|
|
95
|
+
return address.trim().toLowerCase().replace(/^0x/, "").padStart(64, "0");
|
|
96
|
+
}
|
|
97
|
+
function isSuiAddress(value) {
|
|
98
|
+
return SUI_ADDRESS.test(value.trim());
|
|
99
|
+
}
|
|
100
|
+
function literal(value) {
|
|
101
|
+
return JSON.stringify(value);
|
|
102
|
+
}
|
|
103
|
+
function buildDocument(chunk) {
|
|
104
|
+
const roots = [];
|
|
105
|
+
const ids = chunk.flatMap(item => (item.kind === "object" ? [item.id] : []));
|
|
106
|
+
if (ids.length > 0) {
|
|
107
|
+
const keys = ids.map(id => `{ address: ${literal(id)} }`).join(", ");
|
|
108
|
+
roots.push(`${OBJECTS_ALIAS}: multiGetObjects(keys: [${keys}]) ${OBJECT_SELECTION}`);
|
|
109
|
+
}
|
|
110
|
+
chunk.forEach((item, index) => {
|
|
111
|
+
if (item.kind === "field")
|
|
112
|
+
roots.push(`r${index}: ${item.field}`);
|
|
113
|
+
});
|
|
114
|
+
return `{ ${roots.join(" ")} }`;
|
|
115
|
+
}
|
|
116
|
+
function distribute(chunk, body) {
|
|
117
|
+
const aliasErrors = new Map();
|
|
118
|
+
const objectErrors = new Map();
|
|
119
|
+
const rootErrors = [];
|
|
120
|
+
for (const error of body.errors ?? []) {
|
|
121
|
+
const alias = error.path?.[0];
|
|
122
|
+
if (typeof alias !== "string") {
|
|
123
|
+
rootErrors.push(error.message);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const position = error.path?.[1];
|
|
127
|
+
if (alias === OBJECTS_ALIAS && typeof position === "number") {
|
|
128
|
+
objectErrors.set(position, error.message);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
aliasErrors.set(alias, error.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (rootErrors.length > 0 || (body.data == null && (body.errors?.length ?? 0) > 0)) {
|
|
135
|
+
const message = `sui graphql: ${[...rootErrors, ...aliasErrors.values(), ...objectErrors.values()].join("; ")}`;
|
|
136
|
+
if (chunk.length > 1)
|
|
137
|
+
throw new ChunkTooLargeError(message);
|
|
138
|
+
throw new Error(message);
|
|
139
|
+
}
|
|
140
|
+
const data = body.data ?? {};
|
|
141
|
+
const objectsByAddress = new Map();
|
|
142
|
+
for (const node of data[OBJECTS_ALIAS] ?? []) {
|
|
143
|
+
const contents = node?.asMoveObject?.contents;
|
|
144
|
+
if (!node || !contents)
|
|
145
|
+
continue;
|
|
146
|
+
objectsByAddress.set(normalizeAddress(node.address), {
|
|
147
|
+
address: node.address,
|
|
148
|
+
type: contents.type.repr,
|
|
149
|
+
json: contents.json
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
let objectPosition = -1;
|
|
153
|
+
return chunk.map((item, index) => {
|
|
154
|
+
if (item.kind === "object") {
|
|
155
|
+
objectPosition += 1;
|
|
156
|
+
const message = objectErrors.get(objectPosition) ?? aliasErrors.get(OBJECTS_ALIAS);
|
|
157
|
+
if (message !== undefined) {
|
|
158
|
+
return { ok: false, error: new Error(`sui graphql: ${message}`) };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, value: objectsByAddress.get(normalizeAddress(item.id)) };
|
|
161
|
+
}
|
|
162
|
+
const alias = `r${index}`;
|
|
163
|
+
const message = aliasErrors.get(alias);
|
|
164
|
+
if (message !== undefined) {
|
|
165
|
+
return { ok: false, error: new Error(`sui graphql: ${message}`) };
|
|
166
|
+
}
|
|
167
|
+
return { ok: true, value: data[alias] };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export function graphqlSuiReader(options = {}) {
|
|
171
|
+
const config = typeof options === "string" ? { endpoint: options } : options;
|
|
172
|
+
const endpoint = config.endpoint ?? DEFAULT_SUI_GRAPHQL_ENDPOINT;
|
|
173
|
+
const fetchImpl = config.fetch ??
|
|
174
|
+
((input, init) => globalThis.fetch(input, init));
|
|
175
|
+
const onRequest = config.onRequest;
|
|
176
|
+
const send = async (chunk, attempt) => {
|
|
177
|
+
const document = buildDocument(chunk);
|
|
178
|
+
let response;
|
|
179
|
+
try {
|
|
180
|
+
response = await fetchImpl(endpoint, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "Content-Type": "application/json" },
|
|
183
|
+
body: JSON.stringify({ query: document }),
|
|
184
|
+
signal: config.signal
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (isAbortError(error))
|
|
189
|
+
throw error;
|
|
190
|
+
onRequest?.({ items: chunk.length, attempt, status: "network-error" });
|
|
191
|
+
throw new TransientError(`sui graphql: ${error instanceof Error ? error.message : String(error)}`);
|
|
192
|
+
}
|
|
193
|
+
onRequest?.({ items: chunk.length, attempt, status: response.status });
|
|
194
|
+
if (isTransientStatus(response.status)) {
|
|
195
|
+
throw new TransientError(`sui graphql ${response.status}`, retryAfterMs(response.headers?.get("retry-after")));
|
|
196
|
+
}
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const message = `sui graphql ${response.status}`;
|
|
199
|
+
if (chunk.length > 1)
|
|
200
|
+
throw new ChunkTooLargeError(message);
|
|
201
|
+
throw new Error(message);
|
|
202
|
+
}
|
|
203
|
+
const body = (await response.json());
|
|
204
|
+
if (!body.data && !body.errors?.length) {
|
|
205
|
+
throw new Error("sui graphql: empty response");
|
|
206
|
+
}
|
|
207
|
+
return distribute(chunk, body);
|
|
208
|
+
};
|
|
209
|
+
const batcher = new Batcher({
|
|
210
|
+
send,
|
|
211
|
+
key: item => item.kind === "object" ? `object:${normalizeAddress(item.id)}` : item.field,
|
|
212
|
+
weight: item => (item.kind === "object" ? OBJECT_KEY_WEIGHT : 1),
|
|
213
|
+
bytes: item => (item.kind === "object" ? OBJECT_KEY_BYTES : item.field.length),
|
|
214
|
+
maxWeight: config.maxRootFields ?? 10,
|
|
215
|
+
maxBytes: config.maxBytes ?? 4500,
|
|
216
|
+
concurrency: config.concurrency,
|
|
217
|
+
retries: config.retries,
|
|
218
|
+
baseDelayMs: config.baseDelayMs,
|
|
219
|
+
maxDelayMs: config.maxDelayMs,
|
|
220
|
+
sleep: config.sleep,
|
|
221
|
+
signal: config.signal
|
|
222
|
+
});
|
|
223
|
+
const field = (gql) => batcher.enqueue({ kind: "field", field: gql });
|
|
224
|
+
const requireAddress = (value, what) => {
|
|
225
|
+
if (!isSuiAddress(value))
|
|
226
|
+
throw new Error(`sui graphql: ${what} is not a sui address`);
|
|
227
|
+
};
|
|
228
|
+
return {
|
|
229
|
+
async objects(ids) {
|
|
230
|
+
const unique = [...new Set(ids)];
|
|
231
|
+
for (const id of unique)
|
|
232
|
+
requireAddress(id, `object id ${id}`);
|
|
233
|
+
const found = await Promise.all(unique.map(id => batcher.enqueue({ kind: "object", id })));
|
|
234
|
+
const out = {};
|
|
235
|
+
for (const object of found) {
|
|
236
|
+
if (object)
|
|
237
|
+
out[object.address] = object;
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
},
|
|
241
|
+
async balance(owner, coinType) {
|
|
242
|
+
requireAddress(owner, `owner ${owner}`);
|
|
243
|
+
const data = await field(`address(address: ${literal(owner)}) { balance(coinType: ${literal(coinType)}) { totalBalance } }`);
|
|
244
|
+
return data?.balance?.totalBalance ?? "0";
|
|
245
|
+
},
|
|
246
|
+
async ownedObjects(owner, type) {
|
|
247
|
+
requireAddress(owner, `owner ${owner}`);
|
|
248
|
+
const owned = [];
|
|
249
|
+
let cursor = null;
|
|
250
|
+
for (;;) {
|
|
251
|
+
const after = cursor ? `, after: ${literal(cursor)}` : "";
|
|
252
|
+
const page = await field(`address(address: ${literal(owner)}) { objects(first: ${GRAPHQL_PAGE_SIZE}${after}, filter: { type: ${literal(type)} }) { pageInfo { hasNextPage endCursor } nodes { address contents { type { repr } json } } } }`);
|
|
253
|
+
const container = page?.objects;
|
|
254
|
+
if (!container)
|
|
255
|
+
break;
|
|
256
|
+
for (const node of container.nodes ?? []) {
|
|
257
|
+
if (!node.contents)
|
|
258
|
+
continue;
|
|
259
|
+
owned.push({
|
|
260
|
+
address: node.address,
|
|
261
|
+
type: node.contents.type.repr,
|
|
262
|
+
json: node.contents.json
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (!container.pageInfo?.hasNextPage)
|
|
266
|
+
break;
|
|
267
|
+
cursor = container.pageInfo.endCursor;
|
|
268
|
+
}
|
|
269
|
+
return owned;
|
|
270
|
+
},
|
|
271
|
+
async dynamicField(parentId, key) {
|
|
272
|
+
requireAddress(parentId, `parent ${parentId}`);
|
|
273
|
+
const data = await field(`address(address: ${literal(parentId)}) { dynamicField(name: { type: ${literal(key.type)}, bcs: ${literal(key.bcs)} }) { value { ... on MoveValue { json } } } }`);
|
|
274
|
+
return data?.dynamicField?.value?.json;
|
|
275
|
+
},
|
|
276
|
+
async dynamicFields(parentId, limit) {
|
|
277
|
+
requireAddress(parentId, `parent ${parentId}`);
|
|
278
|
+
const fields = [];
|
|
279
|
+
let cursor = null;
|
|
280
|
+
for (;;) {
|
|
281
|
+
const after = cursor ? `, after: ${literal(cursor)}` : "";
|
|
282
|
+
const page = await field(`address(address: ${literal(parentId)}) { dynamicFields(first: ${GRAPHQL_PAGE_SIZE}${after}) { pageInfo { hasNextPage endCursor } nodes { name { json } value { ... on MoveValue { type { repr } json } } } } }`);
|
|
283
|
+
const container = page?.dynamicFields;
|
|
284
|
+
if (!container)
|
|
285
|
+
break;
|
|
286
|
+
for (const node of container.nodes ?? []) {
|
|
287
|
+
fields.push({
|
|
288
|
+
name: node.name?.json,
|
|
289
|
+
valueType: node.value?.type?.repr ?? "",
|
|
290
|
+
value: node.value?.json
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (limit !== undefined && fields.length >= limit)
|
|
294
|
+
break;
|
|
295
|
+
if (!container.pageInfo?.hasNextPage)
|
|
296
|
+
break;
|
|
297
|
+
cursor = container.pageInfo.endCursor;
|
|
298
|
+
}
|
|
299
|
+
return limit === undefined ? fields : fields.slice(0, limit);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ChainType } from "../common/constants.js";
|
|
2
|
+
/** Identifier for a protocol adapter, e.g. "bluefin-spot". */
|
|
3
|
+
export type ProtocolId = string;
|
|
4
|
+
export interface ChainRef {
|
|
5
|
+
type: ChainType;
|
|
6
|
+
/** EVM network id. Absent on chains that have none. */
|
|
7
|
+
chainId?: number;
|
|
8
|
+
/** Chain identifier, e.g. "sui-mainnet". */
|
|
9
|
+
identifier?: string;
|
|
10
|
+
}
|
|
11
|
+
/** An Ember vault receipt coin, as listed by the vaults API. */
|
|
12
|
+
export interface ReceiptCoin {
|
|
13
|
+
/** Sui coin type, or EVM token address. */
|
|
14
|
+
coin: string;
|
|
15
|
+
chain: ChainRef;
|
|
16
|
+
decimals: number;
|
|
17
|
+
symbol?: string;
|
|
18
|
+
/** Display name of the vault this coin belongs to. */
|
|
19
|
+
vault?: string;
|
|
20
|
+
/** RPC endpoint for this coin's chain, when one is published. */
|
|
21
|
+
rpcUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
export type PointerOrigin = "Registry" | "Manual";
|
|
24
|
+
/** Somewhere a receipt coin is listed. Carries no amount. */
|
|
25
|
+
export interface PlacementRef {
|
|
26
|
+
/**
|
|
27
|
+
* Stable identity for this placement — the right thing to key a list on.
|
|
28
|
+
* A protocol can list the same coin at several venues (the same pair at two
|
|
29
|
+
* fee tiers, the two tranches of one market), so protocol and label
|
|
30
|
+
* together do not identify a row.
|
|
31
|
+
*/
|
|
32
|
+
id?: string;
|
|
33
|
+
protocol: ProtocolId;
|
|
34
|
+
chain: ChainRef;
|
|
35
|
+
/** Pool, market or vault address. */
|
|
36
|
+
venue: string;
|
|
37
|
+
coin: string;
|
|
38
|
+
/** Decimals of `coin`. */
|
|
39
|
+
decimals: number;
|
|
40
|
+
/**
|
|
41
|
+
* The protocol's display name, e.g. "Current Finance". Absent when the
|
|
42
|
+
* vaults API has no catalogue entry — render `protocol` then.
|
|
43
|
+
*/
|
|
44
|
+
protocolName?: string;
|
|
45
|
+
/** Icon for the protocol, when one is catalogued. */
|
|
46
|
+
protocolLogoUrl?: string;
|
|
47
|
+
source: PointerOrigin;
|
|
48
|
+
/** Human-readable venue name, for display only. */
|
|
49
|
+
label?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The token an account holds for its position here, when that is not the
|
|
52
|
+
* venue itself. Defaults to `venue`.
|
|
53
|
+
*/
|
|
54
|
+
shareToken?: string;
|
|
55
|
+
/** Deep link to the venue, for the caller to render. */
|
|
56
|
+
url?: string;
|
|
57
|
+
/**
|
|
58
|
+
* RPC endpoint for this chain, when one is published. Absent means fall
|
|
59
|
+
* back to whatever the reader was configured with.
|
|
60
|
+
*/
|
|
61
|
+
rpcUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
/** A resolved pointer: what one account holds there. */
|
|
64
|
+
export interface Placement extends PlacementRef {
|
|
65
|
+
/** The account's own position, in base units of `coin`. */
|
|
66
|
+
amount: string;
|
|
67
|
+
readAt: string;
|
|
68
|
+
}
|
|
69
|
+
export interface PlacementFailure {
|
|
70
|
+
protocol: ProtocolId;
|
|
71
|
+
stage: "discover" | "read";
|
|
72
|
+
coin?: string;
|
|
73
|
+
reason: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One vault's shares for one account.
|
|
77
|
+
*
|
|
78
|
+
* A non-empty `failures` means `placements` is incomplete — a caller showing a
|
|
79
|
+
* total should say so rather than quietly under-report.
|
|
80
|
+
*/
|
|
81
|
+
export interface AccountShares {
|
|
82
|
+
vaultId: string;
|
|
83
|
+
coin: ReceiptCoin;
|
|
84
|
+
/** Held in the account's own wallet. Base units of `coin`. */
|
|
85
|
+
available: string;
|
|
86
|
+
/** Wrapped into a cover vault. Absent when the vault is not covered. */
|
|
87
|
+
covered?: string;
|
|
88
|
+
placements: Placement[];
|
|
89
|
+
failures: PlacementFailure[];
|
|
90
|
+
unresolved?: true;
|
|
91
|
+
}
|
|
92
|
+
export interface SuiObjectContents {
|
|
93
|
+
address: string;
|
|
94
|
+
type: string;
|
|
95
|
+
json: Record<string, unknown>;
|
|
96
|
+
}
|
|
97
|
+
export interface SuiDynamicField {
|
|
98
|
+
name: unknown;
|
|
99
|
+
valueType: string;
|
|
100
|
+
value: unknown;
|
|
101
|
+
}
|
|
102
|
+
/** Narrow read port over Sui, so a caller can supply its own transport. */
|
|
103
|
+
export interface SuiReader {
|
|
104
|
+
objects(ids: string[]): Promise<Record<string, SuiObjectContents>>;
|
|
105
|
+
dynamicFields(parentId: string, limit?: number): Promise<SuiDynamicField[]>;
|
|
106
|
+
/**
|
|
107
|
+
* One dynamic field addressed by its BCS-encoded key. Resolves to undefined
|
|
108
|
+
* when the key is absent.
|
|
109
|
+
*/
|
|
110
|
+
dynamicField(parentId: string, key: {
|
|
111
|
+
type: string;
|
|
112
|
+
bcs: string;
|
|
113
|
+
}): Promise<unknown | undefined>;
|
|
114
|
+
/** Total balance an address holds of one coin type, in base units. */
|
|
115
|
+
balance(owner: string, coinType: string): Promise<string>;
|
|
116
|
+
/** Objects of one type owned by an address. */
|
|
117
|
+
ownedObjects(owner: string, type: string): Promise<SuiObjectContents[]>;
|
|
118
|
+
}
|
|
119
|
+
/** Narrow read port over EVM: one `eth_call` is all these reads need. */
|
|
120
|
+
export interface EvmReader {
|
|
121
|
+
/** `endpoint`, when given, overrides whatever the reader is configured with. */
|
|
122
|
+
call(chainId: number, to: string, data: string, endpoint?: string): Promise<string>;
|
|
123
|
+
}
|
|
124
|
+
/** Where pointers come from. */
|
|
125
|
+
export interface PointerSource {
|
|
126
|
+
placements(protocol: ProtocolId, coin: ReceiptCoin): Promise<PlacementRef[]>;
|
|
127
|
+
}
|
|
128
|
+
/** Resolves a vault to the receipt coins it issues, one per chain it runs on. */
|
|
129
|
+
export interface VaultCoinsResolver {
|
|
130
|
+
coinsForVault(vaultId: string): Promise<ReceiptCoin[]>;
|
|
131
|
+
coverVault?(coin: ReceiptCoin): Promise<string | undefined>;
|
|
132
|
+
}
|
|
133
|
+
export interface PlacementsContext {
|
|
134
|
+
sui?: SuiReader;
|
|
135
|
+
evm?: EvmReader;
|
|
136
|
+
pointers: PointerSource;
|
|
137
|
+
vaults?: VaultCoinsResolver;
|
|
138
|
+
}
|
|
139
|
+
/** The extension seam. One protocol, registered with `registerAdapter`. */
|
|
140
|
+
export interface ProtocolAdapter {
|
|
141
|
+
readonly id: ProtocolId;
|
|
142
|
+
readonly chain: ChainType;
|
|
143
|
+
/** Pointers only, carrying no amounts. */
|
|
144
|
+
discover(coin: ReceiptCoin, ctx: PlacementsContext): Promise<PlacementRef[]>;
|
|
145
|
+
/** What `account` holds at each venue, in base units of `ref.coin`. */
|
|
146
|
+
read(refs: PlacementRef[], account: string, ctx: PlacementsContext): Promise<Placement[]>;
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PlatformEnv } from "../common/constants.js";
|
|
2
|
+
import { VaultCoinsResolver } from "./types.js";
|
|
3
|
+
export interface HttpVaultCoinsOptions {
|
|
4
|
+
/** Vaults API environment. Defaults to prod. */
|
|
5
|
+
env?: PlatformEnv;
|
|
6
|
+
/** Overrides `env` entirely. For a local deployment, or a proxy. */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a vault to the receipt coins it issues, from the vaults API.
|
|
11
|
+
*
|
|
12
|
+
* A vault runs on one or more chains and issues a distinct receipt coin on
|
|
13
|
+
* each, so this returns one entry per chain rather than assuming a single coin.
|
|
14
|
+
*/
|
|
15
|
+
export declare function httpVaultCoinsResolver(options?: HttpVaultCoinsOptions): VaultCoinsResolver;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { environmentConfig } from "../common/constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a vault to the receipt coins it issues, from the vaults API.
|
|
4
|
+
*
|
|
5
|
+
* A vault runs on one or more chains and issues a distinct receipt coin on
|
|
6
|
+
* each, so this returns one entry per chain rather than assuming a single coin.
|
|
7
|
+
*/
|
|
8
|
+
export function httpVaultCoinsResolver(options = {}) {
|
|
9
|
+
const baseUrl = (options.baseUrl ?? environmentConfig[options.env ?? "prod"].apiHost).replace(/\/$/, "");
|
|
10
|
+
const get = async (path) => {
|
|
11
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
12
|
+
headers: { Accept: "application/json" }
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
throw new Error(`vaults api ${response.status} for ${path}`);
|
|
16
|
+
return (await response.json());
|
|
17
|
+
};
|
|
18
|
+
// The chain catalogue is what carries the EVM network id. Without it an EVM
|
|
19
|
+
// coin has no chainId, and every read for it fails rather than returning a
|
|
20
|
+
// balance — so this is fetched once and shared, not guessed per coin.
|
|
21
|
+
let chains;
|
|
22
|
+
const chainRefs = () => {
|
|
23
|
+
if (chains)
|
|
24
|
+
return chains;
|
|
25
|
+
chains = get("/api/v2/vaults/supported-chains").then(supported => {
|
|
26
|
+
const byIdentifier = new Map();
|
|
27
|
+
for (const chain of supported) {
|
|
28
|
+
if (!chain.identifier)
|
|
29
|
+
continue;
|
|
30
|
+
byIdentifier.set(chain.identifier.toLowerCase(), {
|
|
31
|
+
type: chain.type?.toUpperCase() === "SUI" ? "sui" : "evm",
|
|
32
|
+
chainId: chain.networkId,
|
|
33
|
+
identifier: chain.identifier,
|
|
34
|
+
rpcUrl: chain.publicRpcUrl
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return byIdentifier;
|
|
38
|
+
});
|
|
39
|
+
// Cache the value, not the rejection: one transient 502 here would
|
|
40
|
+
// otherwise fail every later call on this resolver for good.
|
|
41
|
+
chains.catch(() => {
|
|
42
|
+
chains = undefined;
|
|
43
|
+
});
|
|
44
|
+
return chains;
|
|
45
|
+
};
|
|
46
|
+
/** Falls back to the identifier's shape when the catalogue has no entry. */
|
|
47
|
+
const chainRefFor = (identifier, catalogue) => catalogue.get(identifier.toLowerCase()) ?? {
|
|
48
|
+
type: identifier.toLowerCase().startsWith("sui") ? "sui" : "evm",
|
|
49
|
+
identifier
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
async coinsForVault(vaultId) {
|
|
53
|
+
const [vaults, catalogue] = await Promise.all([
|
|
54
|
+
get(`/api/v2/vaults?vaultId=${encodeURIComponent(vaultId)}`),
|
|
55
|
+
chainRefs()
|
|
56
|
+
]);
|
|
57
|
+
const list = Array.isArray(vaults) ? vaults : [vaults];
|
|
58
|
+
const vault = list.find(v => v.id === vaultId) ?? list[0];
|
|
59
|
+
if (!vault)
|
|
60
|
+
throw new Error(`vault ${vaultId} not found`);
|
|
61
|
+
// An ATOMIC vault has no receipt token, but the API still fills
|
|
62
|
+
// receiptCoin with a placeholder so the field never breaks older
|
|
63
|
+
// consumers. Reading it would hand back a coin that does not exist.
|
|
64
|
+
if (vault.vaultType === "ATOMIC") {
|
|
65
|
+
throw new Error(`vault ${vaultId} is atomic and issues no receipt coin`);
|
|
66
|
+
}
|
|
67
|
+
const coins = [];
|
|
68
|
+
for (const [identifier, details] of Object.entries(vault.detailsByChain ?? {})) {
|
|
69
|
+
const receipt = details.receiptCoin;
|
|
70
|
+
if (!receipt?.address)
|
|
71
|
+
continue;
|
|
72
|
+
if (typeof receipt.decimals !== "number") {
|
|
73
|
+
throw new Error(`${receipt.address} has no decimals`);
|
|
74
|
+
}
|
|
75
|
+
const { rpcUrl, ...chain } = chainRefFor(details.chainIdentifier ?? identifier, catalogue);
|
|
76
|
+
coins.push({
|
|
77
|
+
coin: receipt.address,
|
|
78
|
+
chain,
|
|
79
|
+
decimals: receipt.decimals,
|
|
80
|
+
// The wallet balance is not a placement, so it has no
|
|
81
|
+
// pointer to borrow an endpoint from. Carry the published
|
|
82
|
+
// one here or the caller must configure every chain itself.
|
|
83
|
+
rpcUrl,
|
|
84
|
+
symbol: receipt.symbol,
|
|
85
|
+
// longName is empty on every vault the API returns
|
|
86
|
+
// today, and "" is not nullish — ?? would keep it and
|
|
87
|
+
// hand the caller a blank name to render.
|
|
88
|
+
vault: vault.longName || vault.name
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (coins.length === 0) {
|
|
92
|
+
throw new Error(`vault ${vaultId} issues no receipt coin`);
|
|
93
|
+
}
|
|
94
|
+
return coins;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ember-finance/sdk",
|
|
3
3
|
"description": "Ember Protocol SDK",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.13.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
"import": "./dist/src/common/index.js",
|
|
45
45
|
"default": "./dist/src/common/index.js"
|
|
46
46
|
},
|
|
47
|
+
"./placements": {
|
|
48
|
+
"types": "./dist/src/placements/index.d.ts",
|
|
49
|
+
"import": "./dist/src/placements/index.js",
|
|
50
|
+
"default": "./dist/src/placements/index.js"
|
|
51
|
+
},
|
|
47
52
|
"./abis": {
|
|
48
53
|
"types": "./dist/src/abis/index.d.ts",
|
|
49
54
|
"import": "./dist/src/abis/index.js",
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"abi": [
|
|
3
|
-
{
|
|
4
|
-
"type": "function",
|
|
5
|
-
"name": "getFee",
|
|
6
|
-
"stateMutability": "view",
|
|
7
|
-
"inputs": [
|
|
8
|
-
{ "name": "destChainSelector", "type": "uint64" },
|
|
9
|
-
{
|
|
10
|
-
"name": "message",
|
|
11
|
-
"type": "tuple",
|
|
12
|
-
"components": [
|
|
13
|
-
{ "name": "receiver", "type": "bytes" },
|
|
14
|
-
{ "name": "data", "type": "bytes" },
|
|
15
|
-
{
|
|
16
|
-
"name": "tokenAmounts",
|
|
17
|
-
"type": "tuple[]",
|
|
18
|
-
"components": [
|
|
19
|
-
{ "name": "token", "type": "address" },
|
|
20
|
-
{ "name": "amount", "type": "uint256" }
|
|
21
|
-
]
|
|
22
|
-
},
|
|
23
|
-
{ "name": "feeToken", "type": "address" },
|
|
24
|
-
{ "name": "extraArgs", "type": "bytes" }
|
|
25
|
-
]
|
|
26
|
-
}
|
|
27
|
-
],
|
|
28
|
-
"outputs": [{ "name": "fee", "type": "uint256" }]
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"type": "function",
|
|
32
|
-
"name": "ccipSend",
|
|
33
|
-
"stateMutability": "payable",
|
|
34
|
-
"inputs": [
|
|
35
|
-
{ "name": "destinationChainSelector", "type": "uint64" },
|
|
36
|
-
{
|
|
37
|
-
"name": "message",
|
|
38
|
-
"type": "tuple",
|
|
39
|
-
"components": [
|
|
40
|
-
{ "name": "receiver", "type": "bytes" },
|
|
41
|
-
{ "name": "data", "type": "bytes" },
|
|
42
|
-
{
|
|
43
|
-
"name": "tokenAmounts",
|
|
44
|
-
"type": "tuple[]",
|
|
45
|
-
"components": [
|
|
46
|
-
{ "name": "token", "type": "address" },
|
|
47
|
-
{ "name": "amount", "type": "uint256" }
|
|
48
|
-
]
|
|
49
|
-
},
|
|
50
|
-
{ "name": "feeToken", "type": "address" },
|
|
51
|
-
{ "name": "extraArgs", "type": "bytes" }
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
],
|
|
55
|
-
"outputs": [{ "name": "messageId", "type": "bytes32" }]
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|