@ar.io/sdk 4.0.0-solana.32 → 4.0.0-solana.33

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.
@@ -76,3 +76,37 @@ export function computeLiveDelegationBalance({ delegatedStake, rewardDebt, cumul
76
76
  const capped = live > U64_MAX ? U64_MAX : live;
77
77
  return Number(capped);
78
78
  }
79
+ /**
80
+ * Select the delegations worth compounding, from decoded delegations + a map
81
+ * of their gateways' reward accumulators. Pure (no I/O) so it's unit-testable
82
+ * independently of RPC; `SolanaARIOReadable.getDelegationsToCompound` is just
83
+ * fetch+decode wrapped around this.
84
+ *
85
+ * A delegation is included when its pending reward (live balance − settled
86
+ * principal) exceeds `minPendingRewards`, EXCEPT when its gateway is `leaving`
87
+ * (those settle via `claim_delegate_from_leaving_gateway`, not compounding) or
88
+ * its gateway is missing/unreadable. Compounding sub-threshold dust only
89
+ * advances `reward_debt` for no balance gain, so it's filtered out.
90
+ */
91
+ export function selectCompoundableDelegations(delegations, gatewaysByOperator, minPendingRewards = 0) {
92
+ const out = [];
93
+ for (const del of delegations) {
94
+ const gw = gatewaysByOperator.get(del.gateway);
95
+ if (!gw || gw.status === 'leaving')
96
+ continue;
97
+ const live = computeLiveDelegationBalance({
98
+ delegatedStake: del.delegatedStake,
99
+ rewardDebt: del.rewardDebt,
100
+ cumulativeRewardPerToken: gw.cumulativeRewardPerToken,
101
+ });
102
+ const pendingRewards = live - del.delegatedStake;
103
+ if (pendingRewards <= minPendingRewards)
104
+ continue;
105
+ out.push({
106
+ gatewayAddress: del.gateway,
107
+ delegatorAddress: del.delegator,
108
+ pendingRewards,
109
+ });
110
+ }
111
+ return out;
112
+ }
@@ -29,7 +29,7 @@ import { Logger } from '../common/logger.js';
29
29
  import { SolanaANTRegistryReadable } from './ant-registry-readable.js';
30
30
  import { getAssociatedTokenAddressKit } from './ata.js';
31
31
  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';
32
- import { computeLiveDelegationBalance } from './delegation-math.js';
32
+ import { computeLiveDelegationBalance, selectCompoundableDelegations, } from './delegation-math.js';
33
33
  import { deserializeAllowlist, deserializeArioConfig, deserializeArnsRecord, deserializeDelegation, deserializeDemandFactor, deserializeEpoch, deserializeEpochSettings, deserializeEpochSettingsFull, deserializeGarSettings, deserializeGarSupplyCounters, deserializeGateway, deserializeGatewayWithAccumulator, deserializeObservation, deserializePrimaryName, deserializePrimaryNameRequest, deserializeRedelegationRecord, deserializeReservedName, deserializeReturnedName, deserializeVault, deserializeWithdrawal, } from './deserialize.js';
34
34
  import { TOKEN_PROGRAM_ADDRESS } from './instruction.js';
35
35
  import { getArioConfigPDA, getArnsRecordPDA, getArnsRecordPDAFromHash, getArnsSettingsPDA, getDemandFactorPDA, getEpochPDA, getEpochSettingsPDA, getGarSettingsPDA, getGatewayPDA, getGatewayRegistryPDA, getObserverLookupPDA, getPrimaryNamePDA, getPrimaryNameRequestPDA, getReservedNamePDA, getReturnedNamePDA, getVaultPDA, } from './pda.js';
@@ -1639,6 +1639,56 @@ export class SolanaARIOReadable {
1639
1639
  }));
1640
1640
  return paginate(items, params);
1641
1641
  }
