@ar.io/sdk 3.24.0 → 4.0.0-alpha.1
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/README.md +682 -600
- package/lib/esm/cli/cli.js +188 -152
- package/lib/esm/cli/commands/antCommands.js +23 -58
- package/lib/esm/cli/commands/arnsPurchaseCommands.js +48 -30
- package/lib/esm/cli/commands/escrowCommands.js +221 -0
- package/lib/esm/cli/commands/gatewayWriteCommands.js +142 -23
- package/lib/esm/cli/commands/pruneCommands.js +150 -0
- package/lib/esm/cli/commands/readCommands.js +22 -3
- package/lib/esm/cli/commands/transfer.js +6 -6
- package/lib/esm/cli/options.js +124 -58
- package/lib/esm/cli/utils.js +280 -174
- package/lib/esm/common/ant-registry.js +17 -143
- package/lib/esm/common/ant.js +44 -1167
- package/lib/esm/common/faucet.js +11 -6
- package/lib/esm/common/index.js +0 -4
- package/lib/esm/common/io.js +25 -1412
- package/lib/esm/constants.js +13 -19
- package/lib/esm/solana/ant-readable.js +724 -0
- package/lib/esm/solana/ant-registry-readable.js +133 -0
- package/lib/esm/solana/ant-registry-writeable.js +472 -0
- package/lib/esm/solana/ant-writeable.js +384 -0
- package/lib/esm/solana/ata.js +70 -0
- package/lib/esm/solana/canonical-message.js +128 -0
- package/lib/esm/solana/clusters.js +111 -0
- package/lib/esm/solana/constants.js +146 -0
- package/lib/esm/solana/delegation-math.js +112 -0
- package/lib/esm/solana/deserialize.js +711 -0
- package/lib/esm/solana/escrow.js +839 -0
- package/lib/{cjs/utils/json.js → esm/solana/events.js} +15 -10
- package/lib/esm/solana/funding-plan.js +699 -0
- package/lib/esm/solana/index.js +126 -0
- package/lib/esm/solana/instruction.js +39 -0
- package/lib/esm/solana/io-readable.js +2182 -0
- package/lib/esm/solana/io-writeable.js +3196 -0
- package/lib/esm/solana/json-rpc.js +90 -0
- package/lib/esm/solana/metadata.js +81 -0
- package/lib/esm/solana/mpl-core.js +192 -0
- package/lib/esm/solana/pda.js +332 -0
- package/lib/esm/solana/predict-prescribed-observers.js +110 -0
- package/lib/esm/solana/retry.js +117 -0
- package/lib/esm/solana/rpc-circuit-breaker.js +258 -0
- package/lib/esm/solana/send.js +372 -0
- package/lib/esm/solana/spawn-ant.js +224 -0
- package/lib/esm/solana/types.js +1 -0
- package/lib/esm/types/ant.js +27 -15
- package/lib/esm/types/io.js +8 -11
- package/lib/esm/utils/ant.js +0 -63
- package/lib/esm/utils/index.js +0 -3
- package/lib/esm/version.js +1 -1
- package/lib/types/cli/commands/antCommands.d.ts +5 -13
- package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +33 -7
- package/lib/types/cli/commands/escrowCommands.d.ts +68 -0
- package/lib/types/cli/commands/gatewayWriteCommands.d.ts +12 -11
- package/lib/types/cli/commands/pruneCommands.d.ts +31 -0
- package/lib/types/cli/commands/readCommands.d.ts +27 -22
- package/lib/types/cli/commands/transfer.d.ts +9 -9
- package/lib/types/cli/options.d.ts +76 -21
- package/lib/types/cli/types.d.ts +11 -13
- package/lib/types/cli/utils.d.ts +71 -31
- package/lib/types/common/ant-registry.d.ts +49 -47
- package/lib/types/common/ant.d.ts +54 -539
- package/lib/types/common/faucet.d.ts +20 -8
- package/lib/types/common/index.d.ts +0 -3
- package/lib/types/common/io.d.ts +51 -263
- package/lib/types/constants.d.ts +11 -18
- package/lib/types/solana/ant-readable.d.ts +180 -0
- package/lib/types/solana/ant-registry-readable.d.ts +105 -0
- package/lib/types/solana/ant-registry-writeable.d.ts +249 -0
- package/lib/types/solana/ant-writeable.d.ts +177 -0
- package/lib/types/solana/ata.d.ts +44 -0
- package/lib/types/solana/canonical-message.d.ts +121 -0
- package/lib/types/solana/clusters.d.ts +109 -0
- package/lib/types/solana/constants.d.ts +119 -0
- package/lib/types/solana/delegation-math.d.ts +45 -0
- package/lib/types/solana/deserialize.d.ts +262 -0
- package/lib/types/solana/escrow.d.ts +480 -0
- package/lib/types/solana/events.d.ts +38 -0
- package/lib/types/solana/funding-plan.d.ts +225 -0
- package/lib/types/solana/index.d.ts +87 -0
- package/lib/types/solana/instruction.d.ts +39 -0
- package/lib/types/solana/io-readable.d.ts +499 -0
- package/lib/types/solana/io-writeable.d.ts +893 -0
- package/lib/types/solana/json-rpc.d.ts +47 -0
- package/lib/types/solana/metadata.d.ts +84 -0
- package/lib/types/solana/mpl-core.d.ts +120 -0
- package/lib/types/solana/pda.d.ts +95 -0
- package/lib/types/solana/predict-prescribed-observers.d.ts +28 -0
- package/lib/types/solana/retry.d.ts +62 -0
- package/lib/types/solana/rpc-circuit-breaker.d.ts +78 -0
- package/lib/types/solana/send.d.ts +94 -0
- package/lib/types/solana/spawn-ant.d.ts +145 -0
- package/lib/types/solana/types.d.ts +82 -0
- package/lib/types/types/ant-registry.d.ts +43 -4
- package/lib/types/types/ant.d.ts +114 -96
- package/lib/types/types/common.d.ts +18 -74
- package/lib/types/types/faucet.d.ts +2 -2
- package/lib/types/types/io.d.ts +244 -158
- package/lib/types/types/token.d.ts +0 -12
- package/lib/types/utils/ant.d.ts +1 -12
- package/lib/types/utils/index.d.ts +0 -3
- package/lib/types/version.d.ts +1 -1
- package/package.json +36 -33
- package/lib/cjs/cli/cli.js +0 -822
- package/lib/cjs/cli/commands/antCommands.js +0 -113
- package/lib/cjs/cli/commands/arnsPurchaseCommands.js +0 -212
- package/lib/cjs/cli/commands/gatewayWriteCommands.js +0 -210
- package/lib/cjs/cli/commands/readCommands.js +0 -215
- package/lib/cjs/cli/commands/transfer.js +0 -159
- package/lib/cjs/cli/options.js +0 -470
- package/lib/cjs/cli/types.js +0 -2
- package/lib/cjs/cli/utils.js +0 -639
- package/lib/cjs/common/ant-registry.js +0 -155
- package/lib/cjs/common/ant-versions.js +0 -93
- package/lib/cjs/common/ant.js +0 -1182
- package/lib/cjs/common/arweave.js +0 -27
- package/lib/cjs/common/contracts/ao-process.js +0 -224
- package/lib/cjs/common/error.js +0 -64
- package/lib/cjs/common/faucet.js +0 -150
- package/lib/cjs/common/hyperbeam/hb.js +0 -173
- package/lib/cjs/common/index.js +0 -42
- package/lib/cjs/common/io.js +0 -1423
- package/lib/cjs/common/logger.js +0 -83
- package/lib/cjs/common/loggers/winston.js +0 -68
- package/lib/cjs/common/marketplace.js +0 -731
- package/lib/cjs/common/turbo.js +0 -223
- package/lib/cjs/constants.js +0 -41
- package/lib/cjs/node/index.js +0 -39
- package/lib/cjs/package.json +0 -1
- package/lib/cjs/types/ant-registry.js +0 -2
- package/lib/cjs/types/ant.js +0 -168
- package/lib/cjs/types/common.js +0 -2
- package/lib/cjs/types/faucet.js +0 -2
- package/lib/cjs/types/index.js +0 -37
- package/lib/cjs/types/io.js +0 -51
- package/lib/cjs/types/token.js +0 -116
- package/lib/cjs/utils/ant.js +0 -108
- package/lib/cjs/utils/ao.js +0 -432
- package/lib/cjs/utils/arweave.js +0 -285
- package/lib/cjs/utils/base64.js +0 -62
- package/lib/cjs/utils/hash.js +0 -56
- package/lib/cjs/utils/index.js +0 -38
- package/lib/cjs/utils/processes.js +0 -173
- package/lib/cjs/utils/random.js +0 -30
- package/lib/cjs/utils/schema.js +0 -15
- package/lib/cjs/utils/url.js +0 -37
- package/lib/cjs/version.js +0 -20
- package/lib/cjs/web/index.js +0 -41
- package/lib/esm/common/ant-versions.js +0 -87
- package/lib/esm/common/arweave.js +0 -21
- package/lib/esm/common/contracts/ao-process.js +0 -220
- package/lib/esm/common/hyperbeam/hb.js +0 -169
- package/lib/esm/common/marketplace.js +0 -724
- package/lib/esm/common/turbo.js +0 -215
- package/lib/esm/node/index.js +0 -20
- package/lib/esm/utils/ao.js +0 -420
- package/lib/esm/utils/arweave.js +0 -271
- package/lib/esm/utils/processes.js +0 -167
- package/lib/esm/web/index.js +0 -20
- package/lib/types/common/ant-versions.d.ts +0 -39
- package/lib/types/common/arweave.d.ts +0 -17
- package/lib/types/common/contracts/ao-process.d.ts +0 -47
- package/lib/types/common/hyperbeam/hb.d.ts +0 -88
- package/lib/types/common/marketplace.d.ts +0 -568
- package/lib/types/common/turbo.d.ts +0 -61
- package/lib/types/node/index.d.ts +0 -20
- package/lib/types/utils/ao.d.ts +0 -80
- package/lib/types/utils/arweave.d.ts +0 -79
- package/lib/types/utils/processes.d.ts +0 -39
- package/lib/types/web/index.d.ts +0 -20
|
@@ -0,0 +1,3196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Solana implementation of ARIOWrite interface.
|
|
18
|
+
*
|
|
19
|
+
* Extends SolanaARIOReadable with write operations that build and send
|
|
20
|
+
* Solana transactions via Codama-generated instruction builders.
|
|
21
|
+
*
|
|
22
|
+
* All instruction encoding (discriminators, account ordering, Borsh codecs,
|
|
23
|
+
* default value resolution for token/system programs) is delegated to the
|
|
24
|
+
* generated builders in `./generated/{core,gar,arns}/instructions/`. The
|
|
25
|
+
* builders are derived from the on-chain IDL and stay in sync via codegen.
|
|
26
|
+
*
|
|
27
|
+
* This file's job is just to:
|
|
28
|
+
* 1. Translate the AO-style SDK params into the builder's input shape.
|
|
29
|
+
* 2. Pre-derive the PDAs that the *Async builders can't infer (the ones
|
|
30
|
+
* whose seeds depend on runtime state — e.g. the next withdrawal id
|
|
31
|
+
* from the on-chain counter, or the buyer's ATA from a runtime mint).
|
|
32
|
+
* 3. Append remaining_accounts (gateway PDAs, name registry) for the
|
|
33
|
+
* epoch crank instructions, since Codama doesn't generate a typed
|
|
34
|
+
* surface for them.
|
|
35
|
+
*/
|
|
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, getUpdateDemandFactorInstruction, getUpgradeNameFromDelegationInstructionAsync, getUpgradeNameFromFundingPlanInstructionAsync, getUpgradeNameFromOperatorStakeInstructionAsync, getUpgradeNameFromWithdrawalInstructionAsync, getUpgradeNameInstructionAsync, } from '@ar.io/solana-contracts/arns';
|
|
38
|
+
import { FundingSourceKind as GeneratedFundingSourceKindEnum } from '@ar.io/solana-contracts/gar';
|
|
39
|
+
import { buildCreateAtaIdempotentIx, getAssociatedTokenAddressKit, } from './ata.js';
|
|
40
|
+
import { deserializeArnsRecord, deserializeDemandFactor, deserializeEpochSettingsFull, deserializePrimaryName, } from './deserialize.js';
|
|
41
|
+
import { buildFundingPlan as buildFundingPlanCore, buildFundingPlanRemainingAccounts, computeResidueIndexes, predictResidueVaults, } from './funding-plan.js';
|
|
42
|
+
/** Maps the SDK's user-facing FundingSourceKind string union to the
|
|
43
|
+
* Codama-generated enum used by the on-chain ix payload. */
|
|
44
|
+
function toGeneratedFundingSourceSpec(s) {
|
|
45
|
+
const kindMap = {
|
|
46
|
+
balance: GeneratedFundingSourceKindEnum.Balance,
|
|
47
|
+
delegation: GeneratedFundingSourceKindEnum.Delegation,
|
|
48
|
+
operatorStake: GeneratedFundingSourceKindEnum.OperatorStake,
|
|
49
|
+
withdrawal: GeneratedFundingSourceKindEnum.Withdrawal,
|
|
50
|
+
};
|
|
51
|
+
return { kind: kindMap[s.kind], amount: s.amount };
|
|
52
|
+
}
|
|
53
|
+
import { getSyncAttributesInstruction } from '@ar.io/solana-contracts/ant';
|
|
54
|
+
import { getApprovePrimaryNameInstructionAsync, getCloseExpiredRequestInstruction, getCreateVaultInstructionAsync, getExtendVaultInstructionAsync, getIncreaseVaultInstructionAsync, getReleaseVaultInstructionAsync, getRemovePrimaryNameInstructionAsync, getRequestAndSetPrimaryNameFromFundingPlanInstructionAsync, getRequestAndSetPrimaryNameInstructionAsync, getRequestPrimaryNameFromFundingPlanInstructionAsync, getRequestPrimaryNameInstructionAsync, getRevokeVaultInstructionAsync, getVaultedTransferInstructionAsync, } from '@ar.io/solana-contracts/core';
|
|
55
|
+
import { getDelegationDecoder, getGatewayDecoder, } 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
|
+
import { getTransferCheckedInstruction } from '@solana-program/token';
|
|
58
|
+
import { ARIO_ANT_PROGRAM_ID, TOKEN_DECIMALS } from './constants.js';
|
|
59
|
+
import { SolanaARIOReadable } from './io-readable.js';
|
|
60
|
+
import { getAntConfigPDA, getAntRecordPDA, getArioConfigPDA, getArnsRecordPDA, getArnsRegistryPDA, getArnsSettingsPDA, getDelegationPDA, getDemandFactorPDA, getEpochPDA, getEpochSettingsPDA, getGarSettingsPDA, getGatewayPDA, getGatewayRegistryPDA, getObservationPDA, getObserverLookupPDA, getPrimaryNamePDA, getPrimaryNameRequestPDA, getPrimaryNameReversePDA, getReservedNamePDA, getReturnedNamePDA, getVaultPDA, getWithdrawalCounterPDA, getWithdrawalPDA, hashName, } from './pda.js';
|
|
61
|
+
import { predictPrescribedObservers, } from './predict-prescribed-observers.js';
|
|
62
|
+
import { reclaimLookupTablesForSigner, sendAndConfirm, sendWithEphemeralLookupTable, } from './send.js';
|
|
63
|
+
const addressDecoder = getAddressDecoder();
|
|
64
|
+
/** Resolve mARIOToken | number to a plain number */
|
|
65
|
+
function toAmount(qty) {
|
|
66
|
+
if (typeof qty === 'number')
|
|
67
|
+
return qty;
|
|
68
|
+
return qty.valueOf();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Append additional `AccountMeta`s to a Codama-generated instruction.
|
|
72
|
+
*
|
|
73
|
+
* The generated `getXInstruction[Async]` builders return frozen objects with
|
|
74
|
+
* a typed, fixed `accounts` tuple. The Solana program accepts extra
|
|
75
|
+
* `remaining_accounts` for epoch crank ops (gateway PDAs, name registry) and
|
|
76
|
+
* for primary-name authorization (arnsRecord, demandFactor, antRecord — see
|
|
77
|
+
* `_buildPrimaryNameValidationAccounts`), but
|
|
78
|
+
* Codama has no typed surface for them — so we splice them in here.
|
|
79
|
+
*/
|
|
80
|
+
function withRemainingAccounts(ix, remaining) {
|
|
81
|
+
const accounts = [
|
|
82
|
+
...(ix.accounts ?? []),
|
|
83
|
+
...remaining,
|
|
84
|
+
];
|
|
85
|
+
return { ...ix, accounts };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Pick the swapped-gateway operator that `finalize_gone` needs as a writable
|
|
89
|
+
* `remaining_accounts[0]`.
|
|
90
|
+
*
|
|
91
|
+
* `finalize_gone` reclaims a gateway's slot from the compact GatewayRegistry by
|
|
92
|
+
* moving the LAST active slot into it and rewriting that swapped gateway's
|
|
93
|
+
* stored `registry_index`. When the finalized gateway is NOT already the last
|
|
94
|
+
* slot, the on-chain handler requires the swapped Gateway PDA (writable) at
|
|
95
|
+
* `remaining_accounts[0]`; when it IS the last slot, no swap occurs and no
|
|
96
|
+
* extra account is needed. See
|
|
97
|
+
* `programs/ario-gar/src/instructions/gateway.rs::finalize_gone`.
|
|
98
|
+
*
|
|
99
|
+
* `registryAddresses` MUST be the active registry operator addresses in slot
|
|
100
|
+
* order (`getRegistryGatewayAddresses()` — length === on-chain
|
|
101
|
+
* `registry.count`), so `registryAddresses[length - 1]` is exactly the
|
|
102
|
+
* `registry.gateways[count - 1].address` the on-chain swap reads.
|
|
103
|
+
*
|
|
104
|
+
* @returns the swapped gateway's operator address, or `null` when the finalized
|
|
105
|
+
* gateway already occupies the last slot.
|
|
106
|
+
* @throws if `registryIndex` is outside the active registry count (mirrors the
|
|
107
|
+
* on-chain `index < registry.count` guard, surfacing a stale index early).
|
|
108
|
+
*/
|
|
109
|
+
export function selectFinalizeGoneSwapOperator(registryIndex, registryAddresses) {
|
|
110
|
+
if (registryIndex < 0 || registryIndex >= registryAddresses.length) {
|
|
111
|
+
throw new Error(`finalizeGone: registry index ${registryIndex} is outside the active ` +
|
|
112
|
+
`registry count ${registryAddresses.length}`);
|
|
113
|
+
}
|
|
114
|
+
const lastIndex = registryAddresses.length - 1;
|
|
115
|
+
return registryIndex === lastIndex ? null : registryAddresses[lastIndex];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Split a primary name into its undername + base parts using the same rule
|
|
119
|
+
* as the on-chain `splitn(2, '_')` in `programs/ario-core/src/instructions/primary_name.rs`:
|
|
120
|
+
* everything before the first '_' is the undername, the rest is the base.
|
|
121
|
+
*
|
|
122
|
+
* Exposed as a top-level helper so it can be unit-tested without spinning up
|
|
123
|
+
* an `SolanaARIOWriteable`. Lowercases the input to match contract behavior.
|
|
124
|
+
*/
|
|
125
|
+
export function splitPrimaryName(name) {
|
|
126
|
+
const lower = name.toLowerCase();
|
|
127
|
+
const ix = lower.indexOf('_');
|
|
128
|
+
if (ix === -1) {
|
|
129
|
+
return { isUndername: false, baseName: lower, undername: null };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
isUndername: true,
|
|
133
|
+
baseName: lower.slice(ix + 1),
|
|
134
|
+
undername: lower.slice(0, ix),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Solana-backed read-write client for the AR.IO protocol.
|
|
139
|
+
*
|
|
140
|
+
* Usage:
|
|
141
|
+
* ```ts
|
|
142
|
+
* import {
|
|
143
|
+
* createSolanaRpc,
|
|
144
|
+
* createSolanaRpcSubscriptions,
|
|
145
|
+
* generateKeyPairSigner,
|
|
146
|
+
* } from '@solana/kit';
|
|
147
|
+
* import { SolanaARIOWriteable } from '@ar.io/sdk/solana';
|
|
148
|
+
*
|
|
149
|
+
* const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
|
|
150
|
+
* const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');
|
|
151
|
+
* const signer = await generateKeyPairSigner();
|
|
152
|
+
* const ario = new SolanaARIOWriteable({ rpc, rpcSubscriptions, signer });
|
|
153
|
+
*
|
|
154
|
+
* await ario.transfer({ target: 'RecipientPubkey...', qty: 100_000_000 });
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
// =========================================================================
|
|
158
|
+
// save_observations encoding helpers
|
|
159
|
+
// =========================================================================
|
|
160
|
+
// Extracted as pure functions so the bitmap-pack + base64url-decode logic
|
|
161
|
+
// can be unit-tested without standing up the rpc/signer plumbing of the
|
|
162
|
+
// SolanaARIOWriteable class. The on-chain ABI:
|
|
163
|
+
// - gateway_results: [u8; 375] bit i = 1 (pass) / 0 (fail) for the
|
|
164
|
+
// gateway at registry index i.
|
|
165
|
+
// - gateway_count: u16 must equal epoch.active_gateway_count.
|
|
166
|
+
// - report_tx_id: [u8; 32] raw 32-byte Arweave hash (base64url
|
|
167
|
+
// decoded from its 43-char string form).
|
|
168
|
+
/** Build the gateway_results bitmap for save_observations.
|
|
169
|
+
* All bits start as 1 (pass) for the first `registryAddresses.length`
|
|
170
|
+
* positions; positions named in `failedGateways` get cleared to 0; all
|
|
171
|
+
* positions beyond `registryAddresses.length` are 0. */
|
|
172
|
+
export function buildObservationBitmap(registryAddresses, failedGateways) {
|
|
173
|
+
const buf = Buffer.alloc(375, 0xff);
|
|
174
|
+
const failedSet = new Set(failedGateways);
|
|
175
|
+
for (let i = 0; i < registryAddresses.length; i++) {
|
|
176
|
+
if (failedSet.has(registryAddresses[i])) {
|
|
177
|
+
buf[Math.floor(i / 8)] &= ~(1 << (i % 8));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Clear bits beyond the active gateway count so the bitmap is exactly
|
|
181
|
+
// the prescribed shape (1s only at indices < gatewayCount that passed).
|
|
182
|
+
for (let i = registryAddresses.length; i < 3000; i++) {
|
|
183
|
+
buf[Math.floor(i / 8)] &= ~(1 << (i % 8));
|
|
184
|
+
}
|
|
185
|
+
return buf;
|
|
186
|
+
}
|
|
187
|
+
/** Encode an Arweave TX ID into the on-chain `[u8; 32]` slot.
|
|
188
|
+
*
|
|
189
|
+
* An Arweave TX ID **is** a 32-byte SHA-256 hash; the 43-char base64url
|
|
190
|
+
* string is just its presentation encoding. We decode here so the
|
|
191
|
+
* on-chain bytes are the raw hash — lossless and trivially reversible
|
|
192
|
+
* via base64url-encode on the consumer side. Without this, on-chain
|
|
193
|
+
* bytes alone couldn't be used to look up the original report bundle
|
|
194
|
+
* on permaweb (the whole point of recording the txid for auditability).
|
|
195
|
+
*
|
|
196
|
+
* Empty / undefined input → 32 zero bytes ("no permaweb archive
|
|
197
|
+
* configured for this submission" — the report still lives off-chain
|
|
198
|
+
* in the observer's local sinks but isn't anchored on Arweave).
|
|
199
|
+
*
|
|
200
|
+
* Throws on malformed input: the base64url string must be exactly 43
|
|
201
|
+
* chars and decode to 32 bytes. Strict validation here is desirable —
|
|
202
|
+
* silently truncating or accepting bad input would erode the
|
|
203
|
+
* auditability that the field exists for.
|
|
204
|
+
*/
|
|
205
|
+
export function encodeReportTxId(reportTxId) {
|
|
206
|
+
const out = Buffer.alloc(32);
|
|
207
|
+
if (reportTxId === undefined || reportTxId === '') {
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
// base64url → base64. The 43-char Arweave form has no padding; add it
|
|
211
|
+
// back so Node's `Buffer.from(_, 'base64')` accepts the input.
|
|
212
|
+
const padded = reportTxId
|
|
213
|
+
.replace(/-/g, '+')
|
|
214
|
+
.replace(/_/g, '/')
|
|
215
|
+
.padEnd(Math.ceil(reportTxId.length / 4) * 4, '=');
|
|
216
|
+
// Reject non-base64url chars up front — `Buffer.from` silently
|
|
217
|
+
// tolerates them, which would mask typos.
|
|
218
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(padded)) {
|
|
219
|
+
throw new Error(`reportTxId contains non-base64url characters: "${reportTxId}". ` +
|
|
220
|
+
`Expected a 43-char Arweave TX ID using A-Z, a-z, 0-9, -, _.`);
|
|
221
|
+
}
|
|
222
|
+
const decoded = Buffer.from(padded, 'base64');
|
|
223
|
+
if (decoded.length !== 32) {
|
|
224
|
+
throw new Error(`reportTxId must be a 43-char base64url Arweave TX ID decoding to 32 bytes; ` +
|
|
225
|
+
`got ${reportTxId.length} chars decoding to ${decoded.length} bytes.`);
|
|
226
|
+
}
|
|
227
|
+
decoded.copy(out);
|
|
228
|
+
return out;
|
|
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
|
+
/** Observation PDAs closed per tx before close_epoch (each ix carries Epoch +
|
|
237
|
+
* Observation + payer + system accounts — keep well under the tx account cap). */
|
|
238
|
+
const MAX_CLOSE_OBSERVATION_BATCH = 8;
|
|
239
|
+
/** Demand-factor period length (seconds) — mirrors `PERIOD_LENGTH_SECONDS` in ario-arns. */
|
|
240
|
+
const DEMAND_FACTOR_PERIOD_SECONDS = 86_400;
|
|
241
|
+
/**
|
|
242
|
+
* Detect the GAR `InvalidGatewayAccount` error by Anchor error name/message
|
|
243
|
+
* (walking the cause chain + `context.logs`), NOT by numeric code — codes are
|
|
244
|
+
* `6000 + enum-index` and shift across program versions, but the name and
|
|
245
|
+
* message are stable. `prescribe_epoch` raises this when a supplied observer
|
|
246
|
+
* Gateway PDA is missing/spoofed (e.g. a predicted observer left the registry
|
|
247
|
+
* between prediction and tx landing).
|
|
248
|
+
*/
|
|
249
|
+
export function isInvalidGatewayAccountError(error) {
|
|
250
|
+
const parts = [];
|
|
251
|
+
let cur = error;
|
|
252
|
+
for (let i = 0; cur != null && i < 8; i++) {
|
|
253
|
+
const e = cur;
|
|
254
|
+
if (e.message)
|
|
255
|
+
parts.push(e.message);
|
|
256
|
+
if (Array.isArray(e.context?.logs))
|
|
257
|
+
parts.push(e.context.logs.join('\n'));
|
|
258
|
+
cur = e.cause;
|
|
259
|
+
}
|
|
260
|
+
const text = parts.join('\n');
|
|
261
|
+
return (text.includes('InvalidGatewayAccount') ||
|
|
262
|
+
text.includes('Invalid gateway account'));
|
|
263
|
+
}
|
|
264
|
+
export class SolanaARIOWriteable extends SolanaARIOReadable {
|
|
265
|
+
signer;
|
|
266
|
+
rpcSubscriptions;
|
|
267
|
+
constructor(config) {
|
|
268
|
+
super(config);
|
|
269
|
+
this.signer = config.signer;
|
|
270
|
+
this.rpcSubscriptions = config.rpcSubscriptions;
|
|
271
|
+
}
|
|
272
|
+
/** The signer's on-chain address. */
|
|
273
|
+
get signerAddress() {
|
|
274
|
+
return this.signer.address;
|
|
275
|
+
}
|
|
276
|
+
async sendTransaction(instructions, computeUnitLimit = 400_000) {
|
|
277
|
+
return sendAndConfirm({
|
|
278
|
+
rpc: this.rpc,
|
|
279
|
+
rpcSubscriptions: this.rpcSubscriptions,
|
|
280
|
+
signer: this.signer,
|
|
281
|
+
instructions,
|
|
282
|
+
commitment: this.commitment,
|
|
283
|
+
computeUnitLimit,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/** Helper to get the ARIO mint and treasury from ArioConfig */
|
|
287
|
+
async getCoreConfig() {
|
|
288
|
+
const [configPda] = await getArioConfigPDA(this.coreProgram);
|
|
289
|
+
const account = await fetchEncodedAccount(this.rpc, configPda, {
|
|
290
|
+
commitment: this.commitment,
|
|
291
|
+
});
|
|
292
|
+
if (!account.exists)
|
|
293
|
+
throw new Error('ArioConfig not found');
|
|
294
|
+
const data = Buffer.from(account.data);
|
|
295
|
+
// ArioConfig: [8 disc][32 authority][32 mint][32 arns_program][32 treasury]
|
|
296
|
+
const mint = addressDecoder.decode(data.subarray(40, 72));
|
|
297
|
+
const treasury = addressDecoder.decode(data.subarray(104, 136));
|
|
298
|
+
return { mint, treasury };
|
|
299
|
+
}
|
|
300
|
+
async getMint() {
|
|
301
|
+
return (await this.getCoreConfig()).mint;
|
|
302
|
+
}
|
|
303
|
+
/** Helper to get ArNS config fields (mint and treasury) */
|
|
304
|
+
async getArnsConfig() {
|
|
305
|
+
const [settingsPda] = await getArnsSettingsPDA(this.arnsProgram);
|
|
306
|
+
const account = await fetchEncodedAccount(this.rpc, settingsPda, {
|
|
307
|
+
commitment: this.commitment,
|
|
308
|
+
});
|
|
309
|
+
if (!account.exists)
|
|
310
|
+
throw new Error('ArnsConfig not found');
|
|
311
|
+
const data = Buffer.from(account.data);
|
|
312
|
+
// ArnsConfig layout: [8 disc][32 authority][32 mint][32 treasury]...
|
|
313
|
+
const mint = addressDecoder.decode(data.subarray(40, 72));
|
|
314
|
+
const treasury = addressDecoder.decode(data.subarray(72, 104));
|
|
315
|
+
return { mint, treasury };
|
|
316
|
+
}
|
|
317
|
+
/** Helper to get GAR config fields (mint, stake pool, protocol pool) */
|
|
318
|
+
async getGarConfig() {
|
|
319
|
+
const [settingsPda] = await getGarSettingsPDA(this.garProgram);
|
|
320
|
+
const account = await fetchEncodedAccount(this.rpc, settingsPda, {
|
|
321
|
+
commitment: this.commitment,
|
|
322
|
+
});
|
|
323
|
+
if (!account.exists)
|
|
324
|
+
throw new Error('GarSettings not found');
|
|
325
|
+
const data = Buffer.from(account.data);
|
|
326
|
+
// GarSettings: [8 disc][32 authority][32 mint][8+8+8+8+8+8=48 u64s][4 u32][1 bool]
|
|
327
|
+
// [32 migration_authority][32 stake_token_account][32 protocol_token_account][1 bump]
|
|
328
|
+
const mint = addressDecoder.decode(data.subarray(40, 72));
|
|
329
|
+
const stakeTokenAccount = addressDecoder.decode(data.subarray(157, 189));
|
|
330
|
+
const protocolTokenAccount = addressDecoder.decode(data.subarray(189, 221));
|
|
331
|
+
return { mint, stakeTokenAccount, protocolTokenAccount };
|
|
332
|
+
}
|
|
333
|
+
// =========================================
|
|
334
|
+
// Codama default-PDA injection helpers
|
|
335
|
+
// =========================================
|
|
336
|
+
//
|
|
337
|
+
// Codama's auto-generated `getXInstructionAsync` builders fall back to
|
|
338
|
+
// calling `find<Account>Pda()` (no args) when a "defaultable" account is
|
|
339
|
+
// omitted from the input. Those `find*Pda` helpers default to the
|
|
340
|
+
// **placeholder** program addresses baked into the generated client
|
|
341
|
+
// (`ARioArnsProgXXX...`, `ArioCoreProgXXX...`, etc.), *not* the env- or
|
|
342
|
+
// constructor-overridden program ID we actually deploy at. The result is a
|
|
343
|
+
// PDA derived against the wrong program id, which on-chain shows up as
|
|
344
|
+
// Anchor `AccountNotInitialized` (#3012) for `config` / `demand_factor` /
|
|
345
|
+
// `name_registry` / `settings` / etc.
|
|
346
|
+
//
|
|
347
|
+
// The wrappers below pre-derive each program's defaultable PDAs against
|
|
348
|
+
// the **real** program id and merge them into the input so codama never
|
|
349
|
+
// touches its placeholder defaults. Caller-provided values still win
|
|
350
|
+
// because spread order is `(...defaults, ...input)`.
|
|
351
|
+
/**
|
|
352
|
+
* Inject ARNS default PDAs (config, demandFactor, nameRegistry).
|
|
353
|
+
*
|
|
354
|
+
* Extra fields not consumed by a given builder are harmlessly ignored
|
|
355
|
+
* (codama only reads the named keys from `input`).
|
|
356
|
+
*/
|
|
357
|
+
async withArnsDefaults(input) {
|
|
358
|
+
const [config] = await getArnsSettingsPDA(this.arnsProgram);
|
|
359
|
+
const [demandFactor] = await getDemandFactorPDA(this.arnsProgram);
|
|
360
|
+
const [nameRegistry] = await getArnsRegistryPDA(this.arnsProgram);
|
|
361
|
+
return { config, demandFactor, nameRegistry, ...input };
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* If the on-chain ArnsRecord for `name` hasn't been migrated to the
|
|
365
|
+
* current schema (name_hash at offset 8 doesn't match the expected
|
|
366
|
+
* hash), return a `migrate_arns_record` instruction that must be
|
|
367
|
+
* prepended to any operation referencing the record with PDA seed
|
|
368
|
+
* verification.
|
|
369
|
+
*
|
|
370
|
+
* Returns an empty array when the record is already up-to-date or
|
|
371
|
+
* doesn't exist.
|
|
372
|
+
*/
|
|
373
|
+
async _buildMigrateArnsRecordIxIfNeeded(name) {
|
|
374
|
+
const [arnsRecordPda] = await getArnsRecordPDA(name, this.arnsProgram);
|
|
375
|
+
const account = await fetchEncodedAccount(this.rpc, arnsRecordPda, {
|
|
376
|
+
commitment: this.commitment,
|
|
377
|
+
});
|
|
378
|
+
if (!account.exists)
|
|
379
|
+
return [];
|
|
380
|
+
const data = Buffer.from(account.data);
|
|
381
|
+
const expectedHash = hashName(name);
|
|
382
|
+
const storedHash = data.subarray(8, 40);
|
|
383
|
+
if (storedHash.equals(expectedHash))
|
|
384
|
+
return [];
|
|
385
|
+
return [
|
|
386
|
+
getMigrateArnsRecordInstruction({
|
|
387
|
+
record: arnsRecordPda,
|
|
388
|
+
payer: this.signer,
|
|
389
|
+
}, { programAddress: this.arnsProgram }),
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
/** Inject ARIO core default PDAs (config). */
|
|
393
|
+
async withCoreDefaults(input) {
|
|
394
|
+
const [config] = await getArioConfigPDA(this.coreProgram);
|
|
395
|
+
return { config, ...input };
|
|
396
|
+
}
|
|
397
|
+
/** Inject GAR default PDAs (settings, epochSettings, registry). */
|
|
398
|
+
async withGarDefaults(input) {
|
|
399
|
+
const [settings] = await getGarSettingsPDA(this.garProgram);
|
|
400
|
+
const [epochSettings] = await getEpochSettingsPDA(this.garProgram);
|
|
401
|
+
const [registry] = await getGatewayRegistryPDA(this.garProgram);
|
|
402
|
+
return { settings, epochSettings, registry, ...input };
|
|
403
|
+
}
|
|
404
|
+
/** Read WithdrawalCounter's next_id (returns 0n if not yet created) */
|
|
405
|
+
async getNextWithdrawalId(owner) {
|
|
406
|
+
const [counterPda] = await getWithdrawalCounterPDA(owner, this.garProgram);
|
|
407
|
+
const account = await fetchEncodedAccount(this.rpc, counterPda, {
|
|
408
|
+
commitment: this.commitment,
|
|
409
|
+
});
|
|
410
|
+
if (!account.exists)
|
|
411
|
+
return 0n;
|
|
412
|
+
// WithdrawalCounter: [8 disc][32 owner][8 next_id]
|
|
413
|
+
return Buffer.from(account.data).readBigUInt64LE(40);
|
|
414
|
+
}
|
|
415
|
+
// =========================================
|
|
416
|
+
// Token operations (ario-core)
|
|
417
|
+
// =========================================
|
|
418
|
+
async transfer(params, _options) {
|
|
419
|
+
const amount = toAmount(params.qty);
|
|
420
|
+
const recipient = address(params.target);
|
|
421
|
+
const mint = await this.getMint();
|
|
422
|
+
const fromATA = await getAssociatedTokenAddressKit(mint, this.signer.address);
|
|
423
|
+
const toATA = await getAssociatedTokenAddressKit(mint, recipient);
|
|
424
|
+
// SPL `transferChecked` requires the recipient ATA to exist; bundle
|
|
425
|
+
// an idempotent ATA-create so fresh recipients just work. Same
|
|
426
|
+
// pattern as `vaultedTransfer` below.
|
|
427
|
+
const createToAtaIx = buildCreateAtaIdempotentIx(this.signer.address, toATA, recipient, mint);
|
|
428
|
+
// Standard SPL Token `transferChecked`. The custom `ario-core::transfer`
|
|
429
|
+
// ix is deprecated — it added no protocol-level accounting, just wrapped
|
|
430
|
+
// this same CPI plus a `TransferEvent` emission that no major Solana
|
|
431
|
+
// indexer needs (Helius, Solscan, etc. all track SPL transfers natively).
|
|
432
|
+
// See `docs/REMOVE_CUSTOM_TRANSFER_PLAN.md` in `ar-io/solana-ar-io`.
|
|
433
|
+
// `transferChecked` (vs `transfer`) validates the mint + decimals
|
|
434
|
+
// on-chain, preventing cross-mint mistakes.
|
|
435
|
+
const ix = getTransferCheckedInstruction({
|
|
436
|
+
source: fromATA,
|
|
437
|
+
mint,
|
|
438
|
+
destination: toATA,
|
|
439
|
+
authority: this.signer,
|
|
440
|
+
amount,
|
|
441
|
+
decimals: TOKEN_DECIMALS,
|
|
442
|
+
});
|
|
443
|
+
const sig = await this.sendTransaction([createToAtaIx, ix]);
|
|
444
|
+
return { id: sig };
|
|
445
|
+
}
|
|
446
|
+
async vaultedTransfer(params, _options) {
|
|
447
|
+
const amount = toAmount(params.quantity);
|
|
448
|
+
const lockSeconds = Math.floor(params.lockLengthMs / 1000);
|
|
449
|
+
const recipient = address(params.recipient);
|
|
450
|
+
const mint = await this.getMint();
|
|
451
|
+
// Vault PDA depends on the recipient's *current* vault counter id, which
|
|
452
|
+
// the codegen builder can't infer — derive it manually.
|
|
453
|
+
const nextId = await this.getNextVaultId(recipient);
|
|
454
|
+
const [vaultPda] = await getVaultPDA(recipient, nextId, this.coreProgram);
|
|
455
|
+
const senderATA = await getAssociatedTokenAddressKit(mint, this.signer.address);
|
|
456
|
+
const vaultATA = await getAssociatedTokenAddressKit(mint, vaultPda, true);
|
|
457
|
+
const ix = await getVaultedTransferInstructionAsync(await this.withCoreDefaults({
|
|
458
|
+
vault: vaultPda,
|
|
459
|
+
senderTokenAccount: senderATA,
|
|
460
|
+
vaultTokenAccount: vaultATA,
|
|
461
|
+
recipient,
|
|
462
|
+
sender: this.signer,
|
|
463
|
+
amount,
|
|
464
|
+
lockDurationSeconds: lockSeconds,
|
|
465
|
+
revocable: params.revokable ?? false,
|
|
466
|
+
}), { programAddress: this.coreProgram });
|
|
467
|
+
// The on-chain CreateVault / VaultedTransfer constraint is
|
|
468
|
+
// `Account<TokenAccount>` (NOT `init`) — Anchor expects the vault ATA to
|
|
469
|
+
// already exist. Bundle an idempotent CreateAssociatedTokenAccount in
|
|
470
|
+
// the same tx so the caller doesn't need a separate setup step.
|
|
471
|
+
const createVaultAtaIx = buildCreateAtaIdempotentIx(this.signer.address, vaultATA, vaultPda, mint);
|
|
472
|
+
const sig = await this.sendTransaction([createVaultAtaIx, ix]);
|
|
473
|
+
return { id: sig };
|
|
474
|
+
}
|
|
475
|
+
async createVault(params, _options) {
|
|
476
|
+
const amount = toAmount(params.quantity);
|
|
477
|
+
const lockSeconds = Math.floor(params.lockLengthMs / 1000);
|
|
478
|
+
const mint = await this.getMint();
|
|
479
|
+
const nextId = await this.getNextVaultId(this.signer.address);
|
|
480
|
+
const [vaultPda] = await getVaultPDA(this.signer.address, nextId, this.coreProgram);
|
|
481
|
+
const ownerATA = await getAssociatedTokenAddressKit(mint, this.signer.address);
|
|
482
|
+
const vaultATA = await getAssociatedTokenAddressKit(mint, vaultPda, true);
|
|
483
|
+
const ix = await getCreateVaultInstructionAsync(await this.withCoreDefaults({
|
|
484
|
+
vault: vaultPda,
|
|
485
|
+
ownerTokenAccount: ownerATA,
|
|
486
|
+
vaultTokenAccount: vaultATA,
|
|
487
|
+
owner: this.signer,
|
|
488
|
+
amount,
|
|
489
|
+
lockDurationSeconds: lockSeconds,
|
|
490
|
+
}), { programAddress: this.coreProgram });
|
|
491
|
+
// See note in vaultedTransfer above — vault ATA is not init'd by the
|
|
492
|
+
// on-chain handler; bundle the create.
|
|
493
|
+
const createVaultAtaIx = buildCreateAtaIdempotentIx(this.signer.address, vaultATA, vaultPda, mint);
|
|
494
|
+
const sig = await this.sendTransaction([createVaultAtaIx, ix]);
|
|
495
|
+
return { id: sig };
|
|
496
|
+
}
|
|
497
|
+
/** Read VaultCounter's next_id (returns 0n if not yet created). */
|
|
498
|
+
async getNextVaultId(owner) {
|
|
499
|
+
// VaultCounter PDA derivation lives in pda.ts as getVaultCounterPDA.
|
|
500
|
+
const { getVaultCounterPDA } = await import('./pda.js');
|
|
501
|
+
const [counterPda] = await getVaultCounterPDA(owner, this.coreProgram);
|
|
502
|
+
const account = await fetchEncodedAccount(this.rpc, counterPda, {
|
|
503
|
+
commitment: this.commitment,
|
|
504
|
+
});
|
|
505
|
+
if (!account.exists)
|
|
506
|
+
return 0n;
|
|
507
|
+
return Buffer.from(account.data).readBigUInt64LE(40);
|
|
508
|
+
}
|
|
509
|
+
async extendVault(params, _options) {
|
|
510
|
+
const additionalSeconds = Math.floor(params.extendLengthMs / 1000);
|
|
511
|
+
const [vaultPda] = await getVaultPDA(this.signer.address, BigInt(params.vaultId), this.coreProgram);
|
|
512
|
+
const ix = await getExtendVaultInstructionAsync(await this.withCoreDefaults({
|
|
513
|
+
vault: vaultPda,
|
|
514
|
+
owner: this.signer,
|
|
515
|
+
additionalSeconds,
|
|
516
|
+
}), { programAddress: this.coreProgram });
|
|
517
|
+
const sig = await this.sendTransaction([ix]);
|
|
518
|
+
return { id: sig };
|
|
519
|
+
}
|
|
520
|
+
async increaseVault(params, _options) {
|
|
521
|
+
const amount = toAmount(params.quantity);
|
|
522
|
+
const mint = await this.getMint();
|
|
523
|
+
const [vaultPda] = await getVaultPDA(this.signer.address, BigInt(params.vaultId), this.coreProgram);
|
|
524
|
+
const ownerATA = await getAssociatedTokenAddressKit(mint, this.signer.address);
|
|
525
|
+
const vaultATA = await getAssociatedTokenAddressKit(mint, vaultPda, true);
|
|
526
|
+
const ix = await getIncreaseVaultInstructionAsync(await this.withCoreDefaults({
|
|
527
|
+
vault: vaultPda,
|
|
528
|
+
ownerTokenAccount: ownerATA,
|
|
529
|
+
vaultTokenAccount: vaultATA,
|
|
530
|
+
owner: this.signer,
|
|
531
|
+
amount,
|
|
532
|
+
}), { programAddress: this.coreProgram });
|
|
533
|
+
const sig = await this.sendTransaction([ix]);
|
|
534
|
+
return { id: sig };
|
|
535
|
+
}
|
|
536
|
+
async revokeVault(params, _options) {
|
|
537
|
+
const recipient = address(params.recipient);
|
|
538
|
+
const mint = await this.getMint();
|
|
539
|
+
const [vaultPda] = await getVaultPDA(recipient, BigInt(params.vaultId), this.coreProgram);
|
|
540
|
+
const vaultATA = await getAssociatedTokenAddressKit(mint, vaultPda, true);
|
|
541
|
+
const controllerATA = await getAssociatedTokenAddressKit(mint, this.signer.address);
|
|
542
|
+
const ix = await getRevokeVaultInstructionAsync(await this.withCoreDefaults({
|
|
543
|
+
vault: vaultPda,
|
|
544
|
+
vaultTokenAccount: vaultATA,
|
|
545
|
+
controllerTokenAccount: controllerATA,
|
|
546
|
+
controller: this.signer,
|
|
547
|
+
}), { programAddress: this.coreProgram });
|
|
548
|
+
const sig = await this.sendTransaction([ix]);
|
|
549
|
+
return { id: sig };
|
|
550
|
+
}
|
|
551
|
+
// =========================================
|
|
552
|
+
// Gateway operations (ario-gar)
|
|
553
|
+
// =========================================
|
|
554
|
+
async joinNetwork(params, _options) {
|
|
555
|
+
const garConfig = await this.getGarConfig();
|
|
556
|
+
const operatorATA = await getAssociatedTokenAddressKit(garConfig.mint, this.signer.address);
|
|
557
|
+
const observerAddress = params.observerAddress
|
|
558
|
+
? address(params.observerAddress)
|
|
559
|
+
: this.signer.address;
|
|
560
|
+
const [observerLookupPda] = await getObserverLookupPDA(observerAddress, this.garProgram);
|
|
561
|
+
const ix = await getJoinNetworkInstructionAsync(await this.withGarDefaults({
|
|
562
|
+
operatorTokenAccount: operatorATA,
|
|
563
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
564
|
+
observerLookup: observerLookupPda,
|
|
565
|
+
operator: this.signer,
|
|
566
|
+
operatorStake: BigInt(params.operatorStake),
|
|
567
|
+
label: params.label ?? '',
|
|
568
|
+
fqdn: params.fqdn ?? '',
|
|
569
|
+
port: params.port ?? 443,
|
|
570
|
+
protocol: Protocol.Https,
|
|
571
|
+
properties: params.properties ?? null,
|
|
572
|
+
note: params.note ?? null,
|
|
573
|
+
allowDelegatedStaking: params.allowDelegatedStaking === true ||
|
|
574
|
+
params.allowDelegatedStaking === 'allowlist',
|
|
575
|
+
delegateRewardShareRatio: params.delegateRewardShareRatio ?? 0,
|
|
576
|
+
minDelegateStake: params.minDelegatedStake !== undefined
|
|
577
|
+
? BigInt(params.minDelegatedStake)
|
|
578
|
+
: null,
|
|
579
|
+
observerAddress,
|
|
580
|
+
}), { programAddress: this.garProgram });
|
|
581
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
582
|
+
return { id: sig };
|
|
583
|
+
}
|
|
584
|
+
async leaveNetwork(_options) {
|
|
585
|
+
// BD-102: leave_network may produce 1 or 2 Withdrawal PDAs. The
|
|
586
|
+
// protected exit vault uses `next_id`; the optional excess vault
|
|
587
|
+
// uses `next_id + 1`. The SDK always derives both PDAs and passes
|
|
588
|
+
// them to the codama-emitted builder — the contract's
|
|
589
|
+
// `Option<UncheckedAccount>` excess slot is consumed only when the
|
|
590
|
+
// post-stake excess is positive. Passing it unconditionally keeps
|
|
591
|
+
// the SDK side stateless (no need to fetch gateway.operator_stake +
|
|
592
|
+
// settings.min_operator_stake to decide).
|
|
593
|
+
const nextId = await this.getNextWithdrawalId(this.signer.address);
|
|
594
|
+
const [exitVaultPda] = await getWithdrawalPDA(this.signer.address, nextId, this.garProgram);
|
|
595
|
+
const [excessVaultPda] = await getWithdrawalPDA(this.signer.address, nextId + 1n, this.garProgram);
|
|
596
|
+
const ix = await getLeaveNetworkInstructionAsync(await this.withGarDefaults({
|
|
597
|
+
withdrawal: exitVaultPda,
|
|
598
|
+
excessWithdrawal: excessVaultPda,
|
|
599
|
+
operator: this.signer,
|
|
600
|
+
}), { programAddress: this.garProgram });
|
|
601
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
602
|
+
return { id: sig };
|
|
603
|
+
}
|
|
604
|
+
async updateGatewaySettings(params, _options) {
|
|
605
|
+
const ixs = [];
|
|
606
|
+
// Settings fields (label, fqdn, port, etc.) — only emit when at least one
|
|
607
|
+
// non-observer field is provided so we don't send a no-op instruction.
|
|
608
|
+
const { observerAddress: _observer, ...settingsFields } = params;
|
|
609
|
+
if (Object.keys(settingsFields).length > 0) {
|
|
610
|
+
const settingsIx = await getUpdateGatewaySettingsInstructionAsync(await this.withGarDefaults({
|
|
611
|
+
operator: this.signer,
|
|
612
|
+
label: params.label ?? null,
|
|
613
|
+
fqdn: params.fqdn ?? null,
|
|
614
|
+
port: params.port ?? null,
|
|
615
|
+
// Codama exposes `protocol` as Option<Protocol>. We only ever updated
|
|
616
|
+
// the URL parts above, so leave protocol untouched (None).
|
|
617
|
+
protocol: null,
|
|
618
|
+
properties: params.properties ?? null,
|
|
619
|
+
note: params.note ?? null,
|
|
620
|
+
allowDelegatedStaking: typeof params.allowDelegatedStaking === 'boolean'
|
|
621
|
+
? params.allowDelegatedStaking
|
|
622
|
+
: null,
|
|
623
|
+
delegateRewardShareRatio: params.delegateRewardShareRatio ?? null,
|
|
624
|
+
minDelegateStake: params.minDelegatedStake !== undefined
|
|
625
|
+
? BigInt(params.minDelegatedStake)
|
|
626
|
+
: null,
|
|
627
|
+
}), { programAddress: this.garProgram });
|
|
628
|
+
ixs.push(settingsIx);
|
|
629
|
+
}
|
|
630
|
+
// Observer address update — uses a separate on-chain instruction that
|
|
631
|
+
// swaps the observer lookup PDA from old → new.
|
|
632
|
+
if (params.observerAddress !== undefined) {
|
|
633
|
+
const newObserver = address(params.observerAddress);
|
|
634
|
+
const gateway = await this.getGateway({
|
|
635
|
+
address: this.signer.address,
|
|
636
|
+
});
|
|
637
|
+
const oldObserver = address(gateway.observerAddress);
|
|
638
|
+
const [oldObserverLookupPda] = await getObserverLookupPDA(oldObserver, this.garProgram);
|
|
639
|
+
const [newObserverLookupPda] = await getObserverLookupPDA(newObserver, this.garProgram);
|
|
640
|
+
const observerIx = await getUpdateObserverAddressInstructionAsync(await this.withGarDefaults({
|
|
641
|
+
operator: this.signer,
|
|
642
|
+
oldObserverLookup: oldObserverLookupPda,
|
|
643
|
+
newObserverLookup: newObserverLookupPda,
|
|
644
|
+
newObserver,
|
|
645
|
+
}), { programAddress: this.garProgram });
|
|
646
|
+
ixs.push(observerIx);
|
|
647
|
+
}
|
|
648
|
+
const sig = await this.sendTransaction(ixs, 1_000_000);
|
|
649
|
+
return { id: sig };
|
|
650
|
+
}
|
|
651
|
+
async increaseOperatorStake(params, _options) {
|
|
652
|
+
const amount = toAmount(params.increaseQty);
|
|
653
|
+
const garConfig = await this.getGarConfig();
|
|
654
|
+
const operatorATA = await getAssociatedTokenAddressKit(garConfig.mint, this.signer.address);
|
|
655
|
+
const ix = await getIncreaseOperatorStakeInstructionAsync(await this.withGarDefaults({
|
|
656
|
+
operatorTokenAccount: operatorATA,
|
|
657
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
658
|
+
operator: this.signer,
|
|
659
|
+
amount,
|
|
660
|
+
}), { programAddress: this.garProgram });
|
|
661
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
662
|
+
return { id: sig };
|
|
663
|
+
}
|
|
664
|
+
async decreaseOperatorStake(params, _options) {
|
|
665
|
+
const amount = toAmount(params.decreaseQty);
|
|
666
|
+
const nextId = await this.getNextWithdrawalId(this.signer.address);
|
|
667
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, nextId, this.garProgram);
|
|
668
|
+
const ix = await getDecreaseOperatorStakeInstructionAsync(await this.withGarDefaults({
|
|
669
|
+
withdrawal: withdrawalPda,
|
|
670
|
+
operator: this.signer,
|
|
671
|
+
amount,
|
|
672
|
+
}), { programAddress: this.garProgram });
|
|
673
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
674
|
+
return { id: sig };
|
|
675
|
+
}
|
|
676
|
+
async delegateStake(params, _options) {
|
|
677
|
+
const amount = toAmount(params.stakeQty);
|
|
678
|
+
const target = address(params.target);
|
|
679
|
+
const garConfig = await this.getGarConfig();
|
|
680
|
+
const [gatewayPda] = await getGatewayPDA(target, this.garProgram);
|
|
681
|
+
const [delegationPda] = await getDelegationPDA(target, this.signer.address, this.garProgram);
|
|
682
|
+
const delegatorATA = await getAssociatedTokenAddressKit(garConfig.mint, this.signer.address);
|
|
683
|
+
const ix = await getDelegateStakeInstructionAsync(await this.withGarDefaults({
|
|
684
|
+
gateway: gatewayPda,
|
|
685
|
+
delegation: delegationPda,
|
|
686
|
+
delegatorTokenAccount: delegatorATA,
|
|
687
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
688
|
+
delegator: this.signer,
|
|
689
|
+
amount,
|
|
690
|
+
}), { programAddress: this.garProgram });
|
|
691
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
692
|
+
return { id: sig };
|
|
693
|
+
}
|
|
694
|
+
async decreaseDelegateStake(params, _options) {
|
|
695
|
+
const amount = toAmount(params.decreaseQty);
|
|
696
|
+
const target = address(params.target);
|
|
697
|
+
const [gatewayPda] = await getGatewayPDA(target, this.garProgram);
|
|
698
|
+
const [delegationPda] = await getDelegationPDA(target, this.signer.address, this.garProgram);
|
|
699
|
+
const nextId = await this.getNextWithdrawalId(this.signer.address);
|
|
700
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, nextId, this.garProgram);
|
|
701
|
+
const ix = await getDecreaseDelegateStakeInstructionAsync(await this.withGarDefaults({
|
|
702
|
+
gateway: gatewayPda,
|
|
703
|
+
delegation: delegationPda,
|
|
704
|
+
withdrawal: withdrawalPda,
|
|
705
|
+
delegator: this.signer,
|
|
706
|
+
amount,
|
|
707
|
+
}), { programAddress: this.garProgram });
|
|
708
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
709
|
+
if (!params.instant) {
|
|
710
|
+
return { id: sig };
|
|
711
|
+
}
|
|
712
|
+
// Instant boolean on decrease delegated stake (vs protected exit vault) is not currently supported by the contract — Instead we call InstantWithdrawal after creating the withdrawal, which achieves the same effect but requires two transactions. The protected exit vault is still created in the first transaction, but will be empty and can be ignored when instant = true.
|
|
713
|
+
await this.instantWithdrawal({ vaultId: nextId.toString() }, _options);
|
|
714
|
+
return { id: sig };
|
|
715
|
+
}
|
|
716
|
+
async instantWithdrawal(params, _options) {
|
|
717
|
+
const garConfig = await this.getGarConfig();
|
|
718
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, BigInt(params.vaultId), this.garProgram);
|
|
719
|
+
const ownerATA = await getAssociatedTokenAddressKit(garConfig.mint, this.signer.address);
|
|
720
|
+
const ix = await getInstantWithdrawalInstructionAsync(await this.withGarDefaults({
|
|
721
|
+
withdrawal: withdrawalPda,
|
|
722
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
723
|
+
ownerTokenAccount: ownerATA,
|
|
724
|
+
protocolTokenAccount: garConfig.protocolTokenAccount,
|
|
725
|
+
owner: this.signer,
|
|
726
|
+
}), { programAddress: this.garProgram });
|
|
727
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
728
|
+
return { id: sig };
|
|
729
|
+
}
|
|
730
|
+
async cancelWithdrawal(params, _options) {
|
|
731
|
+
const gateway = params.gatewayAddress
|
|
732
|
+
? address(params.gatewayAddress)
|
|
733
|
+
: this.signer.address;
|
|
734
|
+
const [gatewayPda] = await getGatewayPDA(gateway, this.garProgram);
|
|
735
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, BigInt(params.vaultId), this.garProgram);
|
|
736
|
+
const isDelegate = gateway !== this.signer.address;
|
|
737
|
+
const delegation = isDelegate
|
|
738
|
+
? (await getDelegationPDA(gateway, this.signer.address, this.garProgram))[0]
|
|
739
|
+
: undefined;
|
|
740
|
+
const [settingsPda] = await getGarSettingsPDA(this.garProgram);
|
|
741
|
+
const ix = getCancelWithdrawalInstruction({
|
|
742
|
+
settings: settingsPda,
|
|
743
|
+
gateway: gatewayPda,
|
|
744
|
+
withdrawal: withdrawalPda,
|
|
745
|
+
delegation,
|
|
746
|
+
owner: this.signer,
|
|
747
|
+
}, { programAddress: this.garProgram });
|
|
748
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
749
|
+
return { id: sig };
|
|
750
|
+
}
|
|
751
|
+
async saveObservations(params, _options) {
|
|
752
|
+
let epochIndex;
|
|
753
|
+
if (params.epochIndex !== undefined) {
|
|
754
|
+
epochIndex = params.epochIndex;
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
const [settingsPda] = await getEpochSettingsPDA(this.garProgram);
|
|
758
|
+
const settingsAccount = await fetchEncodedAccount(this.rpc, settingsPda, {
|
|
759
|
+
commitment: this.commitment,
|
|
760
|
+
});
|
|
761
|
+
if (!settingsAccount.exists)
|
|
762
|
+
throw new Error('EpochSettings not found');
|
|
763
|
+
const settings = deserializeEpochSettingsFull(Buffer.from(settingsAccount.data));
|
|
764
|
+
epochIndex = settings.currentEpochIndex;
|
|
765
|
+
}
|
|
766
|
+
// Build the [u8; 375] gateway_results bitfield. On-chain convention:
|
|
767
|
+
// bit set (1) = passed, bit clear (0) = failed.
|
|
768
|
+
let resultsBuf;
|
|
769
|
+
let gatewayCount;
|
|
770
|
+
if (params.gatewayResults) {
|
|
771
|
+
resultsBuf = Buffer.alloc(375);
|
|
772
|
+
resultsBuf.set(params.gatewayResults.subarray(0, 375));
|
|
773
|
+
gatewayCount = params.gatewayCount ?? 0;
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
const registryAddresses = await this.getRegistryGatewayAddresses();
|
|
777
|
+
gatewayCount = registryAddresses.length;
|
|
778
|
+
resultsBuf = buildObservationBitmap(registryAddresses, params.failedGateways);
|
|
779
|
+
}
|
|
780
|
+
const reportTxId = encodeReportTxId(params.reportTxId);
|
|
781
|
+
const ix = await getSaveObservationsInstructionAsync({
|
|
782
|
+
observer: this.signer,
|
|
783
|
+
epochIndex: BigInt(epochIndex),
|
|
784
|
+
gatewayResults: new Uint8Array(resultsBuf),
|
|
785
|
+
gatewayCount,
|
|
786
|
+
reportTxId: new Uint8Array(reportTxId),
|
|
787
|
+
}, { programAddress: this.garProgram });
|
|
788
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
789
|
+
return { id: sig };
|
|
790
|
+
}
|
|
791
|
+
async redelegateStake(params, _options) {
|
|
792
|
+
const amount = toAmount(params.stakeQty);
|
|
793
|
+
const source = address(params.source);
|
|
794
|
+
const target = address(params.target);
|
|
795
|
+
const garConfig = await this.getGarConfig();
|
|
796
|
+
const [sourceGatewayPda] = await getGatewayPDA(source, this.garProgram);
|
|
797
|
+
const [targetGatewayPda] = await getGatewayPDA(target, this.garProgram);
|
|
798
|
+
const [sourceDelegationPda] = await getDelegationPDA(source, this.signer.address, this.garProgram);
|
|
799
|
+
const [targetDelegationPda] = await getDelegationPDA(target, this.signer.address, this.garProgram);
|
|
800
|
+
const delegatorATA = await getAssociatedTokenAddressKit(garConfig.mint, this.signer.address);
|
|
801
|
+
const ix = await getRedelegateStakeInstructionAsync(await this.withGarDefaults({
|
|
802
|
+
sourceGateway: sourceGatewayPda,
|
|
803
|
+
targetGateway: targetGatewayPda,
|
|
804
|
+
sourceDelegation: sourceDelegationPda,
|
|
805
|
+
targetDelegation: targetDelegationPda,
|
|
806
|
+
delegatorTokenAccount: delegatorATA,
|
|
807
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
808
|
+
protocolTokenAccount: garConfig.protocolTokenAccount,
|
|
809
|
+
delegator: this.signer,
|
|
810
|
+
amount,
|
|
811
|
+
}), { programAddress: this.garProgram });
|
|
812
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
813
|
+
return { id: sig };
|
|
814
|
+
}
|
|
815
|
+
// =========================================
|
|
816
|
+
// ArNS operations (ario-arns)
|
|
817
|
+
// =========================================
|
|
818
|
+
async buyRecord(params, _options) {
|
|
819
|
+
const arnsConfig = await this.getArnsConfig();
|
|
820
|
+
const buyerATA = await getAssociatedTokenAddressKit(arnsConfig.mint, this.signer.address);
|
|
821
|
+
const antPubkey = address(params.processId ?? '11111111111111111111111111111111');
|
|
822
|
+
const [arnsRecord] = await getArnsRecordPDA(params.name, this.arnsProgram);
|
|
823
|
+
const [reservedNameCheck] = await getReservedNamePDA(params.name, this.arnsProgram);
|
|
824
|
+
const [returnedNameCheck] = await getReturnedNamePDA(params.name, this.arnsProgram);
|
|
825
|
+
const buyNameParams = {
|
|
826
|
+
name: params.name,
|
|
827
|
+
purchaseType: params.type === 'permabuy' ? PurchaseType.Permabuy : PurchaseType.Lease,
|
|
828
|
+
years: params.years ?? 1,
|
|
829
|
+
ant: antPubkey,
|
|
830
|
+
};
|
|
831
|
+
// Phase 4 of FUND_FROM_PLAN.md: dispatch on params.fundFrom. The pre-Phase-4
|
|
832
|
+
// path always fell through to the balance-funded `buyName` ix even when
|
|
833
|
+
// CLI-set `--fund-from stakes`; we now route to the corresponding on-chain
|
|
834
|
+
// wrapper for each mode.
|
|
835
|
+
let ix;
|
|
836
|
+
if (params.fundFrom === 'stakes' && params.gatewayAddress) {
|
|
837
|
+
const gatewayAddr = address(params.gatewayAddress);
|
|
838
|
+
const garConfig = await this.getGarConfig();
|
|
839
|
+
const [garSettings] = await getGarSettingsPDA(this.garProgram);
|
|
840
|
+
const [gatewayPda] = await getGatewayPDA(gatewayAddr, this.garProgram);
|
|
841
|
+
const baseShared = {
|
|
842
|
+
config: await this.arnsConfigPda(),
|
|
843
|
+
demandFactor: await this.demandFactorPda(),
|
|
844
|
+
arnsRecord,
|
|
845
|
+
nameRegistry: await this.nameRegistryPda(),
|
|
846
|
+
reservedNameCheck,
|
|
847
|
+
returnedNameCheck,
|
|
848
|
+
garSettings,
|
|
849
|
+
gateway: gatewayPda,
|
|
850
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
851
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
852
|
+
buyer: this.signer,
|
|
853
|
+
garProgram: this.garProgram,
|
|
854
|
+
params: buyNameParams,
|
|
855
|
+
};
|
|
856
|
+
if (params.fundAsOperator) {
|
|
857
|
+
ix = await getBuyNameFromOperatorStakeInstructionAsync(baseShared, {
|
|
858
|
+
programAddress: this.arnsProgram,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
const [delegationPda] = await getDelegationPDA(gatewayAddr, this.signer.address, this.garProgram);
|
|
863
|
+
ix = await getBuyNameFromDelegationInstructionAsync({ ...baseShared, delegation: delegationPda }, { programAddress: this.arnsProgram });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else if (params.fundFrom === 'withdrawal' &&
|
|
867
|
+
params.withdrawalId !== undefined) {
|
|
868
|
+
const garConfig = await this.getGarConfig();
|
|
869
|
+
const [garSettings] = await getGarSettingsPDA(this.garProgram);
|
|
870
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, params.withdrawalId, this.garProgram);
|
|
871
|
+
ix = await getBuyNameFromWithdrawalInstructionAsync({
|
|
872
|
+
config: await this.arnsConfigPda(),
|
|
873
|
+
demandFactor: await this.demandFactorPda(),
|
|
874
|
+
arnsRecord,
|
|
875
|
+
nameRegistry: await this.nameRegistryPda(),
|
|
876
|
+
reservedNameCheck,
|
|
877
|
+
returnedNameCheck,
|
|
878
|
+
garSettings,
|
|
879
|
+
withdrawal: withdrawalPda,
|
|
880
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
881
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
882
|
+
buyer: this.signer,
|
|
883
|
+
garProgram: this.garProgram,
|
|
884
|
+
params: buyNameParams,
|
|
885
|
+
}, { programAddress: this.arnsProgram });
|
|
886
|
+
}
|
|
887
|
+
else if (params.fundFrom === 'plan' ||
|
|
888
|
+
params.fundFrom === 'any' ||
|
|
889
|
+
params.fundFrom === 'stakes' ||
|
|
890
|
+
params.fundFrom === 'withdrawal') {
|
|
891
|
+
// 'stakes'/'withdrawal' WITHOUT an explicit gatewayAddress/withdrawalId
|
|
892
|
+
// land here (the single-source branches above handle the explicit case).
|
|
893
|
+
// Route through the funding planner, which constrains sources to the
|
|
894
|
+
// chosen mode — it never silently spends liquid balance and auto-splits
|
|
895
|
+
// across the caller's delegations/vaults. (Previously these fell through
|
|
896
|
+
// to the balance path below, silently draining liquid ARIO.)
|
|
897
|
+
ix = await this._buildBuyNameFromFundingPlanIx({
|
|
898
|
+
params,
|
|
899
|
+
antPubkey,
|
|
900
|
+
arnsRecord,
|
|
901
|
+
reservedNameCheck,
|
|
902
|
+
returnedNameCheck,
|
|
903
|
+
buyNameParams,
|
|
904
|
+
arnsConfig,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
else if (!params.fundFrom ||
|
|
908
|
+
params.fundFrom === 'balance' ||
|
|
909
|
+
params.fundFrom === 'turbo') {
|
|
910
|
+
// Direct balance-funded buy.
|
|
911
|
+
ix = await getBuyNameInstructionAsync(await this.withArnsDefaults({
|
|
912
|
+
arnsRecord,
|
|
913
|
+
buyerTokenAccount: buyerATA,
|
|
914
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
915
|
+
reservedNameCheck,
|
|
916
|
+
returnedNameCheck,
|
|
917
|
+
buyer: this.signer,
|
|
918
|
+
params: buyNameParams,
|
|
919
|
+
}), { programAddress: this.arnsProgram });
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
throw new Error(`unsupported fundFrom mode '${params.fundFrom}' for buyRecord`);
|
|
923
|
+
}
|
|
924
|
+
// Sprint 4 / ADR-016: bundle `ant.sync_attributes` IFF the buyer
|
|
925
|
+
// owns the ANT (preserves BD-096 — non-holder buys defer the trait
|
|
926
|
+
// sync to a later `syncAttributes()` call by the actual owner).
|
|
927
|
+
// Pass `antPubkey` as assetOverride: the ArnsRecord PDA is CREATED
|
|
928
|
+
// by the buy_name ix, so it doesn't exist on-chain at SDK build
|
|
929
|
+
// time — the helper would 404 if it tried to read it.
|
|
930
|
+
const syncIx = await this._buildSyncAttributesIxIfOwner(params.name, antPubkey);
|
|
931
|
+
const sig = await this.sendTransaction(syncIx ? [ix, syncIx] : [ix]);
|
|
932
|
+
return { id: sig };
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Resolve a `FundingPlan` for a fee-paying ArNS ix. When `params.sources`
|
|
936
|
+
* is set, use it verbatim (caller-supplied plan); otherwise discover the
|
|
937
|
+
* user's sources and build a plan via the Lua-faithful planner.
|
|
938
|
+
*
|
|
939
|
+
* Throws `InsufficientFundingError` (as a thrown Error with the structured
|
|
940
|
+
* payload as `cause`) when no plan covers `amountNeeded`.
|
|
941
|
+
*/
|
|
942
|
+
async _resolveFundingPlan(params, amountNeeded) {
|
|
943
|
+
// `'plan'` is the explicit "I'll supply my own sources, skip discovery"
|
|
944
|
+
// mode (per FUNDING_MODES.md). Fail loudly if a caller picks `'plan'`
|
|
945
|
+
// without sources — pre-2026-05 the SDK silently fell through to
|
|
946
|
+
// discovery, making `'plan'` a synonym for `'any'`. The new semantic
|
|
947
|
+
// matches the doc: `'any'` discovers, `'plan'` uses what you give it.
|
|
948
|
+
if (params.fundFrom === 'plan' && !params.sources?.length) {
|
|
949
|
+
throw new Error("fundFrom: 'plan' requires explicit `sources`. Pass them via " +
|
|
950
|
+
"params.sources, or use fundFrom: 'any' to let the SDK discover " +
|
|
951
|
+
'and plan automatically.');
|
|
952
|
+
}
|
|
953
|
+
if (params.sources?.length) {
|
|
954
|
+
// Caller supplied an explicit plan; build the FundingPlan envelope
|
|
955
|
+
// around it without source discovery. Each Delegation/OperatorStake
|
|
956
|
+
// source MUST carry an explicit `gateway` field so the executor knows
|
|
957
|
+
// which gateway PDA to slot in. Earlier single-gateway flows used
|
|
958
|
+
// `params.gatewayAddress` as a fallback for the first stake source —
|
|
959
|
+
// we preserve that for back-compat in the explicit-plan path.
|
|
960
|
+
const hasBalance = params.sources.some((s) => s.kind === 'balance');
|
|
961
|
+
const fallbackGateway = params.gatewayAddress
|
|
962
|
+
? address(params.gatewayAddress)
|
|
963
|
+
: undefined;
|
|
964
|
+
const gatewayPerSource = params.sources.map((s) => {
|
|
965
|
+
if (s.kind !== 'delegation' && s.kind !== 'operatorStake')
|
|
966
|
+
return undefined;
|
|
967
|
+
const explicit = s.gateway;
|
|
968
|
+
if (explicit)
|
|
969
|
+
return address(explicit);
|
|
970
|
+
if (fallbackGateway)
|
|
971
|
+
return fallbackGateway;
|
|
972
|
+
throw new Error('sources includes delegation/operatorStake but no gateway is set on that source and gatewayAddress is unset');
|
|
973
|
+
});
|
|
974
|
+
// Auto-detect residue: for each Delegation source, fetch the live
|
|
975
|
+
// Delegation + Gateway PDAs and compute the post-drain. If it lands
|
|
976
|
+
// in (0, min_delegation_amount), that source will trigger an on-
|
|
977
|
+
// chain residue auto-vault and we must reserve a residue PDA slot.
|
|
978
|
+
const residueDelegationIndexes = await this._detectResidueIndexes(params.sources, gatewayPerSource);
|
|
979
|
+
return {
|
|
980
|
+
sources: params.sources.map((s) => ({
|
|
981
|
+
kind: s.kind,
|
|
982
|
+
amount: s.amount,
|
|
983
|
+
...(s.withdrawalId !== undefined
|
|
984
|
+
? { withdrawalId: s.withdrawalId }
|
|
985
|
+
: {}),
|
|
986
|
+
})),
|
|
987
|
+
gatewayPerSource,
|
|
988
|
+
residueDelegationIndexes,
|
|
989
|
+
hasBalanceSource: hasBalance,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
// No explicit sources: discover + plan.
|
|
993
|
+
this.logger.debug(`[funding] discovering sources for ${this.signer.address}`, {
|
|
994
|
+
fundFrom: params.fundFrom,
|
|
995
|
+
amountNeeded: amountNeeded.toString(),
|
|
996
|
+
});
|
|
997
|
+
const arnsConfig = await this.getArnsConfig();
|
|
998
|
+
const { discoverFundingSources } = await import('./funding-plan.js');
|
|
999
|
+
const sources = await discoverFundingSources(this.rpc, this.signer.address, {
|
|
1000
|
+
arioMint: arnsConfig.mint,
|
|
1001
|
+
garProgram: this.garProgram,
|
|
1002
|
+
});
|
|
1003
|
+
this.logger.debug(`[funding] discovered ${sources.length} source(s)`, {
|
|
1004
|
+
sources: sources.map((s) => {
|
|
1005
|
+
if (s.kind === 'balance')
|
|
1006
|
+
return { kind: s.kind, available: s.available.toString() };
|
|
1007
|
+
if (s.kind === 'delegation')
|
|
1008
|
+
return {
|
|
1009
|
+
kind: s.kind,
|
|
1010
|
+
gateway: s.gateway,
|
|
1011
|
+
available: s.available.toString(),
|
|
1012
|
+
minDelegationAmount: s.minDelegationAmount.toString(),
|
|
1013
|
+
};
|
|
1014
|
+
if (s.kind === 'operatorStake')
|
|
1015
|
+
return {
|
|
1016
|
+
kind: s.kind,
|
|
1017
|
+
gateway: s.gateway,
|
|
1018
|
+
available: s.available.toString(),
|
|
1019
|
+
};
|
|
1020
|
+
return {
|
|
1021
|
+
kind: s.kind,
|
|
1022
|
+
withdrawalId: s.withdrawalId.toString(),
|
|
1023
|
+
gateway: s.gateway,
|
|
1024
|
+
available: s.available.toString(),
|
|
1025
|
+
availableAt: s.availableAt.toString(),
|
|
1026
|
+
};
|
|
1027
|
+
}),
|
|
1028
|
+
});
|
|
1029
|
+
const plan = buildFundingPlanCore(sources, amountNeeded, {
|
|
1030
|
+
fundFrom: params.fundFrom,
|
|
1031
|
+
preferGateway: params.gatewayAddress
|
|
1032
|
+
? address(params.gatewayAddress)
|
|
1033
|
+
: undefined,
|
|
1034
|
+
fundAsOperator: params.fundAsOperator,
|
|
1035
|
+
});
|
|
1036
|
+
if ('kind' in plan) {
|
|
1037
|
+
this.logger.debug(`[funding] plan failed: ${plan.message}`);
|
|
1038
|
+
const err = new Error(plan.message);
|
|
1039
|
+
err.cause = plan;
|
|
1040
|
+
throw err;
|
|
1041
|
+
}
|
|
1042
|
+
this.logger.debug(`[funding] built plan with ${plan.sources.length} source(s)`, {
|
|
1043
|
+
sources: plan.sources.map((s, i) => ({
|
|
1044
|
+
kind: s.kind,
|
|
1045
|
+
amount: s.amount.toString(),
|
|
1046
|
+
gateway: plan.gatewayPerSource[i] ?? null,
|
|
1047
|
+
})),
|
|
1048
|
+
residueDelegationIndexes: plan.residueDelegationIndexes,
|
|
1049
|
+
hasBalanceSource: plan.hasBalanceSource,
|
|
1050
|
+
});
|
|
1051
|
+
return plan;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Build a `buy_name_from_funding_plan` ix using the funding-plan module.
|
|
1055
|
+
* Resolves per-source PDAs (Delegation, Withdrawal) and the residue-vault
|
|
1056
|
+
* PDA prediction in one shot.
|
|
1057
|
+
*/
|
|
1058
|
+
async _buildBuyNameFromFundingPlanIx(args) {
|
|
1059
|
+
const garConfig = await this.getGarConfig();
|
|
1060
|
+
const [garSettings] = await getGarSettingsPDA(this.garProgram);
|
|
1061
|
+
const buyerATA = await getAssociatedTokenAddressKit(args.arnsConfig.mint, this.signer.address);
|
|
1062
|
+
const cost = await this._simulateTokenCost({
|
|
1063
|
+
intent: CostIntent.BuyName,
|
|
1064
|
+
name: args.buyNameParams.name,
|
|
1065
|
+
years: args.buyNameParams.years,
|
|
1066
|
+
purchaseType: args.buyNameParams.purchaseType,
|
|
1067
|
+
});
|
|
1068
|
+
const plan = await this._resolveFundingPlan(args.params, cost);
|
|
1069
|
+
const { remainingAccounts, withdrawalCounter, residueVaultCount } = await this._materializeFundingPlan(args.params, plan);
|
|
1070
|
+
return await getBuyNameFromFundingPlanInstructionAsync({
|
|
1071
|
+
config: await this.arnsConfigPda(),
|
|
1072
|
+
demandFactor: await this.demandFactorPda(),
|
|
1073
|
+
arnsRecord: args.arnsRecord,
|
|
1074
|
+
nameRegistry: await this.nameRegistryPda(),
|
|
1075
|
+
reservedNameCheck: args.reservedNameCheck,
|
|
1076
|
+
returnedNameCheck: args.returnedNameCheck,
|
|
1077
|
+
garSettings,
|
|
1078
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
1079
|
+
protocolTokenAccount: args.arnsConfig.treasury,
|
|
1080
|
+
payerTokenAccount: plan.hasBalanceSource ? buyerATA : undefined,
|
|
1081
|
+
buyer: this.signer,
|
|
1082
|
+
withdrawalCounter,
|
|
1083
|
+
garProgram: this.garProgram,
|
|
1084
|
+
params: args.buyNameParams,
|
|
1085
|
+
sources: plan.sources.map(toGeneratedFundingSourceSpec),
|
|
1086
|
+
discountAccountCount: 0,
|
|
1087
|
+
residueVaultCount,
|
|
1088
|
+
}, {
|
|
1089
|
+
programAddress: this.arnsProgram,
|
|
1090
|
+
}).then((ix) => remainingAccounts.length > 0
|
|
1091
|
+
? withRemainingAccounts(ix, remainingAccounts)
|
|
1092
|
+
: ix);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* For an explicit caller-supplied plan, detect which Delegation sources
|
|
1096
|
+
* will trigger an on-chain residue auto-vault. Reads each (Delegation,
|
|
1097
|
+
* Gateway) pair in parallel; computes post-drain; flags `(0, min)`.
|
|
1098
|
+
*
|
|
1099
|
+
* Hard-fails on RPC error or missing PDA — silently skipping would let
|
|
1100
|
+
* the on-chain handler reject the tx with `MissingResidueVault` and
|
|
1101
|
+
* burn fees. The error message points at remediation.
|
|
1102
|
+
*/
|
|
1103
|
+
async _detectResidueIndexes(sources, gatewayPerSource) {
|
|
1104
|
+
const delegationIndexes = sources
|
|
1105
|
+
.map((s, i) => ({ s, i }))
|
|
1106
|
+
.filter(({ s }) => s.kind === 'delegation');
|
|
1107
|
+
if (delegationIndexes.length === 0)
|
|
1108
|
+
return [];
|
|
1109
|
+
const owner = this.signer.address;
|
|
1110
|
+
const reads = await Promise.all(delegationIndexes.map(async ({ i }) => {
|
|
1111
|
+
const gateway = gatewayPerSource[i];
|
|
1112
|
+
if (!gateway) {
|
|
1113
|
+
throw new Error(`funding plan source #${i} is a Delegation but gatewayPerSource[${i}] is undefined; set source.gateway or params.gatewayAddress`);
|
|
1114
|
+
}
|
|
1115
|
+
const [delegationPda] = await getDelegationPDA(gateway, owner, this.garProgram);
|
|
1116
|
+
const [gatewayPda] = await getGatewayPDA(gateway, this.garProgram);
|
|
1117
|
+
const [delAcct, gwAcct] = await Promise.all([
|
|
1118
|
+
fetchEncodedAccount(this.rpc, delegationPda, {
|
|
1119
|
+
commitment: this.commitment,
|
|
1120
|
+
}),
|
|
1121
|
+
fetchEncodedAccount(this.rpc, gatewayPda, {
|
|
1122
|
+
commitment: this.commitment,
|
|
1123
|
+
}),
|
|
1124
|
+
]);
|
|
1125
|
+
if (!delAcct.exists) {
|
|
1126
|
+
throw new Error(`residue auto-detect failed: Delegation PDA ${delegationPda} not found for source #${i} (gateway ${gateway}); ensure the delegation exists or pass an explicit funding plan with residue hints`);
|
|
1127
|
+
}
|
|
1128
|
+
if (!gwAcct.exists) {
|
|
1129
|
+
throw new Error(`residue auto-detect failed: Gateway PDA ${gatewayPda} not found for source #${i}`);
|
|
1130
|
+
}
|
|
1131
|
+
return { i, delAcct, gwAcct };
|
|
1132
|
+
}));
|
|
1133
|
+
const delegationDecoder = getDelegationDecoder();
|
|
1134
|
+
const gatewayDecoder = getGatewayDecoder();
|
|
1135
|
+
const states = new Array(sources.length).fill(undefined);
|
|
1136
|
+
for (const { i, delAcct, gwAcct } of reads) {
|
|
1137
|
+
const delegation = delegationDecoder.decode(delAcct.data);
|
|
1138
|
+
const gateway = gatewayDecoder.decode(gwAcct.data);
|
|
1139
|
+
states[i] = {
|
|
1140
|
+
delegationAmount: delegation.amount,
|
|
1141
|
+
minDelegationAmount: gateway.settings.minDelegationAmount,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
return computeResidueIndexes(sources, states);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Materialize a `FundingPlan` into the on-chain ix's per-source remaining
|
|
1148
|
+
* accounts, residue-vault PDAs, and the withdrawal_counter slot. Shared
|
|
1149
|
+
* across all 5 ArNS funding-plan ix dispatches and the 2 ario-core
|
|
1150
|
+
* primary-name funding-plan dispatches.
|
|
1151
|
+
*
|
|
1152
|
+
* Throws when the plan has Withdrawal sources whose ids cannot be
|
|
1153
|
+
* resolved from either `spec.withdrawalId` (preferred) or
|
|
1154
|
+
* `params.withdrawalId` (single-withdrawal back-compat).
|
|
1155
|
+
*/
|
|
1156
|
+
async _materializeFundingPlan(params, plan) {
|
|
1157
|
+
// Resolve withdrawalIds: prefer per-source `spec.withdrawalId` (the
|
|
1158
|
+
// multi-withdrawal canonical path — discoverFundingSources sets it
|
|
1159
|
+
// and explicit caller plans can too); fall back to params.withdrawalId
|
|
1160
|
+
// for single-withdrawal CLI back-compat.
|
|
1161
|
+
const withdrawalSpecs = (params.sources ?? plan.sources).filter((s) => s.kind === 'withdrawal');
|
|
1162
|
+
const withdrawalIds = withdrawalSpecs.map((s, idx) => {
|
|
1163
|
+
const specId = s.withdrawalId;
|
|
1164
|
+
if (specId !== undefined)
|
|
1165
|
+
return BigInt(specId);
|
|
1166
|
+
if (params.withdrawalId !== undefined && idx === 0) {
|
|
1167
|
+
return BigInt(params.withdrawalId);
|
|
1168
|
+
}
|
|
1169
|
+
throw new Error(`funding plan Withdrawal source #${idx} has no withdrawalId; set it on the source spec or pass params.withdrawalId for single-withdrawal plans`);
|
|
1170
|
+
});
|
|
1171
|
+
const { residueVaults, withdrawalCounter } = await predictResidueVaults(this.rpc, this.signer.address, plan, { garProgram: this.garProgram });
|
|
1172
|
+
const remainingAccounts = await buildFundingPlanRemainingAccounts(plan, this.signer.address, { withdrawalIds, residueVaults, garProgram: this.garProgram });
|
|
1173
|
+
return {
|
|
1174
|
+
remainingAccounts: remainingAccounts,
|
|
1175
|
+
withdrawalCounter,
|
|
1176
|
+
residueVaultCount: residueVaults.length,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Simulate the on-chain `get_token_cost` instruction and return the exact
|
|
1181
|
+
* cost as a `bigint` (mARIO). This guarantees byte-exact agreement with the
|
|
1182
|
+
* on-chain pricing math, avoiding integer-division rounding divergences
|
|
1183
|
+
* that plagued the previous client-side reimplementation.
|
|
1184
|
+
*
|
|
1185
|
+
* The program writes a LE-u64 cost into the transaction's return data;
|
|
1186
|
+
* we parse it from the simulation result.
|
|
1187
|
+
*/
|
|
1188
|
+
async _simulateTokenCost(params) {
|
|
1189
|
+
const demandFactorAddr = await this.demandFactorPda();
|
|
1190
|
+
this.logger.debug(`[funding] simulating token cost`, {
|
|
1191
|
+
intent: params.intent,
|
|
1192
|
+
name: params.name,
|
|
1193
|
+
years: params.years,
|
|
1194
|
+
quantity: params.quantity,
|
|
1195
|
+
purchaseType: params.purchaseType,
|
|
1196
|
+
arnsProgram: this.arnsProgram,
|
|
1197
|
+
demandFactor: demandFactorAddr,
|
|
1198
|
+
});
|
|
1199
|
+
// Roll the demand factor to the current period BEFORE pricing. The
|
|
1200
|
+
// `GetTokenCost` view reads the STORED demand factor, which is stale until a
|
|
1201
|
+
// crank (or a buy/extend) rolls it — but the `*FromFundingPlan` handlers
|
|
1202
|
+
// roll it inline before computing the cost. Without this, at a period
|
|
1203
|
+
// rollover the plan is sized to the old factor while the program charges
|
|
1204
|
+
// the new one → FundingPlanAmountMismatch (#6066). `update_demand_factor`
|
|
1205
|
+
// is permissionless + idempotent (no-op within the same period), and the
|
|
1206
|
+
// write is local to this simulation; the subsequent `GetTokenCost` in the
|
|
1207
|
+
// same tx sees the rolled value.
|
|
1208
|
+
const updateDfIx = getUpdateDemandFactorInstruction({ demandFactor: demandFactorAddr, payer: this.signer }, { programAddress: this.arnsProgram });
|
|
1209
|
+
const ix = await getGetTokenCostInstructionAsync({
|
|
1210
|
+
demandFactor: demandFactorAddr,
|
|
1211
|
+
payer: this.signer,
|
|
1212
|
+
intent: params.intent,
|
|
1213
|
+
name: params.name,
|
|
1214
|
+
years: params.years ?? null,
|
|
1215
|
+
quantity: params.quantity ?? null,
|
|
1216
|
+
purchaseType: params.purchaseType ?? null,
|
|
1217
|
+
}, { programAddress: this.arnsProgram });
|
|
1218
|
+
const { value: latestBlockhash } = await this.rpc
|
|
1219
|
+
.getLatestBlockhash()
|
|
1220
|
+
.send();
|
|
1221
|
+
const message = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayerSigner(this.signer, m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstructions([updateDfIx, ix], m));
|
|
1222
|
+
const compiled = compileTransaction(message);
|
|
1223
|
+
const wire = getBase64EncodedWireTransaction(compiled);
|
|
1224
|
+
const sim = await this.rpc
|
|
1225
|
+
.simulateTransaction(wire, {
|
|
1226
|
+
sigVerify: false,
|
|
1227
|
+
replaceRecentBlockhash: true,
|
|
1228
|
+
encoding: 'base64',
|
|
1229
|
+
})
|
|
1230
|
+
.send();
|
|
1231
|
+
if (sim.value.err) {
|
|
1232
|
+
throw new Error(`get_token_cost simulation failed (arnsProgram=${this.arnsProgram}, demandFactor=${demandFactorAddr}): ${JSON.stringify(sim.value.err)}` +
|
|
1233
|
+
(sim.value.logs ? '\n' + sim.value.logs.join('\n') : ''));
|
|
1234
|
+
}
|
|
1235
|
+
const returnData = sim.value.returnData;
|
|
1236
|
+
if (!returnData?.data?.[0]) {
|
|
1237
|
+
throw new Error('get_token_cost simulation returned no data; expected a u64 cost');
|
|
1238
|
+
}
|
|
1239
|
+
const buf = Buffer.from(returnData.data[0], 'base64');
|
|
1240
|
+
if (buf.length < 8) {
|
|
1241
|
+
throw new Error(`get_token_cost return data too short: ${buf.length} bytes (expected 8)`);
|
|
1242
|
+
}
|
|
1243
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1244
|
+
const cost = dv.getBigUint64(0, true);
|
|
1245
|
+
this.logger.debug(`[funding] simulated token cost: ${cost} mARIO`, {
|
|
1246
|
+
name: params.name,
|
|
1247
|
+
});
|
|
1248
|
+
return cost;
|
|
1249
|
+
}
|
|
1250
|
+
async arnsConfigPda() {
|
|
1251
|
+
// The ArNS config (`Settings`) PDA is seeded with `arns_config` — NOT
|
|
1252
|
+
// the `ario_config` seed used by the ario-core `Config` PDA. Earlier
|
|
1253
|
+
// revisions of this helper called `getArioConfigPDA(this.arnsProgram)`
|
|
1254
|
+
// which composes the wrong seed against the right program; the
|
|
1255
|
+
// resulting PDA points at an account that never exists, and every
|
|
1256
|
+
// fund-from-stakes / withdrawal / funding-plan path simulated as
|
|
1257
|
+
// AccountNotInitialized (#3012) on the `config` slot.
|
|
1258
|
+
const [pda] = await getArnsSettingsPDA(this.arnsProgram);
|
|
1259
|
+
return pda;
|
|
1260
|
+
}
|
|
1261
|
+
async demandFactorPda() {
|
|
1262
|
+
const [pda] = await getDemandFactorPDA(this.arnsProgram);
|
|
1263
|
+
return pda;
|
|
1264
|
+
}
|
|
1265
|
+
async nameRegistryPda() {
|
|
1266
|
+
const [pda] = await getArnsRegistryPDA(this.arnsProgram);
|
|
1267
|
+
return pda;
|
|
1268
|
+
}
|
|
1269
|
+
async upgradeRecord(params, _options) {
|
|
1270
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(params.name);
|
|
1271
|
+
const ix = await this._buildManageStakeIx({
|
|
1272
|
+
params,
|
|
1273
|
+
operation: 'upgrade',
|
|
1274
|
+
});
|
|
1275
|
+
const syncIx = await this._buildSyncAttributesIxIfOwner(params.name);
|
|
1276
|
+
const sig = await this.sendTransaction(syncIx ? [...migrateIxs, ix, syncIx] : [...migrateIxs, ix]);
|
|
1277
|
+
return { id: sig };
|
|
1278
|
+
}
|
|
1279
|
+
async syncAttributes(params, _options) {
|
|
1280
|
+
// Public method — caller is asking explicitly to sync. Build the ix
|
|
1281
|
+
// unconditionally; if they don't actually own the ANT, the on-chain
|
|
1282
|
+
// handler returns NotNftHolder. (The bundle path uses
|
|
1283
|
+
// `_buildSyncAttributesIxIfOwner`, which skips when not the owner so
|
|
1284
|
+
// the wrapping arns ix can still succeed for non-holder management
|
|
1285
|
+
// — see BD-095.)
|
|
1286
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(params.name);
|
|
1287
|
+
const ix = await this._buildSyncAttributesIxUnconditional(params.name);
|
|
1288
|
+
const sig = await this.sendTransaction([...migrateIxs, ix]);
|
|
1289
|
+
return { id: sig };
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Build a `sync_attributes` instruction for `name` IFF the signer is
|
|
1293
|
+
* the current MPL Core asset owner. Returns `null` otherwise.
|
|
1294
|
+
*
|
|
1295
|
+
* Sprint 4 / ADR-016: bundle helper for `buyRecord`, `upgradeRecord`,
|
|
1296
|
+
* `increaseUndernameLimit`, `reassignName`, and `buyReturnedName`.
|
|
1297
|
+
* Returning `null` lets the caller send the arns ix alone — preserves
|
|
1298
|
+
* BD-095 (non-holder ArNS lease management) and BD-096 (deferred
|
|
1299
|
+
* trait sync for non-holder buyers). The actual ANT owner reconciles
|
|
1300
|
+
* state later via the public `syncAttributes()`.
|
|
1301
|
+
*
|
|
1302
|
+
* `extendLease` is NOT a caller of this helper — extend_lease changes
|
|
1303
|
+
* only `end_timestamp`, which isn't mirrored in any Attributes-plugin
|
|
1304
|
+
* trait (BD-095 last row). `releaseName` is also excluded — release_name
|
|
1305
|
+
* closes the ArnsRecord PDA, so a follow-up `sync_attributes` would
|
|
1306
|
+
* fail the PDA-existence check.
|
|
1307
|
+
*
|
|
1308
|
+
* `assetOverride` MUST be set when the bundling ix mutates
|
|
1309
|
+
* `record.ant` mid-tx (i.e. `reassign_name`). The on-chain record
|
|
1310
|
+
* still points at the OLD asset at SDK build time, so without the
|
|
1311
|
+
* override the bundled sync would target the wrong asset and fail
|
|
1312
|
+
* the post-reassign `record.ant == asset.key()` check. The owner
|
|
1313
|
+
* check runs against the supplied asset (= new ANT for reassign),
|
|
1314
|
+
* matching the pre-reshape "new owner reconciles later" semantic.
|
|
1315
|
+
*/
|
|
1316
|
+
async _buildSyncAttributesIxIfOwner(name, assetOverride) {
|
|
1317
|
+
let asset;
|
|
1318
|
+
if (assetOverride !== undefined) {
|
|
1319
|
+
asset = assetOverride;
|
|
1320
|
+
}
|
|
1321
|
+
else {
|
|
1322
|
+
const record = await this.getArNSRecord({ name });
|
|
1323
|
+
asset = address(record.processId);
|
|
1324
|
+
}
|
|
1325
|
+
const { fetchMplCoreOwner } = await import('./mpl-core.js');
|
|
1326
|
+
const owner = await fetchMplCoreOwner(this.rpc, asset, {
|
|
1327
|
+
commitment: this.commitment,
|
|
1328
|
+
});
|
|
1329
|
+
if (owner === null || owner !== this.signer.address) {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
return this._buildSyncAttributesIxFor(name, asset);
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Build a `sync_attributes` instruction unconditionally (no owner
|
|
1336
|
+
* check). Used by the public `syncAttributes()` method, where the
|
|
1337
|
+
* caller is asking explicitly — if they aren't the owner the chain
|
|
1338
|
+
* returns NotNftHolder.
|
|
1339
|
+
*/
|
|
1340
|
+
async _buildSyncAttributesIxUnconditional(name) {
|
|
1341
|
+
const record = await this.getArNSRecord({ name });
|
|
1342
|
+
return this._buildSyncAttributesIxFor(name, address(record.processId));
|
|
1343
|
+
}
|
|
1344
|
+
/** Pure builder; no RPC. Used by both gated + unconditional helpers. */
|
|
1345
|
+
async _buildSyncAttributesIxFor(name, asset) {
|
|
1346
|
+
const [arnsRecord] = await getArnsRecordPDA(name, this.arnsProgram);
|
|
1347
|
+
return getSyncAttributesInstruction({
|
|
1348
|
+
asset,
|
|
1349
|
+
payer: this.signer,
|
|
1350
|
+
authority: this.signer,
|
|
1351
|
+
arnsRecord,
|
|
1352
|
+
name,
|
|
1353
|
+
}, { programAddress: this.antProgram });
|
|
1354
|
+
}
|
|
1355
|
+
async extendLease(params, _options) {
|
|
1356
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(params.name);
|
|
1357
|
+
const ix = await this._buildManageStakeIx({
|
|
1358
|
+
params,
|
|
1359
|
+
operation: 'extend',
|
|
1360
|
+
years: params.years,
|
|
1361
|
+
});
|
|
1362
|
+
// BD-095: extend_lease changes only `end_timestamp`, which isn't
|
|
1363
|
+
// mirrored in any Metaplex Attributes plugin trait. No bundle.
|
|
1364
|
+
const sig = await this.sendTransaction([...migrateIxs, ix]);
|
|
1365
|
+
return { id: sig };
|
|
1366
|
+
}
|
|
1367
|
+
async increaseUndernameLimit(params, _options) {
|
|
1368
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(params.name);
|
|
1369
|
+
const ix = await this._buildManageStakeIx({
|
|
1370
|
+
params,
|
|
1371
|
+
operation: 'increaseUndername',
|
|
1372
|
+
quantity: params.increaseCount,
|
|
1373
|
+
});
|
|
1374
|
+
const syncIx = await this._buildSyncAttributesIxIfOwner(params.name);
|
|
1375
|
+
const sig = await this.sendTransaction(syncIx ? [...migrateIxs, ix, syncIx] : [...migrateIxs, ix]);
|
|
1376
|
+
return { id: sig };
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Shared dispatch helper for the 3 manage variants (upgrade / extend /
|
|
1380
|
+
* increaseUndername). They share the same account shape per the
|
|
1381
|
+
* `manage_from_delegation_accounts!` / `manage_from_operator_stake_accounts!`
|
|
1382
|
+
* macros in `programs/ario-arns/src/instructions/manage_from_stake.rs`.
|
|
1383
|
+
* Only the operation-specific extra args differ (years for extend,
|
|
1384
|
+
* quantity for increaseUndername).
|
|
1385
|
+
*/
|
|
1386
|
+
async _buildManageStakeIx(args) {
|
|
1387
|
+
const arnsConfig = await this.getArnsConfig();
|
|
1388
|
+
const callerATA = await getAssociatedTokenAddressKit(arnsConfig.mint, this.signer.address);
|
|
1389
|
+
const [arnsRecord] = await getArnsRecordPDA(args.params.name, this.arnsProgram);
|
|
1390
|
+
// Balance / undefined → original direct-transfer ix (matches pre-Phase-4).
|
|
1391
|
+
if (!args.params.fundFrom ||
|
|
1392
|
+
args.params.fundFrom === 'balance' ||
|
|
1393
|
+
args.params.fundFrom === 'turbo') {
|
|
1394
|
+
const baseAccounts = await this.withArnsDefaults({
|
|
1395
|
+
arnsRecord,
|
|
1396
|
+
callerTokenAccount: callerATA,
|
|
1397
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
1398
|
+
caller: this.signer,
|
|
1399
|
+
});
|
|
1400
|
+
if (args.operation === 'upgrade') {
|
|
1401
|
+
return getUpgradeNameInstructionAsync(baseAccounts, {
|
|
1402
|
+
programAddress: this.arnsProgram,
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
if (args.operation === 'extend') {
|
|
1406
|
+
return getExtendLeaseInstructionAsync({ ...baseAccounts, years: args.years }, { programAddress: this.arnsProgram });
|
|
1407
|
+
}
|
|
1408
|
+
return getIncreaseUndernameLimitInstructionAsync({ ...baseAccounts, quantity: args.quantity }, { programAddress: this.arnsProgram });
|
|
1409
|
+
}
|
|
1410
|
+
// Stake / withdrawal / funding-plan paths share an account skeleton.
|
|
1411
|
+
const garConfig = await this.getGarConfig();
|
|
1412
|
+
const [garSettings] = await getGarSettingsPDA(this.garProgram);
|
|
1413
|
+
const sharedManageBase = {
|
|
1414
|
+
config: await this.arnsConfigPda(),
|
|
1415
|
+
demandFactor: await this.demandFactorPda(),
|
|
1416
|
+
arnsRecord,
|
|
1417
|
+
garSettings,
|
|
1418
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
1419
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
1420
|
+
caller: this.signer,
|
|
1421
|
+
garProgram: this.garProgram,
|
|
1422
|
+
};
|
|
1423
|
+
if (args.params.fundFrom === 'stakes' && args.params.gatewayAddress) {
|
|
1424
|
+
const gatewayAddr = address(args.params.gatewayAddress);
|
|
1425
|
+
const [gatewayPda] = await getGatewayPDA(gatewayAddr, this.garProgram);
|
|
1426
|
+
const stakeBase = { ...sharedManageBase, gateway: gatewayPda };
|
|
1427
|
+
if (args.params.fundAsOperator) {
|
|
1428
|
+
if (args.operation === 'upgrade')
|
|
1429
|
+
return getUpgradeNameFromOperatorStakeInstructionAsync(stakeBase, {
|
|
1430
|
+
programAddress: this.arnsProgram,
|
|
1431
|
+
});
|
|
1432
|
+
if (args.operation === 'extend')
|
|
1433
|
+
return getExtendLeaseFromOperatorStakeInstructionAsync({ ...stakeBase, years: args.years }, { programAddress: this.arnsProgram });
|
|
1434
|
+
return getIncreaseUndernameLimitFromOperatorStakeInstructionAsync({ ...stakeBase, quantity: args.quantity }, { programAddress: this.arnsProgram });
|
|
1435
|
+
}
|
|
1436
|
+
const [delegationPda] = await getDelegationPDA(gatewayAddr, this.signer.address, this.garProgram);
|
|
1437
|
+
const delBase = { ...stakeBase, delegation: delegationPda };
|
|
1438
|
+
if (args.operation === 'upgrade')
|
|
1439
|
+
return getUpgradeNameFromDelegationInstructionAsync(delBase, {
|
|
1440
|
+
programAddress: this.arnsProgram,
|
|
1441
|
+
});
|
|
1442
|
+
if (args.operation === 'extend')
|
|
1443
|
+
return getExtendLeaseFromDelegationInstructionAsync({ ...delBase, years: args.years }, { programAddress: this.arnsProgram });
|
|
1444
|
+
return getIncreaseUndernameLimitFromDelegationInstructionAsync({ ...delBase, quantity: args.quantity }, { programAddress: this.arnsProgram });
|
|
1445
|
+
}
|
|
1446
|
+
if (args.params.fundFrom === 'withdrawal' &&
|
|
1447
|
+
args.params.withdrawalId !== undefined) {
|
|
1448
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, args.params.withdrawalId, this.garProgram);
|
|
1449
|
+
const wBase = { ...sharedManageBase, withdrawal: withdrawalPda };
|
|
1450
|
+
if (args.operation === 'upgrade')
|
|
1451
|
+
return getUpgradeNameFromWithdrawalInstructionAsync(wBase, {
|
|
1452
|
+
programAddress: this.arnsProgram,
|
|
1453
|
+
});
|
|
1454
|
+
if (args.operation === 'extend')
|
|
1455
|
+
return getExtendLeaseFromWithdrawalInstructionAsync({ ...wBase, years: args.years }, { programAddress: this.arnsProgram });
|
|
1456
|
+
return getIncreaseUndernameLimitFromWithdrawalInstructionAsync({ ...wBase, quantity: args.quantity }, { programAddress: this.arnsProgram });
|
|
1457
|
+
}
|
|
1458
|
+
if (args.params.fundFrom === 'plan' ||
|
|
1459
|
+
args.params.fundFrom === 'any' ||
|
|
1460
|
+
args.params.fundFrom === 'stakes' ||
|
|
1461
|
+
args.params.fundFrom === 'withdrawal') {
|
|
1462
|
+
// 'stakes'/'withdrawal' without an explicit gatewayAddress/withdrawalId
|
|
1463
|
+
// route here (the single-source branches above handle the explicit
|
|
1464
|
+
// case). The planner constrains sources to the chosen mode and never
|
|
1465
|
+
// spends liquid balance.
|
|
1466
|
+
// Cost estimation for manage variants: each operation has its own
|
|
1467
|
+
// pricing path. Keep it pragmatic — let the planner build the plan
|
|
1468
|
+
// around the user's desired total (caller can pass explicit sources
|
|
1469
|
+
// to bypass cost estimation entirely).
|
|
1470
|
+
const intentMap = {
|
|
1471
|
+
upgrade: CostIntent.UpgradeName,
|
|
1472
|
+
extend: CostIntent.ExtendLease,
|
|
1473
|
+
increaseUndername: CostIntent.IncreaseUndernameLimit,
|
|
1474
|
+
};
|
|
1475
|
+
const cost = await this._simulateTokenCost({
|
|
1476
|
+
intent: intentMap[args.operation],
|
|
1477
|
+
name: args.params.name,
|
|
1478
|
+
years: args.years,
|
|
1479
|
+
quantity: args.quantity,
|
|
1480
|
+
});
|
|
1481
|
+
const plan = await this._resolveFundingPlan(args.params, cost);
|
|
1482
|
+
const buyerATA = await getAssociatedTokenAddressKit(arnsConfig.mint, this.signer.address);
|
|
1483
|
+
const { remainingAccounts, withdrawalCounter, residueVaultCount } = await this._materializeFundingPlan(args.params, plan);
|
|
1484
|
+
const fpBase = {
|
|
1485
|
+
config: await this.arnsConfigPda(),
|
|
1486
|
+
demandFactor: await this.demandFactorPda(),
|
|
1487
|
+
arnsRecord,
|
|
1488
|
+
garSettings,
|
|
1489
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
1490
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
1491
|
+
payerTokenAccount: plan.hasBalanceSource ? buyerATA : undefined,
|
|
1492
|
+
caller: this.signer,
|
|
1493
|
+
withdrawalCounter,
|
|
1494
|
+
garProgram: this.garProgram,
|
|
1495
|
+
sources: plan.sources.map(toGeneratedFundingSourceSpec),
|
|
1496
|
+
discountAccountCount: 0,
|
|
1497
|
+
residueVaultCount,
|
|
1498
|
+
};
|
|
1499
|
+
let ix;
|
|
1500
|
+
if (args.operation === 'upgrade')
|
|
1501
|
+
ix = await getUpgradeNameFromFundingPlanInstructionAsync(fpBase, {
|
|
1502
|
+
programAddress: this.arnsProgram,
|
|
1503
|
+
});
|
|
1504
|
+
else if (args.operation === 'extend')
|
|
1505
|
+
ix = await getExtendLeaseFromFundingPlanInstructionAsync({ ...fpBase, years: args.years }, { programAddress: this.arnsProgram });
|
|
1506
|
+
else
|
|
1507
|
+
ix = await getIncreaseUndernameLimitFromFundingPlanInstructionAsync({ ...fpBase, quantity: args.quantity }, { programAddress: this.arnsProgram });
|
|
1508
|
+
return remainingAccounts.length > 0
|
|
1509
|
+
? withRemainingAccounts(ix, remainingAccounts)
|
|
1510
|
+
: ix;
|
|
1511
|
+
}
|
|
1512
|
+
throw new Error(`unsupported fundFrom mode '${args.params.fundFrom}' for ${args.operation}`);
|
|
1513
|
+
}
|
|
1514
|
+
// =========================================
|
|
1515
|
+
// Primary name operations (ario-core)
|
|
1516
|
+
// =========================================
|
|
1517
|
+
/**
|
|
1518
|
+
* If the signer already has a primary name set, build the instruction(s)
|
|
1519
|
+
* needed to remove it so they can be prepended to a request/set tx —
|
|
1520
|
+
* enabling single-tx "change primary name" flows.
|
|
1521
|
+
*
|
|
1522
|
+
* Returns an empty array when the signer has no existing primary name.
|
|
1523
|
+
*
|
|
1524
|
+
* Throws when the signer has a legacy primary-name state (forward
|
|
1525
|
+
* `PrimaryName` PDA exists but its paired `PrimaryNameReverse` PDA does
|
|
1526
|
+
* NOT). Both `remove_primary_name` AND `request_and_set_primary_name`
|
|
1527
|
+
* require the reverse PDA on-chain — the latter rejects with
|
|
1528
|
+
* `MustRemoveExistingPrimaryName` (0x1786, code 6022) any time a
|
|
1529
|
+
* forward record already exists for the signer, regardless of reverse
|
|
1530
|
+
* state. Silently skipping the remove would queue a tx guaranteed to
|
|
1531
|
+
* fail with that opaque error. Surfacing it at the client with a clear
|
|
1532
|
+
* remediation pointer is the only safe behavior.
|
|
1533
|
+
*
|
|
1534
|
+
* The legacy state should not exist on any cluster post-snapshot/import
|
|
1535
|
+
* PR #159 (which emits PrimaryNameReverse in lockstep with PrimaryName)
|
|
1536
|
+
* — it's a relic of pre-#159 imports. Operators on affected clusters
|
|
1537
|
+
* must run `yarn workspace @ar-io/migration-import backfill:primary-name-reverse`
|
|
1538
|
+
* (in the solana-ar-io repo) before this method can succeed.
|
|
1539
|
+
*/
|
|
1540
|
+
async _buildRemoveExistingPrimaryNameIxs() {
|
|
1541
|
+
const [primaryNamePda] = await getPrimaryNamePDA(this.signer.address, this.coreProgram);
|
|
1542
|
+
const account = await fetchEncodedAccount(this.rpc, primaryNamePda, {
|
|
1543
|
+
commitment: this.commitment,
|
|
1544
|
+
});
|
|
1545
|
+
if (!account.exists)
|
|
1546
|
+
return [];
|
|
1547
|
+
const { name: oldName } = deserializePrimaryName(Buffer.from(account.data));
|
|
1548
|
+
const [primaryNameReversePda] = await getPrimaryNameReversePDA(oldName, this.coreProgram);
|
|
1549
|
+
const reverseAccount = await fetchEncodedAccount(this.rpc, primaryNameReversePda, { commitment: this.commitment });
|
|
1550
|
+
if (!reverseAccount.exists) {
|
|
1551
|
+
// Fail fast with an actionable message. See method docstring for
|
|
1552
|
+
// why request_and_set would reject this regardless.
|
|
1553
|
+
throw new Error(`Cannot change primary name: signer "${this.signer.address}" has a ` +
|
|
1554
|
+
`legacy PrimaryName ("${oldName}") with no paired PrimaryNameReverse PDA ` +
|
|
1555
|
+
`(${primaryNameReversePda}). The on-chain remove_primary_name and ` +
|
|
1556
|
+
`request_and_set_primary_name ixs both require the reverse PDA — ` +
|
|
1557
|
+
`request_and_set will reject with MustRemoveExistingPrimaryName (code 6022). ` +
|
|
1558
|
+
`Run \`yarn workspace @ar-io/migration-import backfill:primary-name-reverse\` ` +
|
|
1559
|
+
`against this cluster's ario-core program to materialize the missing reverse ` +
|
|
1560
|
+
`PDA, then retry.`);
|
|
1561
|
+
}
|
|
1562
|
+
return [
|
|
1563
|
+
await getRemovePrimaryNameInstructionAsync({
|
|
1564
|
+
primaryName: primaryNamePda,
|
|
1565
|
+
primaryNameReverse: primaryNameReversePda,
|
|
1566
|
+
owner: this.signer,
|
|
1567
|
+
reverseLookupHash: hashName(oldName),
|
|
1568
|
+
}, { programAddress: this.coreProgram }),
|
|
1569
|
+
];
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Build the `remaining_accounts` slice + the `antProgramId` arg the
|
|
1573
|
+
* four ario-core primary-name instructions consume. Sprint 2/5
|
|
1574
|
+
* reshape (ADR-016): ario-core no longer reads MPL Core asset bytes.
|
|
1575
|
+
* Authorization is "caller is the effective AntRecord owner for this
|
|
1576
|
+
* name", resolved from the `AntRecord` + `AntConfig` PDAs (freshness-
|
|
1577
|
+
* gated against `AntConfig.last_known_owner` — see ario-core BD-097 /
|
|
1578
|
+
* BD-109). Both are program-PDA-pinned lookups.
|
|
1579
|
+
*
|
|
1580
|
+
* Layouts the on-chain handlers expect:
|
|
1581
|
+
* request_primary_name: [arnsRecord, demandFactor]
|
|
1582
|
+
* request_and_set_primary_name: [arnsRecord, demandFactor, antRecord, antConfig]
|
|
1583
|
+
* approve_primary_name: [arnsRecord, antRecord, antConfig]
|
|
1584
|
+
* remove_primary_name_for_base_name: [arnsRecord, antRecord(@), antConfig]
|
|
1585
|
+
*
|
|
1586
|
+
* `antRecord` keys off the undername part for undernames (e.g.
|
|
1587
|
+
* "blog_arweave" → AntRecord at "blog") or the canonical "@" sentinel
|
|
1588
|
+
* for base names. `removeForBaseName` is special — it always uses "@"
|
|
1589
|
+
* regardless of whether the primary name being removed is an undername,
|
|
1590
|
+
* since the *base* name owner is the one revoking it.
|
|
1591
|
+
*
|
|
1592
|
+
* `antProgram` honors ADR-016 / BD-100 pluggability: the asset's
|
|
1593
|
+
* `ANT Program` Attributes-plugin trait selects which program owns the
|
|
1594
|
+
* AntRecord PDA. Absent / unparseable → canonical fallback. The detected
|
|
1595
|
+
* trait is untrusted asset/RPC data, so it is honored only when it matches
|
|
1596
|
+
* the canonical program or this client's explicitly-configured
|
|
1597
|
+
* `this.antProgram`; any other value falls back to the configured program
|
|
1598
|
+
* (see the SECURITY note in the body). Both the PDA derivation here and the
|
|
1599
|
+
* `ant_program_id` arg the caller passes to the on-chain ix MUST agree (the
|
|
1600
|
+
* handler re-derives and rejects mismatches).
|
|
1601
|
+
*/
|
|
1602
|
+
async _buildPrimaryNameValidationAccounts(name, variant) {
|
|
1603
|
+
const { isUndername, baseName, undername } = splitPrimaryName(name);
|
|
1604
|
+
const [arnsRecordPda] = await getArnsRecordPDA(baseName, this.arnsProgram);
|
|
1605
|
+
const remaining = [
|
|
1606
|
+
{ address: arnsRecordPda, role: AccountRole.READONLY },
|
|
1607
|
+
];
|
|
1608
|
+
// Fee-charging variants need the demand factor for fee scaling.
|
|
1609
|
+
if (variant === 'request' || variant === 'requestAndSet') {
|
|
1610
|
+
const [demandFactorPda] = await getDemandFactorPDA(this.arnsProgram);
|
|
1611
|
+
remaining.push({ address: demandFactorPda, role: AccountRole.READONLY });
|
|
1612
|
+
}
|
|
1613
|
+
// ANT-auth variants need the AntRecord PDA. We have to read the
|
|
1614
|
+
// ArnsRecord first to recover the ANT mint, then read the asset's
|
|
1615
|
+
// `ANT Program` trait (ADR-016 / BD-100) to pick the right program
|
|
1616
|
+
// for the AntRecord PDA derivation.
|
|
1617
|
+
let antProgram = this.antProgram;
|
|
1618
|
+
if (variant !== 'request') {
|
|
1619
|
+
const arnsAccount = await fetchEncodedAccount(this.rpc, arnsRecordPda, {
|
|
1620
|
+
commitment: this.commitment,
|
|
1621
|
+
});
|
|
1622
|
+
if (!arnsAccount.exists) {
|
|
1623
|
+
throw new Error(`ArNS record not found for base name: ${baseName}`);
|
|
1624
|
+
}
|
|
1625
|
+
const arnsRecord = deserializeArnsRecord(Buffer.from(arnsAccount.data));
|
|
1626
|
+
const antMint = address(arnsRecord.processId);
|
|
1627
|
+
const { fetchAntProgramFromAsset } = await import('./mpl-core.js');
|
|
1628
|
+
const detected = await fetchAntProgramFromAsset(this.rpc, antMint, {
|
|
1629
|
+
commitment: this.commitment,
|
|
1630
|
+
});
|
|
1631
|
+
// SECURITY (BD-100 / SDK ANT-program auth finding): the asset's
|
|
1632
|
+
// `ANT Program` trait is untrusted asset/RPC data. Only honor a detected
|
|
1633
|
+
// value when it matches the canonical program or the program this client
|
|
1634
|
+
// was explicitly configured with (`this.antProgram`); otherwise fall back
|
|
1635
|
+
// to the configured program so a spoofed trait can't redirect the
|
|
1636
|
+
// AntRecord PDA derivation. This path only derives a READONLY validation
|
|
1637
|
+
// account (the instruction itself targets the canonical core program), so
|
|
1638
|
+
// the fallback is silent rather than a throw — but the gate keeps an
|
|
1639
|
+
// attacker from steering PDA derivation. Heterogeneous BYO-ANT primary
|
|
1640
|
+
// names (an asset on a non-configured program) await the contract-side
|
|
1641
|
+
// resolution of the `ant_program == ario_ant::ID` pin; see the
|
|
1642
|
+
// accompanying security note.
|
|
1643
|
+
antProgram =
|
|
1644
|
+
detected !== null &&
|
|
1645
|
+
(detected === ARIO_ANT_PROGRAM_ID || detected === this.antProgram)
|
|
1646
|
+
? detected
|
|
1647
|
+
: this.antProgram;
|
|
1648
|
+
// removeForBaseName always uses the "@" undername (the base-name
|
|
1649
|
+
// owner's record). The other ANT-auth variants use the undername
|
|
1650
|
+
// part if the primary name is an undername.
|
|
1651
|
+
const antUndername = variant === 'removeForBaseName' || !isUndername || undername === null
|
|
1652
|
+
? '@'
|
|
1653
|
+
: undername;
|
|
1654
|
+
const [antRecordPda] = await getAntRecordPDA(antMint, antUndername, antProgram);
|
|
1655
|
+
remaining.push({ address: antRecordPda, role: AccountRole.READONLY });
|
|
1656
|
+
// AntConfig PDA (ANT-level owner snapshot). ario-core reads
|
|
1657
|
+
// `AntConfig.last_known_owner` as the implicit-owner source and to
|
|
1658
|
+
// freshness-gate the per-record `AntRecord.owner` delegate (BD-097 /
|
|
1659
|
+
// BD-109). Derived under the SAME resolved `antProgram` as the
|
|
1660
|
+
// AntRecord above. Required trailing account for all three ANT-auth
|
|
1661
|
+
// variants (and the funding-plan variant, whose validation_account_count
|
|
1662
|
+
// is derived from this array's length downstream).
|
|
1663
|
+
const [antConfigPda] = await getAntConfigPDA(antMint, antProgram);
|
|
1664
|
+
remaining.push({ address: antConfigPda, role: AccountRole.READONLY });
|
|
1665
|
+
}
|
|
1666
|
+
return { remaining, antProgram };
|
|
1667
|
+
}
|
|
1668
|
+
async requestPrimaryName(params, _options) {
|
|
1669
|
+
// If the caller already has a primary name, prepend remove ixs so
|
|
1670
|
+
// the on-chain handler doesn't reject with MustRemoveExistingPrimaryName.
|
|
1671
|
+
const removeIxs = await this._buildRemoveExistingPrimaryNameIxs();
|
|
1672
|
+
const { baseName } = splitPrimaryName(params.name);
|
|
1673
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(baseName);
|
|
1674
|
+
const coreConfig = await this.getCoreConfig();
|
|
1675
|
+
const signerATA = await getAssociatedTokenAddressKit(coreConfig.mint, this.signer.address);
|
|
1676
|
+
const { remaining } = await this._buildPrimaryNameValidationAccounts(params.name, 'request');
|
|
1677
|
+
// Phase 4 of FUND_FROM_PLAN.md: dispatch primary-name funding via the
|
|
1678
|
+
// funding-plan ix when caller asks for stakes/withdrawal/plan/any. The
|
|
1679
|
+
// direct-transfer ix stays the path for fundFrom='balance' / undefined.
|
|
1680
|
+
let ix;
|
|
1681
|
+
if (!params.fundFrom ||
|
|
1682
|
+
params.fundFrom === 'balance' ||
|
|
1683
|
+
params.fundFrom === 'turbo') {
|
|
1684
|
+
ix = withRemainingAccounts(await getRequestPrimaryNameInstructionAsync(await this.withCoreDefaults({
|
|
1685
|
+
initiatorTokenAccount: signerATA,
|
|
1686
|
+
protocolTokenAccount: coreConfig.treasury,
|
|
1687
|
+
initiator: this.signer,
|
|
1688
|
+
name: params.name,
|
|
1689
|
+
}), { programAddress: this.coreProgram }), remaining);
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
ix = await this._buildPrimaryNameFromFundingPlanIx({
|
|
1693
|
+
params,
|
|
1694
|
+
coreConfig,
|
|
1695
|
+
validationAccounts: remaining,
|
|
1696
|
+
operation: 'request',
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
const sig = await this.sendTransaction([...removeIxs, ...migrateIxs, ix]);
|
|
1700
|
+
return { id: sig };
|
|
1701
|
+
}
|
|
1702
|
+
async setPrimaryName(params, _options) {
|
|
1703
|
+
// setPrimaryName routes to the on-chain `request_and_set_primary_name`
|
|
1704
|
+
// path — the auto-approve flow when the caller owns the AntRecord
|
|
1705
|
+
// for the matching name (undername part, or "@" for base names).
|
|
1706
|
+
// If the caller already has a primary name, prepend remove ixs so
|
|
1707
|
+
// the "change" is atomic in a single transaction.
|
|
1708
|
+
const removeIxs = await this._buildRemoveExistingPrimaryNameIxs();
|
|
1709
|
+
const { baseName } = splitPrimaryName(params.name);
|
|
1710
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(baseName);
|
|
1711
|
+
const coreConfig = await this.getCoreConfig();
|
|
1712
|
+
const signerATA = await getAssociatedTokenAddressKit(coreConfig.mint, this.signer.address);
|
|
1713
|
+
const { remaining, antProgram } = await this._buildPrimaryNameValidationAccounts(params.name, 'requestAndSet');
|
|
1714
|
+
let ix;
|
|
1715
|
+
if (!params.fundFrom ||
|
|
1716
|
+
params.fundFrom === 'balance' ||
|
|
1717
|
+
params.fundFrom === 'turbo') {
|
|
1718
|
+
ix = withRemainingAccounts(await getRequestAndSetPrimaryNameInstructionAsync(await this.withCoreDefaults({
|
|
1719
|
+
initiatorTokenAccount: signerATA,
|
|
1720
|
+
protocolTokenAccount: coreConfig.treasury,
|
|
1721
|
+
initiator: this.signer,
|
|
1722
|
+
name: params.name,
|
|
1723
|
+
reverseLookupHash: hashName(params.name),
|
|
1724
|
+
antProgramId: antProgram,
|
|
1725
|
+
}), { programAddress: this.coreProgram }), remaining);
|
|
1726
|
+
}
|
|
1727
|
+
else {
|
|
1728
|
+
ix = await this._buildPrimaryNameFromFundingPlanIx({
|
|
1729
|
+
params,
|
|
1730
|
+
coreConfig,
|
|
1731
|
+
validationAccounts: remaining,
|
|
1732
|
+
operation: 'requestAndSet',
|
|
1733
|
+
antProgramId: antProgram,
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
const sig = await this.sendTransaction([...removeIxs, ...migrateIxs, ix]);
|
|
1737
|
+
return { id: sig };
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Build a `request_primary_name_from_funding_plan` or
|
|
1741
|
+
* `request_and_set_primary_name_from_funding_plan` ix. Forwards both:
|
|
1742
|
+
* - validation accounts (ArnsRecord + DemandFactor [+ ant_asset
|
|
1743
|
+
* [+ AntRecord]]) — passed via remaining_accounts at indices
|
|
1744
|
+
* [0..validation_account_count)
|
|
1745
|
+
* - per-source PDAs from the funding plan — passed at indices
|
|
1746
|
+
* [validation_account_count..)
|
|
1747
|
+
*
|
|
1748
|
+
* The on-chain handler (programs/ario-core/src/instructions/primary_name.rs)
|
|
1749
|
+
* splits remaining_accounts at `validation_account_count` and forwards the
|
|
1750
|
+
* funding-source slice to ario-gar's pay_from_funding_plan via CPI.
|
|
1751
|
+
*/
|
|
1752
|
+
async _buildPrimaryNameFromFundingPlanIx(args) {
|
|
1753
|
+
const fee = await this._simulateTokenCost({
|
|
1754
|
+
intent: CostIntent.PrimaryNameRequest,
|
|
1755
|
+
name: args.params.name,
|
|
1756
|
+
});
|
|
1757
|
+
const plan = await this._resolveFundingPlan(args.params, fee);
|
|
1758
|
+
const garConfig = await this.getGarConfig();
|
|
1759
|
+
const [garSettings] = await getGarSettingsPDA(this.garProgram);
|
|
1760
|
+
const initiatorATA = await getAssociatedTokenAddressKit(args.coreConfig.mint, this.signer.address);
|
|
1761
|
+
const { remainingAccounts: fundingRemaining, withdrawalCounter, residueVaultCount, } = await this._materializeFundingPlan(args.params, plan);
|
|
1762
|
+
const allRemaining = [
|
|
1763
|
+
...args.validationAccounts,
|
|
1764
|
+
...fundingRemaining,
|
|
1765
|
+
];
|
|
1766
|
+
const validationAccountCount = args.validationAccounts.length;
|
|
1767
|
+
const baseFp = {
|
|
1768
|
+
config: await this._coreConfigPda(),
|
|
1769
|
+
garSettings,
|
|
1770
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
1771
|
+
protocolTokenAccount: args.coreConfig.treasury,
|
|
1772
|
+
payerTokenAccount: plan.hasBalanceSource ? initiatorATA : undefined,
|
|
1773
|
+
initiator: this.signer,
|
|
1774
|
+
withdrawalCounter,
|
|
1775
|
+
garProgram: this.garProgram,
|
|
1776
|
+
sources: plan.sources.map(toGeneratedFundingSourceSpec),
|
|
1777
|
+
validationAccountCount,
|
|
1778
|
+
residueVaultCount,
|
|
1779
|
+
};
|
|
1780
|
+
let ix;
|
|
1781
|
+
if (args.operation === 'request') {
|
|
1782
|
+
const [requestPda] = await getPrimaryNameRequestPDA(this.signer.address, this.coreProgram);
|
|
1783
|
+
ix = await getRequestPrimaryNameFromFundingPlanInstructionAsync({ ...baseFp, request: requestPda, name: args.params.name }, { programAddress: this.coreProgram });
|
|
1784
|
+
}
|
|
1785
|
+
else {
|
|
1786
|
+
const [primaryNamePda] = await getPrimaryNamePDA(this.signer.address, this.coreProgram);
|
|
1787
|
+
const [primaryNameReversePda] = await getPrimaryNameReversePDA(args.params.name, this.coreProgram);
|
|
1788
|
+
ix = await getRequestAndSetPrimaryNameFromFundingPlanInstructionAsync({
|
|
1789
|
+
...baseFp,
|
|
1790
|
+
primaryName: primaryNamePda,
|
|
1791
|
+
primaryNameReverse: primaryNameReversePda,
|
|
1792
|
+
name: args.params.name,
|
|
1793
|
+
reverseLookupHash: hashName(args.params.name),
|
|
1794
|
+
antProgramId: args.antProgramId ?? this.antProgram,
|
|
1795
|
+
}, { programAddress: this.coreProgram });
|
|
1796
|
+
}
|
|
1797
|
+
return allRemaining.length > 0
|
|
1798
|
+
? withRemainingAccounts(ix, allRemaining)
|
|
1799
|
+
: ix;
|
|
1800
|
+
}
|
|
1801
|
+
async _coreConfigPda() {
|
|
1802
|
+
const [pda] = await getArioConfigPDA(this.coreProgram);
|
|
1803
|
+
return pda;
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Approve a previously-created primary name request. The signer must be
|
|
1807
|
+
* the AntRecord.owner for the requested name (undername part for
|
|
1808
|
+
* undernames, "@" for base names) — Sprint 2 / ADR-016 reshape.
|
|
1809
|
+
*
|
|
1810
|
+
* Mirrors the on-chain `approve_primary_name` instruction
|
|
1811
|
+
* (`programs/ario-core/src/instructions/primary_name.rs`).
|
|
1812
|
+
* remaining_accounts: [arns_record(base), ant_record(undername | @), ant_config].
|
|
1813
|
+
*/
|
|
1814
|
+
async approvePrimaryName(params, _options) {
|
|
1815
|
+
const { baseName } = splitPrimaryName(params.name);
|
|
1816
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(baseName);
|
|
1817
|
+
const [requestPda] = await getPrimaryNameRequestPDA(params.initiator, this.coreProgram);
|
|
1818
|
+
const [primaryNamePda] = await getPrimaryNamePDA(params.initiator, this.coreProgram);
|
|
1819
|
+
const [primaryNameReversePda] = await getPrimaryNameReversePDA(params.name, this.coreProgram);
|
|
1820
|
+
const { remaining, antProgram } = await this._buildPrimaryNameValidationAccounts(params.name, 'approve');
|
|
1821
|
+
// withCoreDefaults injects `config` as the ArioConfig PDA derived against
|
|
1822
|
+
// *this.coreProgram*. Without it, Codama's `findConfigPda()` fallback
|
|
1823
|
+
// resolves to the source-pinned `ARioCoreProgramXXXX...` placeholder
|
|
1824
|
+
// program id (declare_id! literal pre-anchor-keys-sync), which on any
|
|
1825
|
+
// non-mainnet deployment produces an unallocated PDA → Anchor #3012
|
|
1826
|
+
// AccountNotInitialized on `config`. requestPrimaryName /
|
|
1827
|
+
// requestAndSetPrimaryName already use withCoreDefaults; this was the
|
|
1828
|
+
// last setPrimaryName-family entrypoint missing it.
|
|
1829
|
+
const ix = withRemainingAccounts(await getApprovePrimaryNameInstructionAsync(await this.withCoreDefaults({
|
|
1830
|
+
request: requestPda,
|
|
1831
|
+
initiator: params.initiator,
|
|
1832
|
+
primaryName: primaryNamePda,
|
|
1833
|
+
primaryNameReverse: primaryNameReversePda,
|
|
1834
|
+
nameOwner: this.signer,
|
|
1835
|
+
reverseLookupHash: hashName(params.name),
|
|
1836
|
+
antProgramId: antProgram,
|
|
1837
|
+
}), { programAddress: this.coreProgram }), remaining);
|
|
1838
|
+
const sig = await this.sendTransaction([...migrateIxs, ix]);
|
|
1839
|
+
return { id: sig };
|
|
1840
|
+
}
|
|
1841
|
+
// =========================================
|
|
1842
|
+
// Vault release (ario-core)
|
|
1843
|
+
// =========================================
|
|
1844
|
+
/** Release tokens from an unlocked vault back to the owner. */
|
|
1845
|
+
async releaseVault(params, _options) {
|
|
1846
|
+
const mint = await this.getMint();
|
|
1847
|
+
const [vaultPda] = await getVaultPDA(this.signer.address, BigInt(params.vaultId), this.coreProgram);
|
|
1848
|
+
const vaultATA = await getAssociatedTokenAddressKit(mint, vaultPda, true);
|
|
1849
|
+
const ownerATA = await getAssociatedTokenAddressKit(mint, this.signer.address);
|
|
1850
|
+
const ix = await getReleaseVaultInstructionAsync(await this.withCoreDefaults({
|
|
1851
|
+
vault: vaultPda,
|
|
1852
|
+
vaultTokenAccount: vaultATA,
|
|
1853
|
+
ownerTokenAccount: ownerATA,
|
|
1854
|
+
owner: this.signer,
|
|
1855
|
+
}), { programAddress: this.coreProgram });
|
|
1856
|
+
const sig = await this.sendTransaction([ix]);
|
|
1857
|
+
return { id: sig };
|
|
1858
|
+
}
|
|
1859
|
+
// =========================================
|
|
1860
|
+
// Close expired primary name request (ario-core)
|
|
1861
|
+
// =========================================
|
|
1862
|
+
/** Close an expired primary name request (permissionless — anyone can call). */
|
|
1863
|
+
async closeExpiredRequest(params, _options) {
|
|
1864
|
+
const initiatorPubkey = address(params.initiator);
|
|
1865
|
+
const [requestPda] = await getPrimaryNameRequestPDA(initiatorPubkey, this.coreProgram);
|
|
1866
|
+
const ix = getCloseExpiredRequestInstruction({
|
|
1867
|
+
request: requestPda,
|
|
1868
|
+
initiator: initiatorPubkey,
|
|
1869
|
+
payer: this.signer,
|
|
1870
|
+
}, { programAddress: this.coreProgram });
|
|
1871
|
+
const sig = await this.sendTransaction([ix]);
|
|
1872
|
+
return { id: sig };
|
|
1873
|
+
}
|
|
1874
|
+
// =========================================
|
|
1875
|
+
// Withdrawal claim (ario-gar)
|
|
1876
|
+
// =========================================
|
|
1877
|
+
/** Claim tokens from a completed withdrawal (after lock period). */
|
|
1878
|
+
async claimWithdrawal(params, _options) {
|
|
1879
|
+
const garConfig = await this.getGarConfig();
|
|
1880
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, BigInt(params.withdrawalId), this.garProgram);
|
|
1881
|
+
const ownerATA = await getAssociatedTokenAddressKit(garConfig.mint, this.signer.address);
|
|
1882
|
+
const ix = await getClaimWithdrawalInstructionAsync(await this.withGarDefaults({
|
|
1883
|
+
withdrawal: withdrawalPda,
|
|
1884
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
1885
|
+
ownerTokenAccount: ownerATA,
|
|
1886
|
+
owner: this.signer,
|
|
1887
|
+
}), { programAddress: this.garProgram });
|
|
1888
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
1889
|
+
return { id: sig };
|
|
1890
|
+
}
|
|
1891
|
+
// =========================================
|
|
1892
|
+
// Claim delegation from leaving gateway (ario-gar)
|
|
1893
|
+
// =========================================
|
|
1894
|
+
/** Claim delegated stake from a gateway that is leaving the network. */
|
|
1895
|
+
async claimDelegateFromLeavingGateway(params, _options) {
|
|
1896
|
+
const gateway = address(params.gatewayAddress);
|
|
1897
|
+
const [gatewayPda] = await getGatewayPDA(gateway, this.garProgram);
|
|
1898
|
+
const [delegationPda] = await getDelegationPDA(gateway, this.signer.address, this.garProgram);
|
|
1899
|
+
const nextId = await this.getNextWithdrawalId(this.signer.address);
|
|
1900
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, nextId, this.garProgram);
|
|
1901
|
+
// The on-chain handler is permissionless since `af38a40` (delegator +
|
|
1902
|
+
// payer split — anyone can crank). The IDL exposes both fields:
|
|
1903
|
+
// `delegator: Address` (no signature, just the seeds-derivation key)
|
|
1904
|
+
// and `payer: Signer` (covers rent on the init_if_needed withdrawal
|
|
1905
|
+
// counter + the new withdrawal account). The SDK's primary self-
|
|
1906
|
+
// claim path passes the signer for both roles; cranker callers can
|
|
1907
|
+
// pass distinct addresses by adding a `payer?` param to this method
|
|
1908
|
+
// (out of scope here — feature gap; tracked in
|
|
1909
|
+
// docs/E2E_TEST_COVERAGE_PLAN.md Phase 3.3).
|
|
1910
|
+
const ix = await getClaimDelegateFromLeavingGatewayInstructionAsync({
|
|
1911
|
+
gateway: gatewayPda,
|
|
1912
|
+
delegation: delegationPda,
|
|
1913
|
+
withdrawal: withdrawalPda,
|
|
1914
|
+
delegator: this.signer.address,
|
|
1915
|
+
payer: this.signer,
|
|
1916
|
+
}, { programAddress: this.garProgram });
|
|
1917
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
1918
|
+
return { id: sig };
|
|
1919
|
+
}
|
|
1920
|
+
// =========================================
|
|
1921
|
+
// Claim delegation from gateway with delegation DISABLED (ario-gar, Fix #6)
|
|
1922
|
+
// =========================================
|
|
1923
|
+
/**
|
|
1924
|
+
* Claim a delegate's stake out of a gateway that has DISABLED delegation
|
|
1925
|
+
* (`allow_delegated_staking == false`), moving it into the delegate's own
|
|
1926
|
+
* withdrawal vault (WP §6.3 / Fix #6). This is the disabled-gateway analog of
|
|
1927
|
+
* {@link claimDelegateFromLeavingGateway}: the on-chain instruction is
|
|
1928
|
+
* permissionless, so a cranker can sweep delegates out (the operator cannot
|
|
1929
|
+
* re-enable delegation until `total_delegated_stake == 0` and the cooldown
|
|
1930
|
+
* elapses). The withdrawal-counter and withdrawal PDAs are seeded by the
|
|
1931
|
+
* DELEGATOR, so a cranker must pass that delegate's `delegatorAddress`.
|
|
1932
|
+
*
|
|
1933
|
+
* @param params.gatewayAddress The gateway whose delegation was disabled.
|
|
1934
|
+
* @param params.delegatorAddress The delegate to claim for. Defaults to the
|
|
1935
|
+
* signer (self-claim). Pass another address to crank on a delegate's behalf;
|
|
1936
|
+
* the signer covers rent (`payer`) but stake still routes to the delegate's
|
|
1937
|
+
* own vault (the delegator key is bound by the delegation PDA seeds).
|
|
1938
|
+
*/
|
|
1939
|
+
async claimDelegateFromDisabledGateway(params, _options) {
|
|
1940
|
+
const gateway = address(params.gatewayAddress);
|
|
1941
|
+
const delegator = params.delegatorAddress
|
|
1942
|
+
? address(params.delegatorAddress)
|
|
1943
|
+
: this.signer.address;
|
|
1944
|
+
const [gatewayPda] = await getGatewayPDA(gateway, this.garProgram);
|
|
1945
|
+
const [delegationPda] = await getDelegationPDA(gateway, delegator, this.garProgram);
|
|
1946
|
+
// Withdrawal counter + vault are PDA-seeded by the delegator, not the payer.
|
|
1947
|
+
const nextId = await this.getNextWithdrawalId(delegator);
|
|
1948
|
+
const [withdrawalPda] = await getWithdrawalPDA(delegator, nextId, this.garProgram);
|
|
1949
|
+
const ix = await getClaimDelegateFromDisabledGatewayInstructionAsync({
|
|
1950
|
+
gateway: gatewayPda,
|
|
1951
|
+
delegation: delegationPda,
|
|
1952
|
+
withdrawal: withdrawalPda,
|
|
1953
|
+
// `delegator` is an unsigned seeds-derivation key; `payer` (the signer)
|
|
1954
|
+
// covers rent on the init_if_needed counter + the new withdrawal.
|
|
1955
|
+
delegator,
|
|
1956
|
+
payer: this.signer,
|
|
1957
|
+
}, { programAddress: this.garProgram });
|
|
1958
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
1959
|
+
return { id: sig };
|
|
1960
|
+
}
|
|
1961
|
+
// =========================================
|
|
1962
|
+
// Delegation allowlist (ario-gar)
|
|
1963
|
+
// =========================================
|
|
1964
|
+
/** Add an address to the gateway's delegation allowlist. */
|
|
1965
|
+
async allowDelegate(params, _options) {
|
|
1966
|
+
const ix = await getAllowDelegateInstructionAsync({
|
|
1967
|
+
delegate: address(params.delegate),
|
|
1968
|
+
operator: this.signer,
|
|
1969
|
+
}, { programAddress: this.garProgram });
|
|
1970
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
1971
|
+
return { id: sig };
|
|
1972
|
+
}
|
|
1973
|
+
/** Remove an address from the gateway's delegation allowlist. */
|
|
1974
|
+
async disallowDelegate(params, _options) {
|
|
1975
|
+
const ix = await getDisallowDelegateInstructionAsync({
|
|
1976
|
+
delegate: address(params.delegate),
|
|
1977
|
+
operator: this.signer,
|
|
1978
|
+
}, { programAddress: this.garProgram });
|
|
1979
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
1980
|
+
return { id: sig };
|
|
1981
|
+
}
|
|
1982
|
+
/** Enable or disable the delegation allowlist for the gateway. */
|
|
1983
|
+
async setAllowlistEnabled(params, _options) {
|
|
1984
|
+
const ix = await getSetAllowlistEnabledInstructionAsync(await this.withGarDefaults({
|
|
1985
|
+
operator: this.signer,
|
|
1986
|
+
enabled: params.enabled,
|
|
1987
|
+
}), { programAddress: this.garProgram });
|
|
1988
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
1989
|
+
return { id: sig };
|
|
1990
|
+
}
|
|
1991
|
+
// =========================================
|
|
1992
|
+
// Buy returned name (ario-arns)
|
|
1993
|
+
// =========================================
|
|
1994
|
+
/**
|
|
1995
|
+
* Buy a name from the returned name auction (Dutch auction with premium).
|
|
1996
|
+
*
|
|
1997
|
+
* Phase 4: now dispatches on `params.fundFrom`. Note that for
|
|
1998
|
+
* `buyReturnedName`, only the protocol share funds from the chosen source;
|
|
1999
|
+
* the initiator share is always a direct buyer-ATA → initiator-ATA SPL
|
|
2000
|
+
* transfer (matches the on-chain `_from_*` variant behavior).
|
|
2001
|
+
*/
|
|
2002
|
+
async buyReturnedName(params, _options) {
|
|
2003
|
+
const arnsConfig = await this.getArnsConfig();
|
|
2004
|
+
const buyerATA = await getAssociatedTokenAddressKit(arnsConfig.mint, this.signer.address);
|
|
2005
|
+
const antPubkey = address(params.processId);
|
|
2006
|
+
// Read the ReturnedName to find the original initiator (gets the premium).
|
|
2007
|
+
const [returnedNamePda] = await getReturnedNamePDA(params.name, this.arnsProgram);
|
|
2008
|
+
const returnedNameAccount = await fetchEncodedAccount(this.rpc, returnedNamePda, { commitment: this.commitment });
|
|
2009
|
+
if (!returnedNameAccount.exists) {
|
|
2010
|
+
throw new Error(`Returned name not found: ${params.name}`);
|
|
2011
|
+
}
|
|
2012
|
+
const returnedNameData = Buffer.from(returnedNameAccount.data);
|
|
2013
|
+
const nameLen = returnedNameData.readUInt32LE(8);
|
|
2014
|
+
const initiatorOffset = 8 + 4 + nameLen + 32;
|
|
2015
|
+
const initiator = addressDecoder.decode(returnedNameData.subarray(initiatorOffset, initiatorOffset + 32));
|
|
2016
|
+
const initiatorATA = await getAssociatedTokenAddressKit(arnsConfig.mint, initiator);
|
|
2017
|
+
const [arnsRecord] = await getArnsRecordPDA(params.name, this.arnsProgram);
|
|
2018
|
+
const buyParams = {
|
|
2019
|
+
name: params.name,
|
|
2020
|
+
purchaseType: params.type === 'permabuy' ? PurchaseType.Permabuy : PurchaseType.Lease,
|
|
2021
|
+
years: params.years ?? 1,
|
|
2022
|
+
ant: antPubkey,
|
|
2023
|
+
};
|
|
2024
|
+
// Returned-name price is a per-slot-decaying Dutch auction, so the
|
|
2025
|
+
// multi-source funding plan (which pre-commits exact source amounts) can't
|
|
2026
|
+
// match the execution-time cost → FundingPlanAmountMismatch (#6066). Prefer
|
|
2027
|
+
// a single-source stake path: it carries no amount, so the program computes
|
|
2028
|
+
// and draws the live cost itself. When the caller asked to fund from
|
|
2029
|
+
// stakes/withdrawal/any without naming a specific gateway/vault,
|
|
2030
|
+
// auto-resolve a single source with enough stake to cover the
|
|
2031
|
+
// (premium-inclusive) cost.
|
|
2032
|
+
let resolvedGateway = params.gatewayAddress;
|
|
2033
|
+
let resolvedFundAsOperator = params.fundAsOperator ?? false;
|
|
2034
|
+
let resolvedWithdrawalId = params.withdrawalId;
|
|
2035
|
+
const wantsStake = params.fundFrom === 'stakes' ||
|
|
2036
|
+
params.fundFrom === 'withdrawal' ||
|
|
2037
|
+
params.fundFrom === 'any';
|
|
2038
|
+
if (wantsStake &&
|
|
2039
|
+
resolvedGateway === undefined &&
|
|
2040
|
+
resolvedWithdrawalId === undefined &&
|
|
2041
|
+
!params.sources?.length) {
|
|
2042
|
+
const picked = await this._autoPickReturnedNameStakeSource(params);
|
|
2043
|
+
if (picked?.kind === 'delegation') {
|
|
2044
|
+
resolvedGateway = picked.gateway;
|
|
2045
|
+
resolvedFundAsOperator = false;
|
|
2046
|
+
}
|
|
2047
|
+
else if (picked?.kind === 'operatorStake') {
|
|
2048
|
+
resolvedGateway = picked.gateway;
|
|
2049
|
+
resolvedFundAsOperator = true;
|
|
2050
|
+
}
|
|
2051
|
+
else if (picked?.kind === 'withdrawal') {
|
|
2052
|
+
resolvedWithdrawalId = picked.withdrawalId;
|
|
2053
|
+
}
|
|
2054
|
+
else if (params.fundFrom !== 'any') {
|
|
2055
|
+
// 'stakes'/'withdrawal' explicitly requested but nothing covers it.
|
|
2056
|
+
throw new Error(`buyReturnedName: no ${params.fundFrom === 'withdrawal'
|
|
2057
|
+
? 'matured withdrawal vault'
|
|
2058
|
+
: 'delegation/operator stake'} large enough to fund '${params.name}' was found for ` +
|
|
2059
|
+
`${this.signer.address}. Fund from balance, or add stake first.`);
|
|
2060
|
+
}
|
|
2061
|
+
// 'any' with nothing found → falls through to the balance path.
|
|
2062
|
+
}
|
|
2063
|
+
let ix;
|
|
2064
|
+
const useBalance = !params.fundFrom ||
|
|
2065
|
+
params.fundFrom === 'balance' ||
|
|
2066
|
+
params.fundFrom === 'turbo' ||
|
|
2067
|
+
(params.fundFrom === 'any' &&
|
|
2068
|
+
resolvedGateway === undefined &&
|
|
2069
|
+
resolvedWithdrawalId === undefined &&
|
|
2070
|
+
!params.sources?.length);
|
|
2071
|
+
if (useBalance) {
|
|
2072
|
+
ix = await getBuyReturnedNameInstructionAsync(await this.withArnsDefaults({
|
|
2073
|
+
arnsRecord,
|
|
2074
|
+
returnedName: returnedNamePda,
|
|
2075
|
+
buyerTokenAccount: buyerATA,
|
|
2076
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
2077
|
+
initiatorTokenAccount: initiatorATA,
|
|
2078
|
+
buyer: this.signer,
|
|
2079
|
+
params: buyParams,
|
|
2080
|
+
}), { programAddress: this.arnsProgram });
|
|
2081
|
+
}
|
|
2082
|
+
else {
|
|
2083
|
+
const garConfig = await this.getGarConfig();
|
|
2084
|
+
const [garSettings] = await getGarSettingsPDA(this.garProgram);
|
|
2085
|
+
const sharedReturnedBase = {
|
|
2086
|
+
config: await this.arnsConfigPda(),
|
|
2087
|
+
demandFactor: await this.demandFactorPda(),
|
|
2088
|
+
returnedName: returnedNamePda,
|
|
2089
|
+
arnsRecord,
|
|
2090
|
+
nameRegistry: await this.nameRegistryPda(),
|
|
2091
|
+
buyerTokenAccount: buyerATA,
|
|
2092
|
+
initiatorTokenAccount: initiatorATA,
|
|
2093
|
+
garSettings,
|
|
2094
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
2095
|
+
protocolTokenAccount: arnsConfig.treasury,
|
|
2096
|
+
buyer: this.signer,
|
|
2097
|
+
garProgram: this.garProgram,
|
|
2098
|
+
params: buyParams,
|
|
2099
|
+
};
|
|
2100
|
+
if (resolvedGateway !== undefined) {
|
|
2101
|
+
const gatewayAddr = address(resolvedGateway);
|
|
2102
|
+
const [gatewayPda] = await getGatewayPDA(gatewayAddr, this.garProgram);
|
|
2103
|
+
if (resolvedFundAsOperator) {
|
|
2104
|
+
ix = await getBuyReturnedNameFromOperatorStakeInstructionAsync({ ...sharedReturnedBase, gateway: gatewayPda }, { programAddress: this.arnsProgram });
|
|
2105
|
+
}
|
|
2106
|
+
else {
|
|
2107
|
+
const [delegationPda] = await getDelegationPDA(gatewayAddr, this.signer.address, this.garProgram);
|
|
2108
|
+
ix = await getBuyReturnedNameFromDelegationInstructionAsync({
|
|
2109
|
+
...sharedReturnedBase,
|
|
2110
|
+
gateway: gatewayPda,
|
|
2111
|
+
delegation: delegationPda,
|
|
2112
|
+
}, { programAddress: this.arnsProgram });
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
else if (resolvedWithdrawalId !== undefined) {
|
|
2116
|
+
const [withdrawalPda] = await getWithdrawalPDA(this.signer.address, resolvedWithdrawalId, this.garProgram);
|
|
2117
|
+
ix = await getBuyReturnedNameFromWithdrawalInstructionAsync({ ...sharedReturnedBase, withdrawal: withdrawalPda }, { programAddress: this.arnsProgram });
|
|
2118
|
+
}
|
|
2119
|
+
else if (params.fundFrom === 'plan' && params.sources?.length) {
|
|
2120
|
+
// Explicit caller-supplied plan only: the caller owns the source
|
|
2121
|
+
// amounts and accepts the decay risk (the price moves per slot, so a
|
|
2122
|
+
// stale plan trips FundingPlanAmountMismatch). We do NOT auto-discover
|
|
2123
|
+
// a multi-source plan for returned names — see the single-source note
|
|
2124
|
+
// above.
|
|
2125
|
+
const cost = await this._simulateTokenCost({
|
|
2126
|
+
intent: CostIntent.BuyName,
|
|
2127
|
+
name: params.name,
|
|
2128
|
+
years: buyParams.years,
|
|
2129
|
+
purchaseType: buyParams.purchaseType,
|
|
2130
|
+
});
|
|
2131
|
+
const plan = await this._resolveFundingPlan(params, cost);
|
|
2132
|
+
const { remainingAccounts, withdrawalCounter, residueVaultCount } = await this._materializeFundingPlan(params, plan);
|
|
2133
|
+
ix = await getBuyReturnedNameFromFundingPlanInstructionAsync({
|
|
2134
|
+
...sharedReturnedBase,
|
|
2135
|
+
payerTokenAccount: plan.hasBalanceSource ? buyerATA : undefined,
|
|
2136
|
+
withdrawalCounter,
|
|
2137
|
+
sources: plan.sources.map(toGeneratedFundingSourceSpec),
|
|
2138
|
+
discountAccountCount: 0,
|
|
2139
|
+
residueVaultCount,
|
|
2140
|
+
}, { programAddress: this.arnsProgram });
|
|
2141
|
+
if (remainingAccounts.length > 0)
|
|
2142
|
+
ix = withRemainingAccounts(ix, remainingAccounts);
|
|
2143
|
+
}
|
|
2144
|
+
else {
|
|
2145
|
+
throw new Error(`unsupported fundFrom mode '${params.fundFrom}' for buyReturnedName`);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
// The on-chain `buy_returned_name*` handlers take `initiator_token_account`
|
|
2149
|
+
// and `buyer_token_account` as `Account<TokenAccount>` (NOT `init`), so
|
|
2150
|
+
// Anchor requires both ATAs to already exist or fails with
|
|
2151
|
+
// AccountNotInitialized (#3012). The original initiator may never have held
|
|
2152
|
+
// ARIO, and the premium always settles from the buyer's liquid ATA — bundle
|
|
2153
|
+
// idempotent ATA creates so the buy succeeds without a separate setup step
|
|
2154
|
+
// (mirrors the vault-ATA handling above). Idempotent: ~1500 CU each, no-op
|
|
2155
|
+
// when the account already exists.
|
|
2156
|
+
const createInitiatorAtaIx = buildCreateAtaIdempotentIx(this.signer.address, initiatorATA, initiator, arnsConfig.mint);
|
|
2157
|
+
const createBuyerAtaIx = buildCreateAtaIdempotentIx(this.signer.address, buyerATA, this.signer.address, arnsConfig.mint);
|
|
2158
|
+
// Sprint 4 / ADR-016: bundle ant.sync_attributes after the buy so the
|
|
2159
|
+
// Attributes plugin reflects the new record holder. assetOverride =
|
|
2160
|
+
// antPubkey because the ArnsRecord PDA is created by buy_returned_name
|
|
2161
|
+
// and doesn't exist on-chain at SDK build time.
|
|
2162
|
+
const syncIx = await this._buildSyncAttributesIxIfOwner(params.name, antPubkey);
|
|
2163
|
+
const sig = await this.sendTransaction([
|
|
2164
|
+
createBuyerAtaIx,
|
|
2165
|
+
createInitiatorAtaIx,
|
|
2166
|
+
ix,
|
|
2167
|
+
...(syncIx ? [syncIx] : []),
|
|
2168
|
+
]);
|
|
2169
|
+
return { id: sig };
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Pick a single stake-derived funding source that can cover a returned-name
|
|
2173
|
+
* purchase, for the single-source `buy_returned_name_from_*` paths.
|
|
2174
|
+
*
|
|
2175
|
+
* Returned-name prices decay per slot, so the multi-source funding plan
|
|
2176
|
+
* (which pre-commits exact amounts) can't match the execution-time cost. The
|
|
2177
|
+
* single-source paths carry no amount — the program draws the live cost — so
|
|
2178
|
+
* we only need to pick ONE source with enough stake. We size the pick against
|
|
2179
|
+
* the premium-inclusive estimate (an upper bound, since the price only falls
|
|
2180
|
+
* from now) and choose the largest matching source. Returns `null` when no
|
|
2181
|
+
* single source covers the estimate.
|
|
2182
|
+
*/
|
|
2183
|
+
async _autoPickReturnedNameStakeSource(params) {
|
|
2184
|
+
const estimate = BigInt(Math.ceil(await this.getTokenCost({
|
|
2185
|
+
intent: 'Buy-Name',
|
|
2186
|
+
name: params.name,
|
|
2187
|
+
type: params.type,
|
|
2188
|
+
years: params.years ?? 1,
|
|
2189
|
+
})));
|
|
2190
|
+
const arnsConfig = await this.getArnsConfig();
|
|
2191
|
+
const { discoverFundingSources } = await import('./funding-plan.js');
|
|
2192
|
+
const sources = await discoverFundingSources(this.rpc, this.signer.address, { arioMint: arnsConfig.mint, garProgram: this.garProgram });
|
|
2193
|
+
// 'withdrawal' mode → matured withdrawal vaults only; otherwise prefer
|
|
2194
|
+
// operator stake when the caller asked, else a delegation.
|
|
2195
|
+
const wantKind = params.fundFrom === 'withdrawal'
|
|
2196
|
+
? 'withdrawal'
|
|
2197
|
+
: params.fundAsOperator === true
|
|
2198
|
+
? 'operatorStake'
|
|
2199
|
+
: 'delegation';
|
|
2200
|
+
const candidates = sources
|
|
2201
|
+
.filter((s) => s.kind === wantKind && s.available >= estimate)
|
|
2202
|
+
.sort((a, b) => b.available > a.available ? 1 : b.available < a.available ? -1 : 0);
|
|
2203
|
+
return candidates[0] ?? null;
|
|
2204
|
+
}
|
|
2205
|
+
// =========================================
|
|
2206
|
+
// Name management (ario-arns)
|
|
2207
|
+
// =========================================
|
|
2208
|
+
/** Reassign an ArNS name to a different ANT. */
|
|
2209
|
+
async reassignName(params, _options) {
|
|
2210
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(params.name);
|
|
2211
|
+
const newAnt = address(params.processId);
|
|
2212
|
+
const [arnsRecord] = await getArnsRecordPDA(params.name, this.arnsProgram);
|
|
2213
|
+
const record = await this.getArNSRecord({ name: params.name });
|
|
2214
|
+
const antAsset = address(record.processId);
|
|
2215
|
+
const ix = await getReassignNameInstructionAsync(await this.withArnsDefaults({
|
|
2216
|
+
arnsRecord,
|
|
2217
|
+
antAsset,
|
|
2218
|
+
caller: this.signer,
|
|
2219
|
+
newAnt,
|
|
2220
|
+
}), { programAddress: this.arnsProgram });
|
|
2221
|
+
// Post-reassign the record points at `newAnt`. The bundled
|
|
2222
|
+
// `sync_attributes` MUST target `newAnt` — without the override, the
|
|
2223
|
+
// helper would read the on-chain record at SDK build time (still
|
|
2224
|
+
// pointing at the OLD asset), build a sync ix for the OLD asset, and
|
|
2225
|
+
// fail the post-reassign `record.ant == asset.key()` check. The
|
|
2226
|
+
// owner-check inside _buildSyncAttributesIxIfOwner runs against
|
|
2227
|
+
// `newAnt`, so the bundle fires only when the reassign caller is also
|
|
2228
|
+
// the new ANT's holder; otherwise the ix is sent alone and the new
|
|
2229
|
+
// owner runs `syncAttributes()` later (BD-095/096).
|
|
2230
|
+
const syncIx = await this._buildSyncAttributesIxIfOwner(params.name, newAnt);
|
|
2231
|
+
const reassignWithMetas = withRemainingAccounts(ix, [
|
|
2232
|
+
{ address: newAnt, role: AccountRole.READONLY },
|
|
2233
|
+
]);
|
|
2234
|
+
const sig = await this.sendTransaction(syncIx
|
|
2235
|
+
? [...migrateIxs, reassignWithMetas, syncIx]
|
|
2236
|
+
: [...migrateIxs, reassignWithMetas]);
|
|
2237
|
+
return { id: sig };
|
|
2238
|
+
}
|
|
2239
|
+
/** Release a permabuy name back to the registry (creates a returned name auction). */
|
|
2240
|
+
async releaseName(params, _options) {
|
|
2241
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(params.name);
|
|
2242
|
+
const [returnedNamePda] = await getReturnedNamePDA(params.name, this.arnsProgram);
|
|
2243
|
+
const [arnsRecord] = await getArnsRecordPDA(params.name, this.arnsProgram);
|
|
2244
|
+
const record = await this.getArNSRecord({ name: params.name });
|
|
2245
|
+
const antAsset = address(record.processId);
|
|
2246
|
+
const ix = await getReleaseNameInstructionAsync(await this.withArnsDefaults({
|
|
2247
|
+
arnsRecord,
|
|
2248
|
+
returnedName: returnedNamePda,
|
|
2249
|
+
antAsset,
|
|
2250
|
+
caller: this.signer,
|
|
2251
|
+
}), { programAddress: this.arnsProgram });
|
|
2252
|
+
// Note: no sync_attributes bundle here — release_name closes the
|
|
2253
|
+
// ArnsRecord PDA, so a follow-up sync would fail PDA validation. The
|
|
2254
|
+
// asset's stale traits remain pointing at the released name; off-chain
|
|
2255
|
+
// resolvers should treat ArnsRecord as the source of truth and ignore
|
|
2256
|
+
// a "ArNS Name" trait that no longer resolves.
|
|
2257
|
+
const sig = await this.sendTransaction([...migrateIxs, ix]);
|
|
2258
|
+
return { id: sig };
|
|
2259
|
+
}
|
|
2260
|
+
// =========================================
|
|
2261
|
+
// Lazy-state crank steps — materialize accumulator/period state.
|
|
2262
|
+
// Both are permissionless + idempotent (safe to crank on a schedule).
|
|
2263
|
+
// =========================================
|
|
2264
|
+
/**
|
|
2265
|
+
* Roll the demand factor forward to the current period. Permissionless and
|
|
2266
|
+
* idempotent — a no-op within the same period. Pricing already rolls the
|
|
2267
|
+
* factor inline on every buy/extend, so this only refreshes the STORED
|
|
2268
|
+
* factor that `getDemandFactor` and between-buy price previews read; a
|
|
2269
|
+
* periodic crank (~once per 24h `PERIOD_LENGTH_SECONDS`) keeps it current.
|
|
2270
|
+
*/
|
|
2271
|
+
async updateDemandFactor(_options) {
|
|
2272
|
+
const [demandFactorPda] = await getDemandFactorPDA(this.arnsProgram);
|
|
2273
|
+
const ix = getUpdateDemandFactorInstruction({ demandFactor: demandFactorPda, payer: this.signer }, { programAddress: this.arnsProgram });
|
|
2274
|
+
const sig = await this.sendTransaction([ix]);
|
|
2275
|
+
return { id: sig };
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Materialize a single delegate's pending rewards into their delegated
|
|
2279
|
+
* stake by settling the gateway's reward-per-share accumulator.
|
|
2280
|
+
* Permissionless — there is no signer beyond the fee payer; `delegator` is
|
|
2281
|
+
* only a PDA-derivation seed. Rewards always accrue correctly in the
|
|
2282
|
+
* accumulator regardless of this call; compounding makes the on-chain
|
|
2283
|
+
* `delegatedStake` reflect them (and earn compound interest in the next
|
|
2284
|
+
* epoch's weighting). Idempotent — a no-op once already settled.
|
|
2285
|
+
*/
|
|
2286
|
+
async compoundDelegationRewards(params, _options) {
|
|
2287
|
+
const ix = await this.buildCompoundDelegationRewardsInstruction(params);
|
|
2288
|
+
const sig = await this.sendTransaction([ix]);
|
|
2289
|
+
return { id: sig };
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Compound many delegates' rewards in a SINGLE transaction — one
|
|
2293
|
+
* `compound_delegation_rewards` instruction per entry. Idempotent and
|
|
2294
|
+
* permissionless, so partial batches are safe to retry. Keep each batch
|
|
2295
|
+
* within the per-tx account/CU budget; grouping entries that share a gateway
|
|
2296
|
+
* lowers the unique-account count (the gateway account is reused across
|
|
2297
|
+
* instructions). Typical cranker usage: enumerate with
|
|
2298
|
+
* `SolanaARIOReadable.getDelegationsToCompound`, chunk, then call this.
|
|
2299
|
+
*/
|
|
2300
|
+
async compoundDelegationRewardsBatch(delegations, _options) {
|
|
2301
|
+
if (delegations.length === 0) {
|
|
2302
|
+
throw new Error('compoundDelegationRewardsBatch: delegations list is empty');
|
|
2303
|
+
}
|
|
2304
|
+
const ixs = await Promise.all(delegations.map((d) => this.buildCompoundDelegationRewardsInstruction(d)));
|
|
2305
|
+
const sig = await this.sendTransaction(ixs, 1_400_000);
|
|
2306
|
+
return { id: sig };
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Build a single `compound_delegation_rewards` instruction (shared by the
|
|
2310
|
+
* single + batch methods). PDAs are derived under the configured gar program
|
|
2311
|
+
* so the program-id override always targets the right cluster.
|
|
2312
|
+
*/
|
|
2313
|
+
async buildCompoundDelegationRewardsInstruction(params) {
|
|
2314
|
+
const gateway = address(params.gateway);
|
|
2315
|
+
const delegator = address(params.delegator);
|
|
2316
|
+
const [gatewayPda] = await getGatewayPDA(gateway, this.garProgram);
|
|
2317
|
+
const [delegationPda] = await getDelegationPDA(gateway, delegator, this.garProgram);
|
|
2318
|
+
return getCompoundDelegationRewardsInstruction({ gateway: gatewayPda, delegation: delegationPda, delegator }, { programAddress: this.garProgram });
|
|
2319
|
+
}
|
|
2320
|
+
// =========================================
|
|
2321
|
+
// Epoch cranking (ario-gar) — permissionless
|
|
2322
|
+
// =========================================
|
|
2323
|
+
/**
|
|
2324
|
+
* Create a new epoch. Permissionless — anyone can call when the next
|
|
2325
|
+
* epoch's start time has arrived.
|
|
2326
|
+
*/
|
|
2327
|
+
async createEpoch(_options) {
|
|
2328
|
+
const garConfig = await this.getGarConfig();
|
|
2329
|
+
const [epochSettingsPda] = await getEpochSettingsPDA(this.garProgram);
|
|
2330
|
+
const settingsAccount = await fetchEncodedAccount(this.rpc, epochSettingsPda, { commitment: this.commitment });
|
|
2331
|
+
if (!settingsAccount.exists)
|
|
2332
|
+
throw new Error('EpochSettings not found');
|
|
2333
|
+
const settings = deserializeEpochSettingsFull(Buffer.from(settingsAccount.data));
|
|
2334
|
+
const epochIndex = settings.currentEpochIndex;
|
|
2335
|
+
const [epochPda] = await getEpochPDA(epochIndex, this.garProgram);
|
|
2336
|
+
const ix = await getCreateEpochInstructionAsync(await this.withGarDefaults({
|
|
2337
|
+
epoch: epochPda,
|
|
2338
|
+
protocolTokenAccount: garConfig.protocolTokenAccount,
|
|
2339
|
+
payer: this.signer,
|
|
2340
|
+
}), { programAddress: this.garProgram });
|
|
2341
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
2342
|
+
return { id: sig };
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Tally weights for a batch of gateways. Permissionless — call repeatedly
|
|
2346
|
+
* until all gateways are processed. Pass gateway PDAs as
|
|
2347
|
+
* `gatewayAccounts`; they're appended as `remaining_accounts`.
|
|
2348
|
+
*/
|
|
2349
|
+
async tallyWeights(params, _options) {
|
|
2350
|
+
const ix = await getTallyWeightsInstructionAsync(await this.withGarDefaults({
|
|
2351
|
+
payer: this.signer,
|
|
2352
|
+
epochIndex: BigInt(params.epochIndex),
|
|
2353
|
+
}), { programAddress: this.garProgram });
|
|
2354
|
+
const remaining = params.gatewayAccounts.map((address) => ({
|
|
2355
|
+
address,
|
|
2356
|
+
role: AccountRole.WRITABLE,
|
|
2357
|
+
}));
|
|
2358
|
+
const sig = await this.sendTransaction([withRemainingAccounts(ix, remaining)], 1_000_000);
|
|
2359
|
+
return { id: sig };
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Prescribe observers and names for an epoch. Permissionless — call after
|
|
2363
|
+
* weights are tallied.
|
|
2364
|
+
*
|
|
2365
|
+
* `gatewayAccounts` MUST be the Gateway PDAs of the SELECTED observers only
|
|
2366
|
+
* — at most `epoch_settings.prescribed_observer_count` (≤50), NOT the whole
|
|
2367
|
+
* registry. The selection is computed on-chain; mirror it off-chain with
|
|
2368
|
+
* {@link predictPrescribedObservers} / {@link getPredictedObserverPDAs} to
|
|
2369
|
+
* learn the set. Passing every registry gateway (e.g. via
|
|
2370
|
+
* {@link getAllRegistryGatewayPDAs}) hits Solana's `MAX_TX_ACCOUNT_LOCKS = 64`
|
|
2371
|
+
* on large registries and the tx fails at pre-flight.
|
|
2372
|
+
*
|
|
2373
|
+
* The selected PDAs are appended as `remaining_accounts`, followed by the
|
|
2374
|
+
* optional `nameRegistryAccount` (must be LAST) which enables the name
|
|
2375
|
+
* prescription leg.
|
|
2376
|
+
*
|
|
2377
|
+
* If a selected gateway leaves between prediction and tx landing, the tx
|
|
2378
|
+
* fails with `InvalidGatewayAccount` — retry once with a fresh prediction.
|
|
2379
|
+
*/
|
|
2380
|
+
async prescribeEpoch(params, _options) {
|
|
2381
|
+
const ix = await getPrescribeEpochInstructionAsync(await this.withGarDefaults({
|
|
2382
|
+
payer: this.signer,
|
|
2383
|
+
epochIndex: BigInt(params.epochIndex),
|
|
2384
|
+
}), { programAddress: this.garProgram });
|
|
2385
|
+
const remaining = params.gatewayAccounts.map((address) => ({
|
|
2386
|
+
address,
|
|
2387
|
+
role: AccountRole.READONLY,
|
|
2388
|
+
}));
|
|
2389
|
+
if (params.nameRegistryAccount) {
|
|
2390
|
+
remaining.push({
|
|
2391
|
+
address: params.nameRegistryAccount,
|
|
2392
|
+
role: AccountRole.READONLY,
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
const fullIx = withRemainingAccounts(ix, remaining);
|
|
2396
|
+
// A prescribe tx with the selected observer set (~50 PDAs) exceeds Solana's
|
|
2397
|
+
// 1232-byte limit once there are more than ~24 remaining accounts, so route
|
|
2398
|
+
// those through an ephemeral Address Lookup Table (create → extend →
|
|
2399
|
+
// compressed v0 tx). Small sets (sparse testnets) take the cheaper inline
|
|
2400
|
+
// path. `prescribe_epoch` searches `remaining_accounts` by PDA, so serving
|
|
2401
|
+
// them via the ALT (which preserves instruction account order) is
|
|
2402
|
+
// transparent — incl. NameRegistry staying last. Validated on staging
|
|
2403
|
+
// (667 gateways, 50 observers): 428k CU, name prescription intact.
|
|
2404
|
+
if (remaining.length > 24) {
|
|
2405
|
+
const id = await sendWithEphemeralLookupTable({
|
|
2406
|
+
rpc: this.rpc,
|
|
2407
|
+
rpcSubscriptions: this.rpcSubscriptions,
|
|
2408
|
+
signer: this.signer,
|
|
2409
|
+
instruction: fullIx,
|
|
2410
|
+
lookupAddresses: remaining.map((a) => a.address),
|
|
2411
|
+
commitment: this.commitment,
|
|
2412
|
+
computeUnitLimit: 1_000_000,
|
|
2413
|
+
});
|
|
2414
|
+
return { id };
|
|
2415
|
+
}
|
|
2416
|
+
const sig = await this.sendTransaction([fullIx], 1_000_000);
|
|
2417
|
+
return { id: sig };
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Distribute rewards for a completed epoch in batches. Permissionless —
|
|
2421
|
+
* call after epoch ends. Gateway PDAs appended as `remaining_accounts`.
|
|
2422
|
+
*/
|
|
2423
|
+
async distributeEpoch(params, _options) {
|
|
2424
|
+
const garConfig = await this.getGarConfig();
|
|
2425
|
+
// ario_gar::distribute_epoch CPIs into ario_core::release_treasury_to_recipient
|
|
2426
|
+
// (signed by the ArioConfig PDA — the canonical treasury authority). The
|
|
2427
|
+
// generated builder expects `arioConfig` + `arioCoreProgram` accounts at
|
|
2428
|
+
// positions 6+7 (post-PR-19 in ar-io-solana-contracts). Pin both to the
|
|
2429
|
+
// configured core program so devnet/testnet deployments don't fall back
|
|
2430
|
+
// to the bundled mainnet default.
|
|
2431
|
+
const [arioConfig] = await getArioConfigPDA(this.coreProgram);
|
|
2432
|
+
const ix = await getDistributeEpochInstructionAsync(await this.withGarDefaults({
|
|
2433
|
+
protocolTokenAccount: garConfig.protocolTokenAccount,
|
|
2434
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
2435
|
+
arioConfig,
|
|
2436
|
+
arioCoreProgram: this.coreProgram,
|
|
2437
|
+
payer: this.signer,
|
|
2438
|
+
epochIndex: BigInt(params.epochIndex),
|
|
2439
|
+
}), { programAddress: this.garProgram });
|
|
2440
|
+
const remaining = params.gatewayAccounts.map((address) => ({
|
|
2441
|
+
address,
|
|
2442
|
+
role: AccountRole.WRITABLE,
|
|
2443
|
+
}));
|
|
2444
|
+
const sig = await this.sendTransaction([withRemainingAccounts(ix, remaining)], 1_000_000);
|
|
2445
|
+
return { id: sig };
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Close an old epoch account and reclaim rent. Permissionless — call after
|
|
2449
|
+
* the epoch is distributed and at least 7 epochs have passed.
|
|
2450
|
+
*/
|
|
2451
|
+
async closeEpoch(params, _options) {
|
|
2452
|
+
const ix = await getCloseEpochInstructionAsync(await this.withGarDefaults({
|
|
2453
|
+
payer: this.signer,
|
|
2454
|
+
epochIndex: BigInt(params.epochIndex),
|
|
2455
|
+
}), { programAddress: this.garProgram });
|
|
2456
|
+
const sig = await this.sendTransaction([ix]);
|
|
2457
|
+
return { id: sig };
|
|
2458
|
+
}
|
|
2459
|
+
// =========================================
|
|
2460
|
+
// Cranker helpers
|
|
2461
|
+
// =========================================
|
|
2462
|
+
/**
|
|
2463
|
+
* Get gateway PDAs for a batch starting at registryIndex.
|
|
2464
|
+
* Reads the GatewayRegistry and derives PDAs for the next `batchSize`
|
|
2465
|
+
* active gateways.
|
|
2466
|
+
*/
|
|
2467
|
+
async getRegistryGatewayPDAs(startIndex, batchSize) {
|
|
2468
|
+
const [registryPda] = await getGatewayRegistryPDA(this.garProgram);
|
|
2469
|
+
const registryAccount = await fetchEncodedAccount(this.rpc, registryPda, {
|
|
2470
|
+
commitment: this.commitment,
|
|
2471
|
+
});
|
|
2472
|
+
if (!registryAccount.exists)
|
|
2473
|
+
return [];
|
|
2474
|
+
const registryData = Buffer.from(registryAccount.data);
|
|
2475
|
+
const count = registryData.readUInt32LE(40); // 8 disc + 32 authority
|
|
2476
|
+
const slotsOffset = 48; // 8 + 32 + 4 + 4
|
|
2477
|
+
// GatewaySlot: address(32) + composite_weight(8) + start_timestamp(8)
|
|
2478
|
+
// + status(1) + _padding(7) = 56 bytes.
|
|
2479
|
+
const SLOT_STRIDE = 56;
|
|
2480
|
+
const pdas = [];
|
|
2481
|
+
const end = Math.min(startIndex + batchSize, count);
|
|
2482
|
+
const zero = '11111111111111111111111111111111';
|
|
2483
|
+
for (let i = startIndex; i < end && i < 3000; i++) {
|
|
2484
|
+
const slotOffset = slotsOffset + i * SLOT_STRIDE;
|
|
2485
|
+
const addr = addressDecoder.decode(registryData.subarray(slotOffset, slotOffset + 32));
|
|
2486
|
+
if (addr === zero)
|
|
2487
|
+
continue;
|
|
2488
|
+
const [gatewayPda] = await getGatewayPDA(addr, this.garProgram);
|
|
2489
|
+
pdas.push(gatewayPda);
|
|
2490
|
+
}
|
|
2491
|
+
return pdas;
|
|
2492
|
+
}
|
|
2493
|
+
/** Get ALL active gateway PDAs from the registry. */
|
|
2494
|
+
async getAllRegistryGatewayPDAs() {
|
|
2495
|
+
const [registryPda] = await getGatewayRegistryPDA(this.garProgram);
|
|
2496
|
+
const registryAccount = await fetchEncodedAccount(this.rpc, registryPda, {
|
|
2497
|
+
commitment: this.commitment,
|
|
2498
|
+
});
|
|
2499
|
+
if (!registryAccount.exists)
|
|
2500
|
+
return [];
|
|
2501
|
+
const registryData = Buffer.from(registryAccount.data);
|
|
2502
|
+
const count = registryData.readUInt32LE(40);
|
|
2503
|
+
const slotsOffset = 48;
|
|
2504
|
+
const SLOT_STRIDE = 56;
|
|
2505
|
+
const pdas = [];
|
|
2506
|
+
const zero = '11111111111111111111111111111111';
|
|
2507
|
+
for (let i = 0; i < count && i < 3000; i++) {
|
|
2508
|
+
const slotOffset = slotsOffset + i * SLOT_STRIDE;
|
|
2509
|
+
const addr = addressDecoder.decode(registryData.subarray(slotOffset, slotOffset + 32));
|
|
2510
|
+
if (addr === zero)
|
|
2511
|
+
continue;
|
|
2512
|
+
const [gatewayPda] = await getGatewayPDA(addr, this.garProgram);
|
|
2513
|
+
pdas.push(gatewayPda);
|
|
2514
|
+
}
|
|
2515
|
+
return pdas;
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Predict the Gateway PDAs that `prescribe_epoch` will select as observers
|
|
2519
|
+
* for `epochIndex`, mirroring the on-chain weighted-roulette selection.
|
|
2520
|
+
*
|
|
2521
|
+
* Returns at most `epoch_settings.prescribed_observer_count` (≤50) PDAs
|
|
2522
|
+
* regardless of registry size — the set to pass as `gatewayAccounts` to
|
|
2523
|
+
* {@link prescribeEpoch}. This is the size-safe replacement for
|
|
2524
|
+
* {@link getAllRegistryGatewayPDAs} on the prescribe path (which oversupplies
|
|
2525
|
+
* and trips `MAX_TX_ACCOUNT_LOCKS = 64` on large registries).
|
|
2526
|
+
*
|
|
2527
|
+
* Reads three accounts (epoch, registry, epoch settings) at the configured
|
|
2528
|
+
* commitment so the prediction reflects live registry weights. If a selected
|
|
2529
|
+
* gateway races out before the tx lands, `prescribeEpoch` throws
|
|
2530
|
+
* `InvalidGatewayAccount` — re-call this and retry once.
|
|
2531
|
+
*/
|
|
2532
|
+
async getPredictedObserverPDAs(epochIndex) {
|
|
2533
|
+
// --- Epoch: hashchain (frozen entropy) + active_gateway_count (walk bound) ---
|
|
2534
|
+
const [epochPda] = await getEpochPDA(epochIndex, this.garProgram);
|
|
2535
|
+
const epochAccount = await fetchEncodedAccount(this.rpc, epochPda, {
|
|
2536
|
+
commitment: this.commitment,
|
|
2537
|
+
});
|
|
2538
|
+
if (!epochAccount.exists)
|
|
2539
|
+
throw new Error(`Epoch ${epochIndex} not found`);
|
|
2540
|
+
const epochData = Buffer.from(epochAccount.data);
|
|
2541
|
+
// After the 8-byte discriminator (see fetchEpochRawFields): 9×u64 = 72
|
|
2542
|
+
// bytes, then hashchain[32], then active_gateway_count(u32).
|
|
2543
|
+
const EPOCH_BASE = 8;
|
|
2544
|
+
const hashchain = epochData.subarray(EPOCH_BASE + 72, EPOCH_BASE + 72 + 32);
|
|
2545
|
+
const activeGatewayCount = epochData.readUInt32LE(EPOCH_BASE + 104);
|
|
2546
|
+
// --- Registry: slots[0..activeGatewayCount] (address + composite_weight) ---
|
|
2547
|
+
const [registryPda] = await getGatewayRegistryPDA(this.garProgram);
|
|
2548
|
+
const registryAccount = await fetchEncodedAccount(this.rpc, registryPda, {
|
|
2549
|
+
commitment: this.commitment,
|
|
2550
|
+
});
|
|
2551
|
+
if (!registryAccount.exists)
|
|
2552
|
+
throw new Error('GatewayRegistry not found');
|
|
2553
|
+
const registryData = Buffer.from(registryAccount.data);
|
|
2554
|
+
const registryCount = registryData.readUInt32LE(40); // 8 disc + 32 authority
|
|
2555
|
+
const SLOTS_OFFSET = 48; // 8 + 32 + 4 count + 4 pad
|
|
2556
|
+
const SLOT_STRIDE = 56; // address(32)+weight(8)+start_ts(8)+status(1)+pad(7)
|
|
2557
|
+
// Walk exactly the on-chain prefix. The roulette uses
|
|
2558
|
+
// registry.gateways[0..epoch.active_gateway_count]; include zero-weight
|
|
2559
|
+
// slots so the cumulative walk and weight sum match byte-for-byte.
|
|
2560
|
+
const walkCount = Math.min(activeGatewayCount, registryCount, 3000);
|
|
2561
|
+
const slots = [];
|
|
2562
|
+
for (let i = 0; i < walkCount; i++) {
|
|
2563
|
+
const slotOffset = SLOTS_OFFSET + i * SLOT_STRIDE;
|
|
2564
|
+
slots.push({
|
|
2565
|
+
address: addressDecoder.decode(registryData.subarray(slotOffset, slotOffset + 32)),
|
|
2566
|
+
compositeWeight: registryData.readBigUInt64LE(slotOffset + 32),
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
// --- Epoch settings: prescribed_observer_count ---
|
|
2570
|
+
const [epochSettingsPda] = await getEpochSettingsPDA(this.garProgram);
|
|
2571
|
+
const settingsAccount = await fetchEncodedAccount(this.rpc, epochSettingsPda, {
|
|
2572
|
+
commitment: this.commitment,
|
|
2573
|
+
});
|
|
2574
|
+
if (!settingsAccount.exists)
|
|
2575
|
+
throw new Error('EpochSettings not found');
|
|
2576
|
+
const settings = deserializeEpochSettingsFull(Buffer.from(settingsAccount.data));
|
|
2577
|
+
// --- Predict selected operators, then derive their Gateway PDAs ---
|
|
2578
|
+
const operators = predictPrescribedObservers(hashchain, slots, settings.prescribedObserverCount);
|
|
2579
|
+
const pdas = [];
|
|
2580
|
+
for (const operator of operators) {
|
|
2581
|
+
const [gatewayPda] = await getGatewayPDA(operator, this.garProgram);
|
|
2582
|
+
pdas.push(gatewayPda);
|
|
2583
|
+
}
|
|
2584
|
+
return pdas;
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Reclaim rent from the ephemeral Address Lookup Tables this signer created
|
|
2588
|
+
* for `prescribe_epoch` (see {@link sendWithEphemeralLookupTable}). Each
|
|
2589
|
+
* prescribe leaves a single-use table allocated (~0.0126 SOL); reclaiming
|
|
2590
|
+
* needs a deactivate → ~513-slot cooldown → close sequence, so it can't run
|
|
2591
|
+
* inline. Call this from a throttled/permissionless cleanup pass (cranker /
|
|
2592
|
+
* observer) to deactivate active tables and close cooled-down ones, refunding
|
|
2593
|
+
* the rent to the signer.
|
|
2594
|
+
*
|
|
2595
|
+
* Discovery reads the signer's transaction history (RPC-portable; the ALT
|
|
2596
|
+
* program can't be enumerated via `getProgramAccounts`). The GAR + ArNS
|
|
2597
|
+
* program IDs are passed as the entry-ownership fingerprint so only genuine
|
|
2598
|
+
* prescribe tables are touched. Best-effort: at most `maxTables` submissions
|
|
2599
|
+
* per call, scanning at most `scanLimit` recent signatures.
|
|
2600
|
+
*/
|
|
2601
|
+
async reclaimLookupTableRent(opts) {
|
|
2602
|
+
return reclaimLookupTablesForSigner({
|
|
2603
|
+
rpc: this.rpc,
|
|
2604
|
+
rpcSubscriptions: this.rpcSubscriptions,
|
|
2605
|
+
signer: this.signer,
|
|
2606
|
+
allowedEntryOwners: [this.garProgram, this.arnsProgram],
|
|
2607
|
+
commitment: this.commitment,
|
|
2608
|
+
maxTables: opts?.maxTables,
|
|
2609
|
+
scanLimit: opts?.scanLimit,
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
/** Read and deserialize the full EpochSettings account. */
|
|
2613
|
+
async getEpochSettingsFull() {
|
|
2614
|
+
const [esPda] = await getEpochSettingsPDA(this.garProgram);
|
|
2615
|
+
const account = await fetchEncodedAccount(this.rpc, esPda, {
|
|
2616
|
+
commitment: this.commitment,
|
|
2617
|
+
});
|
|
2618
|
+
if (!account.exists)
|
|
2619
|
+
throw new Error('EpochSettings not found');
|
|
2620
|
+
return deserializeEpochSettingsFull(Buffer.from(account.data));
|
|
2621
|
+
}
|
|
2622
|
+
/**
|
|
2623
|
+
* Submit `prescribe_epoch` using the off-chain-predicted observer set, with a
|
|
2624
|
+
* single re-predict-and-retry on `InvalidGatewayAccount` (covers a gateway
|
|
2625
|
+
* leaving the registry between the prediction read and the tx landing).
|
|
2626
|
+
*/
|
|
2627
|
+
async prescribeWithPrediction(epochIndex, nameRegistryAccount) {
|
|
2628
|
+
const submit = async () => this.prescribeEpoch({
|
|
2629
|
+
epochIndex,
|
|
2630
|
+
gatewayAccounts: await this.getPredictedObserverPDAs(epochIndex),
|
|
2631
|
+
nameRegistryAccount,
|
|
2632
|
+
});
|
|
2633
|
+
try {
|
|
2634
|
+
return await submit();
|
|
2635
|
+
}
|
|
2636
|
+
catch (err) {
|
|
2637
|
+
if (!isInvalidGatewayAccountError(err))
|
|
2638
|
+
throw err;
|
|
2639
|
+
return submit();
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Advance the epoch lifecycle by ONE on-chain action and return what it did.
|
|
2644
|
+
*
|
|
2645
|
+
* Stateless and idempotent: it reads `EpochSettings` + the current `Epoch`,
|
|
2646
|
+
* determines the single next required step
|
|
2647
|
+
* (`create` → `tally` → `prescribe` → `distribute` → `close`), submits it,
|
|
2648
|
+
* and returns a {@link CrankEpochStepResult}. Call it repeatedly on your own
|
|
2649
|
+
* schedule — it owns *which* on-chain action is correct and *which accounts*
|
|
2650
|
+
* it needs; you own scheduling, logging, error classification, and any
|
|
2651
|
+
* permissionless cleanup.
|
|
2652
|
+
*
|
|
2653
|
+
* Crucially, the `prescribe` leg uses {@link getPredictedObserverPDAs} (only
|
|
2654
|
+
* the ~`prescribed_observer_count` selected Gateway PDAs), so it never trips
|
|
2655
|
+
* `MAX_TX_ACCOUNT_LOCKS = 64` on large registries — and it re-predicts and
|
|
2656
|
+
* retries once on `InvalidGatewayAccount`.
|
|
2657
|
+
*
|
|
2658
|
+
* Errors propagate to the caller (classify/retry as you see fit); the only
|
|
2659
|
+
* internally-handled error is the prescribe `InvalidGatewayAccount` retry.
|
|
2660
|
+
*/
|
|
2661
|
+
async crankEpochStep(opts = {}) {
|
|
2662
|
+
// tally_weights / distribute_epoch append the batch's Gateway PDAs as
|
|
2663
|
+
// remaining_accounts. distribute also CPIs into ario-core (treasury
|
|
2664
|
+
// release) so it carries 10 named accounts; with ~18+ gateway PDAs on top
|
|
2665
|
+
// the tx exceeds Solana's 1232-byte limit. Cap the lifecycle batch at 18 so
|
|
2666
|
+
// an oversized caller `batchSize` can't produce an unsendable tx (verified:
|
|
2667
|
+
// 30 gateways → 1527B; 18 → ~1050B). prescribe is the exception — it needs
|
|
2668
|
+
// ALL selected observers in one tx, so it uses an ALT instead (see
|
|
2669
|
+
// prescribeEpoch).
|
|
2670
|
+
const MAX_LIFECYCLE_BATCH = 18;
|
|
2671
|
+
const batchSize = Math.min(opts.batchSize ?? MAX_LIFECYCLE_BATCH, MAX_LIFECYCLE_BATCH);
|
|
2672
|
+
const enableClose = opts.enableClose ?? true;
|
|
2673
|
+
const retention = opts.epochRetention ?? 7;
|
|
2674
|
+
const enableCompound = opts.enableCompound ?? true;
|
|
2675
|
+
const compoundMinPending = opts.compoundMinPendingRewards ?? 0;
|
|
2676
|
+
const enableDemandFactorRoll = opts.enableDemandFactorRoll ?? true;
|
|
2677
|
+
const enablePrune = opts.enablePrune ?? true;
|
|
2678
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
2679
|
+
const settings = await this.getEpochSettingsFull();
|
|
2680
|
+
if (!settings.enabled)
|
|
2681
|
+
return { action: 'idle', reason: 'epochs_disabled' };
|
|
2682
|
+
const currentIndex = settings.currentEpochIndex;
|
|
2683
|
+
// currentIndex is the NEXT epoch to create; the live one is currentIndex-1.
|
|
2684
|
+
const targetEpochIndex = currentIndex > 0 ? currentIndex - 1 : 0;
|
|
2685
|
+
const nextEpochStart = settings.genesisTimestamp + currentIndex * settings.epochDuration;
|
|
2686
|
+
const epoch = await this.getEpochRaw(targetEpochIndex);
|
|
2687
|
+
// Cold start: the live epoch (targetEpochIndex) doesn't exist yet. Two cases,
|
|
2688
|
+
// handled identically — `create_epoch` always creates epoch[currentIndex] and
|
|
2689
|
+
// then increments the counter, so the NEXT crank finds it live at currentIndex-1:
|
|
2690
|
+
// 1. Genesis: currentIndex === 0 → create epoch 0.
|
|
2691
|
+
// 2. AO→Solana continuity cold start: `admin_set_current_epoch_index` jumped
|
|
2692
|
+
// currentIndex to e.g. 454 with NO prior epochs on-chain (the AO-side
|
|
2693
|
+
// epochs were never created on Solana). targetEpochIndex (453) points at
|
|
2694
|
+
// an epoch that will never exist, so the old `currentIndex === 0`-only
|
|
2695
|
+
// bootstrap deadlocked here ('waiting_for_epoch' forever). Create
|
|
2696
|
+
// epoch[currentIndex] (454) directly — its start was re-anchored to ≈now.
|
|
2697
|
+
if (!epoch) {
|
|
2698
|
+
if (now < nextEpochStart)
|
|
2699
|
+
return {
|
|
2700
|
+
action: 'idle',
|
|
2701
|
+
reason: currentIndex === 0 ? 'waiting_for_genesis' : 'waiting_for_epoch',
|
|
2702
|
+
};
|
|
2703
|
+
const { id } = await this.createEpoch();
|
|
2704
|
+
return { action: 'create', epochIndex: currentIndex, txId: id };
|
|
2705
|
+
}
|
|
2706
|
+
// Tally (batched). activeGatewayCount===0 still needs one tx to flip the flag.
|
|
2707
|
+
if (epoch.weightsTallied === 0) {
|
|
2708
|
+
const gatewayAccounts = epoch.activeGatewayCount > 0
|
|
2709
|
+
? await this.getRegistryGatewayPDAs(epoch.tallyIndex, batchSize)
|
|
2710
|
+
: [];
|
|
2711
|
+
const { id } = await this.tallyWeights({
|
|
2712
|
+
epochIndex: targetEpochIndex,
|
|
2713
|
+
gatewayAccounts,
|
|
2714
|
+
});
|
|
2715
|
+
return {
|
|
2716
|
+
action: 'tally',
|
|
2717
|
+
epochIndex: targetEpochIndex,
|
|
2718
|
+
txId: id,
|
|
2719
|
+
progress: { index: epoch.tallyIndex, total: epoch.activeGatewayCount },
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
// Prescribe (predicted observers only — the size-safe path).
|
|
2723
|
+
if (epoch.prescriptionsDone === 0) {
|
|
2724
|
+
const nameRegistryAccount = opts.nameRegistryAccount === null
|
|
2725
|
+
? undefined
|
|
2726
|
+
: (opts.nameRegistryAccount ??
|
|
2727
|
+
(await getArnsRegistryPDA(this.arnsProgram))[0]);
|
|
2728
|
+
const { id } = await this.prescribeWithPrediction(targetEpochIndex, nameRegistryAccount);
|
|
2729
|
+
return { action: 'prescribe', epochIndex: targetEpochIndex, txId: id };
|
|
2730
|
+
}
|
|
2731
|
+
// Observations happen while the epoch is live. This is the dominant idle
|
|
2732
|
+
// window — fold returned-name pruning in here so it isn't starved on
|
|
2733
|
+
// clusters whose epochs park here (e.g. imported gateways that can't
|
|
2734
|
+
// observe → distribution never advances → the post-distribution tail below
|
|
2735
|
+
// is never reached).
|
|
2736
|
+
if (now < epoch.endTimestamp) {
|
|
2737
|
+
if (enablePrune) {
|
|
2738
|
+
const pruned = await this.maybePruneReturnedNamesStep(opts, now);
|
|
2739
|
+
if (pruned)
|
|
2740
|
+
return pruned;
|
|
2741
|
+
}
|
|
2742
|
+
return { action: 'idle', reason: 'waiting_for_observations' };
|
|
2743
|
+
}
|
|
2744
|
+
// Distribute (batched).
|
|
2745
|
+
if (epoch.rewardsDistributed === 0) {
|
|
2746
|
+
const gatewayAccounts = epoch.activeGatewayCount > 0
|
|
2747
|
+
? await this.getRegistryGatewayPDAs(epoch.distributionIndex, batchSize)
|
|
2748
|
+
: [];
|
|
2749
|
+
const { id } = await this.distributeEpoch({
|
|
2750
|
+
epochIndex: targetEpochIndex,
|
|
2751
|
+
gatewayAccounts,
|
|
2752
|
+
});
|
|
2753
|
+
return {
|
|
2754
|
+
action: 'distribute',
|
|
2755
|
+
epochIndex: targetEpochIndex,
|
|
2756
|
+
txId: id,
|
|
2757
|
+
progress: {
|
|
2758
|
+
index: epoch.distributionIndex,
|
|
2759
|
+
total: epoch.activeGatewayCount,
|
|
2760
|
+
},
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
// Close a fully-distributed epoch past retention (GAR-006).
|
|
2764
|
+
//
|
|
2765
|
+
// CRITICAL: this is cleanup of OLD epochs and must NEVER block creation of
|
|
2766
|
+
// NEW ones. `close_epoch` reverts with EpochObservationsNotClosed until the
|
|
2767
|
+
// epoch's Observation PDAs are closed (observations_closed ==
|
|
2768
|
+
// observations_submitted), so we close those FIRST. The whole branch is
|
|
2769
|
+
// wrapped so any failure falls through to create-next instead of throwing
|
|
2770
|
+
// out of crankEpochStep — otherwise a single un-closeable epoch wedges epoch
|
|
2771
|
+
// progression network-wide (every operator's crank hits the same wall).
|
|
2772
|
+
if (enableClose && targetEpochIndex >= retention) {
|
|
2773
|
+
const closeTarget = targetEpochIndex - retention;
|
|
2774
|
+
try {
|
|
2775
|
+
const old = await this.getEpochRaw(closeTarget);
|
|
2776
|
+
if (old && old.rewardsDistributed === 1) {
|
|
2777
|
+
if (old.observationsSubmitted > old.observationsClosed) {
|
|
2778
|
+
// Open observations remain — close a batch before close_epoch.
|
|
2779
|
+
const observers = await this.getEpochObservers(closeTarget);
|
|
2780
|
+
if (observers.length > 0) {
|
|
2781
|
+
const batch = observers.slice(0, MAX_CLOSE_OBSERVATION_BATCH);
|
|
2782
|
+
const { id } = await this.closeObservations({
|
|
2783
|
+
epochIndex: closeTarget,
|
|
2784
|
+
observers: batch,
|
|
2785
|
+
});
|
|
2786
|
+
return {
|
|
2787
|
+
action: 'close_observation',
|
|
2788
|
+
epochIndex: closeTarget,
|
|
2789
|
+
txId: id,
|
|
2790
|
+
progress: { index: batch.length, total: observers.length },
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
// Counter says open but no Observation PDA exists (orphaned counter):
|
|
2794
|
+
// can't close it, so don't attempt close_epoch (it would revert) and
|
|
2795
|
+
// don't wedge — fall through to create-next.
|
|
2796
|
+
}
|
|
2797
|
+
else {
|
|
2798
|
+
const { id } = await this.closeEpoch({ epochIndex: closeTarget });
|
|
2799
|
+
return { action: 'close', epochIndex: closeTarget, txId: id };
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
catch {
|
|
2804
|
+
// Best-effort cleanup — never block epoch creation. Retried next tick.
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
// Lazy-state maintenance — lower urgency than the lifecycle, reached only
|
|
2808
|
+
// once the live epoch is fully distributed (rewardsDistributed === 1 here).
|
|
2809
|
+
// Compound FIRST so delegated stake reflects the just-distributed rewards
|
|
2810
|
+
// before the next epoch's tally weights it; then roll the demand factor if
|
|
2811
|
+
// its period elapsed. Both are permissionless + idempotent, and run BEFORE
|
|
2812
|
+
// creating the next epoch so the compounded stake is in place for its tally.
|
|
2813
|
+
if (enableCompound) {
|
|
2814
|
+
const compounded = await this.maybeCompoundStep(compoundMinPending);
|
|
2815
|
+
if (compounded)
|
|
2816
|
+
return compounded;
|
|
2817
|
+
}
|
|
2818
|
+
if (enableDemandFactorRoll) {
|
|
2819
|
+
const rolled = await this.maybeRollDemandFactorStep(now);
|
|
2820
|
+
if (rolled)
|
|
2821
|
+
return rolled;
|
|
2822
|
+
}
|
|
2823
|
+
if (enablePrune) {
|
|
2824
|
+
const pruned = await this.maybePruneReturnedNamesStep(opts, now);
|
|
2825
|
+
if (pruned)
|
|
2826
|
+
return pruned;
|
|
2827
|
+
}
|
|
2828
|
+
// Current epoch fully processed — create the next once its start arrives.
|
|
2829
|
+
if (now >= nextEpochStart) {
|
|
2830
|
+
const { id } = await this.createEpoch();
|
|
2831
|
+
return { action: 'create', epochIndex: currentIndex, txId: id };
|
|
2832
|
+
}
|
|
2833
|
+
return { action: 'idle', reason: 'epoch_complete' };
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* One compound batch over delegations with pending rewards (≤
|
|
2837
|
+
* {@link MAX_COMPOUND_BATCH} per tx), or `null` when none are due. Settling
|
|
2838
|
+
* is idempotent, so this converges over a few crank steps then no-ops until
|
|
2839
|
+
* the next epoch's distribution advances the accumulator again.
|
|
2840
|
+
*/
|
|
2841
|
+
async maybeCompoundStep(minPendingRewards) {
|
|
2842
|
+
const pending = await this.getDelegationsToCompound({ minPendingRewards });
|
|
2843
|
+
if (pending.length === 0)
|
|
2844
|
+
return null;
|
|
2845
|
+
const batch = pending.slice(0, MAX_COMPOUND_BATCH).map((p) => ({
|
|
2846
|
+
gateway: p.gatewayAddress,
|
|
2847
|
+
delegator: p.delegatorAddress,
|
|
2848
|
+
}));
|
|
2849
|
+
const { id } = await this.compoundDelegationRewardsBatch(batch);
|
|
2850
|
+
return {
|
|
2851
|
+
action: 'compound',
|
|
2852
|
+
txId: id,
|
|
2853
|
+
progress: { index: batch.length, total: pending.length },
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Roll the demand factor forward if its (wall-clock) period elapsed since the
|
|
2858
|
+
* last stored roll, else `null`. Mirrors the on-chain period math; the roll
|
|
2859
|
+
* itself is idempotent.
|
|
2860
|
+
*/
|
|
2861
|
+
async maybeRollDemandFactorStep(now) {
|
|
2862
|
+
const state = await this.getDemandFactorPeriodState();
|
|
2863
|
+
if (!state)
|
|
2864
|
+
return null;
|
|
2865
|
+
const elapsed = now - state.periodZeroStartTimestamp;
|
|
2866
|
+
const periodForNow = elapsed < 0 ? 1 : Math.floor(elapsed / DEMAND_FACTOR_PERIOD_SECONDS) + 1;
|
|
2867
|
+
if (periodForNow <= state.currentPeriod)
|
|
2868
|
+
return null; // same period — no-op
|
|
2869
|
+
const { id } = await this.updateDemandFactor();
|
|
2870
|
+
return { action: 'update_demand_factor', txId: id };
|
|
2871
|
+
}
|
|
2872
|
+
/** Wall-clock (ms) of the last returned-name prune scan; throttles the
|
|
2873
|
+
* getProgramAccounts scan below the crank poll rate. */
|
|
2874
|
+
lastReturnedNamePruneScanMs = 0;
|
|
2875
|
+
/**
|
|
2876
|
+
* One prune batch over ReturnedName PDAs whose 14-day auction window has
|
|
2877
|
+
* elapsed (≤ {@link CrankEpochStepOptions.pruneBatchSize} per tx), or `null`
|
|
2878
|
+
* when none are due / the scan is throttled. Scans the PDAs directly — it does
|
|
2879
|
+
* NOT gate on `config.next_returned_names_prune_timestamp`, which is never set
|
|
2880
|
+
* for imported returned names and would strand them. The contract re-checks
|
|
2881
|
+
* each account's window, so a slightly-skewed client clock is safe.
|
|
2882
|
+
*/
|
|
2883
|
+
async maybePruneReturnedNamesStep(opts, now) {
|
|
2884
|
+
const scanInterval = opts.pruneScanIntervalMs ?? 60_000;
|
|
2885
|
+
const wallNow = Date.now();
|
|
2886
|
+
if (wallNow - this.lastReturnedNamePruneScanMs < scanInterval)
|
|
2887
|
+
return null;
|
|
2888
|
+
this.lastReturnedNamePruneScanMs = wallNow;
|
|
2889
|
+
const expired = await this.getExpiredReturnedNames(now);
|
|
2890
|
+
if (expired.length === 0)
|
|
2891
|
+
return null;
|
|
2892
|
+
const batchSize = opts.pruneBatchSize ?? 15;
|
|
2893
|
+
const batch = expired.slice(0, batchSize).map((r) => r.pubkey);
|
|
2894
|
+
const { id } = await this.pruneReturnedNames({
|
|
2895
|
+
maxNames: batch.length,
|
|
2896
|
+
returnedNames: batch,
|
|
2897
|
+
});
|
|
2898
|
+
return {
|
|
2899
|
+
action: 'prune_returned_names',
|
|
2900
|
+
txId: id,
|
|
2901
|
+
progress: { index: batch.length, total: expired.length },
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* The DemandFactor account's stored period + period-zero start (seconds) —
|
|
2906
|
+
* the gate for {@link maybeRollDemandFactorStep}. `null` if the account
|
|
2907
|
+
* doesn't exist (pre-genesis).
|
|
2908
|
+
*/
|
|
2909
|
+
async getDemandFactorPeriodState() {
|
|
2910
|
+
const [pda] = await getDemandFactorPDA(this.arnsProgram);
|
|
2911
|
+
const account = await fetchEncodedAccount(this.rpc, pda, {
|
|
2912
|
+
commitment: this.commitment,
|
|
2913
|
+
});
|
|
2914
|
+
if (!account.exists)
|
|
2915
|
+
return null;
|
|
2916
|
+
const df = deserializeDemandFactor(Buffer.from(account.data));
|
|
2917
|
+
return {
|
|
2918
|
+
currentPeriod: df.currentPeriod,
|
|
2919
|
+
periodZeroStartTimestamp: df.periodZeroStartTimestamp,
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Read the raw epoch account data for cranker state inspection.
|
|
2924
|
+
* Returns null if the epoch account doesn't exist yet.
|
|
2925
|
+
*/
|
|
2926
|
+
async getEpochRaw(epochIndex) {
|
|
2927
|
+
const [epochPda] = await getEpochPDA(epochIndex, this.garProgram);
|
|
2928
|
+
const account = await fetchEncodedAccount(this.rpc, epochPda, {
|
|
2929
|
+
commitment: this.commitment,
|
|
2930
|
+
});
|
|
2931
|
+
if (!account.exists)
|
|
2932
|
+
return null;
|
|
2933
|
+
try {
|
|
2934
|
+
return this.fetchEpochRawFields(Buffer.from(account.data));
|
|
2935
|
+
}
|
|
2936
|
+
catch {
|
|
2937
|
+
return null;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Parse raw epoch account data for cranker-relevant fields.
|
|
2942
|
+
* Offsets match the Rust Epoch zero-copy struct (repr(C)).
|
|
2943
|
+
*
|
|
2944
|
+
* Layout after 8-byte discriminator:
|
|
2945
|
+
* [8 epoch_index][8 start_ts][8 end_ts][8 total_eligible][8 per_gw]
|
|
2946
|
+
* [8 per_obs][8 reward_rate][8 weight_lo][8 weight_hi][32 hashchain]
|
|
2947
|
+
* [4 active_gw_count][4 dist_idx][4 tally_idx]
|
|
2948
|
+
* [1 observer_count][1 name_count][1 obs_submitted][1 rewards_dist]
|
|
2949
|
+
* [1 weights_tallied][1 prescriptions_done][1 bump][1 obs_closed]
|
|
2950
|
+
* [6000 failure_counts][1600 prescribed_observers]
|
|
2951
|
+
* [1600 prescribed_observer_gateways][64 prescribed_names]
|
|
2952
|
+
* [7 has_observed][5 _pad2]
|
|
2953
|
+
*
|
|
2954
|
+
* NOTE: byte +123 is `observations_closed` (NOT padding) — `close_epoch`
|
|
2955
|
+
* reverts with EpochObservationsNotClosed until it equals obs_submitted.
|
|
2956
|
+
*/
|
|
2957
|
+
fetchEpochRawFields(data) {
|
|
2958
|
+
const base = 8;
|
|
2959
|
+
const endTimestamp = Number(data.readBigInt64LE(base + 16));
|
|
2960
|
+
const activeGatewayCount = data.readUInt32LE(base + 104);
|
|
2961
|
+
const distributionIndex = data.readUInt32LE(base + 108);
|
|
2962
|
+
const tallyIndex = data.readUInt32LE(base + 112);
|
|
2963
|
+
const observationsSubmitted = data.readUInt8(base + 118);
|
|
2964
|
+
const rewardsDistributed = data.readUInt8(base + 119);
|
|
2965
|
+
const weightsTallied = data.readUInt8(base + 120);
|
|
2966
|
+
const prescriptionsDone = data.readUInt8(base + 121);
|
|
2967
|
+
const observationsClosed = data.readUInt8(base + 123);
|
|
2968
|
+
return {
|
|
2969
|
+
tallyIndex,
|
|
2970
|
+
distributionIndex,
|
|
2971
|
+
weightsTallied,
|
|
2972
|
+
prescriptionsDone,
|
|
2973
|
+
rewardsDistributed,
|
|
2974
|
+
observationsSubmitted,
|
|
2975
|
+
observationsClosed,
|
|
2976
|
+
activeGatewayCount,
|
|
2977
|
+
endTimestamp,
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
// =========================================
|
|
2981
|
+
// Prune / cleanup (permissionless crank)
|
|
2982
|
+
// =========================================
|
|
2983
|
+
//
|
|
2984
|
+
// These mirror `tick()`-driven lazy pruning in the Lua source — on Solana
|
|
2985
|
+
// each is a discrete instruction someone has to call. All are
|
|
2986
|
+
// permissionless except `releaseVault`, which the on-chain handler still
|
|
2987
|
+
// gates on `owner: Signer` (ADR / vault.rs::ReleaseVault). See
|
|
2988
|
+
// docs/CRANKER_PRUNING_PLAN.md for the full design.
|
|
2989
|
+
/**
|
|
2990
|
+
* Batch-prune expired ArnsRecord PDAs from the NameRegistry. The caller
|
|
2991
|
+
* supplies the eligible records as `arnsRecords` — they're appended as
|
|
2992
|
+
* `remaining_accounts` and the on-chain handler verifies each is past
|
|
2993
|
+
* `end_timestamp + grace_period + return_auction_duration` before closing.
|
|
2994
|
+
* `maxNames` caps the per-tx work (u8). Submit in batches of ~10-15.
|
|
2995
|
+
*/
|
|
2996
|
+
async pruneExpiredNames(params, _options) {
|
|
2997
|
+
const ix = await getPruneExpiredNamesInstructionAsync(await this.withArnsDefaults({
|
|
2998
|
+
payer: this.signer,
|
|
2999
|
+
maxNames: params.maxNames,
|
|
3000
|
+
}), { programAddress: this.arnsProgram });
|
|
3001
|
+
const remaining = params.arnsRecords.map((a) => ({
|
|
3002
|
+
address: address(a),
|
|
3003
|
+
role: AccountRole.WRITABLE,
|
|
3004
|
+
}));
|
|
3005
|
+
const sig = await this.sendTransaction([withRemainingAccounts(ix, remaining)], 1_000_000);
|
|
3006
|
+
return { id: sig };
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Convert a single expired-but-not-yet-returned lease into a `ReturnedName`
|
|
3010
|
+
* (kicks off the Dutch auction). Permissionless.
|
|
3011
|
+
*/
|
|
3012
|
+
async pruneNameToReturned(params, _options) {
|
|
3013
|
+
const migrateIxs = await this._buildMigrateArnsRecordIxIfNeeded(params.name);
|
|
3014
|
+
const [arnsRecord] = await getArnsRecordPDA(params.name, this.arnsProgram);
|
|
3015
|
+
const [returnedName] = await getReturnedNamePDA(params.name, this.arnsProgram);
|
|
3016
|
+
const ix = await getPruneNameToReturnedInstructionAsync(await this.withArnsDefaults({
|
|
3017
|
+
arnsRecord,
|
|
3018
|
+
returnedName,
|
|
3019
|
+
payer: this.signer,
|
|
3020
|
+
}), { programAddress: this.arnsProgram });
|
|
3021
|
+
const sig = await this.sendTransaction([...migrateIxs, ix]);
|
|
3022
|
+
return { id: sig };
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Batch-prune expired ReturnedName PDAs (auction window elapsed). Caller
|
|
3026
|
+
* supplies the eligible PDAs as `returnedNames`; they're appended as
|
|
3027
|
+
* `remaining_accounts`. `maxNames` caps per-tx work (u8).
|
|
3028
|
+
*/
|
|
3029
|
+
async pruneReturnedNames(params, _options) {
|
|
3030
|
+
const ix = await getPruneReturnedNamesInstructionAsync(await this.withArnsDefaults({
|
|
3031
|
+
payer: this.signer,
|
|
3032
|
+
maxNames: params.maxNames,
|
|
3033
|
+
}), { programAddress: this.arnsProgram });
|
|
3034
|
+
const remaining = params.returnedNames.map((a) => ({
|
|
3035
|
+
address: address(a),
|
|
3036
|
+
role: AccountRole.WRITABLE,
|
|
3037
|
+
}));
|
|
3038
|
+
// Match `pruneExpiredNames` (1M CU) — both dispatch the same batched
|
|
3039
|
+
// shape over `remaining_accounts`, and the default 400K is too tight
|
|
3040
|
+
// when the cranker batches near `maxNames` (≈15) on a busy registry.
|
|
3041
|
+
const sig = await this.sendTransaction([withRemainingAccounts(ix, remaining)], 1_000_000);
|
|
3042
|
+
return { id: sig };
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Close a single expired ReservedName PDA. Permissionless after
|
|
3046
|
+
* `expires_at`.
|
|
3047
|
+
*/
|
|
3048
|
+
async pruneExpiredReservation(params, _options) {
|
|
3049
|
+
const [reservedName] = await getReservedNamePDA(params.name, this.arnsProgram);
|
|
3050
|
+
const ix = getPruneExpiredReservationInstruction({
|
|
3051
|
+
reservedName,
|
|
3052
|
+
payer: this.signer,
|
|
3053
|
+
}, { programAddress: this.arnsProgram });
|
|
3054
|
+
const sig = await this.sendTransaction([ix]);
|
|
3055
|
+
return { id: sig };
|
|
3056
|
+
}
|
|
3057
|
+
/**
|
|
3058
|
+
* Slash and remove a deficient gateway (`stats.failed_consecutive >=
|
|
3059
|
+
* max_consecutive_failures`). Builds the protected exit vault for the
|
|
3060
|
+
* post-slash min portion plus the optional excess vault for any surplus.
|
|
3061
|
+
* The contract's `excess_withdrawal: Option<UncheckedAccount>` slot is
|
|
3062
|
+
* always passed (PDA derived from `next_id + 1`); the handler consumes
|
|
3063
|
+
* it only when the post-slash stake exceeds `min_operator_stake`.
|
|
3064
|
+
* Permissionless.
|
|
3065
|
+
*/
|
|
3066
|
+
async pruneGateway(params, _options) {
|
|
3067
|
+
const gatewayAddr = address(params.gateway);
|
|
3068
|
+
const garConfig = await this.getGarConfig();
|
|
3069
|
+
const [gatewayPda] = await getGatewayPDA(gatewayAddr, this.garProgram);
|
|
3070
|
+
const [withdrawalCounterPda] = await getWithdrawalCounterPDA(gatewayAddr, this.garProgram);
|
|
3071
|
+
const nextId = await this.getNextWithdrawalId(gatewayAddr);
|
|
3072
|
+
const [withdrawalPda] = await getWithdrawalPDA(gatewayAddr, nextId, this.garProgram);
|
|
3073
|
+
const [excessWithdrawalPda] = await getWithdrawalPDA(gatewayAddr, nextId + 1n, this.garProgram);
|
|
3074
|
+
const ix = await getPruneGatewayInstructionAsync(await this.withGarDefaults({
|
|
3075
|
+
gateway: gatewayPda,
|
|
3076
|
+
withdrawalCounter: withdrawalCounterPda,
|
|
3077
|
+
withdrawal: withdrawalPda,
|
|
3078
|
+
excessWithdrawal: excessWithdrawalPda,
|
|
3079
|
+
stakeTokenAccount: garConfig.stakeTokenAccount,
|
|
3080
|
+
protocolTokenAccount: garConfig.protocolTokenAccount,
|
|
3081
|
+
payer: this.signer,
|
|
3082
|
+
}), { programAddress: this.garProgram });
|
|
3083
|
+
const sig = await this.sendTransaction([ix], 1_000_000);
|
|
3084
|
+
return { id: sig };
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* GC a `Leaving`/`Gone` gateway whose leave window has fully elapsed.
|
|
3088
|
+
* Closes the Gateway PDA and refunds rent to the caller. Permissionless.
|
|
3089
|
+
*/
|
|
3090
|
+
async finalizeGone(params, _options) {
|
|
3091
|
+
const gatewayAddr = address(params.gateway);
|
|
3092
|
+
const [gatewayPda] = await getGatewayPDA(gatewayAddr, this.garProgram);
|
|
3093
|
+
// `finalize_gone` reclaims this gateway's compact-registry slot by
|
|
3094
|
+
// swap-removing the LAST active slot into it. When this gateway is not the
|
|
3095
|
+
// last slot, the on-chain handler rewrites the swapped gateway's stored
|
|
3096
|
+
// registry_index and therefore requires that swapped Gateway PDA as
|
|
3097
|
+
// writable remaining_accounts[0]
|
|
3098
|
+
// (programs/ario-gar/src/instructions/gateway.rs::finalize_gone). Read the
|
|
3099
|
+
// gateway's slot index + the active registry to decide.
|
|
3100
|
+
const gatewayAccount = await fetchEncodedAccount(this.rpc, gatewayPda, {
|
|
3101
|
+
commitment: this.commitment,
|
|
3102
|
+
});
|
|
3103
|
+
if (!gatewayAccount.exists) {
|
|
3104
|
+
throw new Error(`Gateway not found for operator ${params.gateway}`);
|
|
3105
|
+
}
|
|
3106
|
+
const gateway = getGatewayDecoder().decode(gatewayAccount.data);
|
|
3107
|
+
const registryAddresses = await this.getRegistryGatewayAddresses();
|
|
3108
|
+
const swappedOperator = selectFinalizeGoneSwapOperator(gateway.registryIndex.index, registryAddresses);
|
|
3109
|
+
const ix = await getFinalizeGoneInstructionAsync(await this.withGarDefaults({
|
|
3110
|
+
gateway: gatewayPda,
|
|
3111
|
+
caller: this.signer,
|
|
3112
|
+
}), { programAddress: this.garProgram });
|
|
3113
|
+
let finalIx = ix;
|
|
3114
|
+
if (swappedOperator !== null) {
|
|
3115
|
+
const [swappedGatewayPda] = await getGatewayPDA(address(swappedOperator), this.garProgram);
|
|
3116
|
+
finalIx = withRemainingAccounts(ix, [
|
|
3117
|
+
{ address: swappedGatewayPda, role: AccountRole.WRITABLE },
|
|
3118
|
+
]);
|
|
3119
|
+
}
|
|
3120
|
+
const sig = await this.sendTransaction([finalIx]);
|
|
3121
|
+
return { id: sig };
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* Reclaim rent from an Observation PDA whose epoch has been distributed.
|
|
3125
|
+
* Permissionless. Pass `epochIndex` and the `observer` address used as
|
|
3126
|
+
* the Observation seed.
|
|
3127
|
+
*/
|
|
3128
|
+
async closeObservation(params, _options) {
|
|
3129
|
+
const observerAddr = address(params.observer);
|
|
3130
|
+
const [observationPda] = await getObservationPDA(params.epochIndex, observerAddr, this.garProgram);
|
|
3131
|
+
const ix = await getCloseObservationInstructionAsync({
|
|
3132
|
+
observation: observationPda,
|
|
3133
|
+
payer: this.signer,
|
|
3134
|
+
epochIndex: BigInt(params.epochIndex),
|
|
3135
|
+
}, { programAddress: this.garProgram });
|
|
3136
|
+
const sig = await this.sendTransaction([ix]);
|
|
3137
|
+
return { id: sig };
|
|
3138
|
+
}
|
|
3139
|
+
/**
|
|
3140
|
+
* Close multiple Observation PDAs for one epoch in a single tx (each
|
|
3141
|
+
* `close_observation` increments the parent Epoch's `observations_closed`).
|
|
3142
|
+
* Permissionless; rent is refunded to the payer. Used by the crank to satisfy
|
|
3143
|
+
* `close_epoch`'s `observations_closed == observations_submitted` precondition
|
|
3144
|
+
* before closing a retention-aged epoch. Keep the batch small — each ix carries
|
|
3145
|
+
* the Epoch + Observation + payer + system accounts.
|
|
3146
|
+
*/
|
|
3147
|
+
async closeObservations(params, _options) {
|
|
3148
|
+
if (params.observers.length === 0) {
|
|
3149
|
+
throw new Error('closeObservations: observers must be non-empty');
|
|
3150
|
+
}
|
|
3151
|
+
const ixs = await Promise.all(params.observers.map(async (obs) => {
|
|
3152
|
+
const [observationPda] = await getObservationPDA(params.epochIndex, address(obs), this.garProgram);
|
|
3153
|
+
return getCloseObservationInstructionAsync({
|
|
3154
|
+
observation: observationPda,
|
|
3155
|
+
payer: this.signer,
|
|
3156
|
+
epochIndex: BigInt(params.epochIndex),
|
|
3157
|
+
}, { programAddress: this.garProgram });
|
|
3158
|
+
}));
|
|
3159
|
+
const sig = await this.sendTransaction(ixs);
|
|
3160
|
+
return { id: sig };
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Close an empty Delegation PDA (`amount == 0`) and refund rent to the
|
|
3164
|
+
* original delegator (NOT the caller — see GAR-016, prevents griefing).
|
|
3165
|
+
* Permissionless.
|
|
3166
|
+
*/
|
|
3167
|
+
async closeEmptyDelegation(params, _options) {
|
|
3168
|
+
const gatewayAddr = address(params.gateway);
|
|
3169
|
+
const delegatorAddr = address(params.delegator);
|
|
3170
|
+
const [gatewayPda] = await getGatewayPDA(gatewayAddr, this.garProgram);
|
|
3171
|
+
const [delegationPda] = await getDelegationPDA(gatewayAddr, delegatorAddr, this.garProgram);
|
|
3172
|
+
const ix = getCloseEmptyDelegationInstruction({
|
|
3173
|
+
gateway: gatewayPda,
|
|
3174
|
+
delegation: delegationPda,
|
|
3175
|
+
delegator: delegatorAddr,
|
|
3176
|
+
payer: this.signer,
|
|
3177
|
+
}, { programAddress: this.garProgram });
|
|
3178
|
+
const sig = await this.sendTransaction([ix]);
|
|
3179
|
+
return { id: sig };
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* Close a drained Withdrawal PDA (`amount == 0`) and refund rent to the
|
|
3183
|
+
* original owner (NOT the caller). Permissionless.
|
|
3184
|
+
*/
|
|
3185
|
+
async closeDrainedWithdrawal(params, _options) {
|
|
3186
|
+
const ownerAddr = address(params.owner);
|
|
3187
|
+
const [withdrawalPda] = await getWithdrawalPDA(ownerAddr, BigInt(params.withdrawalId), this.garProgram);
|
|
3188
|
+
const ix = getCloseDrainedWithdrawalInstruction({
|
|
3189
|
+
withdrawal: withdrawalPda,
|
|
3190
|
+
owner: ownerAddr,
|
|
3191
|
+
closer: this.signer,
|
|
3192
|
+
}, { programAddress: this.garProgram });
|
|
3193
|
+
const sig = await this.sendTransaction([ix]);
|
|
3194
|
+
return { id: sig };
|
|
3195
|
+
}
|
|
3196
|
+
}
|