@ember-finance/sdk 2.10.4 → 2.11.0-pre.1

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.
Files changed (49) hide show
  1. package/dist/src/api/v2/apis/vaults-api.d.ts +42 -0
  2. package/dist/src/api/v2/apis/vaults-api.js +87 -0
  3. package/dist/src/api/v2/models/chain.d.ts +6 -0
  4. package/dist/src/api/v2/models/coin-placement.d.ts +95 -0
  5. package/dist/src/api/v2/models/coin-placement.js +17 -0
  6. package/dist/src/api/v2/models/coin-placements-response.d.ts +25 -0
  7. package/dist/src/api/v2/models/coin-placements-response.js +14 -0
  8. package/dist/src/api/v2/models/index.d.ts +3 -0
  9. package/dist/src/api/v2/models/index.js +3 -0
  10. package/dist/src/api/v2/models/instant-withdraw-config.d.ts +21 -2
  11. package/dist/src/api/v2/models/poc-report-protocol-asset.d.ts +60 -0
  12. package/dist/src/api/v2/models/poc-report-protocol-asset.js +18 -0
  13. package/dist/src/api/v2/models/poc-report-protocol.d.ts +13 -0
  14. package/dist/src/api/v2/models/transparency-row.d.ts +1 -1
  15. package/dist/src/placements/adapters/alphalend.d.ts +1 -0
  16. package/dist/src/placements/adapters/alphalend.js +58 -0
  17. package/dist/src/placements/adapters/bluefin-spot.d.ts +1 -0
  18. package/dist/src/placements/adapters/bluefin-spot.js +78 -0
  19. package/dist/src/placements/adapters/current.d.ts +1 -0
  20. package/dist/src/placements/adapters/current.js +70 -0
  21. package/dist/src/placements/adapters/evm-protocols.d.ts +2 -0
  22. package/dist/src/placements/adapters/evm-protocols.js +15 -0
  23. package/dist/src/placements/adapters/factory.d.ts +15 -0
  24. package/dist/src/placements/adapters/factory.js +62 -0
  25. package/dist/src/placements/adapters/index.d.ts +13 -0
  26. package/dist/src/placements/adapters/index.js +23 -0
  27. package/dist/src/placements/adapters/navi.d.ts +1 -0
  28. package/dist/src/placements/adapters/navi.js +53 -0
  29. package/dist/src/placements/adapters/pendle.d.ts +9 -0
  30. package/dist/src/placements/adapters/pendle.js +108 -0
  31. package/dist/src/placements/adapters/suilend.d.ts +1 -0
  32. package/dist/src/placements/adapters/suilend.js +57 -0
  33. package/dist/src/placements/client.d.ts +16 -0
  34. package/dist/src/placements/client.js +157 -0
  35. package/dist/src/placements/evm.d.ts +11 -0
  36. package/dist/src/placements/evm.js +48 -0
  37. package/dist/src/placements/index.d.ts +8 -0
  38. package/dist/src/placements/index.js +8 -0
  39. package/dist/src/placements/pointers.d.ts +24 -0
  40. package/dist/src/placements/pointers.js +125 -0
  41. package/dist/src/placements/registry.d.ts +11 -0
  42. package/dist/src/placements/registry.js +19 -0
  43. package/dist/src/placements/sui.d.ts +35 -0
  44. package/dist/src/placements/sui.js +189 -0
  45. package/dist/src/placements/types.d.ts +139 -0
  46. package/dist/src/placements/types.js +1 -0
  47. package/dist/src/placements/vaults.d.ts +15 -0
  48. package/dist/src/placements/vaults.js +97 -0
  49. package/package.json +6 -1
