@ember-finance/sdk 2.11.0-pre.5 → 2.11.0-pre.7

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,10 @@ 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;
14
17
  multicall?: boolean;
15
18
  }
@@ -24,4 +27,3 @@ export declare function decodeAggregate3(data: string, count: number): {
24
27
  data: string;
25
28
  }[];
26
29
  export declare function jsonRpcEvmReader(endpoints?: Record<number, string>, options?: JsonRpcEvmReaderOptions): EvmReader;
27
- export {};
@@ -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) {
@@ -19,6 +19,14 @@ export function decodeUint256(hex) {
19
19
  export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
20
20
  const AGGREGATE3_SELECTOR = "0x82ad56cb";
21
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
+ }
22
30
  function stripHex(value) {
23
31
  return value.trim().replace(/^0x/i, "");
24
32
  }
@@ -90,27 +98,47 @@ function isRateLimited(entry) {
90
98
  function toResult(entry) {
91
99
  if (!entry)
92
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
+ }
93
107
  if (entry.error)
94
108
  return { ok: false, error: new Error(`evm rpc: ${entry.error.message}`) };
95
109
  return { ok: true, value: entry.result ?? "0x" };
96
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
+ }
97
121
  export function jsonRpcEvmReader(endpoints = {}, options = {}) {
98
122
  const fetchImpl = options.fetch ??
99
123
  ((input, init) => globalThis.fetch(input, init));
100
124
  const onRequest = options.onRequest;
101
- const useMulticall = options.multicall ?? true;
125
+ const useMulticall = options.multicall ?? false;
102
126
  const batchers = new Map();
103
127
  const modes = new Map();
128
+ const probes = new Map();
104
129
  const post = async (endpoint, body, items, attempt) => {
105
130
  let response;
106
131
  try {
107
132
  response = await fetchImpl(endpoint, {
108
133
  method: "POST",
109
134
  headers: { "Content-Type": "application/json" },
110
- body: JSON.stringify(body)
135
+ body: JSON.stringify(body),
136
+ signal: options.signal
111
137
  });
112
138
  }
113
139
  catch (error) {
140
+ if (isAbortError(error))
141
+ throw error;
114
142
  onRequest?.({ items, attempt, status: "network-error" });
115
143
  throw new TransientError(`evm rpc: ${error instanceof Error ? error.message : String(error)}`);
116
144
  }
@@ -118,39 +146,58 @@ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
118
146
  if (isTransientStatus(response.status)) {
119
147
  throw new TransientError(`evm rpc ${response.status}`, retryAfterMs(response.headers?.get("retry-after")));
120
148
  }
149
+ if (!response.ok)
150
+ throw new HttpError(response.status);
121
151
  return response;
122
152
  };
123
153
  const rpc = async (endpoint, method, params, items, attempt) => {
124
154
  const response = await post(endpoint, jsonRpcRequest(method, params, 1), items, attempt);
125
- if (!response.ok)
126
- throw new Error(`evm rpc ${response.status}`);
127
155
  const parsed = (await response.json());
128
156
  const entry = Array.isArray(parsed) ? parsed[0] : parsed;
129
157
  if (isRateLimited(entry))
130
158
  throw new TransientError(`evm rpc: ${entry?.error?.message}`);
131
159
  return entry ?? {};
132
160
  };
133
- const detectMode = async (endpoint, attempt) => {
161
+ const detectMode = (endpoint, attempt) => {
134
162
  const known = modes.get(endpoint);
135
163
  if (known)
136
- return known;
137
- let mode = "batch";
138
- if (useMulticall) {
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";
139
174
  try {
140
175
  const code = await rpc(endpoint, "eth_getCode", [MULTICALL3_ADDRESS, "latest"], 1, attempt);
141
176
  if (!code.error && stripHex(code.result ?? "").length > 2)
142
177
  mode = "multicall";
143
178
  }
144
179
  catch (error) {
145
- if (error instanceof TransientError)
180
+ if (error instanceof TransientError || isAbortError(error))
146
181
  throw error;
147
182
  }
148
- }
149
- modes.set(endpoint, mode);
150
- return mode;
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;
151
190
  };
152
191
  const sendMulticall = async (endpoint, calls, attempt) => {
153
- const entry = await rpc(endpoint, "eth_call", [{ to: MULTICALL3_ADDRESS, data: encodeAggregate3(calls) }, "latest"], calls.length, 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
+ }
154
201
  if (entry.error || !entry.result)
155
202
  return undefined;
156
203
  let decoded;
@@ -161,37 +208,37 @@ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
161
208
  return undefined;
162
209
  }
163
210
  return decoded.map(result => result.success
164
- ? { ok: true, value: result.data === "" ? "0x" : result.data }
211
+ ? { ok: true, value: result.data }
165
212
  : { ok: false, error: new Error("evm rpc: execution reverted") });
166
213
  };
167
214
  const sendBatch = async (endpoint, calls, attempt) => {
168
- const response = await post(endpoint, calls.map((call, id) => callRequest(call, id)), calls.length, attempt);
169
- if (!response.ok) {
170
- modes.set(endpoint, "single");
171
- throw new ChunkTooLargeError(`evm rpc ${response.status}`);
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);
223
+ }
224
+ throw error;
172
225
  }
173
226
  const parsed = (await response.json());
174
227
  if (!Array.isArray(parsed)) {
175
228
  modes.set(endpoint, "single");
176
229
  throw new ChunkTooLargeError("evm rpc: batch unsupported");
177
230
  }
178
- const limited = parsed.find(isRateLimited);
179
- if (limited)
180
- throw new TransientError(`evm rpc: ${limited.error?.message}`);
181
231
  const byId = new Map(parsed.map(entry => [Number(entry.id), entry]));
182
232
  return calls.map((_, id) => toResult(byId.get(id)));
183
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
+ };
184
239
  const send = async (endpoint, calls, attempt) => {
185
- if (calls.length === 1) {
186
- const response = await post(endpoint, callRequest(calls[0], 1), 1, attempt);
187
- if (!response.ok)
188
- throw new Error(`evm rpc ${response.status}`);
189
- const parsed = (await response.json());
190
- const entry = Array.isArray(parsed) ? parsed[0] : parsed;
191
- if (isRateLimited(entry))
192
- throw new TransientError(`evm rpc: ${entry?.error?.message}`);
193
- return [toResult(entry)];
194
- }
240
+ if (calls.length === 1)
241
+ return sendSingle(endpoint, calls[0], attempt);
195
242
  const mode = await detectMode(endpoint, attempt);
196
243
  if (mode === "single")
197
244
  throw new ChunkTooLargeError("evm rpc: batch unsupported");
@@ -214,7 +261,9 @@ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
214
261
  concurrency: options.concurrency,
215
262
  retries: options.retries,
216
263
  baseDelayMs: options.baseDelayMs,
217
- sleep: options.sleep
264
+ maxDelayMs: options.maxDelayMs,
265
+ sleep: options.sleep,
266
+ signal: options.signal
218
267
  });
219
268
  batchers.set(endpoint, batcher);
220
269
  return batcher;
@@ -224,6 +273,7 @@ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
224
273
  const endpoint = override ?? endpoints[chainId];
225
274
  if (!endpoint)
226
275
  throw new Error(`no evm endpoint for chain ${chainId}`);
276
+ validateCall(to, data);
227
277
  return batcherFor(endpoint).enqueue({ to, data });
228
278
  }
229
279
  };
@@ -1,5 +1,5 @@
1
1
  export * from "./types.js";
2
- export * from "./batch.js";
2
+ export { AbortedError, isAbortError } from "./batch.js";
3
3
  export * from "./registry.js";
4
4
  export * from "./client.js";
5
5
  export * from "./pointers.js";
@@ -1,5 +1,5 @@
1
1
  export * from "./types.js";
2
- export * from "./batch.js";
2
+ export { AbortedError, isAbortError } from "./batch.js";
3
3
  export * from "./registry.js";
4
4
  export * from "./client.js";
5
5
  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.5",
4
+ "version": "2.11.0-pre.7",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "main": "./dist/index.js",