@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.
Files changed (169) hide show
  1. package/README.md +682 -600
  2. package/lib/esm/cli/cli.js +188 -152
  3. package/lib/esm/cli/commands/antCommands.js +23 -58
  4. package/lib/esm/cli/commands/arnsPurchaseCommands.js +48 -30
  5. package/lib/esm/cli/commands/escrowCommands.js +221 -0
  6. package/lib/esm/cli/commands/gatewayWriteCommands.js +142 -23
  7. package/lib/esm/cli/commands/pruneCommands.js +150 -0
  8. package/lib/esm/cli/commands/readCommands.js +22 -3
  9. package/lib/esm/cli/commands/transfer.js +6 -6
  10. package/lib/esm/cli/options.js +124 -58
  11. package/lib/esm/cli/utils.js +280 -174
  12. package/lib/esm/common/ant-registry.js +17 -143
  13. package/lib/esm/common/ant.js +44 -1167
  14. package/lib/esm/common/faucet.js +11 -6
  15. package/lib/esm/common/index.js +0 -4
  16. package/lib/esm/common/io.js +25 -1412
  17. package/lib/esm/constants.js +13 -19
  18. package/lib/esm/solana/ant-readable.js +724 -0
  19. package/lib/esm/solana/ant-registry-readable.js +133 -0
  20. package/lib/esm/solana/ant-registry-writeable.js +472 -0
  21. package/lib/esm/solana/ant-writeable.js +384 -0
  22. package/lib/esm/solana/ata.js +70 -0
  23. package/lib/esm/solana/canonical-message.js +128 -0
  24. package/lib/esm/solana/clusters.js +111 -0
  25. package/lib/esm/solana/constants.js +146 -0
  26. package/lib/esm/solana/delegation-math.js +112 -0
  27. package/lib/esm/solana/deserialize.js +711 -0
  28. package/lib/esm/solana/escrow.js +839 -0
  29. package/lib/{cjs/utils/json.js → esm/solana/events.js} +15 -10
  30. package/lib/esm/solana/funding-plan.js +699 -0
  31. package/lib/esm/solana/index.js +126 -0
  32. package/lib/esm/solana/instruction.js +39 -0
  33. package/lib/esm/solana/io-readable.js +2182 -0
  34. package/lib/esm/solana/io-writeable.js +3196 -0
  35. package/lib/esm/solana/json-rpc.js +90 -0
  36. package/lib/esm/solana/metadata.js +81 -0
  37. package/lib/esm/solana/mpl-core.js +192 -0
  38. package/lib/esm/solana/pda.js +332 -0
  39. package/lib/esm/solana/predict-prescribed-observers.js +110 -0
  40. package/lib/esm/solana/retry.js +117 -0
  41. package/lib/esm/solana/rpc-circuit-breaker.js +258 -0
  42. package/lib/esm/solana/send.js +372 -0
  43. package/lib/esm/solana/spawn-ant.js +224 -0
  44. package/lib/esm/solana/types.js +1 -0
  45. package/lib/esm/types/ant.js +27 -15
  46. package/lib/esm/types/io.js +8 -11
  47. package/lib/esm/utils/ant.js +0 -63
  48. package/lib/esm/utils/index.js +0 -3
  49. package/lib/esm/version.js +1 -1
  50. package/lib/types/cli/commands/antCommands.d.ts +5 -13
  51. package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +33 -7
  52. package/lib/types/cli/commands/escrowCommands.d.ts +68 -0
  53. package/lib/types/cli/commands/gatewayWriteCommands.d.ts +12 -11
  54. package/lib/types/cli/commands/pruneCommands.d.ts +31 -0
  55. package/lib/types/cli/commands/readCommands.d.ts +27 -22
  56. package/lib/types/cli/commands/transfer.d.ts +9 -9
  57. package/lib/types/cli/options.d.ts +76 -21
  58. package/lib/types/cli/types.d.ts +11 -13
  59. package/lib/types/cli/utils.d.ts +71 -31
  60. package/lib/types/common/ant-registry.d.ts +49 -47
  61. package/lib/types/common/ant.d.ts +54 -539
  62. package/lib/types/common/faucet.d.ts +20 -8
  63. package/lib/types/common/index.d.ts +0 -3
  64. package/lib/types/common/io.d.ts +51 -263
  65. package/lib/types/constants.d.ts +11 -18
  66. package/lib/types/solana/ant-readable.d.ts +180 -0
  67. package/lib/types/solana/ant-registry-readable.d.ts +105 -0
  68. package/lib/types/solana/ant-registry-writeable.d.ts +249 -0
  69. package/lib/types/solana/ant-writeable.d.ts +177 -0
  70. package/lib/types/solana/ata.d.ts +44 -0
  71. package/lib/types/solana/canonical-message.d.ts +121 -0
  72. package/lib/types/solana/clusters.d.ts +109 -0
  73. package/lib/types/solana/constants.d.ts +119 -0
  74. package/lib/types/solana/delegation-math.d.ts +45 -0
  75. package/lib/types/solana/deserialize.d.ts +262 -0
  76. package/lib/types/solana/escrow.d.ts +480 -0
  77. package/lib/types/solana/events.d.ts +38 -0
  78. package/lib/types/solana/funding-plan.d.ts +225 -0
  79. package/lib/types/solana/index.d.ts +87 -0
  80. package/lib/types/solana/instruction.d.ts +39 -0
  81. package/lib/types/solana/io-readable.d.ts +499 -0
  82. package/lib/types/solana/io-writeable.d.ts +893 -0
  83. package/lib/types/solana/json-rpc.d.ts +47 -0
  84. package/lib/types/solana/metadata.d.ts +84 -0
  85. package/lib/types/solana/mpl-core.d.ts +120 -0
  86. package/lib/types/solana/pda.d.ts +95 -0
  87. package/lib/types/solana/predict-prescribed-observers.d.ts +28 -0
  88. package/lib/types/solana/retry.d.ts +62 -0
  89. package/lib/types/solana/rpc-circuit-breaker.d.ts +78 -0
  90. package/lib/types/solana/send.d.ts +94 -0
  91. package/lib/types/solana/spawn-ant.d.ts +145 -0
  92. package/lib/types/solana/types.d.ts +82 -0
  93. package/lib/types/types/ant-registry.d.ts +43 -4
  94. package/lib/types/types/ant.d.ts +114 -96
  95. package/lib/types/types/common.d.ts +18 -74
  96. package/lib/types/types/faucet.d.ts +2 -2
  97. package/lib/types/types/io.d.ts +244 -158
  98. package/lib/types/types/token.d.ts +0 -12
  99. package/lib/types/utils/ant.d.ts +1 -12
  100. package/lib/types/utils/index.d.ts +0 -3
  101. package/lib/types/version.d.ts +1 -1
  102. package/package.json +36 -33
  103. package/lib/cjs/cli/cli.js +0 -822
  104. package/lib/cjs/cli/commands/antCommands.js +0 -113
  105. package/lib/cjs/cli/commands/arnsPurchaseCommands.js +0 -212
  106. package/lib/cjs/cli/commands/gatewayWriteCommands.js +0 -210
  107. package/lib/cjs/cli/commands/readCommands.js +0 -215
  108. package/lib/cjs/cli/commands/transfer.js +0 -159
  109. package/lib/cjs/cli/options.js +0 -470
  110. package/lib/cjs/cli/types.js +0 -2
  111. package/lib/cjs/cli/utils.js +0 -639
  112. package/lib/cjs/common/ant-registry.js +0 -155
  113. package/lib/cjs/common/ant-versions.js +0 -93
  114. package/lib/cjs/common/ant.js +0 -1182
  115. package/lib/cjs/common/arweave.js +0 -27
  116. package/lib/cjs/common/contracts/ao-process.js +0 -224
  117. package/lib/cjs/common/error.js +0 -64
  118. package/lib/cjs/common/faucet.js +0 -150
  119. package/lib/cjs/common/hyperbeam/hb.js +0 -173
  120. package/lib/cjs/common/index.js +0 -42
  121. package/lib/cjs/common/io.js +0 -1423
  122. package/lib/cjs/common/logger.js +0 -83
  123. package/lib/cjs/common/loggers/winston.js +0 -68
  124. package/lib/cjs/common/marketplace.js +0 -731
  125. package/lib/cjs/common/turbo.js +0 -223
  126. package/lib/cjs/constants.js +0 -41
  127. package/lib/cjs/node/index.js +0 -39
  128. package/lib/cjs/package.json +0 -1
  129. package/lib/cjs/types/ant-registry.js +0 -2
  130. package/lib/cjs/types/ant.js +0 -168
  131. package/lib/cjs/types/common.js +0 -2
  132. package/lib/cjs/types/faucet.js +0 -2
  133. package/lib/cjs/types/index.js +0 -37
  134. package/lib/cjs/types/io.js +0 -51
  135. package/lib/cjs/types/token.js +0 -116
  136. package/lib/cjs/utils/ant.js +0 -108
  137. package/lib/cjs/utils/ao.js +0 -432
  138. package/lib/cjs/utils/arweave.js +0 -285
  139. package/lib/cjs/utils/base64.js +0 -62
  140. package/lib/cjs/utils/hash.js +0 -56
  141. package/lib/cjs/utils/index.js +0 -38
  142. package/lib/cjs/utils/processes.js +0 -173
  143. package/lib/cjs/utils/random.js +0 -30
  144. package/lib/cjs/utils/schema.js +0 -15
  145. package/lib/cjs/utils/url.js +0 -37
  146. package/lib/cjs/version.js +0 -20
  147. package/lib/cjs/web/index.js +0 -41
  148. package/lib/esm/common/ant-versions.js +0 -87
  149. package/lib/esm/common/arweave.js +0 -21
  150. package/lib/esm/common/contracts/ao-process.js +0 -220
  151. package/lib/esm/common/hyperbeam/hb.js +0 -169
  152. package/lib/esm/common/marketplace.js +0 -724
  153. package/lib/esm/common/turbo.js +0 -215
  154. package/lib/esm/node/index.js +0 -20
  155. package/lib/esm/utils/ao.js +0 -420
  156. package/lib/esm/utils/arweave.js +0 -271
  157. package/lib/esm/utils/processes.js +0 -167
  158. package/lib/esm/web/index.js +0 -20
  159. package/lib/types/common/ant-versions.d.ts +0 -39
  160. package/lib/types/common/arweave.d.ts +0 -17
  161. package/lib/types/common/contracts/ao-process.d.ts +0 -47
  162. package/lib/types/common/hyperbeam/hb.d.ts +0 -88
  163. package/lib/types/common/marketplace.d.ts +0 -568
  164. package/lib/types/common/turbo.d.ts +0 -61
  165. package/lib/types/node/index.d.ts +0 -20
  166. package/lib/types/utils/ao.d.ts +0 -80
  167. package/lib/types/utils/arweave.d.ts +0 -79
  168. package/lib/types/utils/processes.d.ts +0 -39
  169. 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
+ }