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

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.
@@ -15,12 +15,30 @@
15
15
  * @interface CoinPlacement
16
16
  */
17
17
  export interface CoinPlacement {
18
+ /**
19
+ * Stable identity for this placement, for a client to key a list on. A protocol can list the same coin at several venues — the same pair at two fee tiers, or the two tranches of one market — so protocol and label together do not identify a row. Survives a re-sync.
20
+ * @type {string}
21
+ * @memberof CoinPlacement
22
+ */
23
+ id: string;
18
24
  /**
19
25
  * Adapter id, e.g. \"bluefin-spot\"
20
26
  * @type {string}
21
27
  * @memberof CoinPlacement
22
28
  */
23
29
  protocol: string;
30
+ /**
31
+ * The protocol\'s display name, e.g. \"Current Finance\". Absent when the protocol is not in our catalogue yet — render `protocol` then.
32
+ * @type {string}
33
+ * @memberof CoinPlacement
34
+ */
35
+ protocolName?: string;
36
+ /**
37
+ * Icon for the protocol. Absent when the protocol is not in our catalogue yet.
38
+ * @type {string}
39
+ * @memberof CoinPlacement
40
+ */
41
+ protocolLogoUrl?: string;
24
42
  /**
25
43
  * Chain identifier as catalogued, e.g. \"sui-mainnet\".
26
44
  * @type {string}
@@ -0,0 +1,61 @@
1
+ export interface TransportRequest {
2
+ items: number;
3
+ attempt: number;
4
+ status: number | "network-error";
5
+ }
6
+ export type BatchResult<T> = {
7
+ ok: true;
8
+ value: T;
9
+ } | {
10
+ ok: false;
11
+ error: Error;
12
+ };
13
+ export declare class TransientError extends Error {
14
+ readonly retryAfterMs?: number;
15
+ constructor(message: string, retryAfterMs?: number);
16
+ }
17
+ export declare class ChunkTooLargeError extends Error {
18
+ constructor(message: string);
19
+ }
20
+ export declare function retryAfterMs(header: string | null | undefined): number | undefined;
21
+ export declare function isTransientStatus(status: number): boolean;
22
+ export interface BatcherOptions<Item, Result> {
23
+ send: (items: Item[], attempt: number) => Promise<BatchResult<Result>[]>;
24
+ key: (item: Item) => string;
25
+ weight?: (item: Item) => number;
26
+ bytes?: (item: Item) => number;
27
+ maxWeight?: number;
28
+ maxBytes?: number;
29
+ concurrency?: number;
30
+ retries?: number;
31
+ baseDelayMs?: number;
32
+ sleep?: (ms: number) => Promise<void>;
33
+ schedule?: (flush: () => void) => void;
34
+ }
35
+ export declare class Batcher<Item, Result> {
36
+ private readonly send;
37
+ private readonly key;
38
+ private readonly weight;
39
+ private readonly bytes;
40
+ private readonly maxWeight;
41
+ private readonly maxBytes;
42
+ private readonly concurrency;
43
+ private readonly retries;
44
+ private readonly baseDelayMs;
45
+ private readonly sleep;
46
+ private readonly schedule;
47
+ private readonly queue;
48
+ private readonly inFlight;
49
+ private readonly waiting;
50
+ private active;
51
+ private scheduled;
52
+ constructor(options: BatcherOptions<Item, Result>);
53
+ enqueue(item: Item): Promise<Result>;
54
+ private scheduleFlush;
55
+ private flush;
56
+ private chunk;
57
+ private run;
58
+ private backoff;
59
+ private acquire;
60
+ private release;
61
+ }
@@ -0,0 +1,166 @@
1
+ export class TransientError extends Error {
2
+ constructor(message, retryAfterMs) {
3
+ super(message);
4
+ this.name = "TransientError";
5
+ this.retryAfterMs = retryAfterMs;
6
+ }
7
+ }
8
+ export class ChunkTooLargeError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "ChunkTooLargeError";
12
+ }
13
+ }
14
+ export function retryAfterMs(header) {
15
+ if (!header)
16
+ return undefined;
17
+ const seconds = Number(header);
18
+ if (Number.isFinite(seconds))
19
+ return Math.max(0, seconds * 1000);
20
+ const at = Date.parse(header);
21
+ return Number.isNaN(at) ? undefined : Math.max(0, at - Date.now());
22
+ }
23
+ export function isTransientStatus(status) {
24
+ return status === 429 || status >= 500;
25
+ }
26
+ const defaultSleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
27
+ const defaultSchedule = (flush) => {
28
+ setTimeout(flush, 0);
29
+ };
30
+ export class Batcher {
31
+ constructor(options) {
32
+ this.queue = [];
33
+ this.inFlight = new Map();
34
+ this.waiting = [];
35
+ this.active = 0;
36
+ this.scheduled = false;
37
+ this.send = options.send;
38
+ this.key = options.key;
39
+ this.weight = options.weight ?? (() => 1);
40
+ this.bytes = options.bytes ?? (() => 0);
41
+ this.maxWeight = options.maxWeight ?? 10;
42
+ this.maxBytes = options.maxBytes ?? Number.POSITIVE_INFINITY;
43
+ this.concurrency = options.concurrency ?? 3;
44
+ this.retries = options.retries ?? 3;
45
+ this.baseDelayMs = options.baseDelayMs ?? 300;
46
+ this.sleep = options.sleep ?? defaultSleep;
47
+ this.schedule = options.schedule ?? defaultSchedule;
48
+ }
49
+ enqueue(item) {
50
+ const key = this.key(item);
51
+ const shared = this.inFlight.get(key);
52
+ if (shared)
53
+ return shared;
54
+ const promise = new Promise((resolve, reject) => {
55
+ this.queue.push({ item, resolve, reject });
56
+ });
57
+ this.inFlight.set(key, promise);
58
+ const forget = () => {
59
+ if (this.inFlight.get(key) === promise)
60
+ this.inFlight.delete(key);
61
+ };
62
+ promise.then(forget, forget);
63
+ this.scheduleFlush();
64
+ return promise;
65
+ }
66
+ scheduleFlush() {
67
+ if (this.scheduled)
68
+ return;
69
+ this.scheduled = true;
70
+ this.schedule(() => {
71
+ this.scheduled = false;
72
+ this.flush();
73
+ });
74
+ }
75
+ flush() {
76
+ const pending = this.queue.splice(0);
77
+ for (const chunk of this.chunk(pending)) {
78
+ void this.run(chunk, 1);
79
+ }
80
+ }
81
+ chunk(pending) {
82
+ const chunks = [];
83
+ let current = [];
84
+ let weight = 0;
85
+ let bytes = 0;
86
+ for (const entry of pending) {
87
+ const entryWeight = this.weight(entry.item);
88
+ const entryBytes = this.bytes(entry.item);
89
+ const overflows = weight + entryWeight > this.maxWeight ||
90
+ bytes + entryBytes > this.maxBytes;
91
+ if (current.length > 0 && overflows) {
92
+ chunks.push(current);
93
+ current = [];
94
+ weight = 0;
95
+ bytes = 0;
96
+ }
97
+ current.push(entry);
98
+ weight += entryWeight;
99
+ bytes += entryBytes;
100
+ }
101
+ if (current.length > 0)
102
+ chunks.push(current);
103
+ return chunks;
104
+ }
105
+ async run(chunk, attempt) {
106
+ await this.acquire();
107
+ let results;
108
+ try {
109
+ results = await this.send(chunk.map(entry => entry.item), attempt);
110
+ }
111
+ catch (error) {
112
+ this.release();
113
+ if (error instanceof ChunkTooLargeError && chunk.length > 1) {
114
+ const half = Math.ceil(chunk.length / 2);
115
+ void this.run(chunk.slice(0, half), 1);
116
+ void this.run(chunk.slice(half), 1);
117
+ return;
118
+ }
119
+ if (error instanceof TransientError && attempt <= this.retries) {
120
+ await this.sleep(error.retryAfterMs ?? this.backoff(attempt));
121
+ void this.run(chunk, attempt + 1);
122
+ return;
123
+ }
124
+ const failure = error instanceof Error ? error : new Error(String(error));
125
+ for (const entry of chunk)
126
+ entry.reject(failure);
127
+ return;
128
+ }
129
+ this.release();
130
+ chunk.forEach((entry, index) => {
131
+ const result = results[index];
132
+ if (result === undefined) {
133
+ entry.reject(new Error("batch: no result for item"));
134
+ return;
135
+ }
136
+ if (result.ok === true) {
137
+ entry.resolve(result.value);
138
+ return;
139
+ }
140
+ entry.reject(result.error);
141
+ });
142
+ }
143
+ backoff(attempt) {
144
+ const base = this.baseDelayMs * 2 ** (attempt - 1);
145
+ const jitter = base * 0.3 * (Math.random() * 2 - 1);
146
+ return Math.round(base + jitter);
147
+ }
148
+ acquire() {
149
+ if (this.active < this.concurrency) {
150
+ this.active += 1;
151
+ return Promise.resolve();
152
+ }
153
+ return new Promise(resolve => {
154
+ this.waiting.push(() => {
155
+ this.active += 1;
156
+ resolve();
157
+ });
158
+ });
159
+ }
160
+ release() {
161
+ this.active -= 1;
162
+ const next = this.waiting.shift();
163
+ if (next)
164
+ next();
165
+ }
166
+ }
@@ -8,6 +8,7 @@ export declare class PlacementsClient {
8
8
  constructor(ctx: PlacementsContext);
9
9
  /** One vault, one account. */
10
10
  forAccount(vaultId: string, account: string, options?: ForAccountOptions): Promise<AccountShares[]>;
11
+ forAccounts(vaultIds: string[], account: string, options?: ForAccountOptions): Promise<AccountShares[]>;
11
12
  private sharesForCoin;
12
13
  private walletBalance;
13
14
  private coveredBalance;
@@ -24,6 +24,16 @@ function addressFits(chain, account) {
24
24
  return false;
25
25
  return chain === "evm" ? hex.length <= 40 : hex.length <= 64;
26
26
  }
27
+ function unresolvedVault(vaultId, reason) {
28
+ return {
29
+ vaultId,
30
+ coin: { coin: "", chain: { type: "sui" }, decimals: 0 },
31
+ available: "0",
32
+ covered: undefined,
33
+ placements: [],
34
+ failures: [{ protocol: "vault", stage: "discover", reason }]
35
+ };
36
+ }
27
37
  export class PlacementsClient {
28
38
  constructor(ctx) {
29
39
  this.ctx = ctx;
@@ -35,6 +45,24 @@ export class PlacementsClient {
35
45
  const coins = await this.ctx.vaults.coinsForVault(vaultId);
36
46
  return Promise.all(coins.map(coin => this.sharesForCoin(vaultId, coin, account, options)));
37
47
  }
48
+ async forAccounts(vaultIds, account, options = {}) {
49
+ if (!this.ctx.vaults)
50
+ throw new Error("no vault resolver configured");
51
+ const vaults = this.ctx.vaults;
52
+ const resolved = await Promise.all(vaultIds.map(async (vaultId) => {
53
+ try {
54
+ return { vaultId, coins: await vaults.coinsForVault(vaultId) };
55
+ }
56
+ catch (error) {
57
+ return { vaultId, coins: undefined, reason: reasonOf(error) };
58
+ }
59
+ }));
60
+ return Promise.all(resolved.flatMap(entry => entry.coins
61
+ ? entry.coins.map(coin => this.sharesForCoin(entry.vaultId, coin, account, options))
62
+ : [
63
+ Promise.resolve(unresolvedVault(entry.vaultId, entry.reason ?? ""))
64
+ ]));
65
+ }
38
66
  async sharesForCoin(vaultId, coin, account, options) {
39
67
  const failures = [];
40
68
  // A vault on several chains issues a coin on each, so for any one
@@ -1,11 +1,15 @@
1
1
  import { EvmReader } from "./types.js";
2
+ import { TransportRequest } from "./batch.js";
2
3
  export declare function encodeBalanceOf(owner: string): string;
3
4
  export declare function asContractAddress(address: string): string;
4
5
  export declare function decodeUint256(hex: string): bigint;
5
- /**
6
- * JSON-RPC reader over `fetch`, so it runs unchanged in the browser.
7
- *
8
- * Pass a dedicated endpoint per chain in production: the public RPCs used for
9
- * development are rate-limited and carry no SLA.
10
- */
11
- export declare function jsonRpcEvmReader(endpoints?: Record<number, string>): EvmReader;
6
+ export interface JsonRpcEvmReaderOptions {
7
+ maxCalls?: number;
8
+ concurrency?: number;
9
+ retries?: number;
10
+ baseDelayMs?: number;
11
+ sleep?: (ms: number) => Promise<void>;
12
+ fetch?: typeof globalThis.fetch;
13
+ onRequest?: (request: TransportRequest) => void;
14
+ }
15
+ export declare function jsonRpcEvmReader(endpoints?: Record<number, string>, options?: JsonRpcEvmReaderOptions): EvmReader;
@@ -1,3 +1,4 @@
1
+ import { Batcher, ChunkTooLargeError, TransientError, isTransientStatus, retryAfterMs } from "./batch.js";
1
2
  /** `balanceOf(address)` */
2
3
  const BALANCE_OF_SELECTOR = "0x70a08231";
3
4
  export function encodeBalanceOf(owner) {
@@ -15,34 +16,94 @@ export function decodeUint256(hex) {
15
16
  return 0n;
16
17
  return BigInt(hex);
17
18
  }
18
- /**
19
- * JSON-RPC reader over `fetch`, so it runs unchanged in the browser.
20
- *
21
- * Pass a dedicated endpoint per chain in production: the public RPCs used for
22
- * development are rate-limited and carry no SLA.
23
- */
24
- export function jsonRpcEvmReader(endpoints = {}) {
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"]
25
+ };
26
+ }
27
+ function toResult(entry) {
28
+ if (!entry)
29
+ return { ok: false, error: new Error("evm rpc: no result for call") };
30
+ if (entry.error)
31
+ return { ok: false, error: new Error(`evm rpc: ${entry.error.message}`) };
32
+ return { ok: true, value: entry.result ?? "0x" };
33
+ }
34
+ export function jsonRpcEvmReader(endpoints = {}, options = {}) {
35
+ const fetchImpl = options.fetch ??
36
+ ((input, init) => globalThis.fetch(input, init));
37
+ const onRequest = options.onRequest;
38
+ const batchers = new Map();
39
+ const singleOnly = new Set();
40
+ const post = async (endpoint, body, items, attempt) => {
41
+ try {
42
+ const response = await fetchImpl(endpoint, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify(body)
46
+ });
47
+ onRequest?.({ items, attempt, status: response.status });
48
+ return response;
49
+ }
50
+ catch (error) {
51
+ onRequest?.({ items, attempt, status: "network-error" });
52
+ throw new TransientError(`evm rpc: ${error instanceof Error ? error.message : String(error)}`);
53
+ }
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);
64
+ if (isTransientStatus(response.status)) {
65
+ throw new TransientError(`evm rpc ${response.status}`, retryAfterMs(response.headers?.get("retry-after")));
66
+ }
67
+ if (!response.ok) {
68
+ if (batched) {
69
+ singleOnly.add(endpoint);
70
+ throw new ChunkTooLargeError(`evm rpc ${response.status}`);
71
+ }
72
+ throw new Error(`evm rpc ${response.status}`);
73
+ }
74
+ const parsed = (await response.json());
75
+ if (!batched) {
76
+ return [toResult(Array.isArray(parsed) ? parsed[0] : parsed)];
77
+ }
78
+ if (!Array.isArray(parsed)) {
79
+ singleOnly.add(endpoint);
80
+ throw new ChunkTooLargeError("evm rpc: batch unsupported");
81
+ }
82
+ const byId = new Map(parsed.map(entry => [Number(entry.id), entry]));
83
+ return calls.map((_, id) => toResult(byId.get(id)));
84
+ };
85
+ const batcherFor = (endpoint) => {
86
+ const existing = batchers.get(endpoint);
87
+ if (existing)
88
+ return existing;
89
+ const batcher = new Batcher({
90
+ send: (calls, attempt) => send(endpoint, calls, attempt),
91
+ key: call => `${call.to.toLowerCase()}:${call.data.toLowerCase()}`,
92
+ maxWeight: options.maxCalls ?? 20,
93
+ concurrency: options.concurrency,
94
+ retries: options.retries,
95
+ baseDelayMs: options.baseDelayMs,
96
+ sleep: options.sleep
97
+ });
98
+ batchers.set(endpoint, batcher);
99
+ return batcher;
100
+ };
25
101
  return {
26
102
  async call(chainId, to, data, override) {
27
103
  const endpoint = override ?? endpoints[chainId];
28
104
  if (!endpoint)
29
105
  throw new Error(`no evm endpoint for chain ${chainId}`);
30
- const response = await fetch(endpoint, {
31
- method: "POST",
32
- headers: { "Content-Type": "application/json" },
33
- body: JSON.stringify({
34
- jsonrpc: "2.0",
35
- id: 1,
36
- method: "eth_call",
37
- params: [{ to, data }, "latest"]
38
- })
39
- });
40
- if (!response.ok)
41
- throw new Error(`evm rpc ${response.status}`);
42
- const body = (await response.json());
43
- if (body.error)
44
- throw new Error(`evm rpc: ${body.error.message}`);
45
- return body.result ?? "0x";
106
+ return batcherFor(endpoint).enqueue({ to, data });
46
107
  }
47
108
  };
48
109
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./types.js";
2
+ export * from "./batch.js";
2
3
  export * from "./registry.js";
3
4
  export * from "./client.js";
4
5
  export * from "./pointers.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./types.js";
2
+ export * from "./batch.js";
2
3
  export * from "./registry.js";
3
4
  export * from "./client.js";
4
5
  export * from "./pointers.js";
@@ -1,4 +1,5 @@
1
1
  import { SuiReader } from "./types.js";
2
+ import { TransportRequest } from "./batch.js";
2
3
  /**
3
4
  * Canonical form for a Sui coin type: `0x` stripped from the address segment,
4
5
  * lowercased and left-padded to 64 hex chars.
@@ -28,8 +29,16 @@ export declare function tableId(field: unknown): string | undefined;
28
29
  */
29
30
  export declare function decimalToBaseUnits(field: unknown): bigint;
30
31
  export declare function toBigInt(field: unknown): bigint;
31
- /**
32
- * GraphQL-backed reader. Uses `fetch` only, so it runs in the browser as well
33
- * as Node: the Sui GraphQL endpoint answers with `access-control-allow-origin: *`.
34
- */
35
- export declare function graphqlSuiReader(endpoint?: string): SuiReader;
32
+ export declare const DEFAULT_SUI_GRAPHQL_ENDPOINT = "https://graphql.mainnet.sui.io/graphql";
33
+ export interface GraphqlSuiReaderOptions {
34
+ endpoint?: string;
35
+ maxRootFields?: number;
36
+ maxBytes?: number;
37
+ concurrency?: number;
38
+ retries?: number;
39
+ baseDelayMs?: number;
40
+ sleep?: (ms: number) => Promise<void>;
41
+ fetch?: typeof globalThis.fetch;
42
+ onRequest?: (request: TransportRequest) => void;
43
+ }
44
+ export declare function graphqlSuiReader(options?: string | GraphqlSuiReaderOptions): SuiReader;
@@ -1,3 +1,4 @@
1
+ import { Batcher, ChunkTooLargeError, TransientError, isTransientStatus, retryAfterMs } from "./batch.js";
1
2
  const GRAPHQL_PAGE_SIZE = 50;
2
3
  /**
3
4
  * Canonical form for a Sui coin type: `0x` stripped from the address segment,
@@ -84,59 +85,140 @@ export function toBigInt(field) {
84
85
  return 0n;
85
86
  return BigInt(String(field));
86
87
  }
87
- async function query(endpoint, gql) {
88
- const response = await fetch(endpoint, {
89
- method: "POST",
90
- headers: { "Content-Type": "application/json" },
91
- body: JSON.stringify({ query: gql })
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
+ function normalizeAddress(address) {
94
+ return address.trim().toLowerCase().replace(/^0x/, "").padStart(64, "0");
95
+ }
96
+ function buildDocument(chunk) {
97
+ const roots = [];
98
+ const ids = chunk.flatMap(item => (item.kind === "object" ? [item.id] : []));
99
+ if (ids.length > 0) {
100
+ const keys = ids.map(id => `{ address: "${id}" }`).join(", ");
101
+ roots.push(`${OBJECTS_ALIAS}: multiGetObjects(keys: [${keys}]) ${OBJECT_SELECTION}`);
102
+ }
103
+ chunk.forEach((item, index) => {
104
+ if (item.kind === "field")
105
+ roots.push(`r${index}: ${item.field}`);
92
106
  });
93
- if (!response.ok) {
94
- throw new Error(`sui graphql ${response.status}`);
107
+ return `{ ${roots.join(" ")} }`;
108
+ }
109
+ function distribute(chunk, body) {
110
+ const pathErrors = new Map();
111
+ const rootErrors = [];
112
+ for (const error of body.errors ?? []) {
113
+ const alias = error.path?.[0];
114
+ if (typeof alias === "string") {
115
+ pathErrors.set(alias, error.message);
116
+ }
117
+ else {
118
+ rootErrors.push(error.message);
119
+ }
95
120
  }
96
- const body = (await response.json());
97
- if (body.errors?.length) {
98
- throw new Error(`sui graphql: ${body.errors.map(e => e.message).join("; ")}`);
121
+ if (rootErrors.length > 0) {
122
+ const message = `sui graphql: ${rootErrors.join("; ")}`;
123
+ if (chunk.length > 1)
124
+ throw new ChunkTooLargeError(message);
125
+ throw new Error(message);
99
126
  }
100
- if (!body.data)
101
- throw new Error("sui graphql: empty response");
102
- return body.data;
127
+ const data = body.data ?? {};
128
+ const objectsByAddress = new Map();
129
+ for (const node of data[OBJECTS_ALIAS] ?? []) {
130
+ const contents = node?.asMoveObject?.contents;
131
+ if (!node || !contents)
132
+ continue;
133
+ objectsByAddress.set(normalizeAddress(node.address), {
134
+ address: node.address,
135
+ type: contents.type.repr,
136
+ json: contents.json
137
+ });
138
+ }
139
+ 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
+ if (item.kind === "object") {
146
+ return { ok: true, value: objectsByAddress.get(normalizeAddress(item.id)) };
147
+ }
148
+ return { ok: true, value: data[alias] };
149
+ });
103
150
  }
104
- /**
105
- * GraphQL-backed reader. Uses `fetch` only, so it runs in the browser as well
106
- * as Node: the Sui GraphQL endpoint answers with `access-control-allow-origin: *`.
107
- */
108
- export function graphqlSuiReader(endpoint = "https://graphql.mainnet.sui.io/graphql") {
151
+ export function graphqlSuiReader(options = {}) {
152
+ const config = typeof options === "string" ? { endpoint: options } : options;
153
+ const endpoint = config.endpoint ?? DEFAULT_SUI_GRAPHQL_ENDPOINT;
154
+ const fetchImpl = config.fetch ??
155
+ ((input, init) => globalThis.fetch(input, init));
156
+ const onRequest = config.onRequest;
157
+ const send = async (chunk, attempt) => {
158
+ const document = buildDocument(chunk);
159
+ let response;
160
+ try {
161
+ response = await fetchImpl(endpoint, {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/json" },
164
+ body: JSON.stringify({ query: document })
165
+ });
166
+ }
167
+ catch (error) {
168
+ onRequest?.({ items: chunk.length, attempt, status: "network-error" });
169
+ throw new TransientError(`sui graphql: ${error instanceof Error ? error.message : String(error)}`);
170
+ }
171
+ onRequest?.({ items: chunk.length, attempt, status: response.status });
172
+ if (isTransientStatus(response.status)) {
173
+ throw new TransientError(`sui graphql ${response.status}`, retryAfterMs(response.headers?.get("retry-after")));
174
+ }
175
+ if (!response.ok) {
176
+ const message = `sui graphql ${response.status}`;
177
+ if (chunk.length > 1)
178
+ throw new ChunkTooLargeError(message);
179
+ throw new Error(message);
180
+ }
181
+ const body = (await response.json());
182
+ if (!body.data && !body.errors?.length) {
183
+ throw new Error("sui graphql: empty response");
184
+ }
185
+ return distribute(chunk, body);
186
+ };
187
+ const batcher = new Batcher({
188
+ send,
189
+ key: item => item.kind === "object" ? `object:${normalizeAddress(item.id)}` : item.field,
190
+ weight: item => (item.kind === "object" ? OBJECT_KEY_WEIGHT : 1),
191
+ bytes: item => (item.kind === "object" ? OBJECT_KEY_BYTES : item.field.length),
192
+ maxWeight: config.maxRootFields ?? 10,
193
+ maxBytes: config.maxBytes ?? 4500,
194
+ concurrency: config.concurrency,
195
+ retries: config.retries,
196
+ baseDelayMs: config.baseDelayMs,
197
+ sleep: config.sleep
198
+ });
199
+ const field = (gql) => batcher.enqueue({ kind: "field", field: gql });
109
200
  return {
110
201
  async objects(ids) {
111
202
  const unique = [...new Set(ids)];
112
- if (unique.length === 0)
113
- return {};
114
- const keys = unique.map(id => `{ address: "${id}" }`).join(", ");
115
- const data = await query(endpoint, `{ multiGetObjects(keys: [${keys}]) { address asMoveObject { contents { type { repr } json } } } }`);
203
+ const found = await Promise.all(unique.map(id => batcher.enqueue({ kind: "object", id })));
116
204
  const out = {};
117
- for (const node of data.multiGetObjects ?? []) {
118
- const contents = node?.asMoveObject?.contents;
119
- if (!contents)
120
- continue;
121
- out[node.address] = {
122
- address: node.address,
123
- type: contents.type.repr,
124
- json: contents.json
125
- };
205
+ for (const object of found) {
206
+ if (object)
207
+ out[object.address] = object;
126
208
  }
127
209
  return out;
128
210
  },
129
211
  async balance(owner, coinType) {
130
- const data = await query(endpoint, `{ address(address: "${owner}") { balance(coinType: "${coinType}") { totalBalance } } }`);
131
- return data.address?.balance?.totalBalance ?? "0";
212
+ const data = await field(`address(address: "${owner}") { balance(coinType: "${coinType}") { totalBalance } }`);
213
+ return data?.balance?.totalBalance ?? "0";
132
214
  },
133
215
  async ownedObjects(owner, type) {
134
216
  const owned = [];
135
217
  let cursor = null;
136
218
  for (;;) {
137
219
  const after = cursor ? `, after: "${cursor}"` : "";
138
- const page = await query(endpoint, `{ address(address: "${owner}") { objects(first: ${GRAPHQL_PAGE_SIZE}${after}, filter: { type: "${type}" }) { pageInfo { hasNextPage endCursor } nodes { address contents { type { repr } json } } } } }`);
139
- const container = page.address?.objects;
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 } } } }`);
221
+ const container = page?.objects;
140
222
  if (!container)
141
223
  break;
142
224
  for (const node of container.nodes ?? []) {
@@ -155,19 +237,16 @@ export function graphqlSuiReader(endpoint = "https://graphql.mainnet.sui.io/grap
155
237
  return owned;
156
238
  },
157
239
  async dynamicField(parentId, key) {
158
- const data = await query(endpoint, `{ address(address: "${parentId}") { dynamicField(name: { type: "${key.type}", bcs: "${key.bcs}" }) { value { ... on MoveValue { json } } } } }`);
159
- return data.address?.dynamicField?.value?.json;
240
+ const data = await field(`address(address: "${parentId}") { dynamicField(name: { type: "${key.type}", bcs: "${key.bcs}" }) { value { ... on MoveValue { json } } } }`);
241
+ return data?.dynamicField?.value?.json;
160
242
  },
161
243
  async dynamicFields(parentId, limit) {
162
244
  const fields = [];
163
245
  let cursor = null;
164
- // Paged rather than fetched by name: addressing a table entry by key
165
- // needs the key BCS-encoded, and these tables are small enough
166
- // (tens of entries) that a full walk is cheaper than that machinery.
167
246
  for (;;) {
168
247
  const after = cursor ? `, after: "${cursor}"` : "";
169
- const page = await query(endpoint, `{ address(address: "${parentId}") { dynamicFields(first: ${GRAPHQL_PAGE_SIZE}${after}) { pageInfo { hasNextPage endCursor } nodes { name { json } value { ... on MoveValue { type { repr } json } } } } } }`);
170
- const container = page.address?.dynamicFields;
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 } } } } }`);
249
+ const container = page?.dynamicFields;
171
250
  if (!container)
172
251
  break;
173
252
  for (const node of container.nodes ?? []) {
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.2",
4
+ "version": "2.11.0-pre.4",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "main": "./dist/index.js",