@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.
- package/lib/esm/solana/delegation-math.js +34 -0
- package/lib/esm/solana/io-readable.js +51 -1
- package/lib/esm/solana/io-writeable.js +145 -3
- package/lib/esm/version.js +1 -1
- package/lib/types/solana/delegation-math.d.ts +25 -0
- package/lib/types/solana/io-readable.d.ts +20 -0
- package/lib/types/solana/io-writeable.d.ts +84 -1
- package/lib/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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.
|
package/lib/esm/version.js
CHANGED
|
@@ -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.
|
package/lib/types/version.d.ts
CHANGED