@ar.io/sdk 4.0.0-solana.7 → 4.0.0-solana.9

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.
@@ -16,6 +16,12 @@ export const MPL_CORE_PROGRAM_ID = address('CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH
16
16
  export const TOKEN_DECIMALS = 6;
17
17
  export const ONE_TOKEN = 1_000_000; // 1 ARIO = 1,000,000 mARIO
18
18
  export const RATE_SCALE = 1_000_000;
19
+ /**
20
+ * Scaling factor for the per-share reward accumulator
21
+ * `Gateway.cumulative_reward_per_token` in `ario-gar`. Must match the
22
+ * `REWARD_PRECISION` constant in `programs/ario-gar/src/state/mod.rs`.
23
+ */
24
+ export const REWARD_PRECISION = 1000000000000000000n; // 1e18
19
25
  // =========================================
20
26
  // PDA Seeds — ario-core
21
27
  // =========================================
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Delegation balance math.
3
+ *
4
+ * On-chain, `Delegation.amount` is the last-settled principal. Pending rewards
5
+ * are tracked separately via the gateway's per-share accumulator
6
+ * (`Gateway.cumulative_reward_per_token`) and the delegate's snapshot of that
7
+ * accumulator at the last settlement (`Delegation.reward_debt`). The
8
+ * `distribute_epoch` instruction advances the accumulator but does NOT touch
9
+ * per-`Delegation.amount` — that field is updated lazily on the next
10
+ * delegation interaction (or via the permissionless
11
+ * `compound_delegation_rewards` instruction).
12
+ *
13
+ * Off-chain readers (indexers, wallets, the network portal) that want to show
14
+ * a delegate's current balance must therefore compute the live value from the
15
+ * accumulator + reward_debt; reading `Delegation.amount` directly under-reports
16
+ * every epoch of pending rewards.
17
+ *
18
+ * This module mirrors the on-chain `settle_delegate_rewards` math from
19
+ * `programs/ario-gar/src/state/mod.rs` so the SDK can return live values from
20
+ * its read-side methods (`getGatewayDelegates`, `getDelegations`,
21
+ * `getAllDelegates`) without needing an on-chain settlement call.
22
+ *
23
+ * See `INVARIANTS.md` in the contracts repo for the broader stake-pool
24
+ * invariant context.
25
+ */
26
+ import { REWARD_PRECISION } from './constants.js';
27
+ const U64_MAX = (1n << 64n) - 1n;
28
+ /**
29
+ * Compute the live delegation balance: the last-settled principal plus any
30
+ * pending rewards accrued since the last settlement.
31
+ *
32
+ * Mirrors `settle_delegate_rewards` in
33
+ * `programs/ario-gar/src/state/mod.rs` — including the u128 overflow-safe
34
+ * quotient/remainder split and the saturating-to-`u64::MAX` cap at the end —
35
+ * so a value computed here matches what the on-chain instruction would write
36
+ * if it were called right now.
37
+ *
38
+ * @param delegatedStake - `Delegation.amount` (u64 → number).
39
+ * @param rewardDebt - `Delegation.reward_debt` (u128 → bigint).
40
+ * @param cumulativeRewardPerToken - `Gateway.cumulative_reward_per_token` (u128 → bigint).
41
+ * @returns The delegate's live balance in mARIO, saturating-capped at `u64::MAX`.
42
+ */
43
+ export function computeLiveDelegationBalance({ delegatedStake, rewardDebt, cumulativeRewardPerToken, }) {
44
+ // Fast paths matching the on-chain `if` guards: zero principal or no
45
+ // accumulator delta means no pending rewards to settle.
46
+ if (delegatedStake <= 0 || cumulativeRewardPerToken <= rewardDebt) {
47
+ return delegatedStake;
48
+ }
49
+ const amount = BigInt(delegatedStake);
50
+ const delta = cumulativeRewardPerToken - rewardDebt;
51
+ // Quotient / remainder split mirrors the on-chain `checked_mul ... unwrap_or_else`
52
+ // fallback. BigInt has no native overflow, so we always take the split path
53
+ // for parity; the result is identical either way.
54
+ const quot = delta / REWARD_PRECISION;
55
+ const rem = delta % REWARD_PRECISION;
56
+ const fromQuot = amount * quot; // saturating not needed — BigInt is unbounded
57
+ const fromRem = (amount * rem) / REWARD_PRECISION;
58
+ const pending = fromQuot + fromRem;
59
+ // Saturating cap at u64::MAX to match the on-chain `u64::try_from(...).unwrap_or(u64::MAX)`.
60
+ const live = amount + pending;
61
+ const capped = live > U64_MAX ? U64_MAX : live;
62
+ return Number(capped);
63
+ }
@@ -281,12 +281,18 @@ function scaleToFloat(value, scale = RATE_SCALE) {
281
281
  // Gateway deserialization
282
282
  // =========================================
283
283
  /**
284
- * Deserialize a Gateway account from raw bytes.
285
- * PDA: ["gateway", operator_pubkey] in ario-gar program.
284
+ * Internal variant of {@link deserializeGateway} that additionally surfaces
285
+ * the on-chain `cumulative_reward_per_token` u128 accumulator. Used by the
286
+ * live delegation balance pipeline ({@link computeLiveDelegationBalance} via
287
+ * `getGatewayAccumulators` in `io-readable.ts`).
288
+ *
289
+ * Not part of the public AoGateway shape — `bigint` is not JSON-serializable
290
+ * and would leak through `getGateway` / `getGateways`. Prefer
291
+ * {@link deserializeGateway} everywhere except inside live-balance plumbing.
286
292
  *
287
- * Returns the SDK-compatible AoGateway type.
293
+ * PDA: ["gateway", operator_pubkey] in ario-gar program.
288
294
  */
289
- export function deserializeGateway(data) {
295
+ export function deserializeGatewayWithAccumulator(data) {
290
296
  const r = new BorshReader(data, 8); // skip 8-byte discriminator
291
297
  const operator = r.readPubkey();
292
298
  const label = r.readString();
@@ -329,8 +335,12 @@ export function deserializeGateway(data) {
329
335
  r.readU8(); // _reserved (layout-preserving placeholder for the legacy is_registered byte)
330
336
  // observer_address
331
337
  const observerAddress = r.readPubkey();
332
- // cumulative_reward_per_token (u128, not exposed in SDK type)
333
- r.skip(16);
338
+ // cumulative_reward_per_token: u128 per-share accumulator advanced by
339
+ // distribute_epoch. Combined with each Delegation.reward_debt, this lets
340
+ // off-chain readers compute the live delegate balance without an on-chain
341
+ // settlement call. Not part of the public AoGateway type but surfaced as
342
+ // an extra field on this function's return so internal readers can use it.
343
+ const cumulativeRewardPerToken = r.readU128();
334
344
  // bump
335
345
  r.skip(1);
336
346
  const stats = {
@@ -378,8 +388,24 @@ export function deserializeGateway(data) {
378
388
  operatorStake,
379
389
  status: statusIdx === 0 ? 'joined' : 'leaving',
380
390
  weights,
391
+ cumulativeRewardPerToken,
381
392
  };
382
393
  }
394
+ /**
395
+ * Deserialize a Gateway account from raw bytes.
396
+ *
397
+ * Returns the SDK-compatible AoGateway type (plus `operator: string`).
398
+ * The on-chain `cumulative_reward_per_token` u128 is intentionally NOT
399
+ * surfaced on the returned object — `bigint` is not JSON-serializable,
400
+ * and leaking it through `getGateway` / `getGateways` would break
401
+ * downstream consumers that call `JSON.stringify` on results. Use
402
+ * {@link deserializeGatewayWithAccumulator} from within the live-balance
403
+ * pipeline when the accumulator value is actually needed.
404
+ */
405
+ export function deserializeGateway(data) {
406
+ const { cumulativeRewardPerToken: _unused, ...publicShape } = deserializeGatewayWithAccumulator(data);
407
+ return publicShape;
408
+ }
383
409
  // =========================================
384
410
  // ArNS Record deserialization
385
411
  // =========================================
@@ -477,13 +503,17 @@ export function deserializeDelegation(data) {
477
503
  const delegator = r.readPubkey();
478
504
  const amount = r.readU64AsNumber();
479
505
  const startTimestamp = r.readI64AsNumber();
480
- // reward_debt: u128, bump: u8 skip
481
- r.skip(16 + 1);
506
+ // reward_debt: u128 snapshot of gateway.cumulative_reward_per_token at last
507
+ // settlement. Needed (together with the gateway's current accumulator) to
508
+ // compute the live delegate balance via computeLiveDelegationBalance().
509
+ const rewardDebt = r.readU128();
510
+ r.skip(1); // bump
482
511
  return {
483
512
  gateway: gateway,
484
513
  delegator: delegator,
485
514
  delegatedStake: amount,
486
515
  startTimestamp,
516
+ rewardDebt,
487
517
  };
488
518
  }
489
519
  // =========================================
@@ -14,7 +14,8 @@ import { Logger } from '../common/logger.js';
14
14
  import { SolanaANTRegistryReadable } from './ant-registry-readable.js';
15
15
  import { getAssociatedTokenAddressKit } from './ata.js';
16
16
  import { ARIO_ANT_PROGRAM_ID, ARIO_ARNS_PROGRAM_ID, ARIO_CORE_PROGRAM_ID, ARIO_GAR_PROGRAM_ID, ARNS_RECORD_ANT_OFFSET, RATE_SCALE, } from './constants.js';
17
- import { deserializeAllowlist, deserializeArioConfig, deserializeArnsRecord, deserializeDelegation, deserializeDemandFactor, deserializeEpoch, deserializeEpochSettings, deserializeEpochSettingsFull, deserializeGarSettings, deserializeGarSupplyCounters, deserializeGateway, deserializeObservation, deserializePrimaryName, deserializePrimaryNameRequest, deserializeRedelegationRecord, deserializeReservedName, deserializeReturnedName, deserializeVault, deserializeWithdrawal, } from './deserialize.js';
17
+ import { computeLiveDelegationBalance } from './delegation-math.js';
18
+ import { deserializeAllowlist, deserializeArioConfig, deserializeArnsRecord, deserializeDelegation, deserializeDemandFactor, deserializeEpoch, deserializeEpochSettings, deserializeEpochSettingsFull, deserializeGarSettings, deserializeGarSupplyCounters, deserializeGateway, deserializeGatewayWithAccumulator, deserializeObservation, deserializePrimaryName, deserializePrimaryNameRequest, deserializeRedelegationRecord, deserializeReservedName, deserializeReturnedName, deserializeVault, deserializeWithdrawal, } from './deserialize.js';
18
19
  import { TOKEN_PROGRAM_ADDRESS } from './instruction.js';
19
20
  import { getArioConfigPDA, getArnsRecordPDA, getArnsRecordPDAFromHash, getArnsSettingsPDA, getDemandFactorPDA, getEpochPDA, getEpochSettingsPDA, getGarSettingsPDA, getGatewayPDA, getGatewayRegistryPDA, getObserverLookupPDA, getPrimaryNamePDA, getPrimaryNameRequestPDA, getReservedNamePDA, getReturnedNamePDA, getVaultPDA, } from './pda.js';
20
21
  const addressDecoder = getAddressDecoder();
@@ -179,6 +180,42 @@ export class SolanaARIOReadable {
179
180
  data: Buffer.from(entry.account.data[0], 'base64'),
180
181
  }));
181
182
  }
183
+ /**
184
+ * Batch-fetch the `cumulative_reward_per_token` accumulator for every gateway
185
+ * in `operatorAddresses`. Returns a Map keyed by base58 operator address.
186
+ * Used by the delegate readers below to compute the live delegation balance
187
+ * without an on-chain settlement call (see {@link computeLiveDelegationBalance}
188
+ * and `INVARIANTS.md` in the contracts repo). Missing gateways are silently
189
+ * skipped — callers fall back to the stale `Delegation.amount` for those
190
+ * (the accumulator delta is 0 and live == stored anyway when the gateway
191
+ * has no rewards to distribute).
192
+ */
193
+ async getGatewayAccumulators(operatorAddresses) {
194
+ const unique = Array.from(new Set(operatorAddresses));
195
+ if (unique.length === 0)
196
+ return new Map();
197
+ const pdas = await Promise.all(unique.map(async (op) => (await getGatewayPDA(address(op), this.garProgram))[0]));
198
+ const accounts = await fetchEncodedAccounts(this.rpc, pdas, {
199
+ commitment: this.commitment,
200
+ });
201
+ const out = new Map();
202
+ for (let i = 0; i < accounts.length; i++) {
203
+ const acct = accounts[i];
204
+ if (!acct.exists)
205
+ continue;
206
+ try {
207
+ // Internal variant: surfaces the u128 accumulator that the public
208
+ // `deserializeGateway` deliberately drops (BigInt is not
209
+ // JSON-serializable and would leak through getGateway).
210
+ const gw = deserializeGatewayWithAccumulator(Buffer.from(acct.data));
211
+ out.set(unique[i], gw.cumulativeRewardPerToken);
212
+ }
213
+ catch {
214
+ // Skip malformed; the caller will fall back to the raw delegation amount.
215
+ }
216
+ }
217
+ return out;
218
+ }
182
219
  /** Read the gateway registry and return addresses in registry index order */
183
220
  async getRegistryGatewayAddresses() {
184
221
  const [registryPda] = await getGatewayRegistryPDA(this.garProgram);
@@ -480,13 +517,22 @@ export class SolanaARIOReadable {
480
517
  memcmp: { offset: 8n, bytes: gateway, encoding: 'base58' },
481
518
  },
482
519
  ]);
520
+ // Fetch this gateway's current reward accumulator so we can return live
521
+ // balances (raw `Delegation.amount` is stale between settlements — see
522
+ // INVARIANTS.md and `computeLiveDelegationBalance`).
523
+ const accumulators = await this.getGatewayAccumulators([gateway]);
524
+ const cumulative = accumulators.get(gateway) ?? 0n;
483
525
  const items = [];
484
526
  for (const { data } of accounts) {
485
527
  try {
486
528
  const del = deserializeDelegation(data);
487
529
  items.push({
488
530
  address: del.delegator,
489
- delegatedStake: del.delegatedStake,
531
+ delegatedStake: computeLiveDelegationBalance({
532
+ delegatedStake: del.delegatedStake,
533
+ rewardDebt: del.rewardDebt,
534
+ cumulativeRewardPerToken: cumulative,
535
+ }),
490
536
  startTimestamp: secToMs(del.startTimestamp),
491
537
  });
492
538
  }
@@ -528,22 +574,32 @@ export class SolanaARIOReadable {
528
574
  },
529
575
  },
530
576
  ]);
