@ar.io/sdk 3.24.0 → 4.0.0-alpha.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 (169) hide show
  1. package/README.md +682 -600
  2. package/lib/esm/cli/cli.js +188 -152
  3. package/lib/esm/cli/commands/antCommands.js +23 -58
  4. package/lib/esm/cli/commands/arnsPurchaseCommands.js +48 -30
  5. package/lib/esm/cli/commands/escrowCommands.js +221 -0
  6. package/lib/esm/cli/commands/gatewayWriteCommands.js +142 -23
  7. package/lib/esm/cli/commands/pruneCommands.js +150 -0
  8. package/lib/esm/cli/commands/readCommands.js +22 -3
  9. package/lib/esm/cli/commands/transfer.js +6 -6
  10. package/lib/esm/cli/options.js +124 -58
  11. package/lib/esm/cli/utils.js +280 -174
  12. package/lib/esm/common/ant-registry.js +17 -143
  13. package/lib/esm/common/ant.js +44 -1167
  14. package/lib/esm/common/faucet.js +11 -6
  15. package/lib/esm/common/index.js +0 -4
  16. package/lib/esm/common/io.js +25 -1412
  17. package/lib/esm/constants.js +13 -19
  18. package/lib/esm/solana/ant-readable.js +724 -0
  19. package/lib/esm/solana/ant-registry-readable.js +133 -0
  20. package/lib/esm/solana/ant-registry-writeable.js +472 -0
  21. package/lib/esm/solana/ant-writeable.js +384 -0
  22. package/lib/esm/solana/ata.js +70 -0
  23. package/lib/esm/solana/canonical-message.js +128 -0
  24. package/lib/esm/solana/clusters.js +111 -0
  25. package/lib/esm/solana/constants.js +146 -0
  26. package/lib/esm/solana/delegation-math.js +112 -0
  27. package/lib/esm/solana/deserialize.js +711 -0
  28. package/lib/esm/solana/escrow.js +839 -0
  29. package/lib/{cjs/utils/json.js → esm/solana/events.js} +15 -10
  30. package/lib/esm/solana/funding-plan.js +699 -0
  31. package/lib/esm/solana/index.js +126 -0
  32. package/lib/esm/solana/instruction.js +39 -0
  33. package/lib/esm/solana/io-readable.js +2182 -0
  34. package/lib/esm/solana/io-writeable.js +3196 -0
  35. package/lib/esm/solana/json-rpc.js +90 -0
  36. package/lib/esm/solana/metadata.js +81 -0
  37. package/lib/esm/solana/mpl-core.js +192 -0
  38. package/lib/esm/solana/pda.js +332 -0
  39. package/lib/esm/solana/predict-prescribed-observers.js +110 -0
  40. package/lib/esm/solana/retry.js +117 -0
  41. package/lib/esm/solana/rpc-circuit-breaker.js +258 -0
  42. package/lib/esm/solana/send.js +372 -0
  43. package/lib/esm/solana/spawn-ant.js +224 -0
  44. package/lib/esm/solana/types.js +1 -0
  45. package/lib/esm/types/ant.js +27 -15
  46. package/lib/esm/types/io.js +8 -11
  47. package/lib/esm/utils/ant.js +0 -63
  48. package/lib/esm/utils/index.js +0 -3
  49. package/lib/esm/version.js +1 -1
  50. package/lib/types/cli/commands/antCommands.d.ts +5 -13
  51. package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +33 -7
  52. package/lib/types/cli/commands/escrowCommands.d.ts +68 -0
  53. package/lib/types/cli/commands/gatewayWriteCommands.d.ts +12 -11
  54. package/lib/types/cli/commands/pruneCommands.d.ts +31 -0
  55. package/lib/types/cli/commands/readCommands.d.ts +27 -22
  56. package/lib/types/cli/commands/transfer.d.ts +9 -9
  57. package/lib/types/cli/options.d.ts +76 -21
  58. package/lib/types/cli/types.d.ts +11 -13
  59. package/lib/types/cli/utils.d.ts +71 -31
  60. package/lib/types/common/ant-registry.d.ts +49 -47
  61. package/lib/types/common/ant.d.ts +54 -539
  62. package/lib/types/common/faucet.d.ts +20 -8
  63. package/lib/types/common/index.d.ts +0 -3
  64. package/lib/types/common/io.d.ts +51 -263
  65. package/lib/types/constants.d.ts +11 -18
  66. package/lib/types/solana/ant-readable.d.ts +180 -0
  67. package/lib/types/solana/ant-registry-readable.d.ts +105 -0
  68. package/lib/types/solana/ant-registry-writeable.d.ts +249 -0
  69. package/lib/types/solana/ant-writeable.d.ts +177 -0
  70. package/lib/types/solana/ata.d.ts +44 -0
  71. package/lib/types/solana/canonical-message.d.ts +121 -0
  72. package/lib/types/solana/clusters.d.ts +109 -0
  73. package/lib/types/solana/constants.d.ts +119 -0
  74. package/lib/types/solana/delegation-math.d.ts +45 -0
  75. package/lib/types/solana/deserialize.d.ts +262 -0
  76. package/lib/types/solana/escrow.d.ts +480 -0
  77. package/lib/types/solana/events.d.ts +38 -0
  78. package/lib/types/solana/funding-plan.d.ts +225 -0
  79. package/lib/types/solana/index.d.ts +87 -0
  80. package/lib/types/solana/instruction.d.ts +39 -0
  81. package/lib/types/solana/io-readable.d.ts +499 -0
  82. package/lib/types/solana/io-writeable.d.ts +893 -0
  83. package/lib/types/solana/json-rpc.d.ts +47 -0
  84. package/lib/types/solana/metadata.d.ts +84 -0
  85. package/lib/types/solana/mpl-core.d.ts +120 -0
  86. package/lib/types/solana/pda.d.ts +95 -0
  87. package/lib/types/solana/predict-prescribed-observers.d.ts +28 -0
  88. package/lib/types/solana/retry.d.ts +62 -0
  89. package/lib/types/solana/rpc-circuit-breaker.d.ts +78 -0
  90. package/lib/types/solana/send.d.ts +94 -0
  91. package/lib/types/solana/spawn-ant.d.ts +145 -0
  92. package/lib/types/solana/types.d.ts +82 -0
  93. package/lib/types/types/ant-registry.d.ts +43 -4
  94. package/lib/types/types/ant.d.ts +114 -96
  95. package/lib/types/types/common.d.ts +18 -74
  96. package/lib/types/types/faucet.d.ts +2 -2
  97. package/lib/types/types/io.d.ts +244 -158
  98. package/lib/types/types/token.d.ts +0 -12
  99. package/lib/types/utils/ant.d.ts +1 -12
  100. package/lib/types/utils/index.d.ts +0 -3
  101. package/lib/types/version.d.ts +1 -1
  102. package/package.json +36 -33
  103. package/lib/cjs/cli/cli.js +0 -822
  104. package/lib/cjs/cli/commands/antCommands.js +0 -113
  105. package/lib/cjs/cli/commands/arnsPurchaseCommands.js +0 -212
  106. package/lib/cjs/cli/commands/gatewayWriteCommands.js +0 -210
  107. package/lib/cjs/cli/commands/readCommands.js +0 -215
  108. package/lib/cjs/cli/commands/transfer.js +0 -159
  109. package/lib/cjs/cli/options.js +0 -470
  110. package/lib/cjs/cli/types.js +0 -2
  111. package/lib/cjs/cli/utils.js +0 -639
  112. package/lib/cjs/common/ant-registry.js +0 -155
  113. package/lib/cjs/common/ant-versions.js +0 -93
  114. package/lib/cjs/common/ant.js +0 -1182
  115. package/lib/cjs/common/arweave.js +0 -27
  116. package/lib/cjs/common/contracts/ao-process.js +0 -224
  117. package/lib/cjs/common/error.js +0 -64
  118. package/lib/cjs/common/faucet.js +0 -150
  119. package/lib/cjs/common/hyperbeam/hb.js +0 -173
  120. package/lib/cjs/common/index.js +0 -42
  121. package/lib/cjs/common/io.js +0 -1423
  122. package/lib/cjs/common/logger.js +0 -83
  123. package/lib/cjs/common/loggers/winston.js +0 -68
  124. package/lib/cjs/common/marketplace.js +0 -731
  125. package/lib/cjs/common/turbo.js +0 -223
  126. package/lib/cjs/constants.js +0 -41
  127. package/lib/cjs/node/index.js +0 -39
  128. package/lib/cjs/package.json +0 -1
  129. package/lib/cjs/types/ant-registry.js +0 -2
  130. package/lib/cjs/types/ant.js +0 -168
  131. package/lib/cjs/types/common.js +0 -2
  132. package/lib/cjs/types/faucet.js +0 -2
  133. package/lib/cjs/types/index.js +0 -37
  134. package/lib/cjs/types/io.js +0 -51
  135. package/lib/cjs/types/token.js +0 -116
  136. package/lib/cjs/utils/ant.js +0 -108
  137. package/lib/cjs/utils/ao.js +0 -432
  138. package/lib/cjs/utils/arweave.js +0 -285
  139. package/lib/cjs/utils/base64.js +0 -62
  140. package/lib/cjs/utils/hash.js +0 -56
  141. package/lib/cjs/utils/index.js +0 -38
  142. package/lib/cjs/utils/processes.js +0 -173
  143. package/lib/cjs/utils/random.js +0 -30
  144. package/lib/cjs/utils/schema.js +0 -15
  145. package/lib/cjs/utils/url.js +0 -37
  146. package/lib/cjs/version.js +0 -20
  147. package/lib/cjs/web/index.js +0 -41
  148. package/lib/esm/common/ant-versions.js +0 -87
  149. package/lib/esm/common/arweave.js +0 -21
  150. package/lib/esm/common/contracts/ao-process.js +0 -220
  151. package/lib/esm/common/hyperbeam/hb.js +0 -169
  152. package/lib/esm/common/marketplace.js +0 -724
  153. package/lib/esm/common/turbo.js +0 -215
  154. package/lib/esm/node/index.js +0 -20
  155. package/lib/esm/utils/ao.js +0 -420
  156. package/lib/esm/utils/arweave.js +0 -271
  157. package/lib/esm/utils/processes.js +0 -167
  158. package/lib/esm/web/index.js +0 -20
  159. package/lib/types/common/ant-versions.d.ts +0 -39
  160. package/lib/types/common/arweave.d.ts +0 -17
  161. package/lib/types/common/contracts/ao-process.d.ts +0 -47
  162. package/lib/types/common/hyperbeam/hb.d.ts +0 -88
  163. package/lib/types/common/marketplace.d.ts +0 -568
  164. package/lib/types/common/turbo.d.ts +0 -61
  165. package/lib/types/node/index.d.ts +0 -20
  166. package/lib/types/utils/ao.d.ts +0 -80
  167. package/lib/types/utils/arweave.d.ts +0 -79
  168. package/lib/types/utils/processes.d.ts +0 -39
  169. package/lib/types/web/index.d.ts +0 -20
