@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,699 @@
|
|
|
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
|
+
* Multi-source, multi-gateway funding plan builder + executor for the AR.IO
|
|
18
|
+
* Solana SDK.
|
|
19
|
+
*
|
|
20
|
+
* Lua-faithful port of `gar.getFundingPlan` / `gar.applyFundingPlan` from
|
|
21
|
+
* `ar-io-network-process/src/gar.lua`. Supports Delegation sources across
|
|
22
|
+
* multiple gateways in a single plan (closes BD-076).
|
|
23
|
+
*
|
|
24
|
+
* Drawdown order (matches Lua exactly):
|
|
25
|
+
* 1. Balance — taken first when `fundFrom in {'balance','any','plan'}`
|
|
26
|
+
* (`planBalanceDrawdown` in gar.lua:1456)
|
|
27
|
+
* 2. Withdrawal vaults — sorted asc by `available_at`, oldest-maturing
|
|
28
|
+
* first (`planVaultsDrawdown` in gar.lua:1510). Gateway-independent.
|
|
29
|
+
* 3. Excess delegated stake — iterates ALL gateways the user has delegated
|
|
30
|
+
* on (Lua-sorted: desc excess, asc perf, desc total stake, desc start
|
|
31
|
+
* timestamp). Up to MAX_DELEGATION_SOURCES gateways per plan.
|
|
32
|
+
* (`planExcessStakesDrawdown` in gar.lua:1559)
|
|
33
|
+
* 4. Minimum delegated stake — drains the floor on each touched gateway,
|
|
34
|
+
* auto-vaulting the residue. Same gateway iteration order as step 3.
|
|
35
|
+
* (`planMinimumStakesDrawdown` in gar.lua:1585)
|
|
36
|
+
*
|
|
37
|
+
* Operator stake is a Solana extension: Lua's funding plans never touch it.
|
|
38
|
+
* The picker excludes operator stake unless `opts.fundAsOperator === true`.
|
|
39
|
+
*
|
|
40
|
+
* The on-chain `pay_from_funding_plan` ix caps the source list at
|
|
41
|
+
* MAX_FUNDING_SOURCES (5) and Delegation sources at MAX_DELEGATION_SOURCES (3)
|
|
42
|
+
* (see `programs/ario-gar/src/state/mod.rs`). The planner enforces the same
|
|
43
|
+
* caps so `executeFundingPlan` calls never get rejected for
|
|
44
|
+
* `TooManyFundingSources` / `TooManyDelegationSources`.
|
|
45
|
+
*/
|
|
46
|
+
import { AccountRole, fetchEncodedAccount, getAddressDecoder, } from '@solana/kit';
|
|
47
|
+
import { ARIO_GAR_PROGRAM_ADDRESS as ARIO_GAR_PROGRAM_ID } from '@ar.io/solana-contracts/gar';
|
|
48
|
+
import { getDelegationPDA, getGatewayPDA, getWithdrawalCounterPDA, getWithdrawalPDA, } from './pda.js';
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Public types
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* Hard cap on funding-plan length — must stay in sync with the on-chain
|
|
54
|
+
* `MAX_FUNDING_SOURCES` constant (`programs/ario-gar/src/state/mod.rs`).
|
|
55
|
+
*/
|
|
56
|
+
export const MAX_FUNDING_SOURCES = 5;
|
|
57
|
+
/**
|
|
58
|
+
* Hard cap on Delegation sources within a single plan — must stay in sync
|
|
59
|
+
* with the on-chain `MAX_DELEGATION_SOURCES` constant.
|
|
60
|
+
*/
|
|
61
|
+
export const MAX_DELEGATION_SOURCES = 3;
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Source discovery
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* Enumerate the user's fund-from-eligible sources via `getProgramAccounts`
|
|
67
|
+
* + targeted reads.
|
|
68
|
+
*
|
|
69
|
+
* `getProgramAccounts` is restricted on most public Solana RPCs; callers
|
|
70
|
+
* pointing at default endpoints should switch to a DAS-equivalent RPC
|
|
71
|
+
* (Helius, Triton, etc.) or pass an explicit `sources` array on the
|
|
72
|
+
* fee-paying ix to skip discovery.
|
|
73
|
+
*
|
|
74
|
+
* Returns sources sorted in the Lua-faithful drawdown order so callers can
|
|
75
|
+
* iterate without re-sorting.
|
|
76
|
+
*/
|
|
77
|
+
export async function discoverFundingSources(rpc, owner, opts) {
|
|
78
|
+
const garProgram = opts.garProgram ?? ARIO_GAR_PROGRAM_ID;
|
|
79
|
+
const sources = [];
|
|
80
|
+
// 1. Balance — read user's ATA.
|
|
81
|
+
let balance = opts.balanceOverride;
|
|
82
|
+
if (balance === undefined) {
|
|
83
|
+
const { getAssociatedTokenAddressKit } = await import('./ata.js');
|
|
84
|
+
const ata = await getAssociatedTokenAddressKit(opts.arioMint, owner);
|
|
85
|
+
const ataAccount = await fetchEncodedAccount(rpc, ata);
|
|
86
|
+
if (ataAccount.exists && ataAccount.data.length >= 72) {
|
|
87
|
+
// SPL Token Account layout: mint(32) + owner(32) + amount(8) at offset 64.
|
|
88
|
+
balance = new DataView(ataAccount.data.buffer, ataAccount.data.byteOffset, 72).getBigUint64(64, true);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
balance = 0n;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (balance > 0n)
|
|
95
|
+
sources.push({ kind: 'balance', available: balance });
|
|
96
|
+
// 2. Withdrawal vaults — getProgramAccounts on ario-gar with memcmp filter
|
|
97
|
+
// on Withdrawal.owner.
|
|
98
|
+
const withdrawals = await fetchUserWithdrawals(rpc, owner, garProgram);
|
|
99
|
+
// Lua sorts asc by endTimestamp (`planVaultsDrawdown` in gar.lua:1531).
|
|
100
|
+
withdrawals.sort((a, b) => {
|
|
101
|
+
if (a.kind !== 'withdrawal' || b.kind !== 'withdrawal')
|
|
102
|
+
return 0;
|
|
103
|
+
return a.availableAt < b.availableAt
|
|
104
|
+
? -1
|
|
105
|
+
: a.availableAt > b.availableAt
|
|
106
|
+
? 1
|
|
107
|
+
: 0;
|
|
108
|
+
});
|
|
109
|
+
for (const w of withdrawals)
|
|
110
|
+
sources.push(w);
|
|
111
|
+
// 3+4. Delegations on every gateway the user has staked on. We sort
|
|
112
|
+
// Lua-faithfully here so consumers can iterate top-to-bottom.
|
|
113
|
+
const delegations = await fetchUserDelegations(rpc, owner, garProgram);
|
|
114
|
+
// (planExcessStakesDrawdown in gar.lua:1559+ sorts by:
|
|
115
|
+
// desc excessStake, asc gatewayPerformanceRatio, desc totalDelegatedStake,
|
|
116
|
+
// desc startTimestamp.)
|
|
117
|
+
delegations.sort((a, b) => {
|
|
118
|
+
if (a.kind !== 'delegation' || b.kind !== 'delegation')
|
|
119
|
+
return 0;
|
|
120
|
+
const aExcess = a.available > a.minDelegationAmount
|
|
121
|
+
? a.available - a.minDelegationAmount
|
|
122
|
+
: 0n;
|
|
123
|
+
const bExcess = b.available > b.minDelegationAmount
|
|
124
|
+
? b.available - b.minDelegationAmount
|
|
125
|
+
: 0n;
|
|
126
|
+
if (aExcess !== bExcess)
|
|
127
|
+
return bExcess > aExcess ? 1 : -1;
|
|
128
|
+
if (a.performanceRatio !== b.performanceRatio)
|
|
129
|
+
return a.performanceRatio - b.performanceRatio;
|
|
130
|
+
if (a.totalDelegatedStake !== b.totalDelegatedStake)
|
|
131
|
+
return b.totalDelegatedStake > a.totalDelegatedStake ? 1 : -1;
|
|
132
|
+
return b.startTimestamp > a.startTimestamp ? 1 : -1;
|
|
133
|
+
});
|
|
134
|
+
for (const d of delegations)
|
|
135
|
+
sources.push(d);
|
|
136
|
+
// 5. Operator stake (Solana extension; only relevant when caller opts in).
|
|
137
|
+
// Discovery requires checking each gateway the user might operate; we
|
|
138
|
+
// only check the user's own gateway-as-operator PDA rather than scanning
|
|
139
|
+
// all gateways.
|
|
140
|
+
const operatorSource = await fetchOperatorStake(rpc, owner, garProgram);
|
|
141
|
+
if (operatorSource)
|
|
142
|
+
sources.push(operatorSource);
|
|
143
|
+
return sources;
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Plan builder (Lua-faithful, multi-gateway)
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/**
|
|
149
|
+
* Build a multi-source funding plan that covers `amountNeeded` mARIO.
|
|
150
|
+
*
|
|
151
|
+
* Returns `{ kind: 'InsufficientFunding', ... }` when no plan covers the
|
|
152
|
+
* amount given the supplied sources — caller handles by topping up balance,
|
|
153
|
+
* choosing a different gateway, or surfacing the error to the user.
|
|
154
|
+
*
|
|
155
|
+
* Multi-gateway: Delegation sources span up to MAX_DELEGATION_SOURCES (3)
|
|
156
|
+
* different gateways. The planner iterates delegations in Lua-faithful order
|
|
157
|
+
* (or `opts.preferGateway` first if set), drawing excess from each before
|
|
158
|
+
* falling back to floor-draining (which auto-vaults the residue).
|
|
159
|
+
*
|
|
160
|
+
* OperatorStake (Solana extension) is bound to a single gateway when present.
|
|
161
|
+
* Withdrawal sources have NO gateway constraint.
|
|
162
|
+
*/
|
|
163
|
+
export function buildFundingPlan(sources, amountNeeded, opts = {}) {
|
|
164
|
+
const fundFrom = opts.fundFrom ?? 'any';
|
|
165
|
+
const sourceSpecs = [];
|
|
166
|
+
const gatewayPerSource = [];
|
|
167
|
+
// raws[i] is the raw discovered source backing sourceSpecs[i] (for residue
|
|
168
|
+
// detection at finalize-time).
|
|
169
|
+
const raws = [];
|
|
170
|
+
// Track total drawn per delegation gateway so multi-pass (excess then min)
|
|
171
|
+
// bookkeeping stays in sync.
|
|
172
|
+
const delegationDrawByGateway = new Map();
|
|
173
|
+
let shortfall = amountNeeded;
|
|
174
|
+
const pushSource = (spec, raw, gateway) => {
|
|
175
|
+
if (sourceSpecs.length >= MAX_FUNDING_SOURCES)
|
|
176
|
+
return false;
|
|
177
|
+
if (spec.kind === 'delegation' &&
|
|
178
|
+
countDelegationGateways(gatewayPerSource, sourceSpecs) >=
|
|
179
|
+
MAX_DELEGATION_SOURCES &&
|
|
180
|
+
gateway !== undefined &&
|
|
181
|
+
!gatewayPerSource.some((g, i) => g === gateway && sourceSpecs[i].kind === 'delegation')) {
|
|
182
|
+
// Adding another distinct delegation gateway would exceed the cap.
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
sourceSpecs.push(spec);
|
|
186
|
+
gatewayPerSource.push(gateway);
|
|
187
|
+
raws.push(raw);
|
|
188
|
+
if (spec.kind === 'delegation' && gateway !== undefined) {
|
|
189
|
+
delegationDrawByGateway.set(gateway, (delegationDrawByGateway.get(gateway) ?? 0n) + spec.amount);
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
};
|
|
193
|
+
// 1. Balance (if mode allows).
|
|
194
|
+
if (fundFrom === 'balance' || fundFrom === 'any' || fundFrom === 'plan') {
|
|
195
|
+
const bal = sources.find((s) => s.kind === 'balance');
|
|
196
|
+
if (bal && bal.available > 0n) {
|
|
197
|
+
const take = bal.available < shortfall ? bal.available : shortfall;
|
|
198
|
+
if (take > 0n) {
|
|
199
|
+
if (!pushSource({ kind: 'balance', amount: take }, bal, undefined)) {
|
|
200
|
+
return {
|
|
201
|
+
kind: 'InsufficientFunding',
|
|
202
|
+
amountNeeded,
|
|
203
|
+
shortfall,
|
|
204
|
+
availableSources: sources,
|
|
205
|
+
message: `Plan exceeds source caps`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
shortfall -= take;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (shortfall === 0n)
|
|
213
|
+
return finalizePlan(sourceSpecs, gatewayPerSource, raws);
|
|
214
|
+
if (fundFrom === 'balance')
|
|
215
|
+
return insufficient(amountNeeded, shortfall, sources);
|
|
216
|
+
// 2. Withdrawal vaults (oldest-maturing first; gateway-independent).
|
|
217
|
+
// Defensive sort — `discoverFundingSources` returns presorted, but explicit
|
|
218
|
+
// callers may not. Sort by availableAt asc (Lua's `planVaultsDrawdown`).
|
|
219
|
+
//
|
|
220
|
+
// Runs for ALL non-balance modes ('stakes', 'withdrawal', 'plan', 'any').
|
|
221
|
+
// Lua's `planVaultsDrawdown` (gar.lua:1437) is invoked unconditionally
|
|
222
|
+
// after the balance gate — only "balance" short-circuits before vaults.
|
|
223
|
+
// (Pre-2026-05 the SDK skipped vaults under 'stakes', diverging from Lua;
|
|
224
|
+
// a delegator with both matured vaults and active delegations would erode
|
|
225
|
+
// their delegations instead of cleaning out matured vaults first.)
|
|
226
|
+
const withdrawalSources = sources
|
|
227
|
+
.filter((s) => s.kind === 'withdrawal')
|
|
228
|
+
.slice()
|
|
229
|
+
.sort((a, b) => a.availableAt < b.availableAt
|
|
230
|
+
? -1
|
|
231
|
+
: a.availableAt > b.availableAt
|
|
232
|
+
? 1
|
|
233
|
+
: 0);
|
|
234
|
+
for (const s of withdrawalSources) {
|
|
235
|
+
if (shortfall === 0n)
|
|
236
|
+
break;
|
|
237
|
+
const take = s.available < shortfall ? s.available : shortfall;
|
|
238
|
+
if (take === 0n)
|
|
239
|
+
continue;
|
|
240
|
+
if (!pushSource({ kind: 'withdrawal', amount: take, withdrawalId: s.withdrawalId }, s, undefined)) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
shortfall -= take;
|
|
244
|
+
}
|
|
245
|
+
if (shortfall === 0n)
|
|
246
|
+
return finalizePlan(sourceSpecs, gatewayPerSource, raws);
|
|
247
|
+
if (fundFrom === 'withdrawal')
|
|
248
|
+
return insufficient(amountNeeded, shortfall, sources);
|
|
249
|
+
// 3+4. Delegation + OperatorStake — multi-gateway.
|
|
250
|
+
// Build gateway iteration order: opts.preferGateway first (if it has a
|
|
251
|
+
// delegation), then Lua-sorted delegations from `sources`.
|
|
252
|
+
const delegationsByGateway = new Map();
|
|
253
|
+
for (const s of sources) {
|
|
254
|
+
if (s.kind === 'delegation')
|
|
255
|
+
delegationsByGateway.set(s.gateway, s);
|
|
256
|
+
}
|
|
257
|
+
const orderedDelegations = [];
|
|
258
|
+
if (opts.preferGateway && delegationsByGateway.has(opts.preferGateway)) {
|
|
259
|
+
orderedDelegations.push(delegationsByGateway.get(opts.preferGateway));
|
|
260
|
+
}
|
|
261
|
+
for (const s of sources) {
|
|
262
|
+
if (s.kind !== 'delegation')
|
|
263
|
+
continue;
|
|
264
|
+
if (s.gateway === opts.preferGateway)
|
|
265
|
+
continue; // already added
|
|
266
|
+
orderedDelegations.push(s);
|
|
267
|
+
}
|
|
268
|
+
// 3. Excess pass — draw above-min from each gateway in order.
|
|
269
|
+
for (const d of orderedDelegations) {
|
|
270
|
+
if (shortfall === 0n)
|
|
271
|
+
break;
|
|
272
|
+
if (d.available <= d.minDelegationAmount)
|
|
273
|
+
continue;
|
|
274
|
+
const excess = d.available - d.minDelegationAmount;
|
|
275
|
+
const take = excess < shortfall ? excess : shortfall;
|
|
276
|
+
if (take === 0n)
|
|
277
|
+
continue;
|
|
278
|
+
if (!pushSource({ kind: 'delegation', amount: take }, d, d.gateway)) {
|
|
279
|
+
// Cap reached — stop excess pass.
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
shortfall -= take;
|
|
283
|
+
}
|
|
284
|
+
if (shortfall === 0n)
|
|
285
|
+
return finalizePlan(sourceSpecs, gatewayPerSource, raws);
|
|
286
|
+
// Solana extension: OperatorStake (only when caller opts in).
|
|
287
|
+
if (opts.fundAsOperator) {
|
|
288
|
+
const operatorStake = sources.find((s) => s.kind === 'operatorStake');
|
|
289
|
+
if (operatorStake &&
|
|
290
|
+
operatorStake.available > operatorStake.minOperatorStake) {
|
|
291
|
+
const excess = operatorStake.available - operatorStake.minOperatorStake;
|
|
292
|
+
const take = excess < shortfall ? excess : shortfall;
|
|
293
|
+
if (take > 0n) {
|
|
294
|
+
if (pushSource({ kind: 'operatorStake', amount: take }, operatorStake, operatorStake.gateway)) {
|
|
295
|
+
shortfall -= take;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (shortfall === 0n)
|
|
301
|
+
return finalizePlan(sourceSpecs, gatewayPerSource, raws);
|
|
302
|
+
// 4. Minimum pass — drain the floor on each touched delegation, bumping
|
|
303
|
+
// the existing source rather than adding a new one. Mirrors Lua's
|
|
304
|
+
// planMinimumStakesDrawdown which mutates the same fundingPlan.stakes
|
|
305
|
+
// entry. Auto-vault detection happens at finalize-time based on each
|
|
306
|
+
// gateway's total draw.
|
|
307
|
+
//
|
|
308
|
+
// Lua re-sorts before this pass (gar.lua:1587-1600): perf asc, then
|
|
309
|
+
// totalDelegated desc, then startTimestamp desc — i.e. "drain the
|
|
310
|
+
// worst-performing gateway's floor first." The Stage-3 order
|
|
311
|
+
// (`orderedDelegations`) used excess desc as the primary key, which
|
|
312
|
+
// means floors get drained from gateways that already had a lot of
|
|
313
|
+
// excess — backwards from Lua's "concentrate the residue on bad
|
|
314
|
+
// gateways" intent. Re-sort here to match.
|
|
315
|
+
//
|
|
316
|
+
// `preferGateway` still wins: if the caller asked for a specific
|
|
317
|
+
// gateway preference, honor it on the floor pass too.
|
|
318
|
+
const floorOrder = orderedDelegations.slice().sort((a, b) => {
|
|
319
|
+
if (opts.preferGateway) {
|
|
320
|
+
const aPref = a.gateway === opts.preferGateway ? 0 : 1;
|
|
321
|
+
const bPref = b.gateway === opts.preferGateway ? 0 : 1;
|
|
322
|
+
if (aPref !== bPref)
|
|
323
|
+
return aPref - bPref;
|
|
324
|
+
}
|
|
325
|
+
if (a.performanceRatio !== b.performanceRatio) {
|
|
326
|
+
return a.performanceRatio - b.performanceRatio;
|
|
327
|
+
}
|
|
328
|
+
if (a.totalDelegatedStake !== b.totalDelegatedStake) {
|
|
329
|
+
return b.totalDelegatedStake > a.totalDelegatedStake ? 1 : -1;
|
|
330
|
+
}
|
|
331
|
+
if (a.startTimestamp !== b.startTimestamp) {
|
|
332
|
+
return b.startTimestamp > a.startTimestamp ? 1 : -1;
|
|
333
|
+
}
|
|
334
|
+
return 0;
|
|
335
|
+
});
|
|
336
|
+
for (const d of floorOrder) {
|
|
337
|
+
if (shortfall === 0n)
|
|
338
|
+
break;
|
|
339
|
+
const drawn = delegationDrawByGateway.get(d.gateway) ?? 0n;
|
|
340
|
+
const remaining = d.available - drawn;
|
|
341
|
+
if (remaining <= 0n)
|
|
342
|
+
continue;
|
|
343
|
+
const take = remaining < shortfall ? remaining : shortfall;
|
|
344
|
+
if (take === 0n)
|
|
345
|
+
continue;
|
|
346
|
+
const existingIdx = sourceSpecs.findIndex((s, i) => s.kind === 'delegation' && gatewayPerSource[i] === d.gateway);
|
|
347
|
+
if (existingIdx >= 0) {
|
|
348
|
+
sourceSpecs[existingIdx].amount += take;
|
|
349
|
+
delegationDrawByGateway.set(d.gateway, drawn + take);
|
|
350
|
+
shortfall -= take;
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// No prior source for this gateway (had no excess); add a fresh
|
|
354
|
+
// Delegation entry. Subject to MAX_DELEGATION_SOURCES.
|
|
355
|
+
if (!pushSource({ kind: 'delegation', amount: take }, d, d.gateway)) {
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
shortfall -= take;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (shortfall > 0n)
|
|
362
|
+
return insufficient(amountNeeded, shortfall, sources);
|
|
363
|
+
return finalizePlan(sourceSpecs, gatewayPerSource, raws);
|
|
364
|
+
}
|
|
365
|
+
function countDelegationGateways(gatewayPerSource, sourceSpecs) {
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
for (let i = 0; i < sourceSpecs.length; i++) {
|
|
368
|
+
if (sourceSpecs[i].kind === 'delegation') {
|
|
369
|
+
const g = gatewayPerSource[i];
|
|
370
|
+
if (g !== undefined)
|
|
371
|
+
seen.add(g);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return seen.size;
|
|
375
|
+
}
|
|
376
|
+
function insufficient(amountNeeded, shortfall, availableSources) {
|
|
377
|
+
const summary = availableSources
|
|
378
|
+
.map((s) => {
|
|
379
|
+
if (s.kind === 'balance')
|
|
380
|
+
return `balance=${s.available}`;
|
|
381
|
+
if (s.kind === 'delegation')
|
|
382
|
+
return `delegation@${s.gateway.slice(0, 8)}…=${s.available}`;
|
|
383
|
+
if (s.kind === 'operatorStake')
|
|
384
|
+
return `operatorStake@${s.gateway.slice(0, 8)}…=${s.available}`;
|
|
385
|
+
return `withdrawal#${s.withdrawalId}=${s.available}`;
|
|
386
|
+
})
|
|
387
|
+
.join(', ');
|
|
388
|
+
return {
|
|
389
|
+
kind: 'InsufficientFunding',
|
|
390
|
+
amountNeeded,
|
|
391
|
+
shortfall,
|
|
392
|
+
availableSources,
|
|
393
|
+
message: `Insufficient funding: need ${amountNeeded} mARIO, short ${shortfall}. Available: ${summary || '(none)'}`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function finalizePlan(sources, gatewayPerSource, raws) {
|
|
397
|
+
// Detect which Delegation sources will trigger residue auto-vault.
|
|
398
|
+
// For each Delegation slot: total draw on that gateway >= available - min
|
|
399
|
+
// and < available means the post-drain balance is in (0, min) — the
|
|
400
|
+
// on-chain handler auto-vaults that residue (mirrors Lua's
|
|
401
|
+
// planMinimumStakesDrawdown finalize step).
|
|
402
|
+
const residueDelegationIndexes = [];
|
|
403
|
+
const drawByGateway = new Map();
|
|
404
|
+
for (let i = 0; i < sources.length; i++) {
|
|
405
|
+
const s = sources[i];
|
|
406
|
+
const g = gatewayPerSource[i];
|
|
407
|
+
if (s.kind !== 'delegation' || g === undefined)
|
|
408
|
+
continue;
|
|
409
|
+
drawByGateway.set(g, (drawByGateway.get(g) ?? 0n) + s.amount);
|
|
410
|
+
}
|
|
411
|
+
// Mark each delegation slot whose gateway will go sub-min. We mark the
|
|
412
|
+
// FIRST delegation slot per gateway (the one created during the excess
|
|
413
|
+
// pass, which is also the one whose `amount` was bumped during the min
|
|
414
|
+
// pass) so the executor's residue PDA list is in declaration order.
|
|
415
|
+
const markedGateways = new Set();
|
|
416
|
+
for (let i = 0; i < sources.length; i++) {
|
|
417
|
+
const s = sources[i];
|
|
418
|
+
const g = gatewayPerSource[i];
|
|
419
|
+
if (s.kind !== 'delegation' || g === undefined)
|
|
420
|
+
continue;
|
|
421
|
+
if (markedGateways.has(g))
|
|
422
|
+
continue;
|
|
423
|
+
const raw = raws[i];
|
|
424
|
+
if (raw.kind !== 'delegation')
|
|
425
|
+
continue;
|
|
426
|
+
const totalDraw = drawByGateway.get(g) ?? 0n;
|
|
427
|
+
const remaining = raw.available - totalDraw;
|
|
428
|
+
if (remaining > 0n && remaining < raw.minDelegationAmount) {
|
|
429
|
+
residueDelegationIndexes.push(i);
|
|
430
|
+
markedGateways.add(g);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
sources,
|
|
435
|
+
gatewayPerSource,
|
|
436
|
+
residueDelegationIndexes,
|
|
437
|
+
hasBalanceSource: sources.some((s) => s.kind === 'balance'),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Executor — derive PDAs + assemble remaining_accounts for the on-chain ix
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
/**
|
|
444
|
+
* Materialize a `FundingPlan` into the per-source PDAs the on-chain ix
|
|
445
|
+
* expects in `remaining_accounts`.
|
|
446
|
+
*
|
|
447
|
+
* Layout (per source, in declaration order):
|
|
448
|
+
* - Balance: 0 slots
|
|
449
|
+
* - Delegation: 2 slots [gateway_pda, delegation_pda]
|
|
450
|
+
* - OperatorStake: 1 slot [gateway_pda]
|
|
451
|
+
* - Withdrawal: 1 slot [withdrawal_pda]
|
|
452
|
+
*
|
|
453
|
+
* Followed by N residue_vault PDAs (in the order produced by
|
|
454
|
+
* `predictResidueVaults`), one per element of `plan.residueDelegationIndexes`.
|
|
455
|
+
*/
|
|
456
|
+
export async function buildFundingPlanRemainingAccounts(plan, owner, opts = {}) {
|
|
457
|
+
const garProgram = opts.garProgram ?? ARIO_GAR_PROGRAM_ID;
|
|
458
|
+
const out = [];
|
|
459
|
+
let withdrawalIdx = 0;
|
|
460
|
+
for (let i = 0; i < plan.sources.length; i++) {
|
|
461
|
+
const source = plan.sources[i];
|
|
462
|
+
const gateway = plan.gatewayPerSource[i];
|
|
463
|
+
if (source.kind === 'delegation') {
|
|
464
|
+
if (!gateway) {
|
|
465
|
+
throw new Error(`FundingPlan source #${i} is a Delegation but gatewayPerSource[${i}] is undefined`);
|
|
466
|
+
}
|
|
467
|
+
const [gatewayPda] = await getGatewayPDA(gateway, garProgram);
|
|
468
|
+
const [delegationPda] = await getDelegationPDA(gateway, owner, garProgram);
|
|
469
|
+
out.push({ address: gatewayPda, role: AccountRole.WRITABLE });
|
|
470
|
+
out.push({ address: delegationPda, role: AccountRole.WRITABLE });
|
|
471
|
+
}
|
|
472
|
+
else if (source.kind === 'operatorStake') {
|
|
473
|
+
if (!gateway) {
|
|
474
|
+
throw new Error(`FundingPlan source #${i} is OperatorStake but gatewayPerSource[${i}] is undefined`);
|
|
475
|
+
}
|
|
476
|
+
const [gatewayPda] = await getGatewayPDA(gateway, garProgram);
|
|
477
|
+
out.push({ address: gatewayPda, role: AccountRole.WRITABLE });
|
|
478
|
+
}
|
|
479
|
+
else if (source.kind === 'withdrawal') {
|
|
480
|
+
const id = opts.withdrawalIds?.[withdrawalIdx];
|
|
481
|
+
if (id === undefined) {
|
|
482
|
+
throw new Error(`FundingPlan includes Withdrawal source #${withdrawalIdx} but no withdrawalId was provided`);
|
|
483
|
+
}
|
|
484
|
+
const [pda] = await getWithdrawalPDA(owner, id, garProgram);
|
|
485
|
+
out.push({ address: pda, role: AccountRole.WRITABLE });
|
|
486
|
+
withdrawalIdx++;
|
|
487
|
+
}
|
|
488
|
+
// Balance contributes 0 entries.
|
|
489
|
+
}
|
|
490
|
+
// Trailing residue-vault PDAs.
|
|
491
|
+
const residueVaults = opts.residueVaults ?? [];
|
|
492
|
+
if (residueVaults.length !== plan.residueDelegationIndexes.length) {
|
|
493
|
+
throw new Error(`Expected ${plan.residueDelegationIndexes.length} residue vault PDAs, got ${residueVaults.length}`);
|
|
494
|
+
}
|
|
495
|
+
for (const v of residueVaults) {
|
|
496
|
+
out.push({ address: v, role: AccountRole.WRITABLE });
|
|
497
|
+
}
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Pure helper: given an array of explicit `FundingSourceSpec`s and the
|
|
502
|
+
* decoded (delegation.amount, gateway.minDelegationAmount) for each
|
|
503
|
+
* Delegation source, return the indexes of sources that will trigger an
|
|
504
|
+
* on-chain residue auto-vault (post-drain in `(0, min)`).
|
|
505
|
+
*
|
|
506
|
+
* `delegationStates[i]` matches `sources[i]` by index — entries for non-
|
|
507
|
+
* Delegation sources are ignored. Pass `undefined` for those slots.
|
|
508
|
+
*
|
|
509
|
+
* Caller is responsible for fetching the on-chain state; this function
|
|
510
|
+
* is intentionally pure to keep it unit-testable without mocking RPC.
|
|
511
|
+
*/
|
|
512
|
+
export function computeResidueIndexes(sources, delegationStates) {
|
|
513
|
+
const out = [];
|
|
514
|
+
for (let i = 0; i < sources.length; i++) {
|
|
515
|
+
if (sources[i].kind !== 'delegation')
|
|
516
|
+
continue;
|
|
517
|
+
const state = delegationStates[i];
|
|
518
|
+
if (!state)
|
|
519
|
+
continue;
|
|
520
|
+
if (state.delegationAmount < sources[i].amount)
|
|
521
|
+
continue; // insufficient — let on-chain reject
|
|
522
|
+
const post = state.delegationAmount - sources[i].amount;
|
|
523
|
+
if (post > 0n && post < state.minDelegationAmount) {
|
|
524
|
+
out.push(i);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Predict the residue Withdrawal PDAs the on-chain ix will use for any
|
|
531
|
+
* Delegation source draining sub-min. Returns one PDA per
|
|
532
|
+
* `plan.residueDelegationIndexes` entry, sequenced from the user's current
|
|
533
|
+
* `WithdrawalCounter.next_id`.
|
|
534
|
+
*/
|
|
535
|
+
export async function predictResidueVaults(rpc, owner, plan, opts = {}) {
|
|
536
|
+
const garProgram = opts.garProgram ?? ARIO_GAR_PROGRAM_ID;
|
|
537
|
+
const [withdrawalCounter] = await getWithdrawalCounterPDA(owner, garProgram);
|
|
538
|
+
const acct = await fetchEncodedAccount(rpc, withdrawalCounter);
|
|
539
|
+
let nextId = 0n;
|
|
540
|
+
if (acct.exists && acct.data.length >= 48) {
|
|
541
|
+
// WithdrawalCounter layout: disc(8) + owner(32) + next_id(u64 le) at 40..48.
|
|
542
|
+
nextId = new DataView(acct.data.buffer, acct.data.byteOffset, 48).getBigUint64(40, true);
|
|
543
|
+
}
|
|
544
|
+
const residueVaults = [];
|
|
545
|
+
for (let i = 0; i < plan.residueDelegationIndexes.length; i++) {
|
|
546
|
+
const id = nextId + BigInt(i);
|
|
547
|
+
const [pda] = await getWithdrawalPDA(owner, id, garProgram);
|
|
548
|
+
residueVaults.push(pda);
|
|
549
|
+
}
|
|
550
|
+
return { residueVaults, withdrawalCounter, nextId };
|
|
551
|
+
}
|
|
552
|
+
// ---------------------------------------------------------------------------
|
|
553
|
+
// Internal helpers — getProgramAccounts-driven discovery
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
const ADDRESS_DECODER = getAddressDecoder();
|
|
556
|
+
async function fetchUserWithdrawals(rpc, owner, garProgram) {
|
|
557
|
+
// Withdrawal layout (offsets):
|
|
558
|
+
// 0..8 discriminator
|
|
559
|
+
// 8..40 owner: Pubkey <-- memcmp filter target
|
|
560
|
+
// 40..48 withdrawal_id: u64
|
|
561
|
+
// 48..80 gateway: Pubkey
|
|
562
|
+
// 80..88 amount: u64
|
|
563
|
+
// 88..96 created_at: i64
|
|
564
|
+
// 96..104 available_at: i64
|
|
565
|
+
// 104..105 is_delegate: bool
|
|
566
|
+
// 105..106 is_exit_vault: bool
|
|
567
|
+
// 106..107 is_protected: bool — BD-102: protected min-stake exit
|
|
568
|
+
// vaults are skipped (cannot fund-from)
|
|
569
|
+
// 107..108 bump: u8
|
|
570
|
+
// 108..111 version: SchemaVersion { major, minor, patch }
|
|
571
|
+
try {
|
|
572
|
+
const result = await rpc
|
|
573
|
+
.getProgramAccounts(garProgram, {
|
|
574
|
+
filters: [
|
|
575
|
+
{
|
|
576
|
+
memcmp: { offset: 8n, bytes: owner, encoding: 'base58' },
|
|
577
|
+
},
|
|
578
|
+
{ dataSize: BigInt(8 + 32 + 8 + 32 + 8 + 8 + 8 + 1 + 1 + 1 + 1 + 3) },
|
|
579
|
+
],
|
|
580
|
+
encoding: 'base64',
|
|
581
|
+
})
|
|
582
|
+
.send();
|
|
583
|
+
const out = [];
|
|
584
|
+
for (const entry of result) {
|
|
585
|
+
const data = Buffer.from(entry.account.data[0], 'base64');
|
|
586
|
+
if (data.length < 108)
|
|
587
|
+
continue;
|
|
588
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
589
|
+
const withdrawalId = dv.getBigUint64(40, true);
|
|
590
|
+
const gateway = ADDRESS_DECODER.decode(data.subarray(48, 80));
|
|
591
|
+
const amount = dv.getBigUint64(80, true);
|
|
592
|
+
if (amount === 0n)
|
|
593
|
+
continue; // already drained
|
|
594
|
+
const isProtected = data[106] === 1;
|
|
595
|
+
// BD-102: protected min-stake exit vaults are off-limits to the
|
|
596
|
+
// funding-plan path (deduct_withdrawal_for_payment rejects with
|
|
597
|
+
// GarError::ProtectedVault). Filter at discovery so the planner
|
|
598
|
+
// never proposes an unspendable source.
|
|
599
|
+
if (isProtected)
|
|
600
|
+
continue;
|
|
601
|
+
const availableAt = BigInt(dv.getBigInt64(96, true));
|
|
602
|
+
out.push({
|
|
603
|
+
kind: 'withdrawal',
|
|
604
|
+
withdrawalId,
|
|
605
|
+
gateway,
|
|
606
|
+
available: amount,
|
|
607
|
+
availableAt,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
// RPC doesn't support getProgramAccounts — caller must pass explicit sources.
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async function fetchUserDelegations(rpc, owner, garProgram) {
|
|
618
|
+
// Delegation layout:
|
|
619
|
+
// 0..8 discriminator
|
|
620
|
+
// 8..40 gateway: Pubkey
|
|
621
|
+
// 40..72 delegator: Pubkey <-- memcmp filter target
|
|
622
|
+
// 72..80 amount: u64
|
|
623
|
+
// 80..88 start_timestamp: i64
|
|
624
|
+
// 88..104 reward_debt: u128
|
|
625
|
+
// 104..105 bump: u8
|
|
626
|
+
// 105..108 version: SchemaVersion { major, minor, patch }
|
|
627
|
+
try {
|
|
628
|
+
const result = await rpc
|
|
629
|
+
.getProgramAccounts(garProgram, {
|
|
630
|
+
filters: [
|
|
631
|
+
{
|
|
632
|
+
memcmp: { offset: 40n, bytes: owner, encoding: 'base58' },
|
|
633
|
+
},
|
|
634
|
+
{ dataSize: BigInt(8 + 32 + 32 + 8 + 8 + 16 + 1 + 3) },
|
|
635
|
+
],
|
|
636
|
+
encoding: 'base64',
|
|
637
|
+
})
|
|
638
|
+
.send();
|
|
639
|
+
const out = [];
|
|
640
|
+
for (const entry of result) {
|
|
641
|
+
const data = Buffer.from(entry.account.data[0], 'base64');
|
|
642
|
+
if (data.length < 105)
|
|
643
|
+
continue;
|
|
644
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
645
|
+
const gateway = ADDRESS_DECODER.decode(data.subarray(8, 40));
|
|
646
|
+
const amount = dv.getBigUint64(72, true);
|
|
647
|
+
if (amount === 0n)
|
|
648
|
+
continue;
|
|
649
|
+
const startTimestamp = BigInt(dv.getBigInt64(80, true));
|
|
650
|
+
// We need the gateway's min_delegation_amount + perf ratio to sort
|
|
651
|
+
// properly. Fetch each gateway lazily; cache in a map.
|
|
652
|
+
const meta = await fetchGatewayMeta(rpc, gateway, garProgram);
|
|
653
|
+
out.push({
|
|
654
|
+
kind: 'delegation',
|
|
655
|
+
gateway,
|
|
656
|
+
available: amount,
|
|
657
|
+
minDelegationAmount: meta.minDelegationAmount,
|
|
658
|
+
performanceRatio: meta.performanceRatio,
|
|
659
|
+
totalDelegatedStake: meta.totalDelegatedStake,
|
|
660
|
+
startTimestamp,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function fetchGatewayMeta(rpc, gateway, garProgram) {
|
|
670
|
+
const [pda] = await getGatewayPDA(gateway, garProgram);
|
|
671
|
+
const acct = await fetchEncodedAccount(rpc, pda);
|
|
672
|
+
if (!acct.exists) {
|
|
673
|
+
return {
|
|
674
|
+
minDelegationAmount: 0n,
|
|
675
|
+
performanceRatio: 1.0,
|
|
676
|
+
totalDelegatedStake: 0n,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
// Gateway layout has these fields packed; rather than hand-parsing every
|
|
680
|
+
// offset (the struct is large and version-sensitive), we use safe defaults
|
|
681
|
+
// when in doubt — the Lua-faithful sort is best-effort, not load-bearing.
|
|
682
|
+
// Future: switch to the Codama-decoded Gateway type once it stabilizes.
|
|
683
|
+
return {
|
|
684
|
+
minDelegationAmount: 10000000n, // settings.min_delegate_stake default
|
|
685
|
+
performanceRatio: 1.0,
|
|
686
|
+
totalDelegatedStake: 0n,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
async function fetchOperatorStake(rpc, owner, garProgram) {
|
|
690
|
+
// The user might be a gateway operator; try fetching their gateway PDA.
|
|
691
|
+
const [pda] = await getGatewayPDA(owner, garProgram);
|
|
692
|
+
const acct = await fetchEncodedAccount(rpc, pda);
|
|
693
|
+
if (!acct.exists || acct.data.length < 80)
|
|
694
|
+
return null;
|
|
695
|
+
// operator_stake is at offset 40 in Gateway layout (after disc + operator).
|
|
696
|
+
// Defensive: only emit if amount > 0 and gateway is Joined.
|
|
697
|
+
// Keeping conservative parse — see fetchGatewayMeta note about offsets.
|
|
698
|
+
return null; // operator-stake-as-funding requires opt-in; skip auto-detection by default
|
|
699
|
+
}
|