@ar.io/sdk 4.0.0-solana.26 → 4.0.0-solana.28

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.
@@ -290,9 +290,23 @@ export class SolanaARIOReadable {
290
290
  let staked = 0;
291
291
  let delegated = 0;
292
292
  let withdrawn = 0;
293
+ // `protocolBalance` is the REWARD RESERVE: the live balance of the protocol
294
+ // token account the epoch cranker emits from (ario-gar `distribute_epoch`
295
+ // reads `protocol_token_account.amount`). This matches AO's `protocolBalance`
296
+ // semantics (the qNvAoz0 reserve) and makes the six buckets sum to `total`:
297
+ // circulating + locked + staked + delegated + withdrawn + protocolBalance == total
298
+ //
299
+ // It is deliberately NOT `ArioConfig.protocol_balance`, which is a *folded*
300
+ // accounting field (= reserve + staked + delegated + withdrawn) and so
301
+ // double-counts the staking buckets — surfacing it as "protocol balance"
302
+ // over-reports the reward reserve by tens of millions of ARIO. We fall back
303
+ // to the folded value only when GatewaySettings or the token account can't
304
+ // be read (e.g. pre-init).
305
+ let protocolBalance = config.protocolBalance;
293
306
  if (garSettingsAccount.exists) {
307
+ const garData = Buffer.from(garSettingsAccount.data);
294
308
  try {
295
- const counters = deserializeGarSupplyCounters(Buffer.from(garSettingsAccount.data));
309
+ const counters = deserializeGarSupplyCounters(garData);
296
310
  staked = counters.totalStaked;
297
311
  delegated = counters.totalDelegated;
298
312
  withdrawn = counters.totalWithdrawn;
@@ -300,6 +314,22 @@ export class SolanaARIOReadable {
300
314
  catch {
301
315
  // Old-layout account without supply counters — fall back to 0
302
316
  }
317
+ // The protocol token account is pinned in GatewaySettings at offset 189
318
+ // (see ario-gar state/mod.rs::GatewaySettings: 8 disc + 32 authority +
319
+ // 32 mint + 6×8 economic params + 4 max_delegates + 1 migration_active +
320
+ // 32 migration_authority + 32 stake_token_account = 189; the pubkey runs
321
+ // [189, 221)). Read its live SPL balance as the reward reserve.
322
+ if (garData.length >= 221) {
323
+ try {
324
+ const protocolTokenAccount = addressDecoder.decode(garData.subarray(189, 221));
325
+ const reserve = await this.getTokenAccountAmount(protocolTokenAccount);
326
+ if (reserve !== null)
327
+ protocolBalance = reserve;
328
+ }
329
+ catch {
330
+ // Couldn't read the reserve — keep the folded fallback.
331
+ }
332
+ }
303
333
  }
304
334
  return {
305
335
  total: config.totalSupply,
@@ -308,9 +338,31 @@ export class SolanaARIOReadable {
308
338
  staked,
309
339
  delegated,
310
340
  withdrawn,
311
- protocolBalance: config.protocolBalance,
341
+ protocolBalance,
312
342
  };
313
343
  }
344
+ /**
345
+ * Read the `amount` (in mARIO) of an SPL token account directly by address.
346
+ * Returns `null` if the account doesn't exist or is too small to be a token
347
+ * account, so callers can distinguish "absent" from a real zero balance.
348
+ *
349
+ * Uses a portable little-endian u64 decode — some browser bundlers strip the
350
+ * BigInt readers from the `buffer` shim's prototype (see `getBalance`).
351
+ */
352
+ async getTokenAccountAmount(tokenAccount) {
353
+ const account = await this.getAccount(tokenAccount);
354
+ if (!account.exists)
355
+ return null;
356
+ const data = account.data;
357
+ if (data.length < 72)
358
+ return null;
359
+ let amount = 0n;
360
+ for (let i = 7; i >= 0; i--) {
361
+ amount = (amount << 8n) | BigInt(data[64 + i]);
362
+ }
363
+ // ARIO supply caps at 1B * 1e6 mARIO ≈ 2^50, well under MAX_SAFE_INTEGER.
364
+ return Number(amount);
365
+ }
314
366
  // =========================================
315
367
  // Balance read methods
316
368
  // =========================================
@@ -1723,8 +1775,21 @@ export class SolanaARIOReadable {
1723
1775
  return out;
1724
1776
  }
1725
1777
  /**
1726
- * Enumerate Gateway PDAs whose `status == Gone` (already left the
1727
- * network but PDA not yet GC'd). Eligible for `finalizeGone`.
1778
+ * Enumerate Gateway PDAs that are candidates for `finalizeGone` GC i.e.
1779
+ * those whose `status == Leaving`.
1780
+ *
1781
+ * NOTE: despite the historical name, this returns `Leaving` (not `Gone`)
1782
+ * gateways. `Gone` is NOT a persistent discovery state: the on-chain
1783
+ * `finalize_gone` instruction accepts a `Leaving` gateway, flips it to
1784
+ * `Gone`, and closes the Gateway PDA in the *same* instruction
1785
+ * (programs/ario-gar/src/instructions/gateway.rs::finalize_gone), so a
1786
+ * gateway is never observably parked at `Gone`. Filtering on `Gone` matched
1787
+ * nothing and left Leaving gateways un-GC'd. Time/delegation eligibility is
1788
+ * still enforced on-chain (`finalize_gone` reverts early if the leave window
1789
+ * hasn't elapsed or delegations remain), so over-returning not-yet-eligible
1790
+ * Leaving gateways is safe — the tx just no-ops/reverts.
1791
+ *
1792
+ * @deprecated Prefer {@link getLeavingGateways} — same result, accurate name.
1728
1793
  */
1729
1794
  async getGoneGateways() {
1730
1795
  const accounts = await this.getAccountsByDiscriminator(this.garProgram, GATEWAY_DISCRIMINATOR);
@@ -1733,7 +1798,7 @@ export class SolanaARIOReadable {
1733
1798
  for (const { pubkey, data } of accounts) {
1734
1799
  try {
1735
1800
  const g = decoder.decode(data);
1736
- if (g.status === GatewayStatus.Gone) {
1801
+ if (g.status === GatewayStatus.Leaving) {
1737
1802
  out.push({ pubkey, operator: g.operator });
1738
1803
  }
1739
1804
  }
@@ -1743,6 +1808,14 @@ export class SolanaARIOReadable {
1743
1808
  }
1744
1809
  return out;
1745
1810
  }
1811
+ /**
1812
+ * Enumerate Gateway PDAs whose `status == Leaving` — the persistent
1813
+ * pre-finalization state that `finalizeGone` GC's. Alias for
1814
+ * {@link getGoneGateways} with a name that matches the on-chain state.
1815
+ */
1816
+ async getLeavingGateways() {
1817
+ return this.getGoneGateways();
1818
+ }
1746
1819
  /**
1747
1820
  * Enumerate Joined Gateway PDAs whose delegation has been DISABLED
1748
1821
  * (`allow_delegated_staking == false`) yet still hold delegated stake
@@ -69,6 +69,36 @@ function withRemainingAccounts(ix, remaining) {
69
69
  ];
70
70
  return { ...ix, accounts };
71
71
  }
72
+ /**
73
+ * Pick the swapped-gateway operator that `finalize_gone` needs as a writable
74
+ * `remaining_accounts[0]`.
75
+ *
76
+ * `finalize_gone` reclaims a gateway's slot from the compact GatewayRegistry by
77
+ * moving the LAST active slot into it and rewriting that swapped gateway's
78
+ * stored `registry_index`. When the finalized gateway is NOT already the last
79
+ * slot, the on-chain handler requires the swapped Gateway PDA (writable) at
80
+ * `remaining_accounts[0]`; when it IS the last slot, no swap occurs and no
81
+ * extra account is needed. See
82
+ * `programs/ario-gar/src/instructions/gateway.rs::finalize_gone`.
83
+ *
84
+ * `registryAddresses` MUST be the active registry operator addresses in slot
85
+ * order (`getRegistryGatewayAddresses()` — length === on-chain
86
+ * `registry.count`), so `registryAddresses[length - 1]` is exactly the
87
+ * `registry.gateways[count - 1].address` the on-chain swap reads.
88
+ *
89
+ * @returns the swapped gateway's operator address, or `null` when the finalized
90
+ * gateway already occupies the last slot.
91
+ * @throws if `registryIndex` is outside the active registry count (mirrors the
92
+ * on-chain `index < registry.count` guard, surfacing a stale index early).
93
+ */
94
+ export function selectFinalizeGoneSwapOperator(registryIndex, registryAddresses) {
95
+ if (registryIndex < 0 || registryIndex >= registryAddresses.length) {
96
+ throw new Error(`finalizeGone: registry index ${registryIndex} is outside the active ` +
97
+ `registry count ${registryAddresses.length}`);
98
+ }
99
+ const lastIndex = registryAddresses.length - 1;
100
+ return registryIndex === lastIndex ? null : registryAddresses[lastIndex];
101
+ }
72
102
  /**
73
103
  * Split a primary name into its undername + base parts using the same rule
74
104
  * as the on-chain `splitn(2, '_')` in `programs/ario-core/src/instructions/primary_name.rs`:
@@ -2627,11 +2657,34 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
2627
2657
  async finalizeGone(params, _options) {
2628
2658
  const gatewayAddr = address(params.gateway);
2629
2659
  const [gatewayPda] = await getGatewayPDA(gatewayAddr, this.garProgram);
2660
+ // `finalize_gone` reclaims this gateway's compact-registry slot by
2661
+ // swap-removing the LAST active slot into it. When this gateway is not the
2662
+ // last slot, the on-chain handler rewrites the swapped gateway's stored
2663
+ // registry_index and therefore requires that swapped Gateway PDA as
2664
+ // writable remaining_accounts[0]
2665
+ // (programs/ario-gar/src/instructions/gateway.rs::finalize_gone). Read the
2666
+ // gateway's slot index + the active registry to decide.
2667
+ const gatewayAccount = await fetchEncodedAccount(this.rpc, gatewayPda, {
2668
+ commitment: this.commitment,
2669
+ });
2670
+ if (!gatewayAccount.exists) {
2671
+ throw new Error(`Gateway not found for operator ${params.gateway}`);
2672
+ }
2673
+ const gateway = getGatewayDecoder().decode(gatewayAccount.data);
2674
+ const registryAddresses = await this.getRegistryGatewayAddresses();
2675
+ const swappedOperator = selectFinalizeGoneSwapOperator(gateway.registryIndex.index, registryAddresses);
2630
2676
  const ix = await getFinalizeGoneInstructionAsync(await this.withGarDefaults({
2631
2677
  gateway: gatewayPda,
2632
2678
  caller: this.signer,
2633
2679
  }), { programAddress: this.garProgram });
2634
- const sig = await this.sendTransaction([ix]);
2680
+ let finalIx = ix;
2681
+ if (swappedOperator !== null) {
2682
+ const [swappedGatewayPda] = await getGatewayPDA(address(swappedOperator), this.garProgram);
2683
+ finalIx = withRemainingAccounts(ix, [
2684
+ { address: swappedGatewayPda, role: AccountRole.WRITABLE },
2685
+ ]);
2686
+ }
2687
+ const sig = await this.sendTransaction([finalIx]);
2635
2688
  return { id: sig };
2636
2689
  }
2637
2690
  /**
@@ -14,4 +14,4 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  // AUTOMATICALLY GENERATED FILE - DO NOT TOUCH
17
- export const version = '4.0.0-solana.26';
17
+ export const version = '4.0.0-solana.28';
@@ -83,6 +83,15 @@ export declare class SolanaARIOReadable {
83
83
  LastDistributedEpochIndex: number;
84
84
  }>;
85
85
  getTokenSupply(): Promise<TokenSupplyData>;
86
+ /**
87
+ * Read the `amount` (in mARIO) of an SPL token account directly by address.
88
+ * Returns `null` if the account doesn't exist or is too small to be a token
89
+ * account, so callers can distinguish "absent" from a real zero balance.
90
+ *
91
+ * Uses a portable little-endian u64 decode — some browser bundlers strip the
92
+ * BigInt readers from the `buffer` shim's prototype (see `getBalance`).
93
+ */
94
+ private getTokenAccountAmount;
86
95
  /**
87
96
  * Resolve the ARIO SPL mint address from the on-chain `ArioConfig`.
88
97
  *
@@ -302,13 +311,35 @@ export declare class SolanaARIOReadable {
302
311
  failedConsecutive: number;
303
312
  }>>;
304
313
  /**
305
- * Enumerate Gateway PDAs whose `status == Gone` (already left the
306
- * network but PDA not yet GC'd). Eligible for `finalizeGone`.
314
+ * Enumerate Gateway PDAs that are candidates for `finalizeGone` GC i.e.
315
+ * those whose `status == Leaving`.
316
+ *
317
+ * NOTE: despite the historical name, this returns `Leaving` (not `Gone`)
318
+ * gateways. `Gone` is NOT a persistent discovery state: the on-chain
319
+ * `finalize_gone` instruction accepts a `Leaving` gateway, flips it to
320
+ * `Gone`, and closes the Gateway PDA in the *same* instruction
321
+ * (programs/ario-gar/src/instructions/gateway.rs::finalize_gone), so a
322
+ * gateway is never observably parked at `Gone`. Filtering on `Gone` matched
323
+ * nothing and left Leaving gateways un-GC'd. Time/delegation eligibility is
324
+ * still enforced on-chain (`finalize_gone` reverts early if the leave window
325
+ * hasn't elapsed or delegations remain), so over-returning not-yet-eligible
326
+ * Leaving gateways is safe — the tx just no-ops/reverts.
327
+ *
328
+ * @deprecated Prefer {@link getLeavingGateways} — same result, accurate name.
307
329
  */
308
330
  getGoneGateways(): Promise<Array<{
309
331
  pubkey: Address;
310
332
  operator: Address;
311
333
  }>>;
334
+ /**
335
+ * Enumerate Gateway PDAs whose `status == Leaving` — the persistent
336
+ * pre-finalization state that `finalizeGone` GC's. Alias for
337
+ * {@link getGoneGateways} with a name that matches the on-chain state.
338
+ */
339
+ getLeavingGateways(): Promise<Array<{
340
+ pubkey: Address;
341
+ operator: Address;
342
+ }>>;
312
343
  /**
313
344
  * Enumerate Joined Gateway PDAs whose delegation has been DISABLED
314
345
  * (`allow_delegated_staking == false`) yet still hold delegated stake
@@ -26,6 +26,29 @@ import type { mARIOToken } from '../types/token.js';
26
26
  import { deserializeEpochSettingsFull } from './deserialize.js';
27
27
  import { SolanaARIOReadable } from './io-readable.js';
28
28
  import type { SolanaRpcSubscriptions, SolanaSigner, SolanaWriteConfig } from './types.js';
29
+ /**
30
+ * Pick the swapped-gateway operator that `finalize_gone` needs as a writable
31
+ * `remaining_accounts[0]`.
32
+ *
33
+ * `finalize_gone` reclaims a gateway's slot from the compact GatewayRegistry by
34
+ * moving the LAST active slot into it and rewriting that swapped gateway's
35
+ * stored `registry_index`. When the finalized gateway is NOT already the last
36
+ * slot, the on-chain handler requires the swapped Gateway PDA (writable) at
37
+ * `remaining_accounts[0]`; when it IS the last slot, no swap occurs and no
38
+ * extra account is needed. See
39
+ * `programs/ario-gar/src/instructions/gateway.rs::finalize_gone`.
40
+ *
41
+ * `registryAddresses` MUST be the active registry operator addresses in slot
42
+ * order (`getRegistryGatewayAddresses()` — length === on-chain
43
+ * `registry.count`), so `registryAddresses[length - 1]` is exactly the
44
+ * `registry.gateways[count - 1].address` the on-chain swap reads.
45
+ *
46
+ * @returns the swapped gateway's operator address, or `null` when the finalized
47
+ * gateway already occupies the last slot.
48
+ * @throws if `registryIndex` is outside the active registry count (mirrors the
49
+ * on-chain `index < registry.count` guard, surfacing a stale index early).
50
+ */
51
+ export declare function selectFinalizeGoneSwapOperator(registryIndex: number, registryAddresses: string[]): string | null;
29
52
  /**
30
53
  * Split a primary name into its undername + base parts using the same rule
31
54
  * as the on-chain `splitn(2, '_')` in `programs/ario-core/src/instructions/primary_name.rs`:
@@ -144,6 +144,16 @@ export type EligibleDistribution = {
144
144
  gatewayAddress: WalletAddress;
145
145
  cursorId: string;
146
146
  };
147
+ /**
148
+ * The six ARIO supply buckets. They are mutually exclusive and sum to `total`:
149
+ * circulating + locked + staked + delegated + withdrawn + protocolBalance === total
150
+ *
151
+ * `protocolBalance` is the protocol **reward reserve** (the pool epoch rewards
152
+ * are paid from) — on Solana this is the live balance of the protocol token
153
+ * account, matching AO's `protocolBalance` (the qNvAoz0 reserve). It is NOT the
154
+ * on-chain `ArioConfig.protocol_balance` accounting field, which folds the
155
+ * staking buckets in and would double-count.
156
+ */
147
157
  export type TokenSupplyData = {
148
158
  total: number;
149
159
  circulating: number;
@@ -13,4 +13,4 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- export declare const version = "4.0.0-solana.25";
16
+ export declare const version = "4.0.0-solana.27";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ar.io/sdk",
3
- "version": "4.0.0-solana.26",
3
+ "version": "4.0.0-solana.28",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ar-io/ar-io-sdk.git"