@@ -0,0 +1,724 @@
1
+ /**
2
+ * Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Solana implementation of ANT (Arweave Name Token) read interface.
18
+ *
19
+ * Reads ANT state from Metaplex Core NFT + PDA accounts on Solana.
20
+ * Each ANT is a Metaplex Core NFT with extended state in PDAs:
21
+ * - AntConfig: name, ticker, logo, description, keywords, owner
22
+ * - AntControllers: list of controller pubkeys
23
+ * - AntRecord: undername records (transactionId, ttl, priority, etc.)
24
+ */
25
+ import { address, fetchEncodedAccount, fetchEncodedAccounts, } from '@solana/kit';
26
+ import bs58 from 'bs58';
27
+ import { createHash as __createHash } from 'crypto';
28
+ import { ANT_RECORD_DISCRIMINATOR, ANT_RECORD_METADATA_DISCRIMINATOR, decodeAntConfig, decodeAntControllers, getAntRecordDecoder, getAntRecordMetadataDecoder, } from '@ar.io/solana-contracts/ant';
29
+ import { Logger } from '../common/logger.js';
30
+ import { SolanaANTRegistryReadable } from './ant-registry-readable.js';
31
+ import { ANT_CONFIG_VERSION, ARIO_ANT_PROGRAM_ID } from './constants.js';
32
+ import { getAntConfigPDA, getAntControllersPDA, getAntRecordMetadataPDA, getAntRecordPDA, } from './pda.js';
33
+ import { withRetry } from './retry.js';
34
+ /**
35
+ * Solana-backed read-only client for a single ANT (Arweave Name Token).
36
+ *
37
+ * Usage:
38
+ * ```ts
39
+ * import { createSolanaRpc } from '@solana/kit';
40
+ * import { SolanaANTReadable } from '@ar.io/sdk/solana';
41
+ *
42
+ * const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
43
+ * const ant = new SolanaANTReadable({
44
+ * rpc,
45
+ * processId: 'MetaplexCoreAssetAddress...',
46
+ * });
47
+ *
48
+ * const owner = await ant.getOwner();
49
+ * const record = await ant.getRecord({ undername: '@' });
50
+ * ```
51
+ */
52
+ export class SolanaANTReadable {
53
+ processId;
54
+ rpc;
55
+ commitment;
56
+ logger;
57
+ antProgram;
58
+ mint;
59
+ /**
60
+ * Composed registry instance — the source of truth for the per-user
61
+ * paginated ACL (ADR-012). Sharing one instance across the read and
62
+ * write classes keeps program id / commitment / RPC configuration
63
+ * coherent for both `accessControlList` reads and the maintenance
64
+ * planner used during writes.
65
+ */
66
+ registry;
67
+ constructor(config) {
68
+ this.processId = config.processId;
69
+ this.rpc = config.rpc;
70
+ this.commitment = config.commitment ?? 'confirmed';
71
+ this.logger = config.logger ?? Logger.default;
72
+ this.antProgram = config.antProgramId ?? ARIO_ANT_PROGRAM_ID;
73
+ this.mint = address(config.processId);
74
+ this.registry =
75
+ config.registry ??
76
+ new SolanaANTRegistryReadable({
77
+ rpc: this.rpc,
78
+ commitment: this.commitment,
79
+ logger: this.logger,
80
+ antProgramId: this.antProgram,
81
+ });
82
+ }
83
+ /**
84
+ * Build a `SolanaANTReadable` whose program id is read from the
85
+ * asset's `ANT Program` Attributes-plugin entry (ADR-016 / BD-100).
86
+ *
87
+ * Falls back to the canonical `ARIO_ANT_PROGRAM_ID` when the asset
88
+ * has no plugin section, no `ANT Program` trait, or any layer of the
89
+ * walk fails to decode — matching the on-chain leniency in
90
+ * `programs/ario-core/src/mpl_core.rs::read_ant_program`. This is the
91
+ * factory resolution paths should reach for: it does the asset fetch
92
+ * once, hands the resulting program id to the constructor, and
93
+ * shares one `SolanaANTRegistryReadable` instance with the new ANT.
94
+ *
95
+ * Use the plain constructor when the program id is already known
96
+ * (e.g. inside a freshly-spawned ANT flow where you've just minted
97
+ * the asset and know the program you targeted).
98
+ */
99
+ static async fromAsset(config) {
100
+ const { fetchAntProgramFromAsset } = await import('./mpl-core.js');
101
+ const mint = address(config.processId);
102
+ const fromAsset = await fetchAntProgramFromAsset(config.rpc, mint, {
103
+ commitment: config.commitment ?? 'confirmed',
104
+ });
105
+ return new SolanaANTReadable({
106
+ rpc: config.rpc,
107
+ processId: config.processId,
108
+ commitment: config.commitment,
109
+ logger: config.logger,
110
+ antProgramId: fromAsset ?? ARIO_ANT_PROGRAM_ID,
111
+ });
112
+ }
113
+ async getAccount(pda) {
114
+ return withRetry(() => fetchEncodedAccount(this.rpc, pda, {
115
+ commitment: this.commitment,
116
+ }));
117
+ }
118
+ // =========================================
119
+ // Config reads
120
+ // =========================================
121
+ async fetchConfig() {
122
+ const [pda] = await getAntConfigPDA(this.mint, this.antProgram);
123
+ const account = await this.getAccount(pda);
124
+ if (!account.exists) {
125
+ throw new Error(`ANT config not found for ${this.processId}`);
126
+ }
127
+ const decoded = decodeAntConfig(account).data;
128
+ return {
129
+ mint: decoded.mint,
130
+ name: decoded.name,
131
+ ticker: decoded.ticker,
132
+ logo: decoded.logo,
133
+ description: decoded.description,
134
+ keywords: decoded.keywords,
135
+ owner: decoded.lastKnownOwner,
136
+ version: decoded.version.major,
137
+ };
138
+ }
139
+ async fetchControllers() {
140
+ const [pda] = await getAntControllersPDA(this.mint, this.antProgram);
141
+ const account = await this.getAccount(pda);
142
+ if (!account.exists) {
143
+ return { mint: this.processId, controllers: [] };
144
+ }
145
+ const decoded = decodeAntControllers(account).data;
146
+ return {
147
+ mint: decoded.mint,
148
+ controllers: decoded.controllers.map((c) => c),
149
+ };
150
+ }
151
+ /**
152
+ * Fetch AntConfig + AntControllers in a single `getMultipleAccounts` round
153
+ * trip (instead of two single-account reads). Used by `getState` to shave one
154
+ * RPC per ANT — meaningful when a UI loads many ANTs.
155
+ */
156
+ async _fetchConfigAndControllers() {
157
+ const [[configPda], [controllersPda]] = await Promise.all([
158
+ getAntConfigPDA(this.mint, this.antProgram),
159
+ getAntControllersPDA(this.mint, this.antProgram),
160
+ ]);
161
+ const [configAccount, controllersAccount] = await withRetry(() => fetchEncodedAccounts(this.rpc, [configPda, controllersPda], {
162
+ commitment: this.commitment,
163
+ }));
164
+ if (!configAccount.exists) {
165
+ throw new Error(`ANT config not found for ${this.processId}`);
166
+ }
167
+ const decodedConfig = decodeAntConfig(configAccount).data;
168
+ const config = {
169
+ mint: decodedConfig.mint,
170
+ name: decodedConfig.name,
171
+ ticker: decodedConfig.ticker,
172
+ logo: decodedConfig.logo,
173
+ description: decodedConfig.description,
174
+ keywords: decodedConfig.keywords,
175
+ owner: decodedConfig.lastKnownOwner,
176
+ version: decodedConfig.version.major,
177
+ };
178
+ const controllers = controllersAccount.exists
179
+ ? decodeAntControllers(controllersAccount).data.controllers.map((c) => c)
180
+ : [];
181
+ return { config, controllers };
182
+ }
183
+ async getOwner(_opts) {
184
+ const config = await this.fetchConfig();
185
+ return config.owner;
186
+ }
187
+ /** Get the on-chain schema version of this ANT's config. */
188
+ async getConfigVersion() {
189
+ const config = await this.fetchConfig();
190
+ return config.version;
191
+ }
192
+ /** Check if this ANT needs a schema migration to the latest version. */
193
+ async needsMigration() {
194
+ const version = await this.getConfigVersion();
195
+ return version < ANT_CONFIG_VERSION;
196
+ }
197
+ async getName(_opts) {
198
+ const config = await this.fetchConfig();
199
+ return config.name;
200
+ }
201
+ async getTicker(_opts) {
202
+ const config = await this.fetchConfig();
203
+ return config.ticker;
204
+ }
205
+ async getLogo(_opts) {
206
+ const config = await this.fetchConfig();
207
+ return config.logo;
208
+ }
209
+ async getControllers() {
210
+ const data = await this.fetchControllers();
211
+ return data.controllers;
212
+ }
213
+ // =========================================
214
+ // Record reads
215
+ // =========================================
216
+ async getRecord({ undername }, _opts) {
217
+ const [[recordPda], [metaPda]] = await Promise.all([
218
+ getAntRecordPDA(this.mint, undername, this.antProgram),
219
+ getAntRecordMetadataPDA(this.mint, undername, this.antProgram),
220
+ ]);
221
+ const [recordAccount, metaAccount] = await withRetry(() => fetchEncodedAccounts(this.rpc, [recordPda, metaPda], {
222
+ commitment: this.commitment,
223
+ }));
224
+ if (!recordAccount.exists)
225
+ return undefined;
226
+ const recordDecoder = getAntRecordDecoder();
227
+ const metaDecoder = getAntRecordMetadataDecoder();
228
+ const record = recordDecoder.decode(new Uint8Array(recordAccount.data));
229
+ const meta = metaAccount.exists
230
+ ? metaDecoder.decode(new Uint8Array(metaAccount.data))
231
+ : undefined;
232
+ return {
233
+ transactionId: record.target,
234
+ targetProtocol: record.targetProtocol,
235
+ ttlSeconds: record.ttlSeconds,
236
+ priority: record.priority?.__option === 'Some'
237
+ ? record.priority.value
238
+ : undefined,
239
+ owner: record.owner?.__option === 'Some'
240
+ ? record.owner.value
241
+ : undefined,
242
+ displayName: meta?.displayName?.__option === 'Some'
243
+ ? meta.displayName.value
244
+ : undefined,
245
+ logo: meta?.recordLogo?.__option === 'Some'
246
+ ? meta.recordLogo.value
247
+ : undefined,
248
+ description: meta?.recordDescription?.__option === 'Some'
249
+ ? meta.recordDescription.value
250
+ : undefined,
251
+ keywords: meta?.recordKeywords?.__option === 'Some'
252
+ ? meta.recordKeywords.value
253
+ : undefined,
254
+ };
255
+ }
256
+ async getRecords(opts) {
257
+ // Fetch all AntRecord accounts for this mint. AntRecordMetadata
258
+ // (displayName/logo/description/keywords) is a SECOND program scan and is
259
+ // only needed in detail/edit views, so skip it unless `includeMetadata` is
260
+ // set — halving the per-ANT request cost on list reads. See AntReadOptions.
261
+ const includeMetadata = opts?.includeMetadata === true;
262
+ const gpaFilter = (discriminator) => [
263
+ {
264
+ memcmp: {
265
+ offset: 0n,
266
+ bytes: discriminator,
267
+ encoding: 'base58',
268
+ },
269
+ },
270
+ {
271
+ memcmp: {
272
+ offset: 8n,
273
+ bytes: this.mint,
274
+ encoding: 'base58',
275
+ },
276
+ },
277
+ ];
278
+ const [recordAccounts, metaAccounts] = (await Promise.all([
279
+ withRetry(() => this.rpc
280
+ .getProgramAccounts(this.antProgram, {
281
+ commitment: this.commitment,
282
+ encoding: 'base64',
283
+ filters: gpaFilter(bs58.encode(ANT_RECORD_DISCRIMINATOR)),
284
+ })
285
+ .send()),
286
+ includeMetadata
287
+ ? withRetry(() => this.rpc
288
+ .getProgramAccounts(this.antProgram, {
289
+ commitment: this.commitment,
290
+ encoding: 'base64',
291
+ filters: gpaFilter(bs58.encode(ANT_RECORD_METADATA_DISCRIMINATOR)),
292
+ })
293
+ .send())
294
+ : Promise.resolve([]),
295
+ ]));
296
+ const recordDecoder = getAntRecordDecoder();
297
+ const metaDecoder = getAntRecordMetadataDecoder();
298
+ // Index metadata by undername hash for O(1) lookup.
299
+ // AntRecordMetadata has undername_hash at offset 40 (8 disc + 32 mint).
300
+ const metaByHash = new Map();
301
+ for (const { account } of metaAccounts) {
302
+ try {
303
+ const buf = Buffer.from(account.data[0], 'base64');
304
+ const hash = buf.subarray(40, 72).toString('hex');
305
+ metaByHash.set(hash, metaDecoder.decode(new Uint8Array(buf)));
306
+ }
307
+ catch {
308
+ // Skip malformed
309
+ }
310
+ }
311
+ const result = {};
312
+ let index = 0;
313
+ for (const { account } of recordAccounts) {
314
+ try {
315
+ const buf = Buffer.from(account.data[0], 'base64');
316
+ const record = recordDecoder.decode(new Uint8Array(buf));
317
+ const hash = __createHash('sha256')
318
+ .update(record.undername.toLowerCase())
319
+ .digest('hex');
320
+ const meta = metaByHash.get(hash);
321
+ result[record.undername] = {
322
+ transactionId: record.target,
323
+ targetProtocol: record.targetProtocol,
324
+ ttlSeconds: record.ttlSeconds,
325
+ priority: record.priority?.__option === 'Some'
326
+ ? record.priority.value
327
+ : undefined,
328
+ owner: record.owner?.__option === 'Some'
329
+ ? record.owner.value
330
+ : undefined,
331
+ displayName: meta?.displayName?.__option === 'Some'
332
+ ? meta.displayName.value
333
+ : undefined,
334
+ logo: meta?.recordLogo?.__option === 'Some'
335
+ ? meta.recordLogo.value
336
+ : undefined,
337
+ description: meta?.recordDescription?.__option === 'Some'
338
+ ? meta.recordDescription.value
339
+ : undefined,
340
+ keywords: meta?.recordKeywords?.__option === 'Some'
341
+ ? meta.recordKeywords.value
342
+ : undefined,
343
+ index: index++,
344
+ };
345
+ }
346
+ catch {
347
+ // Skip malformed
348
+ }
349
+ }
350
+ return result;
351
+ }
352
+ /**
353
+ * Bulk-load lightweight {@link ANTSummary} state for many ANTs in a handful
354
+ * of `getMultipleAccounts` calls instead of `N × getState`. For each mint it
355
+ * batches AntConfig + AntControllers + the apex (`@`) AntRecord — everything a
356
+ * portfolio/names table needs. Full undername records are NOT loaded here;
357
+ * fetch them lazily per-ANT via {@link getRecords}/{@link getState} when a
358
+ * name is opened.
359
+ *
360
+ * Requests: ~`ceil(3N / 100)` calls for N mints (10 → 1, 250 → 8), versus
361
+ * ~`4N` with per-ANT `getState`. Assumes every mint is deployed under this
362
+ * instance's `antProgram` (true for the standard AR.IO ANT program).
363
+ *
364
+ * Mints whose AntConfig doesn't exist are omitted from the result.
365
+ */
366
+ async getANTSummaries(mints) {
367
+ const unique = Array.from(new Set(mints));
368
+ if (unique.length === 0)
369
+ return {};
370
+ // Derive config + controllers + apex('@') record PDAs for every mint.
371
+ const triples = await Promise.all(unique.map(async (m) => {
372
+ const mintAddr = address(m);
373
+ const [[configPda], [controllersPda], [apexPda]] = await Promise.all([
374
+ getAntConfigPDA(mintAddr, this.antProgram),
375
+ getAntControllersPDA(mintAddr, this.antProgram),
376
+ getAntRecordPDA(mintAddr, '@', this.antProgram),
377
+ ]);
378
+ return { mint: m, configPda, controllersPda, apexPda };
379
+ }));
380
+ // Batch-fetch all PDAs (3 per mint) — getMultipleAccounts caps at 100.
381
+ const allPdas = triples.flatMap((t) => [
382
+ t.configPda,
383
+ t.controllersPda,
384
+ t.apexPda,
385
+ ]);
386
+ const accounts = [];
387
+ for (let i = 0; i < allPdas.length; i += 100) {
388
+ const chunk = allPdas.slice(i, i + 100);
389
+ const res = await withRetry(() => fetchEncodedAccounts(this.rpc, chunk, { commitment: this.commitment }));
390
+ accounts.push(...res);
391
+ }
392
+ const recordDecoder = getAntRecordDecoder();
393
+ const result = {};
394
+ for (let i = 0; i < triples.length; i++) {
395
+ const { mint } = triples[i];
396
+ const configAccount = accounts[i * 3];
397
+ const controllersAccount = accounts[i * 3 + 1];
398
+ const apexAccount = accounts[i * 3 + 2];
399
+ if (!configAccount?.exists)
400
+ continue;
401
+ const config = decodeAntConfig(configAccount).data;
402
+ const controllers = controllersAccount?.exists
403
+ ? decodeAntControllers(controllersAccount).data.controllers.map((c) => c)
404
+ : [];
405
+ let apexRecord;
406
+ if (apexAccount?.exists) {
407
+ const rec = recordDecoder.decode(new Uint8Array(apexAccount.data));
408
+ apexRecord = {
409
+ transactionId: rec.target,
410
+ targetProtocol: rec.targetProtocol,
411
+ ttlSeconds: rec.ttlSeconds,
412
+ priority: rec.priority?.__option === 'Some' ? rec.priority.value : undefined,
413
+ owner: rec.owner?.__option === 'Some'
414
+ ? rec.owner.value
415
+ : undefined,
416
+ };
417
+ }
418
+ result[mint] = {
419
+ processId: mint,
420
+ name: config.name,
421
+ ticker: config.ticker,
422
+ logo: config.logo,
423
+ description: config.description,
424
+ keywords: config.keywords,
425
+ owner: config.lastKnownOwner,
426
+ controllers,
427
+ apexRecord,
428
+ };
429
+ }
430
+ return result;
431
+ }
432
+ /**
433
+ * Bulk-load FULL {@link ANTState} (including all undername records) for many
434
+ * ANTs in a handful of calls instead of `N × getState`:
435
+ * - AntConfig + AntControllers for every mint via `getMultipleAccounts`
436
+ * (chunked at 100), and
437
+ * - ALL undername records via a SINGLE program-wide `getProgramAccounts`
438
+ * scan grouped by mint (offset 8), instead of one mint-filtered scan per
439
+ * ANT.
440
+ *
441
+ * Requests: ~`ceil(2N / 100) + 1` (+1 when `includeMetadata`) regardless of
442
+ * N — e.g. 10 ANTs → 2 calls, 250 → ~6 — versus ~`2N` with per-ANT
443
+ * `getState`. The records scan reads every ANT's records program-wide (cheap
444
+ * per account, one round trip); prefer per-ANT {@link getState} when you only
445
+ * need one ANT. Mints with no AntConfig are omitted.
446
+ */
447
+ async getANTStates(mints, opts) {
448
+ const unique = Array.from(new Set(mints));
449
+ if (unique.length === 0)
450
+ return {};
451
+ // Config + controllers PDAs for every mint, batched (100 accounts/call).
452
+ const pairs = await Promise.all(unique.map(async (m) => {
453
+ const mintAddr = address(m);
454
+ const [[configPda], [controllersPda]] = await Promise.all([
455
+ getAntConfigPDA(mintAddr, this.antProgram),
456
+ getAntControllersPDA(mintAddr, this.antProgram),
457
+ ]);
458
+ return { mint: m, configPda, controllersPda };
459
+ }));
460
+ const allPdas = pairs.flatMap((p) => [p.configPda, p.controllersPda]);
461
+ const accounts = [];
462
+ for (let i = 0; i < allPdas.length; i += 100) {
463
+ const res = await withRetry(() => fetchEncodedAccounts(this.rpc, allPdas.slice(i, i + 100), {
464
+ commitment: this.commitment,
465
+ }));
466
+ accounts.push(...res);
467
+ }
468
+ const recordsByMint = await this._recordsByMint(opts?.includeMetadata === true);
469
+ const result = {};
470
+ for (let i = 0; i < pairs.length; i++) {
471
+ const { mint } = pairs[i];
472
+ const configAccount = accounts[i * 2];
473
+ const controllersAccount = accounts[i * 2 + 1];
474
+ if (!configAccount?.exists)
475
+ continue;
476
+ const config = decodeAntConfig(configAccount).data;
477
+ const controllers = controllersAccount?.exists
478
+ ? decodeAntControllers(controllersAccount).data.controllers.map((c) => c)
479
+ : [];
480
+ const sorted = recordsByMint.get(mint) ?? {};
481
+ const plainRecords = {};
482
+ for (const [key, val] of Object.entries(sorted)) {
483
+ const { index: _, ...rec } = val;
484
+ plainRecords[key] = rec;
485
+ }
486
+ const owner = config.lastKnownOwner;
487
+ result[mint] = {
488
+ Name: config.name,
489
+ Ticker: config.ticker,
490
+ Description: config.description,
491
+ Keywords: config.keywords,
492
+ Denomination: 0,
493
+ Owner: owner,
494
+ Controllers: controllers,
495
+ Records: plainRecords,
496
+ Balances: { [owner]: 1 },
497
+ Logo: config.logo,
498
+ TotalSupply: 1,
499
+ Initialized: true,
500
+ };
501
+ }
502
+ return result;
503
+ }
504
+ /**
505
+ * Group every AntRecord (+ optional metadata) in the program by mint via a
506
+ * single `getProgramAccounts` scan (the mint sits at offset 8). Used by
507
+ * {@link getANTStates} to load all ANTs' undername records in one round trip
508
+ * instead of one mint-filtered scan per ANT.
509
+ */
510
+ async _recordsByMint(includeMetadata) {
511
+ const discFilter = (discriminator) => [
512
+ {
513
+ memcmp: {
514
+ offset: 0n,
515
+ bytes: discriminator,
516
+ encoding: 'base58',
517
+ },
518
+ },
519
+ ];
520
+ const [recordAccounts, metaAccounts] = (await Promise.all([
521
+ withRetry(() => this.rpc
522
+ .getProgramAccounts(this.antProgram, {
523
+ commitment: this.commitment,
524
+ encoding: 'base64',
525
+ filters: discFilter(bs58.encode(ANT_RECORD_DISCRIMINATOR)),
526
+ })
527
+ .send()),
528
+ includeMetadata
529
+ ? withRetry(() => this.rpc
530
+ .getProgramAccounts(this.antProgram, {
531
+ commitment: this.commitment,
532
+ encoding: 'base64',
533
+ filters: discFilter(bs58.encode(ANT_RECORD_METADATA_DISCRIMINATOR)),
534
+ })
535
+ .send())
536
+ : Promise.resolve([]),
537
+ ]));
538
+ const recordDecoder = getAntRecordDecoder();
539
+ const metaDecoder = getAntRecordMetadataDecoder();
540
+ // Metadata keyed by `${mint}:${undernameHash}` (mint at 8, hash at 40).
541
+ const metaByKey = new Map();
542
+ for (const { account } of metaAccounts) {
543
+ try {
544
+ const buf = Buffer.from(account.data[0], 'base64');
545
+ const mint = bs58.encode(buf.subarray(8, 40));
546
+ const hash = buf.subarray(40, 72).toString('hex');
547
+ metaByKey.set(`${mint}:${hash}`, metaDecoder.decode(new Uint8Array(buf)));
548
+ }
549
+ catch {
550
+ // Skip malformed
551
+ }
552
+ }
553
+ const byMint = new Map();
554
+ const indexByMint = new Map();
555
+ for (const { account } of recordAccounts) {
556
+ try {
557
+ const buf = Buffer.from(account.data[0], 'base64');
558
+ const mint = bs58.encode(buf.subarray(8, 40));
559
+ const record = recordDecoder.decode(new Uint8Array(buf));
560
+ const hash = __createHash('sha256')
561
+ .update(record.undername.toLowerCase())
562
+ .digest('hex');
563
+ const meta = metaByKey.get(`${mint}:${hash}`);
564
+ const idx = indexByMint.get(mint) ?? 0;
565
+ let bucket = byMint.get(mint);
566
+ if (!bucket) {
567
+ bucket = {};
568
+ byMint.set(mint, bucket);
569
+ }
570
+ bucket[record.undername] = {
571
+ transactionId: record.target,
572
+ targetProtocol: record.targetProtocol,
573
+ ttlSeconds: record.ttlSeconds,
574
+ priority: record.priority?.__option === 'Some'
575
+ ? record.priority.value
576
+ : undefined,
577
+ owner: record.owner?.__option === 'Some'
578
+ ? record.owner.value
579
+ : undefined,
580
+ displayName: meta?.displayName?.__option === 'Some'
581
+ ? meta.displayName.value
582
+ : undefined,
583
+ logo: meta?.recordLogo?.__option === 'Some'
584
+ ? meta.recordLogo.value
585
+ : undefined,
586
+ description: meta?.recordDescription?.__option === 'Some'
587
+ ? meta.recordDescription.value
588
+ : undefined,
589
+ keywords: meta?.recordKeywords?.__option === 'Some'
590
+ ? meta.recordKeywords.value
591
+ : undefined,
592
+ index: idx,
593
+ };
594
+ indexByMint.set(mint, idx + 1);
595
+ }
596
+ catch {
597
+ // Skip malformed
598
+ }
599
+ }
600
+ return byMint;
601
+ }
602
+ // =========================================
603
+ // Balance reads (NFT model — owner has balance 1)
604
+ // =========================================
605
+ async getBalance({ address: queryAddress }, _opts) {
606
+ const config = await this.fetchConfig();
607
+ return config.owner === queryAddress ? 1 : 0;
608
+ }
609
+ async getBalances(_opts) {
610
+ const config = await this.fetchConfig();
611
+ return { [config.owner]: 1 };
612
+ }
613
+ // =========================================
614
+ // State / Info composites
615
+ // =========================================
616
+ async getState(opts) {
617
+ const [{ config, controllers }, records] = await Promise.all([
618
+ this._fetchConfigAndControllers(),
619
+ this.getRecords(opts),
620
+ ]);
621
+ // Convert SortedANTRecords to ANTRecords (strip index)
622
+ const plainRecords = {};
623
+ for (const [key, val] of Object.entries(records)) {
624
+ const { index: _, ...record } = val;
625
+ plainRecords[key] = record;
626
+ }
627
+ return {
628
+ Name: config.name,
629
+ Ticker: config.ticker,
630
+ Description: config.description,
631
+ Keywords: config.keywords,
632
+ Denomination: 0,
633
+ Owner: config.owner,
634
+ Controllers: controllers,
635
+ Records: plainRecords,
636
+ Balances: { [config.owner]: 1 },
637
+ Logo: config.logo,
638
+ TotalSupply: 1,
639
+ Initialized: true,
640
+ };
641
+ }
642
+ async getInfo(_opts) {
643
+ const config = await this.fetchConfig();
644
+ return {
645
+ Name: config.name,
646
+ Owner: config.owner,
647
+ Ticker: config.ticker,
648
+ 'Total-Supply': '1',
649
+ Description: config.description,
650
+ Keywords: config.keywords,
651
+ Logo: config.logo,
652
+ Denomination: '0',
653
+ Handlers: [
654
+ 'balance',
655
+ 'balances',
656
+ 'totalSupply',
657
+ 'info',
658
+ 'controllers',
659
+ 'record',
660
+ 'records',
661
+ 'state',
662
+ 'transfer',
663
+ 'addController',
664
+ 'removeController',
665
+ 'setRecord',
666
+ 'removeRecord',
667
+ 'setName',
668
+ 'setTicker',
669
+ 'setDescription',
670
+ 'setKeywords',
671
+ 'setLogo',
672
+ 'initializeState',
673
+ 'releaseName',
674
+ 'reassignName',
675
+ 'approvePrimaryName',
676
+ 'removePrimaryNames',
677
+ 'transferRecordOwnership',
678
+ '_eval',
679
+ '_default',
680
+ ],
681
+ };
682
+ }
683
+ async getHandlers() {
684
+ // Solana ANT supports all standard handlers
685
+ return [
686
+ 'balance',
687
+ 'balances',
688
+ 'totalSupply',
689
+ 'info',
690
+ 'controllers',
691
+ 'record',
692
+ 'records',
693
+ 'state',
694
+ 'transfer',
695
+ 'addController',
696
+ 'removeController',
697
+ 'setRecord',
698
+ 'removeRecord',
699
+ 'setName',
700
+ 'setTicker',
701
+ 'setDescription',
702
+ 'setKeywords',
703
+ 'setLogo',
704
+ 'initializeState',
705
+ 'releaseName',
706
+ 'reassignName',
707
+ 'approvePrimaryName',
708
+ 'removePrimaryNames',
709
+ 'transferRecordOwnership',
710
+ '_eval',
711
+ '_default',
712
+ ];
713
+ }
714
+ async getModuleId(_opts) {
715
+ // Solana programs don't have module IDs — return the program address
716
+ return this.antProgram;
717
+ }
718
+ async getVersion(_opts) {
719
+ return '1.0.0-solana';
720
+ }
721
+ async isLatestVersion(_opts) {
722
+ return true;
723
+ }
724
+ }