531
- const items = [];
577
+ const decoded = [];
532
578
  for (const { pubkey, data } of accounts) {
533
579
  try {
534
- const del = deserializeDelegation(data);
535
- items.push({
536
- type: 'stake',
537
- gatewayAddress: del.gateway,
538
- delegationId: pubkey,
539
- startTimestamp: secToMs(del.startTimestamp),
540
- balance: del.delegatedStake,
580
+ decoded.push({
581
+ pubkey: pubkey,
582
+ del: deserializeDelegation(data),
541
583
  });
542
584
  }
543
585
  catch {
544
586
  // Skip malformed
545
587
  }
546
588
  }
589
+ // Batch-fetch each referenced gateway's reward accumulator so we can
590
+ // return live balances. See INVARIANTS.md and `computeLiveDelegationBalance`.
591
+ const accumulators = await this.getGatewayAccumulators(decoded.map(({ del }) => del.gateway));
592
+ const items = decoded.map(({ pubkey, del }) => ({
593
+ type: 'stake',
594
+ gatewayAddress: del.gateway,
595
+ delegationId: pubkey,
596
+ startTimestamp: secToMs(del.startTimestamp),
597
+ balance: computeLiveDelegationBalance({
598
+ delegatedStake: del.delegatedStake,
599
+ rewardDebt: del.rewardDebt,
600
+ cumulativeRewardPerToken: accumulators.get(del.gateway) ?? 0n,
601
+ }),
602
+ }));
547
603
  return paginate(items, params);
548
604
  }
549
605
  async getAllowedDelegates(params) {
@@ -1290,23 +1346,33 @@ export class SolanaARIOReadable {
1290
1346
  // =========================================
1291
1347
  async getAllDelegates(params) {
1292
1348
  const accounts = await this.getAccountsByDiscriminator(this.garProgram, DELEGATION_DISCRIMINATOR);
1293
- const items = [];
1349
+ const decoded = [];
1294
1350
  for (const { pubkey, data } of accounts) {
1295
1351
  try {
1296
- const del = deserializeDelegation(data);
1297
- items.push({
1298
- address: del.delegator,
1299
- gatewayAddress: del.gateway,
1300
- delegatedStake: del.delegatedStake,
1301
- startTimestamp: secToMs(del.startTimestamp),
1302
- vaultedStake: 0,
1303
- cursorId: pubkey,
1352
+ decoded.push({
1353
+ pubkey: pubkey,
1354
+ del: deserializeDelegation(data),
1304
1355
  });
1305
1356
  }
1306
1357
  catch {
1307
1358
  // Skip malformed
1308
1359
  }
1309
1360
  }
1361
+ // Batch-fetch each referenced gateway's reward accumulator so we can
1362
+ // return live balances. See INVARIANTS.md and `computeLiveDelegationBalance`.
1363
+ const accumulators = await this.getGatewayAccumulators(decoded.map(({ del }) => del.gateway));
1364
+ const items = decoded.map(({ pubkey, del }) => ({
1365
+ address: del.delegator,
1366
+ gatewayAddress: del.gateway,
1367
+ delegatedStake: computeLiveDelegationBalance({
1368
+ delegatedStake: del.delegatedStake,
1369
+ rewardDebt: del.rewardDebt,
1370
+ cumulativeRewardPerToken: accumulators.get(del.gateway) ?? 0n,
1371
+ }),
1372
+ startTimestamp: secToMs(del.startTimestamp),
1373
+ vaultedStake: 0,
1374
+ cursorId: pubkey,
1375
+ }));
1310
1376
  return paginate(items, params);
1311
1377
  }
1312
1378
  async getAllGatewayVaults(params) {
@@ -36,7 +36,7 @@ function toGeneratedFundingSourceSpec(s) {
36
36
  return { kind: kindMap[s.kind], amount: s.amount };
37
37
  }
38
38
  import { getSyncAttributesInstruction } from '@ar.io/solana-contracts/ant';
39
- import { getApprovePrimaryNameInstructionAsync, getCloseExpiredRequestInstruction, getCreateVaultInstructionAsync, getExtendVaultInstructionAsync, getIncreaseVaultInstructionAsync, getReleaseVaultInstructionAsync, getRequestAndSetPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameInstructionAsync, getRevokeVaultInstructionAsync, getVaultedTransferInstructionAsync, } from '@ar.io/solana-contracts/core';
39
+ import { getApprovePrimaryNameInstructionAsync, getCloseExpiredRequestInstruction, getCreateVaultInstructionAsync, getExtendVaultInstructionAsync, getIncreaseVaultInstructionAsync, getReleaseVaultInstructionAsync, getRequestAndSetPrimaryNameFromFundingPlanInstructionAsync, getRequestAndSetPrimaryNameInstructionAsync, getRequestPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameInstructionAsync, getRevokeVaultInstructionAsync, getVaultedTransferInstructionAsync, } from '@ar.io/solana-contracts/core';
40
40
  import { getDelegationDecoder, getGatewayDecoder, } from '@ar.io/solana-contracts/gar';
41
41
  import { Protocol, getAllowDelegateInstructionAsync, getCancelWithdrawalInstruction, getClaimDelegateFromLeavingGatewayInstructionAsync, getClaimWithdrawalInstructionAsync, getCloseDrainedWithdrawalInstruction, getCloseEmptyDelegationInstruction, getCloseEpochInstructionAsync, getCloseObservationInstructionAsync, getCreateEpochInstructionAsync, getDecreaseDelegateStakeInstructionAsync, getDecreaseOperatorStakeInstructionAsync, getDelegateStakeInstructionAsync, getDisallowDelegateInstructionAsync, getDistributeEpochInstructionAsync, getFinalizeGoneInstructionAsync, getIncreaseOperatorStakeInstructionAsync, getInstantWithdrawalInstructionAsync, getJoinNetworkInstructionAsync, getLeaveNetworkInstructionAsync, getPrescribeEpochInstructionAsync, getPruneGatewayInstructionAsync, getRedelegateStakeInstructionAsync, getSaveObservationsInstructionAsync, getSetAllowlistEnabledInstructionAsync, getTallyWeightsInstructionAsync, getUpdateGatewaySettingsInstructionAsync, } from '@ar.io/solana-contracts/gar';
42
42
  import { getTransferCheckedInstruction } from '@solana-program/token';
@@ -1415,23 +1415,28 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
1415
1415
  const coreConfig = await this.getCoreConfig();
1416
1416
  const signerATA = await getAssociatedTokenAddressKit(coreConfig.mint, this.signer.address);
1417
1417
  const { remaining, antProgram } = await this._buildPrimaryNameValidationAccounts(params.name, 'requestAndSet');
1418
+ let ix;
1418
1419
  if (!params.fundFrom ||
1419
1420
  params.fundFrom === 'balance' ||
1420
1421
  params.fundFrom === 'turbo') {
1421
- // Direct-transfer path: delegate to requestPrimaryName for now (this
1422
- // matches the pre-Phase-4 placeholder; the on-chain
1423
- // request_and_set_primary_name ix uses the same fee charge so the
1424
- // routing is functionally identical).
1425
- void signerATA;
1426
- return this.requestPrimaryName(params, _options);
1422
+ ix = withRemainingAccounts(await getRequestAndSetPrimaryNameInstructionAsync(await this.withCoreDefaults({
1423
+ initiatorTokenAccount: signerATA,
1424
+ protocolTokenAccount: coreConfig.treasury,
1425
+ initiator: this.signer,
1426
+ name: params.name,
1427
+ reverseLookupHash: hashName(params.name),
1428
+ antProgramId: antProgram,
1429
+ }), { programAddress: this.coreProgram }), remaining);
1430
+ }
1431
+ else {
1432
+ ix = await this._buildPrimaryNameFromFundingPlanIx({
1433
+ params,
1434
+ coreConfig,
1435
+ validationAccounts: remaining,
1436
+ operation: 'requestAndSet',
1437
+ antProgramId: antProgram,
1438
+ });
1427
1439
  }
1428
- const ix = await this._buildPrimaryNameFromFundingPlanIx({
1429
- params,
1430
- coreConfig,
1431
- validationAccounts: remaining,
1432
- operation: 'requestAndSet',
1433
- antProgramId: antProgram,
1434
- });
1435
1440
  const sig = await this.sendTransaction([ix]);
1436
1441
  return { id: sig };
1437
1442
  }
@@ -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.7';
17
+ export const version = '4.0.0-solana.9';
@@ -14,6 +14,12 @@ export declare const MPL_CORE_PROGRAM_ID: Address;
14
14
  export declare const TOKEN_DECIMALS = 6;
15
15
  export declare const ONE_TOKEN = 1000000;
16
16
  export declare const RATE_SCALE = 1000000;
17
+ /**
18
+ * Scaling factor for the per-share reward accumulator
19
+ * `Gateway.cumulative_reward_per_token` in `ario-gar`. Must match the
20
+ * `REWARD_PRECISION` constant in `programs/ario-gar/src/state/mod.rs`.
21
+ */
22
+ export declare const REWARD_PRECISION = 1000000000000000000n;
17
23
  export declare const ARIO_CONFIG_SEED: Buffer<ArrayBuffer>;
18
24
  export declare const VAULT_SEED: Buffer<ArrayBuffer>;
19
25
  export declare const VAULT_COUNTER_SEED: Buffer<ArrayBuffer>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Compute the live delegation balance: the last-settled principal plus any
3
+ * pending rewards accrued since the last settlement.
4
+ *
5
+ * Mirrors `settle_delegate_rewards` in
6
+ * `programs/ario-gar/src/state/mod.rs` — including the u128 overflow-safe
7
+ * quotient/remainder split and the saturating-to-`u64::MAX` cap at the end —
8
+ * so a value computed here matches what the on-chain instruction would write
9
+ * if it were called right now.
10
+ *
11
+ * @param delegatedStake - `Delegation.amount` (u64 → number).
12
+ * @param rewardDebt - `Delegation.reward_debt` (u128 → bigint).
13
+ * @param cumulativeRewardPerToken - `Gateway.cumulative_reward_per_token` (u128 → bigint).
14
+ * @returns The delegate's live balance in mARIO, saturating-capped at `u64::MAX`.
15
+ */
16
+ export declare function computeLiveDelegationBalance({ delegatedStake, rewardDebt, cumulativeRewardPerToken, }: {
17
+ delegatedStake: number;
18
+ rewardDebt: bigint;
19
+ cumulativeRewardPerToken: bigint;
20
+ }): number;
@@ -81,10 +81,31 @@ declare class BorshWriter {
81
81
  getOffset(): number;
82
82
  }
83
83
  /**
84
- * Deserialize a Gateway account from raw bytes.
84
+ * Internal variant of {@link deserializeGateway} that additionally surfaces
85
+ * the on-chain `cumulative_reward_per_token` u128 accumulator. Used by the
86
+ * live delegation balance pipeline ({@link computeLiveDelegationBalance} via
87
+ * `getGatewayAccumulators` in `io-readable.ts`).
88
+ *
89
+ * Not part of the public AoGateway shape — `bigint` is not JSON-serializable
90
+ * and would leak through `getGateway` / `getGateways`. Prefer
91
+ * {@link deserializeGateway} everywhere except inside live-balance plumbing.
92
+ *
85
93
  * PDA: ["gateway", operator_pubkey] in ario-gar program.
94
+ */
95
+ export declare function deserializeGatewayWithAccumulator(data: Buffer): AoGateway & {
96
+ operator: string;
97
+ cumulativeRewardPerToken: bigint;
98
+ };
99
+ /**
100
+ * Deserialize a Gateway account from raw bytes.
86
101
  *
87
- * Returns the SDK-compatible AoGateway type.
102
+ * Returns the SDK-compatible AoGateway type (plus `operator: string`).
103
+ * The on-chain `cumulative_reward_per_token` u128 is intentionally NOT
104
+ * surfaced on the returned object — `bigint` is not JSON-serializable,
105
+ * and leaking it through `getGateway` / `getGateways` would break
106
+ * downstream consumers that call `JSON.stringify` on results. Use
107
+ * {@link deserializeGatewayWithAccumulator} from within the live-balance
108
+ * pipeline when the accumulator value is actually needed.
88
109
  */
89
110
  export declare function deserializeGateway(data: Buffer): AoGateway & {
90
111
  operator: string;
@@ -119,8 +140,19 @@ export declare function deserializeVault(data: Buffer): AoVaultData & {
119
140
  export type DeserializedDelegation = {
120
141
  gateway: string;
121
142
  delegator: string;
143
+ /**
144
+ * Last-settled principal (raw `Delegation.amount` from on-chain state).
145
+ * This value is stale between epochs — see `INVARIANTS.md` in the contracts
146
+ * repo. Use `computeLiveDelegationBalance` (with the gateway's current
147
+ * `cumulativeRewardPerToken`) to get the actual current balance.
148
+ */
122
149
  delegatedStake: number;
123
150
  startTimestamp: number;
151
+ /**
152
+ * Snapshot of `gateway.cumulative_reward_per_token` at the last settlement.
153
+ * Required input to `computeLiveDelegationBalance`.
154
+ */
155
+ rewardDebt: bigint;
124
156
  };
125
157
  /**
126
158
  * Deserialize a Delegation account from raw bytes.
@@ -52,6 +52,17 @@ export declare class SolanaARIOReadable {
52
52
  * keep the IDL-derived bytes as the single source of truth).
53
53
  */
54
54
  private getAccountsByDiscriminator;
55
+ /**
56
+ * Batch-fetch the `cumulative_reward_per_token` accumulator for every gateway
57
+ * in `operatorAddresses`. Returns a Map keyed by base58 operator address.
58
+ * Used by the delegate readers below to compute the live delegation balance
59
+ * without an on-chain settlement call (see {@link computeLiveDelegationBalance}
60
+ * and `INVARIANTS.md` in the contracts repo). Missing gateways are silently
61
+ * skipped — callers fall back to the stale `Delegation.amount` for those
62
+ * (the accumulator delta is 0 and live == stored anyway when the gateway
63
+ * has no rewards to distribute).
64
+ */
65
+ protected getGatewayAccumulators(operatorAddresses: string[]): Promise<Map<string, bigint>>;
55
66
  /** Read the gateway registry and return addresses in registry index order */
56
67
  protected getRegistryGatewayAddresses(): Promise<string[]>;
57
68
  getInfo(): Promise<{
@@ -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.6";
16
+ export declare const version = "4.0.0-solana.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ar.io/sdk",
3
- "version": "4.0.0-solana.7",
3
+ "version": "4.0.0-solana.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ar-io/ar-io-sdk.git"
@@ -61,10 +61,14 @@
61
61
  "test": "yarn test:unit && yarn test:e2e",
62
62
  "test:esm": "yarn build:esm && yarn link && cd ./tests/e2e/esm && yarn && yarn test",
63
63
  "test:web": "yarn build:esm && yarn link && cd ./tests/e2e/web && yarn && yarn test",
64
- "test:unit": "c8 tsx --test --test-reporter=spec --enable-source-maps --trace-warnings $(find src -type f -name '*.test.ts')",
64
+ "test:unit": "c8 tsx --test --test-reporter=spec --enable-source-maps --trace-warnings $(find src -type f -name '*.test.ts' -not -name '*.localnet.test.ts' -not -name '*.cross.test.ts' -not -name '*.e2e.test.ts')",
65
65
  "test:link": "yarn build && yarn link",
66
66
  "test:e2e": "yarn test:esm && yarn test:web",
67
67
  "test:integration": "yarn build:esm && yarn link && cd ./tests/integration && yarn && yarn test",
68
+ "test:localnet:io-write": "tsx --test --test-reporter=spec src/solana/io-writeable.localnet.test.ts",
69
+ "sdk-e2e:up": "bash scripts/sdk-e2e-bootstrap.sh",
70
+ "test:sdk-e2e:run": "tsx --test --test-reporter=spec src/solana/sdk.e2e.test.ts",
71
+ "test:sdk-e2e": "bash -c 'set -a && source test/.env.e2e && set +a && yarn test:sdk-e2e:run'",
68
72
  "prepare": "husky install",
69
73
  "docs:update": "markdown-toc-gen insert README.md --max-depth 2",
70
74
  "example:esm": "cd examples/esm && yarn && node index.mjs",