@ar.io/sdk 4.0.0-solana.34 → 4.0.0-solana.35
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.
- package/lib/esm/solana/ant-readable.js +306 -20
- package/lib/esm/solana/io-readable.js +57 -16
- package/lib/esm/version.js +1 -1
- package/lib/types/solana/ant-readable.d.ts +47 -3
- package/lib/types/solana/io-readable.d.ts +8 -0
- package/lib/types/types/ant.d.ts +27 -0
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* - AntControllers: list of controller pubkeys
|
|
23
23
|
* - AntRecord: undername records (transactionId, ttl, priority, etc.)
|
|
24
24
|
*/
|
|
25
|
-
import { address, fetchEncodedAccount, } from '@solana/kit';
|
|
25
|
+
import { address, fetchEncodedAccount, fetchEncodedAccounts, } from '@solana/kit';
|
|
26
26
|
import bs58 from 'bs58';
|
|
27
27
|
import { createHash as __createHash } from 'crypto';
|
|
28
28
|
import { ANT_RECORD_DISCRIMINATOR, ANT_RECORD_METADATA_DISCRIMINATOR, decodeAntConfig, decodeAntControllers, getAntRecordDecoder, getAntRecordMetadataDecoder, } from '@ar.io/solana-contracts/ant';
|
|
@@ -148,6 +148,38 @@ export class SolanaANTReadable {
|
|
|
148
148
|
controllers: decoded.controllers.map((c) => c),
|
|
149
149
|
};
|
|
150
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
|
+
}
|
|
151
183
|
async getOwner(_opts) {
|
|
152
184
|
const config = await this.fetchConfig();
|
|
153
185
|
return config.owner;
|
|
@@ -186,10 +218,9 @@ export class SolanaANTReadable {
|
|
|
186
218
|
getAntRecordPDA(this.mint, undername, this.antProgram),
|
|
187
219
|
getAntRecordMetadataPDA(this.mint, undername, this.antProgram),
|
|
188
220
|
]);
|
|
189
|
-
const [recordAccount, metaAccount] = await
|
|
190
|
-
this.
|
|
191
|
-
|
|
192
|
-
]);
|
|
221
|
+
const [recordAccount, metaAccount] = await withRetry(() => fetchEncodedAccounts(this.rpc, [recordPda, metaPda], {
|
|
222
|
+
commitment: this.commitment,
|
|
223
|
+
}));
|
|
193
224
|
if (!recordAccount.exists)
|
|
194
225
|
return undefined;
|
|
195
226
|
const recordDecoder = getAntRecordDecoder();
|
|
@@ -222,8 +253,12 @@ export class SolanaANTReadable {
|
|
|
222
253
|
: undefined,
|
|
223
254
|
};
|
|
224
255
|
}
|
|
225
|
-
async getRecords(
|
|
226
|
-
// Fetch all AntRecord
|
|
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;
|
|
227
262
|
const gpaFilter = (discriminator) => [
|
|
228
263
|
{
|
|
229
264
|
memcmp: {
|
|
@@ -248,13 +283,15 @@ export class SolanaANTReadable {
|
|
|
248
283
|
filters: gpaFilter(bs58.encode(ANT_RECORD_DISCRIMINATOR)),
|
|
249
284
|
})
|
|
250
285
|
.send()),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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([]),
|
|
258
295
|
]));
|
|
259
296
|
const recordDecoder = getAntRecordDecoder();
|
|
260
297
|
const metaDecoder = getAntRecordMetadataDecoder();
|
|
@@ -312,6 +349,256 @@ export class SolanaANTReadable {
|
|
|
312
349
|
}
|
|
313
350
|
return result;
|
|
314
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
|
+
}
|
|
315
602
|
// =========================================
|
|
316
603
|
// Balance reads (NFT model — owner has balance 1)
|
|
317
604
|
// =========================================
|
|
@@ -326,11 +613,10 @@ export class SolanaANTReadable {
|
|
|
326
613
|
// =========================================
|
|
327
614
|
// State / Info composites
|
|
328
615
|
// =========================================
|
|
329
|
-
async getState(
|
|
330
|
-
const [config,
|
|
331
|
-
this.
|
|
332
|
-
this.
|
|
333
|
-
this.getRecords(),
|
|
616
|
+
async getState(opts) {
|
|
617
|
+
const [{ config, controllers }, records] = await Promise.all([
|
|
618
|
+
this._fetchConfigAndControllers(),
|
|
619
|
+
this.getRecords(opts),
|
|
334
620
|
]);
|
|
335
621
|
// Convert SortedANTRecords to ANTRecords (strip index)
|
|
336
622
|
const plainRecords = {};
|
|
@@ -345,7 +631,7 @@ export class SolanaANTReadable {
|
|
|
345
631
|
Keywords: config.keywords,
|
|
346
632
|
Denomination: 0,
|
|
347
633
|
Owner: config.owner,
|
|
348
|
-
Controllers:
|
|
634
|
+
Controllers: controllers,
|
|
349
635
|
Records: plainRecords,
|
|
350
636
|
Balances: { [config.owner]: 1 },
|
|
351
637
|
Logo: config.logo,
|
|
@@ -105,6 +105,12 @@ function arnsRecordToWithName(record) {
|
|
|
105
105
|
: {}),
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* TTL for {@link SolanaARIOReadable.getCachedAccount}. DemandFactor / config
|
|
110
|
+
* accounts change on the order of epochs, so a few seconds of staleness is a
|
|
111
|
+
* safe trade for collapsing burst reads (e.g. per-row cost lookups).
|
|
112
|
+
*/
|
|
113
|
+
const CONFIG_CACHE_TTL_MS = 30_000;
|
|
108
114
|
function paginate(items, params) {
|
|
109
115
|
const limit = params?.limit ?? 100;
|
|
110
116
|
const startIdx = params?.cursor ? parseInt(params.cursor, 10) : 0;
|
|
@@ -146,6 +152,11 @@ export class SolanaARIOReadable {
|
|
|
146
152
|
// Memoized ARIO mint address (read once from ArioConfig.mint and reused
|
|
147
153
|
// for every SPL-ATA derivation in getBalance/getBalances).
|
|
148
154
|
_arioMint;
|
|
155
|
+
// Short-TTL cache for slow-changing config-ish accounts (DemandFactor,
|
|
156
|
+
// ArnsConfig, etc.). Collapses the many identical reads a UI fires in a
|
|
157
|
+
// burst — e.g. the returned-names page running `getCostDetails` per row,
|
|
158
|
+
// each re-reading the same DemandFactor PDA — into a single network call.
|
|
159
|
+
_accountCache = new Map();
|
|
149
160
|
constructor(config) {
|
|
150
161
|
this.rpc = config.rpc;
|
|
151
162
|
this.commitment = config.commitment ?? 'confirmed';
|
|
@@ -161,6 +172,24 @@ export class SolanaARIOReadable {
|
|
|
161
172
|
commitment: this.commitment,
|
|
162
173
|
}));
|
|
163
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Like {@link getAccount} but caches the result per-PDA for `ttlMs`. Use only
|
|
177
|
+
* for accounts that change slowly (DemandFactor, ArnsConfig) where a few
|
|
178
|
+
* seconds of staleness is acceptable in exchange for collapsing repeated
|
|
179
|
+
* reads. A successful fetch is cached; misses (`exists: false`) are not.
|
|
180
|
+
*/
|
|
181
|
+
async getCachedAccount(pda, ttlMs = CONFIG_CACHE_TTL_MS) {
|
|
182
|
+
const key = String(pda);
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
const hit = this._accountCache.get(key);
|
|
185
|
+
if (hit && hit.expiresAt > now)
|
|
186
|
+
return hit.account;
|
|
187
|
+
const account = await this.getAccount(pda);
|
|
188
|
+
if (account.exists) {
|
|
189
|
+
this._accountCache.set(key, { account, expiresAt: now + ttlMs });
|
|
190
|
+
}
|
|
191
|
+
return account;
|
|
192
|
+
}
|
|
164
193
|
/**
|
|
165
194
|
* Helper for `getProgramAccounts` with a discriminator memcmp filter.
|
|
166
195
|
*
|
|
@@ -959,7 +988,11 @@ export class SolanaARIOReadable {
|
|
|
959
988
|
}
|
|
960
989
|
async getArNSReturnedName({ name, }) {
|
|
961
990
|
const [pda] = await getReturnedNamePDA(name, this.arnsProgram);
|
|
962
|
-
|
|
991
|
+
// A ReturnedName account is immutable once created (its premium is derived
|
|
992
|
+
// client-side from the static start/end timestamps), so cache the read.
|
|
993
|
+
// The returned-names price table reads each name's PDA twice (lease +
|
|
994
|
+
// permabuy) and `getTokenCost` reads it again — all share one fetch.
|
|
995
|
+
const account = await this.getCachedAccount(pda);
|
|
963
996
|
if (!account.exists) {
|
|
964
997
|
throw new Error(`Returned name not found: ${name}`);
|
|
965
998
|
}
|
|
@@ -1239,7 +1272,7 @@ export class SolanaARIOReadable {
|
|
|
1239
1272
|
*/
|
|
1240
1273
|
async getTokenCost(params) {
|
|
1241
1274
|
const [dfPda] = await getDemandFactorPDA(this.arnsProgram);
|
|
1242
|
-
const dfAccount = await this.
|
|
1275
|
+
const dfAccount = await this.getCachedAccount(dfPda);
|
|
1243
1276
|
if (!dfAccount.exists)
|
|
1244
1277
|
throw new Error('DemandFactor account not found');
|
|
1245
1278
|
const df = deserializeDemandFactor(Buffer.from(dfAccount.data));
|
|
@@ -1327,14 +1360,22 @@ export class SolanaARIOReadable {
|
|
|
1327
1360
|
const discounts = [];
|
|
1328
1361
|
if (params.fromAddress) {
|
|
1329
1362
|
try {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1363
|
+
// Operator-discount check. Read the gateway PDA through the short-TTL
|
|
1364
|
+
// cache (NOT public `getGateway`, which stays fresh for gateway pages):
|
|
1365
|
+
// a price table calls `getCostDetails` many times for the SAME
|
|
1366
|
+
// `fromAddress`, so this collapses N redundant gateway reads to one.
|
|
1367
|
+
const [gwPda] = await getGatewayPDA(address(params.fromAddress), this.garProgram);
|
|
1368
|
+
const gwAccount = await this.getCachedAccount(gwPda);
|
|
1369
|
+
if (gwAccount.exists) {
|
|
1370
|
+
const gw = deserializeGateway(Buffer.from(gwAccount.data));
|
|
1371
|
+
if (gw.status === 'joined') {
|
|
1372
|
+
const discountAmount = Math.floor((tokenCost * 200_000) / RATE_SCALE);
|
|
1373
|
+
discounts.push({
|
|
1374
|
+
name: 'Gateway Operator',
|
|
1375
|
+
discountTotal: discountAmount,
|
|
1376
|
+
multiplier: 0.8,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1338
1379
|
}
|
|
1339
1380
|
}
|
|
1340
1381
|
catch {
|
|
@@ -1441,7 +1482,7 @@ export class SolanaARIOReadable {
|
|
|
1441
1482
|
}
|
|
1442
1483
|
async getRegistrationFees() {
|
|
1443
1484
|
const [dfPda] = await getDemandFactorPDA(this.arnsProgram);
|
|
1444
|
-
const account = await this.
|
|
1485
|
+
const account = await this.getCachedAccount(dfPda);
|
|
1445
1486
|
if (!account.exists) {
|
|
1446
1487
|
throw new Error('DemandFactor account not found');
|
|
1447
1488
|
}
|
|
@@ -1464,7 +1505,7 @@ export class SolanaARIOReadable {
|
|
|
1464
1505
|
}
|
|
1465
1506
|
async getDemandFactor() {
|
|
1466
1507
|
const [pda] = await getDemandFactorPDA(this.arnsProgram);
|
|
1467
|
-
const account = await this.
|
|
1508
|
+
const account = await this.getCachedAccount(pda);
|
|
1468
1509
|
if (!account.exists) {
|
|
1469
1510
|
throw new Error('DemandFactor account not found');
|
|
1470
1511
|
}
|
|
@@ -1473,7 +1514,7 @@ export class SolanaARIOReadable {
|
|
|
1473
1514
|
}
|
|
1474
1515
|
async getDemandFactorSettings() {
|
|
1475
1516
|
const [pda] = await getDemandFactorPDA(this.arnsProgram);
|
|
1476
|
-
const account = await this.
|
|
1517
|
+
const account = await this.getCachedAccount(pda);
|
|
1477
1518
|
if (!account.exists) {
|
|
1478
1519
|
throw new Error('DemandFactor account not found');
|
|
1479
1520
|
}
|
|
@@ -1730,7 +1771,7 @@ export class SolanaARIOReadable {
|
|
|
1730
1771
|
*/
|
|
1731
1772
|
async getExpiredArnsRecords(now) {
|
|
1732
1773
|
const [arnsConfigPda] = await getArnsSettingsPDA(this.arnsProgram);
|
|
1733
|
-
const cfgAccount = await this.
|
|
1774
|
+
const cfgAccount = await this.getCachedAccount(arnsConfigPda);
|
|
1734
1775
|
if (!cfgAccount.exists)
|
|
1735
1776
|
return [];
|
|
1736
1777
|
const cfg = getArnsConfigDecoder().decode(cfgAccount.data);
|
|
@@ -1765,7 +1806,7 @@ export class SolanaARIOReadable {
|
|
|
1765
1806
|
*/
|
|
1766
1807
|
async getExpiredReturnedNames(now) {
|
|
1767
1808
|
const [arnsConfigPda] = await getArnsSettingsPDA(this.arnsProgram);
|
|
1768
|
-
const cfgAccount = await this.
|
|
1809
|
+
const cfgAccount = await this.getCachedAccount(arnsConfigPda);
|
|
1769
1810
|
if (!cfgAccount.exists)
|
|
1770
1811
|
return [];
|
|
1771
1812
|
const cfg = getArnsConfigDecoder().decode(cfgAccount.data);
|
|
@@ -2011,7 +2052,7 @@ export class SolanaARIOReadable {
|
|
|
2011
2052
|
*/
|
|
2012
2053
|
async getArnsConfigRaw() {
|
|
2013
2054
|
const [pda] = await getArnsSettingsPDA(this.arnsProgram);
|
|
2014
|
-
const account = await this.
|
|
2055
|
+
const account = await this.getCachedAccount(pda);
|
|
2015
2056
|
if (!account.exists)
|
|
2016
2057
|
return null;
|
|
2017
2058
|
const cfg = getArnsConfigDecoder().decode(account.data);
|
package/lib/esm/version.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { type Address, type Commitment } from '@solana/kit';
|
|
26
26
|
import { type ILogger } from '../common/logger.js';
|
|
27
|
-
import type { ANTHandler, ANTInfo, ANTRecord, ANTState, AntReadOptions, SortedANTRecords } from '../types/ant.js';
|
|
27
|
+
import type { ANTHandler, ANTInfo, ANTRecord, ANTState, ANTSummary, AntReadOptions, SortedANTRecords } from '../types/ant.js';
|
|
28
28
|
import type { WalletAddress } from '../types/common.js';
|
|
29
29
|
import { SolanaANTRegistryReadable } from './ant-registry-readable.js';
|
|
30
30
|
import type { SolanaRpc } from './types.js';
|
|
@@ -99,6 +99,12 @@ export declare class SolanaANTReadable {
|
|
|
99
99
|
private getAccount;
|
|
100
100
|
private fetchConfig;
|
|
101
101
|
private fetchControllers;
|
|
102
|
+
/**
|
|
103
|
+
* Fetch AntConfig + AntControllers in a single `getMultipleAccounts` round
|
|
104
|
+
* trip (instead of two single-account reads). Used by `getState` to shave one
|
|
105
|
+
* RPC per ANT — meaningful when a UI loads many ANTs.
|
|
106
|
+
*/
|
|
107
|
+
private _fetchConfigAndControllers;
|
|
102
108
|
getOwner(_opts?: AntReadOptions): Promise<WalletAddress>;
|
|
103
109
|
/** Get the on-chain schema version of this ANT's config. */
|
|
104
110
|
getConfigVersion(): Promise<number>;
|
|
@@ -111,12 +117,50 @@ export declare class SolanaANTReadable {
|
|
|
111
117
|
getRecord({ undername }: {
|
|
112
118
|
undername: string;
|
|
113
119
|
}, _opts?: AntReadOptions): Promise<ANTRecord | undefined>;
|
|
114
|
-
getRecords(
|
|
120
|
+
getRecords(opts?: AntReadOptions): Promise<SortedANTRecords>;
|
|
121
|
+
/**
|
|
122
|
+
* Bulk-load lightweight {@link ANTSummary} state for many ANTs in a handful
|
|
123
|
+
* of `getMultipleAccounts` calls instead of `N × getState`. For each mint it
|
|
124
|
+
* batches AntConfig + AntControllers + the apex (`@`) AntRecord — everything a
|
|
125
|
+
* portfolio/names table needs. Full undername records are NOT loaded here;
|
|
126
|
+
* fetch them lazily per-ANT via {@link getRecords}/{@link getState} when a
|
|
127
|
+
* name is opened.
|
|
128
|
+
*
|
|
129
|
+
* Requests: ~`ceil(3N / 100)` calls for N mints (10 → 1, 250 → 8), versus
|
|
130
|
+
* ~`4N` with per-ANT `getState`. Assumes every mint is deployed under this
|
|
131
|
+
* instance's `antProgram` (true for the standard AR.IO ANT program).
|
|
132
|
+
*
|
|
133
|
+
* Mints whose AntConfig doesn't exist are omitted from the result.
|
|
134
|
+
*/
|
|
135
|
+
getANTSummaries(mints: ReadonlyArray<string>): Promise<Record<string, ANTSummary>>;
|
|
136
|
+
/**
|
|
137
|
+
* Bulk-load FULL {@link ANTState} (including all undername records) for many
|
|
138
|
+
* ANTs in a handful of calls instead of `N × getState`:
|
|
139
|
+
* - AntConfig + AntControllers for every mint via `getMultipleAccounts`
|
|
140
|
+
* (chunked at 100), and
|
|
141
|
+
* - ALL undername records via a SINGLE program-wide `getProgramAccounts`
|
|
142
|
+
* scan grouped by mint (offset 8), instead of one mint-filtered scan per
|
|
143
|
+
* ANT.
|
|
144
|
+
*
|
|
145
|
+
* Requests: ~`ceil(2N / 100) + 1` (+1 when `includeMetadata`) regardless of
|
|
146
|
+
* N — e.g. 10 ANTs → 2 calls, 250 → ~6 — versus ~`2N` with per-ANT
|
|
147
|
+
* `getState`. The records scan reads every ANT's records program-wide (cheap
|
|
148
|
+
* per account, one round trip); prefer per-ANT {@link getState} when you only
|
|
149
|
+
* need one ANT. Mints with no AntConfig are omitted.
|
|
150
|
+
*/
|
|
151
|
+
getANTStates(mints: ReadonlyArray<string>, opts?: AntReadOptions): Promise<Record<string, ANTState>>;
|
|
152
|
+
/**
|
|
153
|
+
* Group every AntRecord (+ optional metadata) in the program by mint via a
|
|
154
|
+
* single `getProgramAccounts` scan (the mint sits at offset 8). Used by
|
|
155
|
+
* {@link getANTStates} to load all ANTs' undername records in one round trip
|
|
156
|
+
* instead of one mint-filtered scan per ANT.
|
|
157
|
+
*/
|
|
158
|
+
private _recordsByMint;
|
|
115
159
|
getBalance({ address: queryAddress }: {
|
|
116
160
|
address: WalletAddress;
|
|
117
161
|
}, _opts?: AntReadOptions): Promise<number>;
|
|
118
162
|
getBalances(_opts?: AntReadOptions): Promise<Record<WalletAddress, number>>;
|
|
119
|
-
getState(
|
|
163
|
+
getState(opts?: AntReadOptions): Promise<ANTState>;
|
|
120
164
|
getInfo(_opts?: AntReadOptions): Promise<ANTInfo>;
|
|
121
165
|
getHandlers(): Promise<ANTHandler[]>;
|
|
122
166
|
getModuleId(_opts?: {
|
|
@@ -49,6 +49,7 @@ export declare class SolanaARIOReadable {
|
|
|
49
49
|
protected readonly arnsProgram: Address;
|
|
50
50
|
protected readonly antProgram: Address;
|
|
51
51
|
private _arioMint?;
|
|
52
|
+
private _accountCache;
|
|
52
53
|
constructor(config: SolanaReadConfig & {
|
|
53
54
|
logger?: ILogger;
|
|
54
55
|
coreProgramId?: Address;
|
|
@@ -58,6 +59,13 @@ export declare class SolanaARIOReadable {
|
|
|
58
59
|
});
|
|
59
60
|
/** Helper to fetch an encoded account (kit's replacement for Connection.getAccountInfo). */
|
|
60
61
|
private getAccount;
|
|
62
|
+
/**
|
|
63
|
+
* Like {@link getAccount} but caches the result per-PDA for `ttlMs`. Use only
|
|
64
|
+
* for accounts that change slowly (DemandFactor, ArnsConfig) where a few
|
|
65
|
+
* seconds of staleness is acceptable in exchange for collapsing repeated
|
|
66
|
+
* reads. A successful fetch is cached; misses (`exists: false`) are not.
|
|
67
|
+
*/
|
|
68
|
+
private getCachedAccount;
|
|
61
69
|
/**
|
|
62
70
|
* Helper for `getProgramAccounts` with a discriminator memcmp filter.
|
|
63
71
|
*
|
package/lib/types/types/ant.d.ts
CHANGED
|
@@ -193,6 +193,25 @@ export declare const AntStateSchema: z.ZodObject<{
|
|
|
193
193
|
Initialized: boolean;
|
|
194
194
|
}>;
|
|
195
195
|
export type ANTState = z.infer<typeof AntStateSchema>;
|
|
196
|
+
/**
|
|
197
|
+
* Lightweight ANT view for portfolio/list rendering. Carries the config fields
|
|
198
|
+
* plus controllers and the apex (`@`) record — everything a names table needs —
|
|
199
|
+
* WITHOUT the full undername record set. Produced in bulk by
|
|
200
|
+
* `getANTSummaries(mints)` in a handful of `getMultipleAccounts` calls; fetch
|
|
201
|
+
* full undernames lazily via `getRecords`/`getState` when a name is opened.
|
|
202
|
+
*/
|
|
203
|
+
export type ANTSummary = {
|
|
204
|
+
processId: string;
|
|
205
|
+
name: string;
|
|
206
|
+
ticker: string;
|
|
207
|
+
logo: string;
|
|
208
|
+
description: string;
|
|
209
|
+
keywords: string[];
|
|
210
|
+
owner: WalletAddress;
|
|
211
|
+
controllers: WalletAddress[];
|
|
212
|
+
/** The apex (`@`) record — the name's primary target — if set. */
|
|
213
|
+
apexRecord?: ANTRecord;
|
|
214
|
+
};
|
|
196
215
|
export declare const SpawnANTStateSchema: z.ZodObject<{
|
|
197
216
|
name: z.ZodString;
|
|
198
217
|
ticker: z.ZodString;
|
|
@@ -324,6 +343,14 @@ export type ANTInfo = z.infer<typeof AntInfoSchema>;
|
|
|
324
343
|
export declare function isANTState(state: object): state is ANTState;
|
|
325
344
|
export type AntReadOptions = {
|
|
326
345
|
strict?: boolean;
|
|
346
|
+
/**
|
|
347
|
+
* Include per-record metadata (`displayName`/`logo`/`description`/`keywords`)
|
|
348
|
+
* when reading undername records. Defaults to `false` — metadata requires a
|
|
349
|
+
* second `getProgramAccounts` scan per ANT and is only needed in detail/edit
|
|
350
|
+
* views, so list reads (`getState`/`getRecords`) skip it by default. Fetch a
|
|
351
|
+
* single record's metadata on demand via `getRecord`.
|
|
352
|
+
*/
|
|
353
|
+
includeMetadata?: boolean;
|
|
327
354
|
};
|
|
328
355
|
export interface ANTRead {
|
|
329
356
|
processId: string;
|
package/lib/types/version.d.ts
CHANGED