@@ -0,0 +1,157 @@
1
+ import { asContractAddress, decodeUint256, encodeBalanceOf } from "./evm.js";
2
+ import { getAdapter, listAdapters } from "./registry.js";
3
+ function reasonOf(error) {
4
+ return error instanceof Error ? error.message : String(error);
5
+ }
6
+ /**
7
+ * Resolves what one account holds of a vault's shares: in its wallet, wrapped
8
+ * for cover, and deployed elsewhere.
9
+ *
10
+ * A protocol that fails is reported in `failures` rather than thrown, so a
11
+ * single unreachable protocol does not zero an otherwise valid answer.
12
+ */
13
+ /**
14
+ * Whether `account` could be an address on this chain.
15
+ *
16
+ * Only one direction is decidable: a 32-byte sui address fed to
17
+ * `balanceOf(address)` overflows the 20-byte word and the node reverts, so it
18
+ * is certainly not evm. A 20-byte address is a valid short-form sui address,
19
+ * so the reverse cannot be ruled out and is left to the chain to answer.
20
+ */
21
+ function addressFits(chain, account) {
22
+ const hex = account.trim().replace(/^0x/i, "");
23
+ if (!/^[0-9a-fA-F]+$/.test(hex))
24
+ return false;
25
+ return chain === "evm" ? hex.length <= 40 : hex.length <= 64;
26
+ }
27
+ export class PlacementsClient {
28
+ constructor(ctx) {
29
+ this.ctx = ctx;
30
+ }
31
+ /** One vault, one account. */
32
+ async forAccount(vaultId, account, options = {}) {
33
+ if (!this.ctx.vaults)
34
+ throw new Error("no vault resolver configured");
35
+ const coins = await this.ctx.vaults.coinsForVault(vaultId);
36
+ return Promise.all(coins.map(coin => this.sharesForCoin(vaultId, coin, account, options)));
37
+ }
38
+ async sharesForCoin(vaultId, coin, account, options) {
39
+ const failures = [];
40
+ // A vault on several chains issues a coin on each, so for any one
41
+ // account all but one of them belongs to a wallet it does not have.
42
+ // Holding none of it is the true answer, and cheaper than asking.
43
+ if (!addressFits(coin.chain.type, account)) {
44
+ return {
45
+ vaultId,
46
+ coin,
47
+ available: "0",
48
+ covered: undefined,
49
+ placements: [],
50
+ failures
51
+ };
52
+ }
53
+ const [available, covered, placements] = await Promise.all([
54
+ this.walletBalance(coin, account).catch(error => {
55
+ failures.push({
56
+ protocol: "wallet",
57
+ stage: "read",
58
+ coin: coin.coin,
59
+ reason: reasonOf(error)
60
+ });
61
+ return "0";
62
+ }),
63
+ this.coveredBalance(coin, account).catch(error => {
64
+ failures.push({
65
+ protocol: "opencover",
66
+ stage: "read",
67
+ coin: coin.coin,
68
+ reason: reasonOf(error)
69
+ });
70
+ return undefined;
71
+ }),
72
+ this.placementsForCoin(coin, account, options, failures)
73
+ ]);
74
+ return { vaultId, coin, available, covered, placements, failures };
75
+ }
76
+ async walletBalance(coin, account) {
77
+ if (coin.chain.type === "sui") {
78
+ if (!this.ctx.sui)
79
+ throw new Error("no sui reader configured");
80
+ return this.ctx.sui.balance(account, coin.coin);
81
+ }
82
+ if (!this.ctx.evm)
83
+ throw new Error("no evm reader configured");
84
+ const chainId = coin.chain.chainId;
85
+ if (chainId === undefined)
86
+ throw new Error(`${coin.coin} has no chainId`);
87
+ const raw = await this.ctx.evm.call(chainId, asContractAddress(coin.coin), encodeBalanceOf(account), coin.rpcUrl);
88
+ return decodeUint256(raw).toString();
89
+ }
90
+ async coveredBalance(coin, account) {
91
+ const coverVault = await this.ctx.vaults?.coverVault?.(coin);
92
+ if (!coverVault)
93
+ return undefined;
94
+ if (!this.ctx.evm)
95
+ throw new Error("no evm reader configured");
96
+ const chainId = coin.chain.chainId;
97
+ if (chainId === undefined)
98
+ throw new Error(`${coin.coin} has no chainId`);
99
+ const raw = await this.ctx.evm.call(chainId, asContractAddress(coverVault), encodeBalanceOf(account), coin.rpcUrl);
100
+ return decodeUint256(raw).toString();
101
+ }
102
+ async placementsForCoin(coin, account, options, failures) {
103
+ const adapters = this.select(options.protocols, failures).filter(adapter => adapter.chain === coin.chain.type);
104
+ const discovered = await Promise.allSettled(adapters.map(adapter => adapter.discover(coin, this.ctx)));
105
+ const jobs = [];
106
+ discovered.forEach((result, index) => {
107
+ const adapter = adapters[index];
108
+ if (result.status === "rejected") {
109
+ failures.push({
110
+ protocol: adapter.id,
111
+ stage: "discover",
112
+ coin: coin.coin,
113
+ reason: reasonOf(result.reason)
114
+ });
115
+ return;
116
+ }
117
+ if (result.value.length > 0)
118
+ jobs.push({ adapter, refs: result.value });
119
+ });
120
+ // One read per adapter rather than per venue, so an adapter can batch
121
+ // every pointer it owns into as few chain round-trips as it can manage.
122
+ const read = await Promise.allSettled(jobs.map(job => job.adapter.read(job.refs, account, this.ctx)));
123
+ const placements = [];
124
+ read.forEach((result, index) => {
125
+ const { adapter } = jobs[index];
126
+ if (result.status === "rejected") {
127
+ failures.push({
128
+ protocol: adapter.id,
129
+ stage: "read",
130
+ coin: coin.coin,
131
+ reason: reasonOf(result.reason)
132
+ });
133
+ return;
134
+ }
135
+ placements.push(...result.value);
136
+ });
137
+ return placements;
138
+ }
139
+ select(protocols, failures) {
140
+ if (!protocols)
141
+ return listAdapters();
142
+ const selected = [];
143
+ for (const id of protocols) {
144
+ const adapter = getAdapter(id);
145
+ if (!adapter) {
146
+ failures.push({
147
+ protocol: id,
148
+ stage: "discover",
149
+ reason: "no adapter registered"
150
+ });
151
+ continue;
152
+ }
153
+ selected.push(adapter);
154
+ }
155
+ return selected;
156
+ }
157
+ }
@@ -0,0 +1,11 @@
1
+ import { EvmReader } from "./types.js";
2
+ export declare function encodeBalanceOf(owner: string): string;
3
+ export declare function asContractAddress(address: string): string;
4
+ 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;
@@ -0,0 +1,48 @@
1
+ /** `balanceOf(address)` */
2
+ const BALANCE_OF_SELECTOR = "0x70a08231";
3
+ export function encodeBalanceOf(owner) {
4
+ const address = owner.toLowerCase().replace(/^0x/, "").padStart(64, "0");
5
+ return `${BALANCE_OF_SELECTOR}${address}`;
6
+ }
7
+ export function asContractAddress(address) {
8
+ // An EVM node rejects an unprefixed `to` outright rather than coercing it,
9
+ // so a pointer carrying a bare address would fail the whole read.
10
+ const trimmed = address.trim();
11
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
12
+ }
13
+ export function decodeUint256(hex) {
14
+ if (!hex || hex === "0x")
15
+ return 0n;
16
+ return BigInt(hex);
17
+ }
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 = {}) {
25
+ return {
26
+ async call(chainId, to, data, override) {
27
+ const endpoint = override ?? endpoints[chainId];
28
+ if (!endpoint)
29
+ 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";
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./registry.js";
3
+ export * from "./client.js";
4
+ export * from "./pointers.js";
5
+ export * from "./vaults.js";
6
+ export * from "./sui.js";
7
+ export * from "./evm.js";
8
+ export * from "./adapters/index.js";
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./registry.js";
3
+ export * from "./client.js";
4
+ export * from "./pointers.js";
5
+ export * from "./vaults.js";
6
+ export * from "./sui.js";
7
+ export * from "./evm.js";
8
+ export * from "./adapters/index.js";
@@ -0,0 +1,24 @@
1
+ import { PlatformEnv } from "../common/constants.js";
2
+ import { PlacementRef, PointerSource } from "./types.js";
3
+ /**
4
+ * Pointers held in memory. Useful for tests, and for pinning a known venue set
5
+ * without a round-trip.
6
+ */
7
+ export declare function staticPointerSource(refs: PlacementRef[]): PointerSource;
8
+ export interface HttpPointerSourceOptions {
9
+ /** Vaults API environment. Defaults to prod. */
10
+ env?: PlatformEnv;
11
+ /** Overrides `env` entirely. For a local deployment, or a proxy. */
12
+ baseUrl?: string;
13
+ path?: string;
14
+ /** How long a coin's rows may be reused, in milliseconds. Default 30s. */
15
+ cacheMs?: number;
16
+ }
17
+ /** Pointers from the vaults API. Defaults to prod. */
18
+ export declare function httpPointerSource(options?: HttpPointerSourceOptions): PointerSource;
19
+ /**
20
+ * Merges what every source returns, de-duplicated by venue. Earlier sources win,
21
+ * so a caller can pin rows ahead of a remote set. A source that throws is
22
+ * skipped rather than failing the set.
23
+ */
24
+ export declare function mergePointerSources(...sources: PointerSource[]): PointerSource;
@@ -0,0 +1,125 @@
1
+ import { environmentConfig } from "../common/constants.js";
2
+ import { coinTypesMatch } from "./sui.js";
3
+ /**
4
+ * Pointers held in memory. Useful for tests, and for pinning a known venue set
5
+ * without a round-trip.
6
+ */
7
+ export function staticPointerSource(refs) {
8
+ return {
9
+ // Coins are compared with `coinTypesMatch` rather than by string
10
+ // equality, so a row pinned in one spelling still matches a caller
11
+ // passing another — a short Sui address against its padded form, say.
12
+ // Exact matching would fail by returning nothing, which reads as
13
+ // "listed nowhere".
14
+ async placements(protocol, coin) {
15
+ return refs.filter(ref => ref.protocol === protocol && coinTypesMatch(ref.coin, coin.coin));
16
+ }
17
+ };
18
+ }
19
+ /**
20
+ * Decimals scale every amount an adapter returns, so a missing or malformed
21
+ * value is a silent power-of-ten error rather than a visible failure.
22
+ */
23
+ function requireDecimals(row) {
24
+ if (!Number.isInteger(row.decimals)) {
25
+ throw new Error(`placement ${row.protocol}/${row.venue} has no decimals`);
26
+ }
27
+ return row.decimals;
28
+ }
29
+ /** Pointers from the vaults API. Defaults to prod. */
30
+ export function httpPointerSource(options = {}) {
31
+ // The cache window exists to collapse the burst a single `forAccount`
32
+ // makes, not to cache across a session: pointers change when a venue is
33
+ // added, and a long window would keep a new one invisible.
34
+ const baseUrl = options.baseUrl ?? environmentConfig[options.env ?? "prod"].apiHost;
35
+ const path = options.path ?? "/api/v2/vaults/placements";
36
+ const cacheMs = options.cacheMs ?? 30000;
37
+ /**
38
+ * One request per coin, not per (coin, protocol).
39
+ *
40
+ * `?coin=` already returns every protocol's rows, so asking once per adapter
41
+ * fetched the same list five times and threw four of them away. Reading one
42
+ * vault meant eight requests where two would do, and a caller sweeping every
43
+ * vault made hundreds — enough to be rate-limited, and a rejected discover
44
+ * drops that venue from the answer.
45
+ *
46
+ * In-flight requests are shared rather than merely cached, so the adapters
47
+ * racing each other at the start of a `forAccount` collapse into one call.
48
+ */
49
+ const inFlight = new Map();
50
+ const rowsForCoin = (coin) => {
51
+ const key = coin.coin.toLowerCase();
52
+ const hit = inFlight.get(key);
53
+ if (hit && Date.now() - hit.at < cacheMs)
54
+ return hit.rows;
55
+ const rows = (async () => {
56
+ const url = new URL(`${baseUrl.replace(/\/$/, "")}${path}`);
57
+ url.searchParams.set("coin", coin.coin);
58
+ const response = await fetch(url.toString(), {
59
+ headers: { Accept: "application/json" }
60
+ });
61
+ if (!response.ok) {
62
+ throw new Error(`pointer source ${response.status}`);
63
+ }
64
+ const body = (await response.json());
65
+ // The chain type comes from the coin, not the wire: an adapter only
66
+ // ever runs on its own chain, so a server that named it again could
67
+ // only ever disagree.
68
+ return (body.placements ?? []).map(placement => ({
69
+ protocol: placement.protocol,
70
+ chain: {
71
+ type: coin.chain.type,
72
+ chainId: placement.chainId,
73
+ identifier: placement.chainIdentifier
74
+ },
75
+ venue: placement.venue,
76
+ coin: placement.coin,
77
+ decimals: requireDecimals(placement),
78
+ protocolName: placement.protocolName,
79
+ protocolLogoUrl: placement.protocolLogoUrl,
80
+ source: placement.source,
81
+ label: placement.label,
82
+ shareToken: placement.shareToken,
83
+ url: placement.url,
84
+ rpcUrl: placement.rpcUrl
85
+ }));
86
+ })();
87
+ // A failure is not cached: the next adapter should retry rather than
88
+ // inherit a rejection it never caused.
89
+ rows.catch(() => inFlight.delete(key));
90
+ inFlight.set(key, { at: Date.now(), rows });
91
+ return rows;
92
+ };
93
+ return {
94
+ async placements(protocol, coin) {
95
+ const rows = await rowsForCoin(coin);
96
+ return rows.filter(ref => ref.protocol === protocol);
97
+ }
98
+ };
99
+ }
100
+ /**
101
+ * Merges what every source returns, de-duplicated by venue. Earlier sources win,
102
+ * so a caller can pin rows ahead of a remote set. A source that throws is
103
+ * skipped rather than failing the set.
104
+ */
105
+ export function mergePointerSources(...sources) {
106
+ return {
107
+ async placements(protocol, coin) {
108
+ const results = await Promise.allSettled(sources.map(source => source.placements(protocol, coin)));
109
+ const byVenue = new Map();
110
+ for (const result of results) {
111
+ if (result.status !== "fulfilled")
112
+ continue;
113
+ for (const ref of result.value) {
114
+ // Key on the chain type as well as its id: an evm row whose
115
+ // chainId is absent would otherwise land in the same slot as
116
+ // a sui row at the same venue string and silently replace it.
117
+ const key = `${ref.chain.type}:${ref.chain.chainId ?? ""}:${ref.venue.toLowerCase()}`;
118
+ if (!byVenue.has(key))
119
+ byVenue.set(key, ref);
120
+ }
121
+ }
122
+ return [...byVenue.values()];
123
+ }
124
+ };
125
+ }
@@ -0,0 +1,11 @@
1
+ import { ChainType } from "../common/constants.js";
2
+ import { ProtocolAdapter, ProtocolId } from "./types.js";
3
+ /**
4
+ * Add a protocol. Re-registering an id replaces it, so a consumer can override
5
+ * a bundled adapter without forking the module.
6
+ */
7
+ export declare function registerAdapter(adapter: ProtocolAdapter): void;
8
+ export declare function getAdapter(id: ProtocolId): ProtocolAdapter | undefined;
9
+ export declare function listAdapters(chain?: ChainType): ProtocolAdapter[];
10
+ /** Drops every registration. Intended for tests. */
11
+ export declare function resetAdapters(): void;
@@ -0,0 +1,19 @@
1
+ const adapters = new Map();
2
+ /**
3
+ * Add a protocol. Re-registering an id replaces it, so a consumer can override
4
+ * a bundled adapter without forking the module.
5
+ */
6
+ export function registerAdapter(adapter) {
7
+ adapters.set(adapter.id, adapter);
8
+ }
9
+ export function getAdapter(id) {
10
+ return adapters.get(id);
11
+ }
12
+ export function listAdapters(chain) {
13
+ const all = [...adapters.values()];
14
+ return chain ? all.filter(adapter => adapter.chain === chain) : all;
15
+ }
16
+ /** Drops every registration. Intended for tests. */
17
+ export function resetAdapters() {
18
+ adapters.clear();
19
+ }
@@ -0,0 +1,35 @@
1
+ import { SuiReader } from "./types.js";
2
+ /**
3
+ * Canonical form for a Sui coin type: `0x` stripped from the address segment,
4
+ * lowercased and left-padded to 64 hex chars.
5
+ *
6
+ * Object JSON returns unprefixed padded types ("0000..02::sui::SUI") while
7
+ * config and RPC use the short prefixed form ("0x2::sui::SUI"). Comparing the
8
+ * two without normalising silently never matches.
9
+ *
10
+ * Only the address is folded. It is hex, so case is not significant there, but
11
+ * the module and struct names are Move identifiers and two that differ only in
12
+ * case are two different types.
13
+ */
14
+ export declare function normalizeCoinType(coinType: string): string;
15
+ export declare function coinTypesMatch(a: string, b: string): boolean;
16
+ /**
17
+ * A Sui address as GraphQL wants a dynamic-field key: base64 of its BCS
18
+ * encoding, which for an address is simply its 32 raw bytes.
19
+ */
20
+ export declare function bcsAddress(address: string): string;
21
+ /** Type arguments of a Move type, e.g. `Pool<A, B>` -> ["A", "B"]. */
22
+ export declare function typeArguments(type: string): string[];
23
+ /** The object id a Move table's entries actually hang off. */
24
+ export declare function tableId(field: unknown): string | undefined;
25
+ /**
26
+ * Some protocols wrap fixed-point numbers as `{ value }` scaled by 1e18.
27
+ * Returns base units, truncated — these feed balances, not accounting.
28
+ */
29
+ export declare function decimalToBaseUnits(field: unknown): bigint;
30
+ 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;
@@ -0,0 +1,189 @@
1
+ const GRAPHQL_PAGE_SIZE = 50;
2
+ /**
3
+ * Canonical form for a Sui coin type: `0x` stripped from the address segment,
4
+ * lowercased and left-padded to 64 hex chars.
5
+ *
6
+ * Object JSON returns unprefixed padded types ("0000..02::sui::SUI") while
7
+ * config and RPC use the short prefixed form ("0x2::sui::SUI"). Comparing the
8
+ * two without normalising silently never matches.
9
+ *
10
+ * Only the address is folded. It is hex, so case is not significant there, but
11
+ * the module and struct names are Move identifiers and two that differ only in
12
+ * case are two different types.
13
+ */
14
+ export function normalizeCoinType(coinType) {
15
+ const parts = coinType.trim().split("::");
16
+ if (parts.length < 2)
17
+ return coinType.trim().toLowerCase();
18
+ const address = parts[0].toLowerCase().replace(/^0x/, "").padStart(64, "0");
19
+ return [address, ...parts.slice(1)].join("::");
20
+ }
21
+ export function coinTypesMatch(a, b) {
22
+ return normalizeCoinType(a) === normalizeCoinType(b);
23
+ }
24
+ /**
25
+ * A Sui address as GraphQL wants a dynamic-field key: base64 of its BCS
26
+ * encoding, which for an address is simply its 32 raw bytes.
27
+ */
28
+ export function bcsAddress(address) {
29
+ // Hand-rolled rather than via Buffer, because this runs in the browser.
30
+ const hex = address.trim().replace(/^0x/, "").toLowerCase().padStart(64, "0");
31
+ if (!/^[0-9a-f]{64}$/.test(hex))
32
+ throw new Error(`not a sui address: ${address}`);
33
+ let binary = "";
34
+ for (let i = 0; i < hex.length; i += 2) {
35
+ binary += String.fromCharCode(Number.parseInt(hex.slice(i, i + 2), 16));
36
+ }
37
+ return btoa(binary);
38
+ }
39
+ /** Type arguments of a Move type, e.g. `Pool<A, B>` -> ["A", "B"]. */
40
+ export function typeArguments(type) {
41
+ const open = type.indexOf("<");
42
+ if (open === -1 || !type.endsWith(">"))
43
+ return [];
44
+ const inner = type.slice(open + 1, -1);
45
+ const args = [];
46
+ let depth = 0;
47
+ let current = "";
48
+ for (const ch of inner) {
49
+ if (ch === "<")
50
+ depth++;
51
+ if (ch === ">")
52
+ depth--;
53
+ if (ch === "," && depth === 0) {
54
+ args.push(current.trim());
55
+ current = "";
56
+ continue;
57
+ }
58
+ current += ch;
59
+ }
60
+ if (current.trim())
61
+ args.push(current.trim());
62
+ return args;
63
+ }
64
+ /** The object id a Move table's entries actually hang off. */
65
+ export function tableId(field) {
66
+ // Protocols wrap tables to different depths — `{ id }`, `{ table: { id } }`,
67
+ // `{ table: { table: { id } } }` — and the outer id resolves to nothing, so
68
+ // reading it returns an empty field list rather than an error.
69
+ const nested = field;
70
+ return nested?.table?.table?.id ?? nested?.table?.id ?? nested?.id;
71
+ }
72
+ /**
73
+ * Some protocols wrap fixed-point numbers as `{ value }` scaled by 1e18.
74
+ * Returns base units, truncated — these feed balances, not accounting.
75
+ */
76
+ export function decimalToBaseUnits(field) {
77
+ const raw = field?.value ?? field;
78
+ if (raw === undefined || raw === null)
79
+ return 0n;
80
+ return BigInt(String(raw)) / 1000000000000000000n;
81
+ }
82
+ export function toBigInt(field) {
83
+ if (field === undefined || field === null)
84
+ return 0n;
85
+ return BigInt(String(field));
86
+ }
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 })
92
+ });
93
+ if (!response.ok) {
94
+ throw new Error(`sui graphql ${response.status}`);
95
+ }
96
+ const body = (await response.json());
97
+ if (body.errors?.length) {
98
+ throw new Error(`sui graphql: ${body.errors.map(e => e.message).join("; ")}`);
99
+ }
100
+ if (!body.data)
101
+ throw new Error("sui graphql: empty response");
102
+ return body.data;
103
+ }
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") {
109
+ return {
110
+ async objects(ids) {
111
+ 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 } } } }`);
116
+ 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
+ };
126
+ }
127
+ return out;
128
+ },
129
+ async balance(owner, coinType) {
130
+ const data = await query(endpoint, `{ address(address: "${owner}") { balance(coinType: "${coinType}") { totalBalance } } }`);
131
+ return data.address?.balance?.totalBalance ?? "0";
132
+ },
133
+ async ownedObjects(owner, type) {
134
+ const owned = [];
135
+ let cursor = null;
136
+ for (;;) {
137
+ 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;
140
+ if (!container)
141
+ break;
142
+ for (const node of container.nodes ?? []) {
143
+ if (!node.contents)
144
+ continue;
145
+ owned.push({
146
+ address: node.address,
147
+ type: node.contents.type.repr,
148
+ json: node.contents.json
149
+ });
150
+ }
151
+ if (!container.pageInfo?.hasNextPage)
152
+ break;
153
+ cursor = container.pageInfo.endCursor;
154
+ }
155
+ return owned;
156
+ },
157
+ 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;
160
+ },
161
+ async dynamicFields(parentId, limit) {
162
+ const fields = [];
163
+ 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
+ for (;;) {
168
+ 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;
171
+ if (!container)
172
+ break;
173
+ for (const node of container.nodes ?? []) {
174
+ fields.push({
175
+ name: node.name?.json,
176
+ valueType: node.value?.type?.repr ?? "",
177
+ value: node.value?.json
178
+ });
179
+ }
180
+ if (limit !== undefined && fields.length >= limit)
181
+ break;
182
+ if (!container.pageInfo?.hasNextPage)
183
+ break;
184
+ cursor = container.pageInfo.endCursor;
185
+ }
186
+ return limit === undefined ? fields : fields.slice(0, limit);
187
+ }
188
+ };
189
+ }