1642
+ /**
1643
+ * Enumerate every delegation that has pending (unsettled) rewards — the work
1644
+ * list for the permissionless `compound_delegation_rewards` crank. Pending is
1645
+ * computed from the gateway's reward-per-share accumulator (mirrors
1646
+ * {@link computeLiveDelegationBalance}); the crank only changes balances, so
1647
+ * rewards already accrue correctly without it.
1648
+ *
1649
+ * Skips delegations whose gateway is `Leaving` (those settle through
1650
+ * `claim_delegate_from_leaving_gateway`, not compounding) and any below
1651
+ * `minPendingRewards` — compounding sub-threshold dust just advances
1652
+ * `reward_debt` for no balance gain. Feed the result, chunked, to
1653
+ * `SolanaARIOWriteable.compoundDelegationRewardsBatch`.
1654
+ */
1655
+ async getDelegationsToCompound(params) {
1656
+ const minPending = params?.minPendingRewards ?? 0;
1657
+ // One scan for gateways → accumulator + status.
1658
+ const gatewayAccounts = await this.getAccountsByDiscriminator(this.garProgram, GATEWAY_DISCRIMINATOR);
1659
+ const gateways = new Map();
1660
+ for (const { data } of gatewayAccounts) {
1661
+ try {
1662
+ const gw = deserializeGatewayWithAccumulator(data);
1663
+ gateways.set(gw.operator, {
1664
+ cumulativeRewardPerToken: gw.cumulativeRewardPerToken,
1665
+ status: gw.status,
1666
+ });
1667
+ }
1668
+ catch {
1669
+ // Skip malformed.
1670
+ }
1671
+ }
1672
+ // One scan for delegations; decode then delegate the selection logic to the
1673
+ // pure `selectCompoundableDelegations` (unit-tested in delegation-math).
1674
+ const delegationAccounts = await this.getAccountsByDiscriminator(this.garProgram, DELEGATION_DISCRIMINATOR);
1675
+ const delegations = [];
1676
+ for (const { data } of delegationAccounts) {
1677
+ try {
1678
+ const del = deserializeDelegation(data);
1679
+ delegations.push({
1680
+ gateway: del.gateway,
1681
+ delegator: del.delegator,
1682
+ delegatedStake: del.delegatedStake,
1683
+ rewardDebt: del.rewardDebt,
1684
+ });
1685
+ }
1686
+ catch {
1687
+ // Skip malformed.
1688
+ }
1689
+ }
1690
+ return selectCompoundableDelegations(delegations, gateways, minPending);
1691
+ }
1642
1692
  async getAllGatewayVaults(params) {
1643
1693
  const accounts = await this.getAccountsByDiscriminator(this.garProgram, WITHDRAWAL_DISCRIMINATOR);
1644
1694
  const items = [];
@@ -34,10 +34,10 @@
34
34
  * surface for them.
35
35
  */
36
36
  import { AccountRole, address, appendTransactionMessageInstructions, compileTransaction, createTransactionMessage, fetchEncodedAccount, getAddressDecoder, getBase64EncodedWireTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
37
- import { CostIntent, PurchaseType, getBuyNameFromDelegationInstructionAsync, getBuyNameFromFundingPlanInstructionAsync, getBuyNameFromOperatorStakeInstructionAsync, getBuyNameFromWithdrawalInstructionAsync, getBuyNameInstructionAsync, getBuyReturnedNameFromDelegationInstructionAsync, getBuyReturnedNameFromFundingPlanInstructionAsync, getBuyReturnedNameFromOperatorStakeInstructionAsync, getBuyReturnedNameFromWithdrawalInstructionAsync, getBuyReturnedNameInstructionAsync, getExtendLeaseFromDelegationInstructionAsync, getExtendLeaseFromFundingPlanInstructionAsync, getExtendLeaseFromOperatorStakeInstructionAsync, getExtendLeaseFromWithdrawalInstructionAsync, getExtendLeaseInstructionAsync, getGetTokenCostInstructionAsync, getIncreaseUndernameLimitFromDelegationInstructionAsync, getIncreaseUndernameLimitFromFundingPlanInstructionAsync, getIncreaseUndernameLimitFromOperatorStakeInstructionAsync, getIncreaseUndernameLimitFromWithdrawalInstructionAsync, getIncreaseUndernameLimitInstructionAsync, getMigrateArnsRecordInstruction, getPruneExpiredNamesInstructionAsync, getPruneExpiredReservationInstruction, getPruneNameToReturnedInstructionAsync, getPruneReturnedNamesInstructionAsync, getReassignNameInstructionAsync, getReleaseNameInstructionAsync, getUpgradeNameFromDelegationInstructionAsync, getUpgradeNameFromFundingPlanInstructionAsync, getUpgradeNameFromOperatorStakeInstructionAsync, getUpgradeNameFromWithdrawalInstructionAsync, getUpgradeNameInstructionAsync, } from '@ar.io/solana-contracts/arns';
37
+ import { CostIntent, PurchaseType, getBuyNameFromDelegationInstructionAsync, getBuyNameFromFundingPlanInstructionAsync, getBuyNameFromOperatorStakeInstructionAsync, getBuyNameFromWithdrawalInstructionAsync, getBuyNameInstructionAsync, getBuyReturnedNameFromDelegationInstructionAsync, getBuyReturnedNameFromFundingPlanInstructionAsync, getBuyReturnedNameFromOperatorStakeInstructionAsync, getBuyReturnedNameFromWithdrawalInstructionAsync, getBuyReturnedNameInstructionAsync, getExtendLeaseFromDelegationInstructionAsync, getExtendLeaseFromFundingPlanInstructionAsync, getExtendLeaseFromOperatorStakeInstructionAsync, getExtendLeaseFromWithdrawalInstructionAsync, getExtendLeaseInstructionAsync, getGetTokenCostInstructionAsync, getIncreaseUndernameLimitFromDelegationInstructionAsync, getIncreaseUndernameLimitFromFundingPlanInstructionAsync, getIncreaseUndernameLimitFromOperatorStakeInstructionAsync, getIncreaseUndernameLimitFromWithdrawalInstructionAsync, getIncreaseUndernameLimitInstructionAsync, getMigrateArnsRecordInstruction, getPruneExpiredNamesInstructionAsync, getPruneExpiredReservationInstruction, getPruneNameToReturnedInstructionAsync, getPruneReturnedNamesInstructionAsync, getReassignNameInstructionAsync, getReleaseNameInstructionAsync, getUpdateDemandFactorInstruction, getUpgradeNameFromDelegationInstructionAsync, getUpgradeNameFromFundingPlanInstructionAsync, getUpgradeNameFromOperatorStakeInstructionAsync, getUpgradeNameFromWithdrawalInstructionAsync, getUpgradeNameInstructionAsync, } from '@ar.io/solana-contracts/arns';
38
38
  import { FundingSourceKind as GeneratedFundingSourceKindEnum } from '@ar.io/solana-contracts/gar';
39
39
  import { buildCreateAtaIdempotentIx, getAssociatedTokenAddressKit, } from './ata.js';
40
- import { deserializeArnsRecord, deserializeEpochSettingsFull, deserializePrimaryName, } from './deserialize.js';
40
+ import { deserializeArnsRecord, deserializeDemandFactor, deserializeEpochSettingsFull, deserializePrimaryName, } from './deserialize.js';
41
41
  import { buildFundingPlan as buildFundingPlanCore, buildFundingPlanRemainingAccounts, computeResidueIndexes, predictResidueVaults, } from './funding-plan.js';
42
42
  /** Maps the SDK's user-facing FundingSourceKind string union to the
43
43
  * Codama-generated enum used by the on-chain ix payload. */
@@ -53,7 +53,7 @@ function toGeneratedFundingSourceSpec(s) {
53
53
  import { getSyncAttributesInstruction } from '@ar.io/solana-contracts/ant';
54
54
  import { getApprovePrimaryNameInstructionAsync, getCloseExpiredRequestInstruction, getCreateVaultInstructionAsync, getExtendVaultInstructionAsync, getIncreaseVaultInstructionAsync, getReleaseVaultInstructionAsync, getRemovePrimaryNameInstructionAsync, getRequestAndSetPrimaryNameFromFundingPlanInstructionAsync, getRequestAndSetPrimaryNameInstructionAsync, getRequestPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameInstructionAsync, getRevokeVaultInstructionAsync, getVaultedTransferInstructionAsync, } from '@ar.io/solana-contracts/core';
55
55
  import { getDelegationDecoder, getGatewayDecoder, } from '@ar.io/solana-contracts/gar';
56
- import { Protocol, getAllowDelegateInstructionAsync, getCancelWithdrawalInstruction, getClaimDelegateFromDisabledGatewayInstructionAsync, getClaimDelegateFromLeavingGatewayInstructionAsync, getClaimWithdrawalInstructionAsync, getCloseDrainedWithdrawalInstruction, getCloseEmptyDelegationInstruction, getCloseEpochInstructionAsync, getCloseObservationInstructionAsync, getCreateEpochInstructionAsync, getDecreaseDelegateStakeInstructionAsync, getDecreaseOperatorStakeInstructionAsync, getDelegateStakeInstructionAsync, getDisallowDelegateInstructionAsync, getDistributeEpochInstructionAsync, getFinalizeGoneInstructionAsync, getIncreaseOperatorStakeInstructionAsync, getInstantWithdrawalInstructionAsync, getJoinNetworkInstructionAsync, getLeaveNetworkInstructionAsync, getPrescribeEpochInstructionAsync, getPruneGatewayInstructionAsync, getRedelegateStakeInstructionAsync, getSaveObservationsInstructionAsync, getSetAllowlistEnabledInstructionAsync, getTallyWeightsInstructionAsync, getUpdateGatewaySettingsInstructionAsync, getUpdateObserverAddressInstructionAsync, } from '@ar.io/solana-contracts/gar';
56
+ import { Protocol, getAllowDelegateInstructionAsync, getCancelWithdrawalInstruction, getClaimDelegateFromDisabledGatewayInstructionAsync, getClaimDelegateFromLeavingGatewayInstructionAsync, getClaimWithdrawalInstructionAsync, getCloseDrainedWithdrawalInstruction, getCloseEmptyDelegationInstruction, getCloseEpochInstructionAsync, getCloseObservationInstructionAsync, getCompoundDelegationRewardsInstruction, getCreateEpochInstructionAsync, getDecreaseDelegateStakeInstructionAsync, getDecreaseOperatorStakeInstructionAsync, getDelegateStakeInstructionAsync, getDisallowDelegateInstructionAsync, getDistributeEpochInstructionAsync, getFinalizeGoneInstructionAsync, getIncreaseOperatorStakeInstructionAsync, getInstantWithdrawalInstructionAsync, getJoinNetworkInstructionAsync, getLeaveNetworkInstructionAsync, getPrescribeEpochInstructionAsync, getPruneGatewayInstructionAsync, getRedelegateStakeInstructionAsync, getSaveObservationsInstructionAsync, getSetAllowlistEnabledInstructionAsync, getTallyWeightsInstructionAsync, getUpdateGatewaySettingsInstructionAsync, getUpdateObserverAddressInstructionAsync, } from '@ar.io/solana-contracts/gar';
57
57
  import { getTransferCheckedInstruction } from '@solana-program/token';
58
58
  import { ARIO_ANT_PROGRAM_ID, TOKEN_DECIMALS } from './constants.js';
59
59
  import { SolanaARIOReadable } from './io-readable.js';
@@ -227,6 +227,14 @@ export function encodeReportTxId(reportTxId) {
227
227
  decoded.copy(out);
228
228
  return out;
229
229
  }
230
+ /**
231
+ * Max `compound_delegation_rewards` instructions packed into one batched tx.
232
+ * Each carries a gateway + delegation + delegator account; 12 stays well within
233
+ * the per-tx account/1232-byte budget even when none share a gateway.
234
+ */
235
+ const MAX_COMPOUND_BATCH = 12;
236
+ /** Demand-factor period length (seconds) — mirrors `PERIOD_LENGTH_SECONDS` in ario-arns. */
237
+ const DEMAND_FACTOR_PERIOD_SECONDS = 86_400;
230
238
  /**
231
239
  * Detect the GAR `InvalidGatewayAccount` error by Anchor error name/message
232
240
  * (walking the cause chain + `context.logs`), NOT by numeric code — codes are
@@ -2124,6 +2132,66 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
2124
2132
  return { id: sig };
2125
2133
  }
2126
2134
  // =========================================
2135
+ // Lazy-state crank steps — materialize accumulator/period state.
2136
+ // Both are permissionless + idempotent (safe to crank on a schedule).
2137
+ // =========================================
2138
+ /**
2139
+ * Roll the demand factor forward to the current period. Permissionless and
2140
+ * idempotent — a no-op within the same period. Pricing already rolls the
2141
+ * factor inline on every buy/extend, so this only refreshes the STORED
2142
+ * factor that `getDemandFactor` and between-buy price previews read; a
2143
+ * periodic crank (~once per 24h `PERIOD_LENGTH_SECONDS`) keeps it current.
2144
+ */
2145
+ async updateDemandFactor(_options) {
2146
+ const [demandFactorPda] = await getDemandFactorPDA(this.arnsProgram);
2147
+ const ix = getUpdateDemandFactorInstruction({ demandFactor: demandFactorPda, payer: this.signer }, { programAddress: this.arnsProgram });
2148
+ const sig = await this.sendTransaction([ix]);
2149
+ return { id: sig };
2150
+ }
2151
+ /**
2152
+ * Materialize a single delegate's pending rewards into their delegated
2153
+ * stake by settling the gateway's reward-per-share accumulator.
2154
+ * Permissionless — there is no signer beyond the fee payer; `delegator` is
2155
+ * only a PDA-derivation seed. Rewards always accrue correctly in the
2156
+ * accumulator regardless of this call; compounding makes the on-chain
2157
+ * `delegatedStake` reflect them (and earn compound interest in the next
2158
+ * epoch's weighting). Idempotent — a no-op once already settled.
2159
+ */
2160
+ async compoundDelegationRewards(params, _options) {
2161
+ const ix = await this.buildCompoundDelegationRewardsInstruction(params);
2162
+ const sig = await this.sendTransaction([ix]);
2163
+ return { id: sig };
2164
+ }
2165
+ /**
2166
+ * Compound many delegates' rewards in a SINGLE transaction — one
2167
+ * `compound_delegation_rewards` instruction per entry. Idempotent and
2168
+ * permissionless, so partial batches are safe to retry. Keep each batch
2169
+ * within the per-tx account/CU budget; grouping entries that share a gateway
2170
+ * lowers the unique-account count (the gateway account is reused across
2171
+ * instructions). Typical cranker usage: enumerate with
2172
+ * `SolanaARIOReadable.getDelegationsToCompound`, chunk, then call this.
2173
+ */
2174
+ async compoundDelegationRewardsBatch(delegations, _options) {
2175
+ if (delegations.length === 0) {
2176
+ throw new Error('compoundDelegationRewardsBatch: delegations list is empty');
2177
+ }
2178
+ const ixs = await Promise.all(delegations.map((d) => this.buildCompoundDelegationRewardsInstruction(d)));
2179
+ const sig = await this.sendTransaction(ixs, 1_400_000);
2180
+ return { id: sig };
2181
+ }
2182
+ /**
2183
+ * Build a single `compound_delegation_rewards` instruction (shared by the
2184
+ * single + batch methods). PDAs are derived under the configured gar program
2185
+ * so the program-id override always targets the right cluster.
2186
+ */
2187
+ async buildCompoundDelegationRewardsInstruction(params) {
2188
+ const gateway = address(params.gateway);
2189
+ const delegator = address(params.delegator);
2190
+ const [gatewayPda] = await getGatewayPDA(gateway, this.garProgram);
2191
+ const [delegationPda] = await getDelegationPDA(gateway, delegator, this.garProgram);
2192
+ return getCompoundDelegationRewardsInstruction({ gateway: gatewayPda, delegation: delegationPda, delegator }, { programAddress: this.garProgram });
2193
+ }
2194
+ // =========================================
2127
2195
  // Epoch cranking (ario-gar) — permissionless
2128
2196
  // =========================================
2129
2197
  /**
@@ -2477,6 +2545,9 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
2477
2545
  const batchSize = Math.min(opts.batchSize ?? MAX_LIFECYCLE_BATCH, MAX_LIFECYCLE_BATCH);
2478
2546
  const enableClose = opts.enableClose ?? true;
2479
2547
  const retention = opts.epochRetention ?? 7;
2548
+ const enableCompound = opts.enableCompound ?? true;
2549
+ const compoundMinPending = opts.compoundMinPendingRewards ?? 0;
2550
+ const enableDemandFactorRoll = opts.enableDemandFactorRoll ?? true;
2480
2551
  const now = opts.now ?? Math.floor(Date.now() / 1000);
2481
2552
  const settings = await this.getEpochSettingsFull();
2482
2553
  if (!settings.enabled)
@@ -2561,6 +2632,22 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
2561
2632
  return { action: 'close', epochIndex: closeTarget, txId: id };
2562
2633
  }
2563
2634
  }
2635
+ // Lazy-state maintenance — lower urgency than the lifecycle, reached only
2636
+ // once the live epoch is fully distributed (rewardsDistributed === 1 here).
2637
+ // Compound FIRST so delegated stake reflects the just-distributed rewards
2638
+ // before the next epoch's tally weights it; then roll the demand factor if
2639
+ // its period elapsed. Both are permissionless + idempotent, and run BEFORE
2640
+ // creating the next epoch so the compounded stake is in place for its tally.
2641
+ if (enableCompound) {
2642
+ const compounded = await this.maybeCompoundStep(compoundMinPending);
2643
+ if (compounded)
2644
+ return compounded;
2645
+ }
2646
+ if (enableDemandFactorRoll) {
2647
+ const rolled = await this.maybeRollDemandFactorStep(now);
2648
+ if (rolled)
2649
+ return rolled;
2650
+ }
2564
2651
  // Current epoch fully processed — create the next once its start arrives.
2565
2652
  if (now >= nextEpochStart) {
2566
2653
  const { id } = await this.createEpoch();
@@ -2568,6 +2655,61 @@ export class SolanaARIOWriteable extends SolanaARIOReadable {
2568
2655
  }
2569
2656
  return { action: 'idle', reason: 'epoch_complete' };
2570
2657
  }
2658
+ /**
2659
+ * One compound batch over delegations with pending rewards (≤
2660
+ * {@link MAX_COMPOUND_BATCH} per tx), or `null` when none are due. Settling
2661
+ * is idempotent, so this converges over a few crank steps then no-ops until
2662
+ * the next epoch's distribution advances the accumulator again.
2663
+ */
2664
+ async maybeCompoundStep(minPendingRewards) {
2665
+ const pending = await this.getDelegationsToCompound({ minPendingRewards });
2666
+ if (pending.length === 0)
2667
+ return null;
2668
+ const batch = pending.slice(0, MAX_COMPOUND_BATCH).map((p) => ({
2669
+ gateway: p.gatewayAddress,
2670
+ delegator: p.delegatorAddress,
2671
+ }));
2672
+ const { id } = await this.compoundDelegationRewardsBatch(batch);
2673
+ return {
2674
+ action: 'compound',
2675
+ txId: id,
2676
+ progress: { index: batch.length, total: pending.length },
2677
+ };
2678
+ }
2679
+ /**
2680
+ * Roll the demand factor forward if its (wall-clock) period elapsed since the
2681
+ * last stored roll, else `null`. Mirrors the on-chain period math; the roll
2682
+ * itself is idempotent.
2683
+ */
2684
+ async maybeRollDemandFactorStep(now) {
2685
+ const state = await this.getDemandFactorPeriodState();
2686
+ if (!state)
2687
+ return null;
2688
+ const elapsed = now - state.periodZeroStartTimestamp;
2689
+ const periodForNow = elapsed < 0 ? 1 : Math.floor(elapsed / DEMAND_FACTOR_PERIOD_SECONDS) + 1;
2690
+ if (periodForNow <= state.currentPeriod)
2691
+ return null; // same period — no-op
2692
+ const { id } = await this.updateDemandFactor();
2693
+ return { action: 'update_demand_factor', txId: id };
2694
+ }
2695
+ /**
2696
+ * The DemandFactor account's stored period + period-zero start (seconds) —
2697
+ * the gate for {@link maybeRollDemandFactorStep}. `null` if the account
2698
+ * doesn't exist (pre-genesis).
2699
+ */
2700
+ async getDemandFactorPeriodState() {
2701
+ const [pda] = await getDemandFactorPDA(this.arnsProgram);
2702
+ const account = await fetchEncodedAccount(this.rpc, pda, {
2703
+ commitment: this.commitment,
2704
+ });
2705
+ if (!account.exists)
2706
+ return null;
2707
+ const df = deserializeDemandFactor(Buffer.from(account.data));
2708
+ return {
2709
+ currentPeriod: df.currentPeriod,
2710
+ periodZeroStartTimestamp: df.periodZeroStartTimestamp,
2711
+ };
2712
+ }
2571
2713
  /**
2572
2714
  * Read the raw epoch account data for cranker state inspection.
2573
2715
  * Returns null if the epoch account doesn't exist yet.
@@ -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.32';
17
+ export const version = '4.0.0-solana.33';
@@ -18,3 +18,28 @@ export declare function computeLiveDelegationBalance({ delegatedStake, rewardDeb
18
18
  rewardDebt: bigint;
19
19
  cumulativeRewardPerToken: bigint;
20
20
  }): number;
21
+ /**
22
+ * Select the delegations worth compounding, from decoded delegations + a map
23
+ * of their gateways' reward accumulators. Pure (no I/O) so it's unit-testable
24
+ * independently of RPC; `SolanaARIOReadable.getDelegationsToCompound` is just
25
+ * fetch+decode wrapped around this.
26
+ *
27
+ * A delegation is included when its pending reward (live balance − settled
28
+ * principal) exceeds `minPendingRewards`, EXCEPT when its gateway is `leaving`
29
+ * (those settle via `claim_delegate_from_leaving_gateway`, not compounding) or
30
+ * its gateway is missing/unreadable. Compounding sub-threshold dust only
31
+ * advances `reward_debt` for no balance gain, so it's filtered out.
32
+ */
33
+ export declare function selectCompoundableDelegations(delegations: Array<{
34
+ gateway: string;
35
+ delegator: string;
36
+ delegatedStake: number;
37
+ rewardDebt: bigint;
38
+ }>, gatewaysByOperator: Map<string, {
39
+ cumulativeRewardPerToken: bigint;
40
+ status: string;
41
+ }>, minPendingRewards?: number): Array<{
42
+ gatewayAddress: string;
43
+ delegatorAddress: string;
44
+ pendingRewards: number;
45
+ }>;
@@ -287,6 +287,26 @@ export declare class SolanaARIOReadable {
287
287
  }): Promise<RedelegationFeeInfo>;
288
288
  getGatewayRegistrySettings(): Promise<GatewayRegistrySettings>;
289
289
  getAllDelegates(params?: PaginationParams<AllDelegates>): Promise<PaginationResult<AllDelegates>>;
290
+ /**
291
+ * Enumerate every delegation that has pending (unsettled) rewards — the work
292
+ * list for the permissionless `compound_delegation_rewards` crank. Pending is
293
+ * computed from the gateway's reward-per-share accumulator (mirrors
294
+ * {@link computeLiveDelegationBalance}); the crank only changes balances, so
295
+ * rewards already accrue correctly without it.
296
+ *
297
+ * Skips delegations whose gateway is `Leaving` (those settle through
298
+ * `claim_delegate_from_leaving_gateway`, not compounding) and any below
299
+ * `minPendingRewards` — compounding sub-threshold dust just advances
300
+ * `reward_debt` for no balance gain. Feed the result, chunked, to
301
+ * `SolanaARIOWriteable.compoundDelegationRewardsBatch`.
302
+ */
303
+ getDelegationsToCompound(params?: {
304
+ minPendingRewards?: number;
305
+ }): Promise<Array<{
306
+ gatewayAddress: string;
307
+ delegatorAddress: string;
308
+ pendingRewards: number;
309
+ }>>;
290
310
  getAllGatewayVaults(params?: PaginationParams<AllGatewayVaults>): Promise<PaginationResult<AllGatewayVaults>>;
291
311
  /**
292
312
  * Enumerate ArnsRecord PDAs whose lease has fully expired
@@ -122,7 +122,7 @@ export declare function buildObservationBitmap(registryAddresses: string[], fail
122
122
  */
123
123
  export declare function encodeReportTxId(reportTxId: string | undefined): Buffer;
124
124
  /** The single on-chain action a {@link SolanaARIOWriteable.crankEpochStep} call performed. */
125
- export type CrankAction = 'create' | 'tally' | 'prescribe' | 'distribute' | 'close' | 'idle';
125
+ export type CrankAction = 'create' | 'tally' | 'prescribe' | 'distribute' | 'compound' | 'update_demand_factor' | 'close' | 'idle';
126
126
  /** Options for {@link SolanaARIOWriteable.crankEpochStep}. */
127
127
  export interface CrankEpochStepOptions {
128
128
  /** Gateways per tally/distribute batch. Default 30. */
@@ -137,6 +137,27 @@ export interface CrankEpochStepOptions {
137
137
  enableClose?: boolean;
138
138
  /** Epochs of retention before an epoch may be closed (GAR-006). Default 7. */
139
139
  epochRetention?: number;
140
+ /**
141
+ * Compound pending delegate rewards (settle the reward-per-share accumulator
142
+ * into delegated stake) once the live epoch is fully distributed, so the next
143
+ * epoch's tally weights the compounded stake. Default true. The delegate
144
+ * rewards are correct in the accumulator regardless — this only materializes
145
+ * them on-chain. Each step compounds one batch; runs only in the otherwise-
146
+ * idle tail (never during tally/distribute).
147
+ */
148
+ enableCompound?: boolean;
149
+ /**
150
+ * Skip compounding delegations whose pending reward is at/below this (mARIO).
151
+ * Avoids dust compounds that only advance `reward_debt`. Default 0.
152
+ */
153
+ compoundMinPendingRewards?: number;
154
+ /**
155
+ * Roll the demand factor forward when its (wall-clock) period has elapsed.
156
+ * Idempotent — only sends a tx when the stored period is behind. Pricing is
157
+ * always lazily correct without this; it keeps the STORED factor (and reads
158
+ * between buys) current. Default true. Runs only in the idle tail.
159
+ */
160
+ enableDemandFactorRoll?: boolean;
140
161
  /** Unix seconds; defaults to the wall clock. Injectable for testing. */
141
162
  now?: number;
142
163
  }
@@ -510,6 +531,46 @@ export declare class SolanaARIOWriteable extends SolanaARIOReadable {
510
531
  releaseName(params: {
511
532
  name: string;
512
533
  }, _options?: WriteOptions): Promise<MessageResult>;
534
+ /**
535
+ * Roll the demand factor forward to the current period. Permissionless and
536
+ * idempotent — a no-op within the same period. Pricing already rolls the
537
+ * factor inline on every buy/extend, so this only refreshes the STORED
538
+ * factor that `getDemandFactor` and between-buy price previews read; a
539
+ * periodic crank (~once per 24h `PERIOD_LENGTH_SECONDS`) keeps it current.
540
+ */
541
+ updateDemandFactor(_options?: WriteOptions): Promise<MessageResult>;
542
+ /**
543
+ * Materialize a single delegate's pending rewards into their delegated
544
+ * stake by settling the gateway's reward-per-share accumulator.
545
+ * Permissionless — there is no signer beyond the fee payer; `delegator` is
546
+ * only a PDA-derivation seed. Rewards always accrue correctly in the
547
+ * accumulator regardless of this call; compounding makes the on-chain
548
+ * `delegatedStake` reflect them (and earn compound interest in the next
549
+ * epoch's weighting). Idempotent — a no-op once already settled.
550
+ */
551
+ compoundDelegationRewards(params: {
552
+ gateway: string;
553
+ delegator: string;
554
+ }, _options?: WriteOptions): Promise<MessageResult>;
555
+ /**
556
+ * Compound many delegates' rewards in a SINGLE transaction — one
557
+ * `compound_delegation_rewards` instruction per entry. Idempotent and
558
+ * permissionless, so partial batches are safe to retry. Keep each batch
559
+ * within the per-tx account/CU budget; grouping entries that share a gateway
560
+ * lowers the unique-account count (the gateway account is reused across
561
+ * instructions). Typical cranker usage: enumerate with
562
+ * `SolanaARIOReadable.getDelegationsToCompound`, chunk, then call this.
563
+ */
564
+ compoundDelegationRewardsBatch(delegations: Array<{
565
+ gateway: string;
566
+ delegator: string;
567
+ }>, _options?: WriteOptions): Promise<MessageResult>;
568
+ /**
569
+ * Build a single `compound_delegation_rewards` instruction (shared by the
570
+ * single + batch methods). PDAs are derived under the configured gar program
571
+ * so the program-id override always targets the right cluster.
572
+ */
573
+ private buildCompoundDelegationRewardsInstruction;
513
574
  /**
514
575
  * Create a new epoch. Permissionless — anyone can call when the next
515
576
  * epoch's start time has arrived.
@@ -640,6 +701,28 @@ export declare class SolanaARIOWriteable extends SolanaARIOReadable {
640
701
  * internally-handled error is the prescribe `InvalidGatewayAccount` retry.
641
702
  */
642
703
  crankEpochStep(opts?: CrankEpochStepOptions): Promise<CrankEpochStepResult>;
704
+ /**
705
+ * One compound batch over delegations with pending rewards (≤
706
+ * {@link MAX_COMPOUND_BATCH} per tx), or `null` when none are due. Settling
707
+ * is idempotent, so this converges over a few crank steps then no-ops until
708
+ * the next epoch's distribution advances the accumulator again.
709
+ */
710
+ private maybeCompoundStep;
711
+ /**
712
+ * Roll the demand factor forward if its (wall-clock) period elapsed since the
713
+ * last stored roll, else `null`. Mirrors the on-chain period math; the roll
714
+ * itself is idempotent.
715
+ */
716
+ private maybeRollDemandFactorStep;
717
+ /**
718
+ * The DemandFactor account's stored period + period-zero start (seconds) —
719
+ * the gate for {@link maybeRollDemandFactorStep}. `null` if the account
720
+ * doesn't exist (pre-genesis).
721
+ */
722
+ getDemandFactorPeriodState(): Promise<{
723
+ currentPeriod: number;
724
+ periodZeroStartTimestamp: number;
725
+ } | null>;
643
726
  /**
644
727
  * Read the raw epoch account data for cranker state inspection.
645
728
  * Returns null if the epoch account doesn't exist yet.
@@ -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.31";
16
+ export declare const version = "4.0.0-solana.32";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ar.io/sdk",
3
- "version": "4.0.0-solana.32",
3
+ "version": "4.0.0-solana.33",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ar-io/ar-io-sdk.git"