@ember-finance/sdk 2.11.0-pre.4 → 2.11.0-pre.6

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.
@@ -17,8 +17,12 @@ export declare class TransientError extends Error {
17
17
  export declare class ChunkTooLargeError extends Error {
18
18
  constructor(message: string);
19
19
  }
20
+ export declare class AbortedError extends Error {
21
+ constructor(message?: string);
22
+ }
20
23
  export declare function retryAfterMs(header: string | null | undefined): number | undefined;
21
24
  export declare function isTransientStatus(status: number): boolean;
25
+ export declare function isAbortError(error: unknown): boolean;
22
26
  export interface BatcherOptions<Item, Result> {
23
27
  send: (items: Item[], attempt: number) => Promise<BatchResult<Result>[]>;
24
28
  key: (item: Item) => string;
@@ -29,8 +33,10 @@ export interface BatcherOptions<Item, Result> {
29
33
  concurrency?: number;
30
34
  retries?: number;
31
35
  baseDelayMs?: number;
36
+ maxDelayMs?: number;
32
37
  sleep?: (ms: number) => Promise<void>;
33
38
  schedule?: (flush: () => void) => void;
39
+ signal?: AbortSignal;
34
40
  }
35
41
  export declare class Batcher<Item, Result> {
36
42
  private readonly send;
@@ -42,8 +48,10 @@ export declare class Batcher<Item, Result> {
42
48
  private readonly concurrency;
43
49
  private readonly retries;
44
50
  private readonly baseDelayMs;
51
+ private readonly maxDelayMs;
45
52
  private readonly sleep;
46
53
  private readonly schedule;
54
+ private readonly signal?;
47
55
  private readonly queue;
48
56
  private readonly inFlight;
49
57
  private readonly waiting;
@@ -55,6 +63,7 @@ export declare class Batcher<Item, Result> {
55
63
  private flush;
56
64
  private chunk;
57
65
  private run;
66
+ private retryLater;
58
67
  private backoff;
59
68
  private acquire;
60
69
  private release;
@@ -11,6 +11,12 @@ export class ChunkTooLargeError extends Error {
11
11
  this.name = "ChunkTooLargeError";
12
12
  }
13
13
  }
14
+ export class AbortedError extends Error {
15
+ constructor(message = "aborted") {
16
+ super(message);
17
+ this.name = "AbortedError";
18
+ }
19
+ }
14
20
  export function retryAfterMs(header) {
15
21
  if (!header)
16
22
  return undefined;
@@ -23,6 +29,10 @@ export function retryAfterMs(header) {
23
29
  export function isTransientStatus(status) {
24
30
  return status === 429 || status >= 500;
25
31
  }
32
+ export function isAbortError(error) {
33
+ return (error instanceof AbortedError ||
34
+ (error instanceof Error && error.name === "AbortError"));
35
+ }
26
36
  const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
27
37
  const defaultSchedule = (flush) => {
28
38
  setTimeout(flush, 0);
@@ -43,10 +53,14 @@ export class Batcher {
43
53
  this.concurrency = options.concurrency ?? 3;
44
54
  this.retries = options.retries ?? 3;
45
55
  this.baseDelayMs = options.baseDelayMs ?? 300;
56
+ this.maxDelayMs = options.maxDelayMs ?? 10000;
46
57
  this.sleep = options.sleep ?? defaultSleep;
47
58
  this.schedule = options.schedule ?? defaultSchedule;
59
+ this.signal = options.signal;
48
60
  }
49
61
  enqueue(item) {
62
+ if (this.signal?.aborted)
63
+ return Promise.reject(new AbortedError());
50
64
  const key = this.key(item);
51
65
  const shared = this.inFlight.get(key);
52
66
  if (shared)
@@ -103,6 +117,11 @@ export class Batcher {
103
117
  return chunks;
104
118
  }
105
119
  async run(chunk, attempt) {
120
+ if (this.signal?.aborted) {
121
+ for (const entry of chunk)
122
+ entry.reject(new AbortedError());
123
+ return;
124
+ }
106
125
  await this.acquire();
107
126
  let results;
108
127
  try {
@@ -117,8 +136,7 @@ export class Batcher {
117
136
  return;
118
137
  }
119
138
  if (error instanceof TransientError && attempt <= this.retries) {
120
- await this.sleep(error.retryAfterMs ?? this.backoff(attempt));
121
- void this.run(chunk, attempt + 1);
139
+ await this.retryLater(chunk, attempt, error.retryAfterMs);
122
140
  return;
123
141
  }
124
142
  const failure = error instanceof Error ? error : new Error(String(error));
@@ -127,6 +145,8 @@ export class Batcher {
127
145
  return;
128
146
  }
129
147
  this.release();
148
+ const throttled = [];
149
+ let retryAfter;
130
150
  chunk.forEach((entry, index) => {
131
151
  const result = results[index];
132
152
  if (result === undefined) {
@@ -137,8 +157,19 @@ export class Batcher {
137
157
  entry.resolve(result.value);
138
158
  return;
139
159
  }
160
+ if (result.error instanceof TransientError && attempt <= this.retries) {
161
+ throttled.push(entry);
162
+ retryAfter = retryAfter ?? result.error.retryAfterMs;
163
+ return;
164
+ }
140
165
  entry.reject(result.error);
141
166
  });
167
+ if (throttled.length > 0)
168
+ await this.retryLater(throttled, attempt, retryAfter);
169
+ }
170
+ async retryLater(chunk, attempt, retryAfter) {
171
+ await this.sleep(Math.min(retryAfter ?? this.backoff(attempt), this.maxDelayMs));
172
+ void this.run(chunk, attempt + 1);
142
173
  }
143
174
  backoff(attempt) {
144
175
  const base = this.baseDelayMs * 2 ** (attempt - 1);
@@ -31,7 +31,8 @@ function unresolvedVault(vaultId, reason) {
31
31
  available: "0",
32
32
  covered: undefined,
33
33
  placements: [],
34
- failures: [{ protocol: "vault", stage: "discover", reason }]
34
+ failures: [{ protocol: "vault", stage: "discover", reason }],
35
+ unresolved: true
35
36
  };
36
37
  }
37
38
  export class PlacementsClient {
@@ -51,7 +52,15 @@ export class PlacementsClient {
51
52
  const vaults = this.ctx.vaults;
52
53
  const resolved = await Promise.all(vaultIds.map(async (vaultId) => {
53
54
  try {
54
- return { vaultId, coins: await vaults.coinsForVault(vaultId) };
55
+ const coins = await vaults.coinsForVault(vaultId);
56
+ if (!Array.isArray(coins) || coins.length === 0) {
57
+ return {
58
+ vaultId,
59
+ coins: undefined,
60
+ reason: "resolver returned no coins"
61
+ };
62
+ }
63
+ return { vaultId, coins };
55
64
  }
56
65
  catch (error) {
57
66
  return { vaultId, coins: undefined, reason: reasonOf(error) };
@@ -1,5 +1,6 @@
1
1
  import { EvmReader } from "./types.js";
2
2
  import { TransportRequest } from "./batch.js";
3
+ export type { TransportRequest } from "./batch.js";
3
4
  export declare function encodeBalanceOf(owner: string): string;
4
5
  export declare function asContractAddress(address: string): string;
5
6
  export declare function decodeUint256(hex: string): bigint;
@@ -8,8 +9,21 @@ export interface JsonRpcEvmReaderOptions {
8
9
  concurrency?: number;
9
10
  retries?: number;
10
11
  baseDelayMs?: number;
12
+ maxDelayMs?: number;
11
13
  sleep?: (ms: number) => Promise<void>;
12
14
  fetch?: typeof globalThis.fetch;
15
+ signal?: AbortSignal;
13
16
  onRequest?: (request: TransportRequest) => void;
17
+ multicall?: boolean;
14
18
  }
19
+ interface EvmCall {
20
+ to: string;
21
+ data: string;
22
+ }
23
+ export declare const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
24
+ export declare function encodeAggregate3(calls: EvmCall[]): string;
25
+ export declare function decodeAggregate3(data: string, count: number): {
26
+ success: boolean;
27
+ data: string;
28
+ }[];
15
29
  export declare function jsonRpcEvmReader(endpoints?: Record<number, string>, options?: JsonRpcEvmReaderOptions): EvmReader;
@@ -1,4 +1,4 @@
1
- import { Batcher, ChunkTooLargeError, TransientError, isTransientStatus, retryAfterMs } from "./batch.js";
1
+ import { Batcher, ChunkTooLargeError, TransientError, isAbortError, isTransientStatus, retryAfterMs } from "./batch.js";
2
2
  /** `balanceOf(address)` */
3
3
  const BALANCE_OF_SELECTOR = "0x70a08231";
4
4
  export function encodeBalanceOf(owner) {
@@ -16,72 +16,240 @@ export function decodeUint256(hex) {
16
16
  return 0n;
17
17
  return BigInt(hex);
18
18
  }
19
- function jsonRpcRequest(call, id) {
20
- return {
21
- jsonrpc: "2.0",
22
- id,
23
- method: "eth_call",
24
- params: [{ to: call.to, data: call.data }, "latest"]
19
+ export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
20
+ const AGGREGATE3_SELECTOR = "0x82ad56cb";
21
+ const RATE_LIMITED = /too many requests|rate ?limit/i;
22
+ const HEX = /^(0x)?[0-9a-fA-F]*$/;
23
+ class HttpError extends Error {
24
+ constructor(status) {
25
+ super(`evm rpc ${status}`);
26
+ this.name = "HttpError";
27
+ this.status = status;
28
+ }
29
+ }
30
+ function stripHex(value) {
31
+ return value.trim().replace(/^0x/i, "");
32
+ }
33
+ function word(value) {
34
+ return BigInt(value).toString(16).padStart(64, "0");
35
+ }
36
+ function addressWord(address) {
37
+ return stripHex(address).toLowerCase().padStart(64, "0");
38
+ }
39
+ function bytesWords(data) {
40
+ const raw = stripHex(data);
41
+ const length = raw.length / 2;
42
+ const padded = raw.padEnd(Math.ceil(length / 32) * 64, "0");
43
+ return word(length) + padded;
44
+ }
45
+ export function encodeAggregate3(calls) {
46
+ const tuples = calls.map(call => addressWord(call.to) + word(1) + word(0x60) + bytesWords(call.data));
47
+ let offset = calls.length * 32;
48
+ const offsets = tuples.map(tuple => {
49
+ const current = offset;
50
+ offset += tuple.length / 2;
51
+ return word(current);
52
+ });
53
+ return (AGGREGATE3_SELECTOR +
54
+ word(0x20) +
55
+ word(calls.length) +
56
+ offsets.join("") +
57
+ tuples.join(""));
58
+ }
59
+ export function decodeAggregate3(data, count) {
60
+ const raw = stripHex(data);
61
+ const read = (byteOffset) => {
62
+ const slice = raw.slice(byteOffset * 2, byteOffset * 2 + 64);
63
+ if (slice.length !== 64)
64
+ throw new Error("multicall: truncated response");
65
+ return Number(BigInt(`0x${slice}`));
25
66
  };
67
+ const arrayOffset = read(0);
68
+ const length = read(arrayOffset);
69
+ if (length !== count)
70
+ throw new Error("multicall: result count mismatch");
71
+ const base = arrayOffset + 32;
72
+ const results = [];
73
+ for (let index = 0; index < length; index += 1) {
74
+ const tupleOffset = base + read(base + index * 32);
75
+ const success = read(tupleOffset) === 1;
76
+ const bytesOffset = tupleOffset + read(tupleOffset + 32);
77
+ const bytesLength = read(bytesOffset);
78
+ const start = (bytesOffset + 32) * 2;
79
+ const body = raw.slice(start, start + bytesLength * 2);
80
+ if (body.length !== bytesLength * 2)
81
+ throw new Error("multicall: truncated response");
82
+ results.push({ success, data: `0x${body}` });
83
+ }
84
+ return results;
85
+ }
86
+ function jsonRpcRequest(method, params, id) {
87
+ return { jsonrpc: "2.0", id, method, params };
88
+ }
89
+ function callRequest(call, id) {
90
+ return jsonRpcRequest("eth_call", [{ to: call.to, data: call.data }, "latest"], id);
91
+ }
92
+ function isRateLimited(entry) {
93
+ const error = entry?.error;
94
+ if (!error)
95
+ return false;
96
+ return error.code === -32005 || RATE_LIMITED.test(error.message ?? "");
26
97
  }
27
98
  function toResult(entry) {
28
99
  if (!entry)
29
100
  return { ok: false, error: new Error("evm rpc: no result for call") };
101
+ if (isRateLimited(entry)) {
102
+ return {
103
+ ok: false,
104
+ error: new TransientError(`evm rpc: ${entry.error?.message}`)
105
+ };
106
+ }
30
107
  if (entry.error)
31
108
  return { ok: false, error: new Error(`evm rpc: ${entry.error.message}`) };
32
109
  return { ok: true, value: entry.result ?? "0x" };
33
110
  }
111
+ function validateCall(to, data) {
112
+ const address = stripHex(to);
113
+ if (!HEX.test(to) || address.length !== 40) {
114
+ throw new Error(`evm rpc: ${to} is not a contract address`);
115
+ }
116
+ const calldata = stripHex(data);
117
+ if (!HEX.test(data) || calldata.length % 2 !== 0) {
118
+ throw new Error(`evm rpc: calldata ${data} is not even-length hex`);
119
+ }
120
+ }
34
121
  export function jsonRpcEvmReader(endpoints = {}, options = {}) {
35
122
  const fetchImpl = options.fetch ??
36
123
  ((input, init) => globalThis.fetch(input, init));
37
124
  const onRequest = options.onRequest;
125
+ const useMulticall = options.multicall ?? false;
38
126
  const batchers = new Map();
39
- const singleOnly = new Set();
127
+ const modes = new Map();
128
+ const probes = new Map();
40
129
  const post = async (endpoint, body, items, attempt) => {
130
+ let response;
41
131
  try {
42
- const response = await fetchImpl(endpoint, {
132
+ response = await fetchImpl(endpoint, {
43
133
  method: "POST",
44
134
  headers: { "Content-Type": "application/json" },
45
- body: JSON.stringify(body)
135
+ body: JSON.stringify(body),
136
+ signal: options.signal
46
137
  });
47
- onRequest?.({ items, attempt, status: response.status });
48
- return response;
49
138
  }
50
139
  catch (error) {
140
+ if (isAbortError(error))
141
+ throw error;
51
142
  onRequest?.({ items, attempt, status: "network-error" });
52
143
  throw new TransientError(`evm rpc: ${error instanceof Error ? error.message : String(error)}`);
53
144
  }
54
- };
55
- const send = async (endpoint, calls, attempt) => {
56
- const batched = calls.length > 1;
57
- if (batched && singleOnly.has(endpoint)) {
58
- throw new ChunkTooLargeError("evm rpc: batch unsupported");
59
- }
60
- const body = batched
61
- ? calls.map((call, id) => jsonRpcRequest(call, id))
62
- : jsonRpcRequest(calls[0], 1);
63
- const response = await post(endpoint, body, calls.length, attempt);
145
+ onRequest?.({ items, attempt, status: response.status });
64
146
  if (isTransientStatus(response.status)) {
65
147
  throw new TransientError(`evm rpc ${response.status}`, retryAfterMs(response.headers?.get("retry-after")));
66
148
  }
67
- if (!response.ok) {
68
- if (batched) {
69
- singleOnly.add(endpoint);
70
- throw new ChunkTooLargeError(`evm rpc ${response.status}`);
149
+ if (!response.ok)
150
+ throw new HttpError(response.status);
151
+ return response;
152
+ };
153
+ const rpc = async (endpoint, method, params, items, attempt) => {
154
+ const response = await post(endpoint, jsonRpcRequest(method, params, 1), items, attempt);
155
+ const parsed = (await response.json());
156
+ const entry = Array.isArray(parsed) ? parsed[0] : parsed;
157
+ if (isRateLimited(entry))
158
+ throw new TransientError(`evm rpc: ${entry?.error?.message}`);
159
+ return entry ?? {};
160
+ };
161
+ const detectMode = (endpoint, attempt) => {
162
+ const known = modes.get(endpoint);
163
+ if (known)
164
+ return Promise.resolve(known);
165
+ if (!useMulticall) {
166
+ modes.set(endpoint, "batch");
167
+ return Promise.resolve("batch");
168
+ }
169
+ const pending = probes.get(endpoint);
170
+ if (pending)
171
+ return pending;
172
+ const probe = (async () => {
173
+ let mode = "batch";
174
+ try {
175
+ const code = await rpc(endpoint, "eth_getCode", [MULTICALL3_ADDRESS, "latest"], 1, attempt);
176
+ if (!code.error && stripHex(code.result ?? "").length > 2)
177
+ mode = "multicall";
178
+ }
179
+ catch (error) {
180
+ if (error instanceof TransientError || isAbortError(error))
181
+ throw error;
182
+ }
183
+ modes.set(endpoint, mode);
184
+ return mode;
185
+ })();
186
+ probes.set(endpoint, probe);
187
+ const clear = () => probes.delete(endpoint);
188
+ probe.then(clear, clear);
189
+ return probe;
190
+ };
191
+ const sendMulticall = async (endpoint, calls, attempt) => {
192
+ let entry;
193
+ try {
194
+ entry = await rpc(endpoint, "eth_call", [{ to: MULTICALL3_ADDRESS, data: encodeAggregate3(calls) }, "latest"], calls.length, attempt);
195
+ }
196
+ catch (error) {
197
+ if (error instanceof TransientError || isAbortError(error))
198
+ throw error;
199
+ return undefined;
200
+ }
201
+ if (entry.error || !entry.result)
202
+ return undefined;
203
+ let decoded;
204
+ try {
205
+ decoded = decodeAggregate3(entry.result, calls.length);
206
+ }
207
+ catch {
208
+ return undefined;
209
+ }
210
+ return decoded.map(result => result.success
211
+ ? { ok: true, value: result.data }
212
+ : { ok: false, error: new Error("evm rpc: execution reverted") });
213
+ };
214
+ const sendBatch = async (endpoint, calls, attempt) => {
215
+ let response;
216
+ try {
217
+ response = await post(endpoint, calls.map((call, id) => callRequest(call, id)), calls.length, attempt);
218
+ }
219
+ catch (error) {
220
+ if (error instanceof HttpError) {
221
+ modes.set(endpoint, "single");
222
+ throw new ChunkTooLargeError(error.message);
71
223
  }
72
- throw new Error(`evm rpc ${response.status}`);
224
+ throw error;
73
225
  }
74
226
  const parsed = (await response.json());
75
- if (!batched) {
76
- return [toResult(Array.isArray(parsed) ? parsed[0] : parsed)];
77
- }
78
227
  if (!Array.isArray(parsed)) {
79
- singleOnly.add(endpoint);
228
+ modes.set(endpoint, "single");
80
229
  throw new ChunkTooLargeError("evm rpc: batch unsupported");
81
230
  }
82
231
  const byId = new Map(parsed.map(entry => [Number(entry.id), entry]));
83
232
  return calls.map((_, id) => toResult(byId.get(id)));
84
233
  };
234
+ const sendSingle = async (endpoint, call, attempt) => {
235
+ const response = await post(endpoint, callRequest(call, 1), 1, attempt);
236
+ const parsed = (await response.json());
237
+ return [toResult(Array.isArray(parsed) ? parsed[0] : parsed)];
238
+ };
239
+ const send = async (endpoint, calls, attempt) => {
240
+ if (calls.length === 1)
241
+ return sendSingle(endpoint, calls[0], attempt);
242
+ const mode = await detectMode(endpoint, attempt);
243
+ if (mode === "single")
244
+ throw new ChunkTooLargeError("evm rpc: batch unsupported");
245
+ if (mode === "multicall") {
246
+ const aggregated = await sendMulticall(endpoint, calls, attempt);
247
+ if (aggregated)
248
+ return aggregated;
249
+ modes.set(endpoint, "batch");
250
+ }
251
+ return sendBatch(endpoint, calls, attempt);
252
+ };
85
253
  const batcherFor = (endpoint) => {
86
254
  const existing = batchers.get(endpoint);
87
255
  if (existing)
@@ -93,7 +261,9 @@ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
93
261
  concurrency: options.concurrency,
94
262
  retries: options.retries,
95
263
  baseDelayMs: options.baseDelayMs,
96
- sleep: options.sleep
264
+ maxDelayMs: options.maxDelayMs,
265
+ sleep: options.sleep,
266
+ signal: options.signal
97
267
  });
98
268
  batchers.set(endpoint, batcher);
99
269
  return batcher;
@@ -103,6 +273,7 @@ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
103
273
  const endpoint = override ?? endpoints[chainId];
104
274
  if (!endpoint)
105
275
  throw new Error(`no evm endpoint for chain ${chainId}`);
276
+ validateCall(to, data);
106
277
  return batcherFor(endpoint).enqueue({ to, data });
107
278
  }
108
279
  };
@@ -1,5 +1,4 @@
1
1
  export * from "./types.js";
2
- export * from "./batch.js";
3
2
  export * from "./registry.js";
4
3
  export * from "./client.js";
5
4
  export * from "./pointers.js";
@@ -1,5 +1,4 @@
1
1
  export * from "./types.js";
2
- export * from "./batch.js";
3
2
  export * from "./registry.js";
4
3
  export * from "./client.js";
5
4
  export * from "./pointers.js";
@@ -1,5 +1,6 @@
1
1
  import { SuiReader } from "./types.js";
2
2
  import { TransportRequest } from "./batch.js";
3
+ export type { TransportRequest } from "./batch.js";
3
4
  /**
4
5
  * Canonical form for a Sui coin type: `0x` stripped from the address segment,
5
6
  * lowercased and left-padded to 64 hex chars.
@@ -37,8 +38,10 @@ export interface GraphqlSuiReaderOptions {
37
38
  concurrency?: number;
38
39
  retries?: number;
39
40
  baseDelayMs?: number;
41
+ maxDelayMs?: number;
40
42
  sleep?: (ms: number) => Promise<void>;
41
43
  fetch?: typeof globalThis.fetch;
44
+ signal?: AbortSignal;
42
45
  onRequest?: (request: TransportRequest) => void;
43
46
  }
44
47
  export declare function graphqlSuiReader(options?: string | GraphqlSuiReaderOptions): SuiReader;
@@ -1,4 +1,4 @@
1
- import { Batcher, ChunkTooLargeError, TransientError, isTransientStatus, retryAfterMs } from "./batch.js";
1
+ import { Batcher, ChunkTooLargeError, TransientError, isAbortError, isTransientStatus, retryAfterMs } from "./batch.js";
2
2
  const GRAPHQL_PAGE_SIZE = 50;
3
3
  /**
4
4
  * Canonical form for a Sui coin type: `0x` stripped from the address segment,
@@ -90,14 +90,21 @@ const OBJECTS_ALIAS = "objects";
90
90
  const OBJECT_SELECTION = "{ address asMoveObject { contents { type { repr } json } } }";
91
91
  const OBJECT_KEY_WEIGHT = 0.25;
92
92
  const OBJECT_KEY_BYTES = 90;
93
+ const SUI_ADDRESS = /^(0x)?[0-9a-fA-F]{1,64}$/;
93
94
  function normalizeAddress(address) {
94
95
  return address.trim().toLowerCase().replace(/^0x/, "").padStart(64, "0");
95
96
  }
97
+ function isSuiAddress(value) {
98
+ return SUI_ADDRESS.test(value.trim());
99
+ }
100
+ function literal(value) {
101
+ return JSON.stringify(value);
102
+ }
96
103
  function buildDocument(chunk) {
97
104
  const roots = [];
98
105
  const ids = chunk.flatMap(item => (item.kind === "object" ? [item.id] : []));
99
106
  if (ids.length > 0) {
100
- const keys = ids.map(id => `{ address: "${id}" }`).join(", ");
107
+ const keys = ids.map(id => `{ address: ${literal(id)} }`).join(", ");
101
108
  roots.push(`${OBJECTS_ALIAS}: multiGetObjects(keys: [${keys}]) ${OBJECT_SELECTION}`);
102
109
  }
103
110
  chunk.forEach((item, index) => {
@@ -107,19 +114,25 @@ function buildDocument(chunk) {
107
114
  return `{ ${roots.join(" ")} }`;
108
115
  }
109
116
  function distribute(chunk, body) {
110
- const pathErrors = new Map();
117
+ const aliasErrors = new Map();
118
+ const objectErrors = new Map();
111
119
  const rootErrors = [];
112
120
  for (const error of body.errors ?? []) {
113
121
  const alias = error.path?.[0];
114
- if (typeof alias === "string") {
115
- pathErrors.set(alias, error.message);
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);
116
129
  }
117
130
  else {
118
- rootErrors.push(error.message);
131
+ aliasErrors.set(alias, error.message);
119
132
  }
120
133
  }
121
- if (rootErrors.length > 0) {
122
- const message = `sui graphql: ${rootErrors.join("; ")}`;
134
+ if (rootErrors.length > 0 || (body.data == null && (body.errors?.length ?? 0) > 0)) {
135
+ const message = `sui graphql: ${[...rootErrors, ...aliasErrors.values(), ...objectErrors.values()].join("; ")}`;
123
136
  if (chunk.length > 1)
124
137
  throw new ChunkTooLargeError(message);
125
138
  throw new Error(message);
@@ -136,15 +149,21 @@ function distribute(chunk, body) {
136
149
  json: contents.json
137
150
  });
138
151
  }
152
+ let objectPosition = -1;
139
153
  return chunk.map((item, index) => {
140
- const alias = item.kind === "object" ? OBJECTS_ALIAS : `r${index}`;
141
- const message = pathErrors.get(alias);
142
- if (message !== undefined) {
143
- return { ok: false, error: new Error(`sui graphql: ${message}`) };
144
- }
145
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
+ }
146
160
  return { ok: true, value: objectsByAddress.get(normalizeAddress(item.id)) };
147
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
+ }
148
167
  return { ok: true, value: data[alias] };
149
168
  });
150
169
  }
@@ -161,10 +180,13 @@ export function graphqlSuiReader(options = {}) {
161
180
  response = await fetchImpl(endpoint, {
162
181
  method: "POST",
163
182
  headers: { "Content-Type": "application/json" },
164
- body: JSON.stringify({ query: document })
183
+ body: JSON.stringify({ query: document }),
184
+ signal: config.signal
165
185
  });
166
186
  }
167
187
  catch (error) {
188
+ if (isAbortError(error))
189
+ throw error;
168
190
  onRequest?.({ items: chunk.length, attempt, status: "network-error" });
169
191
  throw new TransientError(`sui graphql: ${error instanceof Error ? error.message : String(error)}`);
170
192
  }
@@ -194,12 +216,20 @@ export function graphqlSuiReader(options = {}) {
194
216
  concurrency: config.concurrency,
195
217
  retries: config.retries,
196
218
  baseDelayMs: config.baseDelayMs,
197
- sleep: config.sleep
219
+ maxDelayMs: config.maxDelayMs,
220
+ sleep: config.sleep,
221
+ signal: config.signal
198
222
  });
199
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
+ };
200
228
  return {
201
229
  async objects(ids) {
202
230
  const unique = [...new Set(ids)];
231
+ for (const id of unique)
232
+ requireAddress(id, `object id ${id}`);
203
233
  const found = await Promise.all(unique.map(id => batcher.enqueue({ kind: "object", id })));
204
234
  const out = {};
205
235
  for (const object of found) {
@@ -209,15 +239,17 @@ export function graphqlSuiReader(options = {}) {
209
239
  return out;
210
240
  },
211
241
  async balance(owner, coinType) {
212
- const data = await field(`address(address: "${owner}") { balance(coinType: "${coinType}") { totalBalance } }`);
242
+ requireAddress(owner, `owner ${owner}`);
243
+ const data = await field(`address(address: ${literal(owner)}) { balance(coinType: ${literal(coinType)}) { totalBalance } }`);
213
244
  return data?.balance?.totalBalance ?? "0";
214
245
  },
215
246
  async ownedObjects(owner, type) {
247
+ requireAddress(owner, `owner ${owner}`);
216
248
  const owned = [];
217
249
  let cursor = null;
218
250
  for (;;) {
219
- const after = cursor ? `, after: "${cursor}"` : "";
220
- const page = await field(`address(address: "${owner}") { objects(first: ${GRAPHQL_PAGE_SIZE}${after}, filter: { type: "${type}" }) { pageInfo { hasNextPage endCursor } nodes { address contents { type { repr } json } } } }`);
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 } } } }`);
221
253
  const container = page?.objects;
222
254
  if (!container)
223
255
  break;
@@ -237,15 +269,17 @@ export function graphqlSuiReader(options = {}) {
237
269
  return owned;
238
270
  },
239
271
  async dynamicField(parentId, key) {
240
- const data = await field(`address(address: "${parentId}") { dynamicField(name: { type: "${key.type}", bcs: "${key.bcs}" }) { value { ... on MoveValue { json } } } }`);
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 } } } }`);
241
274
  return data?.dynamicField?.value?.json;
242
275
  },
243
276
  async dynamicFields(parentId, limit) {
277
+ requireAddress(parentId, `parent ${parentId}`);
244
278
  const fields = [];
245
279
  let cursor = null;
246
280
  for (;;) {
247
- const after = cursor ? `, after: "${cursor}"` : "";
248
- const page = await field(`address(address: "${parentId}") { dynamicFields(first: ${GRAPHQL_PAGE_SIZE}${after}) { pageInfo { hasNextPage endCursor } nodes { name { json } value { ... on MoveValue { type { repr } json } } } } }`);
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 } } } } }`);
249
283
  const container = page?.dynamicFields;
250
284
  if (!container)
251
285
  break;
@@ -87,6 +87,7 @@ export interface AccountShares {
87
87
  covered?: string;
88
88
  placements: Placement[];
89
89
  failures: PlacementFailure[];
90
+ unresolved?: true;
90
91
  }
91
92
  export interface SuiObjectContents {
92
93
  address: string;
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.11.0-pre.4",
4
+ "version": "2.11.0-pre.6",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "main": "./dist/index.js",