@ar.io/sdk 3.24.0 → 4.0.0-alpha.2

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 +757 -589
  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 +227 -0
  6. package/lib/esm/cli/commands/gatewayWriteCommands.js +140 -23
  7. package/lib/esm/cli/commands/pruneCommands.js +154 -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 +303 -175
  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 +17 -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 +66 -258
  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,258 @@
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
+ * Circuit-breaker wrapper for Solana RPC transports using
18
+ * [opossum](https://nodeshift.dev/opossum/).
19
+ *
20
+ * When the primary RPC endpoint starts failing (rate-limits, downtime, etc.)
21
+ * the circuit opens and subsequent calls are routed transparently to a
22
+ * fallback RPC until the primary recovers.
23
+ *
24
+ * Works at the **transport level** — no Proxy magic required. Every RPC
25
+ * method (`getAccountInfo`, `sendTransaction`, etc.) goes through the same
26
+ * transport function, so a single circuit breaker covers them all.
27
+ *
28
+ * Usage:
29
+ * ```ts
30
+ * import { ARIO, createCircuitBreakerRpc } from '@ar.io/sdk';
31
+ *
32
+ * const rpc = createCircuitBreakerRpc({
33
+ * primaryUrl: 'https://my-premium-rpc.example.com',
34
+ * fallbackUrl: 'https://api.mainnet-beta.solana.com',
35
+ * });
36
+ *
37
+ * const ario = ARIO.init({ rpc });
38
+ * ```
39
+ */
40
+ import { createDefaultRpcTransport, createSolanaRpcFromTransport, } from '@solana/kit';
41
+ import CircuitBreaker from 'opossum';
42
+ import { Logger } from '../common/logger.js';
43
+ const logger = new Logger({ level: 'error' });
44
+ // ---------------------------------------------------------------------------
45
+ // Defaults
46
+ // ---------------------------------------------------------------------------
47
+ const DEFAULT_MAINNET_RPC = 'https://api.mainnet-beta.solana.com';
48
+ const DEFAULT_DEVNET_RPC = 'https://api.devnet.solana.com';
49
+ // ---------------------------------------------------------------------------
50
+ // Adaptive rate gate (token bucket + cooldown)
51
+ // ---------------------------------------------------------------------------
52
+ /** Default ceiling when `maxRequestsPerSecond` is not provided. */
53
+ const DEFAULT_MAX_RPS = 10;
54
+ /** Multiply the current rate by this on a 429 with no usable header. */
55
+ const AIMD_DECREASE = 0.5;
56
+ /** Never throttle below this many requests/second. */
57
+ const MIN_RATE = 1;
58
+ /** Consecutive successes before nudging the rate up by 1 (additive recovery). */
59
+ const RECOVERY_SUCCESSES = 20;
60
+ /** Fraction of a provider-advertised limit to actually use (safety margin). */
61
+ const RATE_SAFETY_FACTOR = 0.9;
62
+ /** Cooldown applied on a 429 that carries no `Retry-After`. */
63
+ const DEFAULT_COOLDOWN_MS = 1_000;
64
+ /**
65
+ * Token-bucket throttle whose rate can be retuned at runtime and which can be
66
+ * paused on demand. Tokens refill continuously at the current rate, capped at
67
+ * one second's worth (the burst allowance); waiters are released FIFO.
68
+ */
69
+ function createRateGate(initialRate) {
70
+ let rate = Math.max(MIN_RATE, initialRate);
71
+ let capacity = Math.max(1, rate);
72
+ let tokens = capacity;
73
+ let lastRefill = Date.now();
74
+ let pausedUntil = 0;
75
+ const queue = [];
76
+ let timer = null;
77
+ const schedule = (ms) => {
78
+ if (timer !== null)
79
+ return;
80
+ timer = setTimeout(() => {
81
+ timer = null;
82
+ pump();
83
+ }, Math.max(ms, 1));
84
+ };
85
+ const refill = () => {
86
+ const now = Date.now();
87
+ const elapsed = (now - lastRefill) / 1000;
88
+ if (elapsed > 0) {
89
+ tokens = Math.min(capacity, tokens + elapsed * rate);
90
+ lastRefill = now;
91
+ }
92
+ };
93
+ const pump = () => {
94
+ const now = Date.now();
95
+ if (pausedUntil > now) {
96
+ schedule(pausedUntil - now);
97
+ return;
98
+ }
99
+ refill();
100
+ while (tokens >= 1) {
101
+ const release = queue.shift();
102
+ if (!release)
103
+ break;
104
+ tokens -= 1;
105
+ release();
106
+ }
107
+ if (queue.length > 0) {
108
+ // Wake when the next whole token will have accrued.
109
+ schedule(Math.ceil(((1 - tokens) / rate) * 1000));
110
+ }
111
+ };
112
+ return {
113
+ acquire: () => new Promise((resolve) => {
114
+ queue.push(resolve);
115
+ pump();
116
+ }),
117
+ setRate: (ratePerSecond) => {
118
+ rate = Math.max(MIN_RATE, ratePerSecond);
119
+ capacity = Math.max(1, rate);
120
+ tokens = Math.min(tokens, capacity);
121
+ lastRefill = Date.now();
122
+ pump();
123
+ },
124
+ pauseFor: (ms) => {
125
+ const until = Date.now() + Math.max(0, ms);
126
+ if (until > pausedUntil)
127
+ pausedUntil = until;
128
+ schedule(ms);
129
+ },
130
+ };
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // 429 / rate-limit header parsing
134
+ // ---------------------------------------------------------------------------
135
+ /**
136
+ * If `err` is a transport HTTP 429, return its response `Headers`; else null.
137
+ * Duck-typed against the `@solana/errors` HTTP-error context
138
+ * (`{ statusCode, headers }`) so we avoid a hard dependency on the error code.
139
+ */
140
+ function http429Headers(err) {
141
+ const ctx = err
142
+ ?.context;
143
+ if (ctx?.statusCode === 429 && ctx.headers instanceof Headers) {
144
+ return ctx.headers;
145
+ }
146
+ return null;
147
+ }
148
+ /** Parse `Retry-After` (delta-seconds or HTTP-date) into ms, or null. */
149
+ function parseRetryAfterMs(headers) {
150
+ const v = headers.get('retry-after');
151
+ if (v === null || v === '')
152
+ return null;
153
+ const secs = Number(v);
154
+ if (Number.isFinite(secs))
155
+ return Math.max(0, secs * 1000);
156
+ const when = Date.parse(v);
157
+ return Number.isNaN(when) ? null : Math.max(0, when - Date.now());
158
+ }
159
+ /** Provider-advertised requests/second limit (`x-ratelimit-rps-limit`), or null. */
160
+ function parseRpsLimit(headers) {
161
+ const v = Number(headers.get('x-ratelimit-rps-limit'));
162
+ return Number.isFinite(v) && v > 0 ? v : null;
163
+ }
164
+ // ---------------------------------------------------------------------------
165
+ // Implementation
166
+ // ---------------------------------------------------------------------------
167
+ /**
168
+ * Create a {@link SolanaRpc} whose transport is backed by an opossum circuit
169
+ * breaker. Reads and writes flow through the primary transport; when it
170
+ * becomes unhealthy the circuit opens and subsequent calls are routed to
171
+ * the fallback transport until the primary recovers.
172
+ */
173
+ export function createCircuitBreakerRpc({ primaryUrl, fallbackUrl, circuitBreakerOptions: opts = {}, }) {
174
+ const primaryTransport = createDefaultRpcTransport({ url: primaryUrl });
175
+ const fallbackTransport = createDefaultRpcTransport({ url: fallbackUrl });
176
+ // Throttling is always on. `maxRequestsPerSecond` is the *ceiling*: every
177
+ // request flows through an adaptive token bucket that backs off on HTTP 429
178
+ // (honoring `Retry-After` and `x-ratelimit-rps-limit` when present, AIMD
179
+ // otherwise) and recovers back toward the ceiling on sustained success.
180
+ const ceilingRate = opts.maxRequestsPerSecond !== undefined && opts.maxRequestsPerSecond > 0
181
+ ? opts.maxRequestsPerSecond
182
+ : DEFAULT_MAX_RPS;
183
+ const gate = createRateGate(ceilingRate);
184
+ let currentRate = ceilingRate;
185
+ let successStreak = 0;
186
+ const onError = (err) => {
187
+ const headers = http429Headers(err);
188
+ if (!headers)
189
+ return; // only adapt to rate-limit (429) failures
190
+ successStreak = 0;
191
+ const advertised = parseRpsLimit(headers);
192
+ const next = advertised !== null
193
+ ? Math.min(ceilingRate, Math.max(MIN_RATE, advertised * RATE_SAFETY_FACTOR))
194
+ : Math.max(MIN_RATE, currentRate * AIMD_DECREASE);
195
+ if (next !== currentRate) {
196
+ currentRate = next;
197
+ gate.setRate(currentRate);
198
+ }
199
+ const retryAfter = parseRetryAfterMs(headers);
200
+ gate.pauseFor(retryAfter ?? DEFAULT_COOLDOWN_MS);
201
+ logger.warn(`[rpc-circuit-breaker] 429 — throttling to ${currentRate.toFixed(1)} req/s` +
202
+ `, cooling down ${retryAfter ?? DEFAULT_COOLDOWN_MS}ms`);
203
+ };
204
+ const onSuccess = () => {
205
+ if (currentRate >= ceilingRate)
206
+ return;
207
+ if (++successStreak >= RECOVERY_SUCCESSES) {
208
+ successStreak = 0;
209
+ currentRate = Math.min(ceilingRate, currentRate + 1);
210
+ gate.setRate(currentRate);
211
+ }
212
+ };
213
+ const breaker = new CircuitBreaker((request) => primaryTransport(request), {
214
+ timeout: opts.timeout ?? 10_000,
215
+ errorThresholdPercentage: opts.errorThresholdPercentage ?? 25,
216
+ resetTimeout: opts.resetTimeout ?? 60_000,
217
+ volumeThreshold: opts.volumeThreshold ?? 3,
218
+ ...(opts.maxConcurrent !== undefined && opts.maxConcurrent > 0
219
+ ? { capacity: opts.maxConcurrent }
220
+ : {}),
221
+ });
222
+ breaker.fallback((request) => fallbackTransport(request));
223
+ breaker.on('open', () => {
224
+ logger.warn('[rpc-circuit-breaker] circuit OPEN — routing to fallback RPC');
225
+ });
226
+ breaker.on('halfOpen', () => {
227
+ logger.info('[rpc-circuit-breaker] circuit HALF-OPEN — probing primary RPC');
228
+ });
229
+ breaker.on('close', () => {
230
+ logger.info('[rpc-circuit-breaker] circuit CLOSED — primary RPC recovered');
231
+ });
232
+ // Adapt the rate to the *primary's* health via opossum's events: `failure`
233
+ // fires whenever the primary call rejects (a 429 included) even when the
234
+ // fallback then masks it by resolving `fire()`, and `success` fires on a
235
+ // healthy primary call. A plain try/catch around `fire()` would miss the
236
+ // fallback-masked 429s entirely.
237
+ breaker.on('failure', (err) => onError(err));
238
+ breaker.on('success', () => onSuccess());
239
+ const transport = (async (request) => {
240
+ // Throttle entry to the breaker so we stay under the provider's rate
241
+ // limit; the queue wait sits outside `fire`, so opossum's per-request
242
+ // timeout only measures the actual transport call.
243
+ await gate.acquire();
244
+ return breaker.fire(request);
245
+ });
246
+ return createSolanaRpcFromTransport(transport);
247
+ }
248
+ /**
249
+ * Convenience: pick a sensible public fallback URL based on the primary URL.
250
+ *
251
+ * - Primary contains `devnet` → devnet public RPC
252
+ * - Everything else → mainnet-beta public RPC
253
+ */
254
+ export function defaultFallbackUrl(primaryUrl) {
255
+ if (/devnet/i.test(primaryUrl))
256
+ return DEFAULT_DEVNET_RPC;
257
+ return DEFAULT_MAINNET_RPC;
258
+ }
@@ -0,0 +1,372 @@
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
+ * Shared helpers for building, signing, and sending Solana transactions
18
+ * with @solana/kit. Used by SolanaARIOWriteable and SolanaANTWriteable.
19
+ *
20
+ * Compute budget instruction builders come from `@solana-program/compute-budget`
21
+ * (kit-flavored Codama client); the previous hand-rolled
22
+ * `setComputeUnitLimitIx` / `setComputeUnitPriceIx` helpers were removed in
23
+ * favor of the official package. See `sendAndConfirm` below for why we always
24
+ * pin BOTH instructions (even with a 0 priority fee).
25
+ */
26
+ import { ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS, getCloseLookupTableInstruction, getCreateLookupTableInstructionAsync, getDeactivateLookupTableInstruction, getExtendLookupTableInstruction, } from '@solana-program/address-lookup-table';
27
+ import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from '@solana-program/compute-budget';
28
+ import { appendTransactionMessageInstructions, compileTransaction, compressTransactionMessageUsingAddressLookupTables, createTransactionMessage, getAddressDecoder, getBase64EncodedWireTransaction, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
29
+ /**
30
+ * Build, sign, send, and confirm a transaction in one call.
31
+ *
32
+ * The caller supplies the core instructions; a compute-unit-limit instruction
33
+ * is prepended automatically.
34
+ */
35
+ export async function sendAndConfirm({ rpc, rpcSubscriptions, signer, instructions, commitment = 'confirmed', computeUnitLimit = 400_000, addressLookupTables, }) {
36
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
37
+ const baseMessage = pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(signer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([
38
+ getSetComputeUnitLimitInstruction({ units: computeUnitLimit }),
39
+ // Always pin the priority fee (even at 0) so wallets like Phantom
40
+ // don't silently *append* their own compute-budget instructions
41
+ // when the transaction is missing either limit or price. That
42
+ // mutation invalidates signatures already attached by paired
43
+ // keypair signers (e.g. the ANT mint signer in `spawnSolanaANT`),
44
+ // producing `Transaction did not pass signature verification` on
45
+ // the validator. Pre-supplying both keeps the wallet from
46
+ // rewriting the message, so signatures over the original bytes
47
+ // still verify.
48
+ getSetComputeUnitPriceInstruction({ microLamports: 0n }),
49
+ ...instructions,
50
+ ], tx));
51
+ // Compress against any supplied lookup tables (v0). No-op when none given.
52
+ const message = addressLookupTables
53
+ ? compressTransactionMessageUsingAddressLookupTables(baseMessage, addressLookupTables)
54
+ : baseMessage;
55
+ const signedTx = await signTransactionMessageWithSigners(message);
56
+ const sendAndConfirmFactory = sendAndConfirmTransactionFactory({
57
+ rpc,
58
+ rpcSubscriptions,
59
+ });
60
+ // Cast narrows the transaction type to what sendAndConfirmFactory expects —
61
+ // the kit signer pipeline produces a fully-signed transaction with a blockhash
62
+ // lifetime, but the factory's argument type doesn't quite line up with the
63
+ // inferred union. The runtime object is correct.
64
+ try {
65
+ await sendAndConfirmFactory(signedTx, { commitment });
66
+ }
67
+ catch (err) {
68
+ logSolanaErrorContext(err);
69
+ await logSimulationDiagnostics(rpc, message, err);
70
+ throw err;
71
+ }
72
+ return getSignatureFromTransaction(signedTx);
73
+ }
74
+ /**
75
+ * Walk the chain of `cause`s on a thrown `SolanaError` and log each one's
76
+ * `.context` (kit packs the server-provided `err`, `logs`, `unitsConsumed`,
77
+ * etc. there). This usually contains the program logs we want without having
78
+ * to re-simulate.
79
+ */
80
+ function logSolanaErrorContext(err) {
81
+ let current = err;
82
+ let depth = 0;
83
+ while (current && depth < 10) {
84
+ const e = current;
85
+ const ctx = e?.context;
86
+ if (ctx && typeof ctx === 'object') {
87
+ // eslint-disable-next-line no-console
88
+ console.warn(`[solana-send] error[${depth}] ${e.name ?? 'Error'}: ${e.message ?? ''}`, { context: ctx });
89
+ const logs = ctx.logs;
90
+ if (Array.isArray(logs) && logs.length > 0) {
91
+ // eslint-disable-next-line no-console
92
+ console.warn(`[solana-send] error[${depth}] program logs:\n` + logs.join('\n'));
93
+ }
94
+ }
95
+ current = e?.cause;
96
+ depth += 1;
97
+ }
98
+ }
99
+ /**
100
+ * On send/confirm failure, re-simulate the transaction with sig-verify off so
101
+ * we can surface program logs + the failed instruction index. Kit's default
102
+ * `SolanaError` only carries the bare `Custom program error: #N (instruction
103
+ * #M)` summary; we also log the full simulation result and the wire-format
104
+ * transaction so the underlying `msg!` lines and account list can be inspected
105
+ * in the browser console.
106
+ *
107
+ * Best-effort and side-effect free aside from console output.
108
+ */
109
+ async function logSimulationDiagnostics(rpc, message, originalError) {
110
+ try {
111
+ const compiled = compileTransaction(message);
112
+ const wire = getBase64EncodedWireTransaction(compiled);
113
+ // eslint-disable-next-line no-console
114
+ console.warn('[solana-send] sendAndConfirm failed; re-running simulateTransaction for diagnostics', { error: originalError });
115
+ const sim = await rpc
116
+ .simulateTransaction(wire, {
117
+ sigVerify: false,
118
+ replaceRecentBlockhash: true,
119
+ encoding: 'base64',
120
+ })
121
+ .send();
122
+ // eslint-disable-next-line no-console
123
+ console.warn('[solana-send] simulateTransaction result:', sim.value);
124
+ if (sim.value.err) {
125
+ // eslint-disable-next-line no-console
126
+ console.warn('[solana-send] simulation err:', sim.value.err);
127
+ }
128
+ if (sim.value.logs) {
129
+ // eslint-disable-next-line no-console
130
+ console.warn('[solana-send] program logs:\n' + sim.value.logs.join('\n'));
131
+ }
132
+ }
133
+ catch (diagErr) {
134
+ // eslint-disable-next-line no-console
135
+ console.warn('[solana-send] failed to collect diagnostics', diagErr);
136
+ }
137
+ }
138
+ /**
139
+ * Submit `instruction` in a v0 transaction whose `lookupAddresses` (read-only
140
+ * accounts) are served from a freshly-created, ephemeral Address Lookup Table,
141
+ * so an instruction touching far more accounts than fit inline (e.g.
142
+ * `prescribe_epoch` with ≤50 observer PDAs + NameRegistry, ~2 KB of keys) still
143
+ * fits Solana's 1232-byte transaction-size limit.
144
+ *
145
+ * Three confirmed steps: create the table, extend it with the addresses (in
146
+ * ≤20-address batches to stay within the extend tx size), then send
147
+ * `instruction` compressed against the table. The sequential confirmations
148
+ * satisfy the rule that appended addresses are only usable the slot AFTER they
149
+ * are added. `signer` is the table's authority + payer; the table's (tiny) rent
150
+ * is left allocated — a future cleanup pass can deactivate + close it.
151
+ */
152
+ export async function sendWithEphemeralLookupTable({ rpc, rpcSubscriptions, signer, instruction, lookupAddresses, commitment = 'confirmed', computeUnitLimit = 1_000_000, }) {
153
+ const recentSlot = await rpc.getSlot({ commitment: 'finalized' }).send();
154
+ const createIx = await getCreateLookupTableInstructionAsync({
155
+ authority: signer.address,
156
+ payer: signer,
157
+ recentSlot,
158
+ });
159
+ const tableAddress = createIx.accounts[0].address;
160
+ // Create the (empty) table.
161
+ await sendAndConfirm({
162
+ rpc,
163
+ rpcSubscriptions,
164
+ signer,
165
+ instructions: [createIx],
166
+ commitment,
167
+ computeUnitLimit: 60_000,
168
+ });
169
+ // Fill it, ≤20 addresses per extend tx.
170
+ const BATCH = 20;
171
+ for (let i = 0; i < lookupAddresses.length; i += BATCH) {
172
+ const extendIx = getExtendLookupTableInstruction({
173
+ address: tableAddress,
174
+ authority: signer,
175
+ payer: signer,
176
+ addresses: lookupAddresses.slice(i, i + BATCH),
177
+ });
178
+ await sendAndConfirm({
179
+ rpc,
180
+ rpcSubscriptions,
181
+ signer,
182
+ instructions: [extendIx],
183
+ commitment,
184
+ computeUnitLimit: 60_000,
185
+ });
186
+ }
187
+ // Wait until the table holds every address AND one slot has elapsed since —
188
+ // addresses appended to a lookup table are only usable the slot AFTER they're
189
+ // added, and the validator that processes the prescribe must already see
190
+ // them. Skipping this yields "address table lookup uses an invalid index".
191
+ await waitForLookupTableActive(rpc, tableAddress, lookupAddresses.length);
192
+ // Send the real instruction, compressed against the now-active table.
193
+ return sendAndConfirm({
194
+ rpc,
195
+ rpcSubscriptions,
196
+ signer,
197
+ instructions: [instruction],
198
+ commitment,
199
+ computeUnitLimit,
200
+ addressLookupTables: { [tableAddress]: lookupAddresses },
201
+ });
202
+ }
203
+ /**
204
+ * Poll until an Address Lookup Table holds at least `expectedCount` addresses
205
+ * AND at least one slot has elapsed since they all landed. Lookup-table entries
206
+ * are only usable the slot AFTER they are appended, and the leader processing
207
+ * the consuming tx must already see them — otherwise the runtime rejects the tx
208
+ * with "address table lookup uses an invalid index". ALT account layout is a
209
+ * 56-byte metadata header followed by 32-byte addresses.
210
+ */
211
+ async function waitForLookupTableActive(rpc, table, expectedCount, maxWaitMs = 30_000) {
212
+ const META = 56;
213
+ const start = Date.now();
214
+ let slotAllPresent = null;
215
+ while (Date.now() - start < maxWaitMs) {
216
+ const acc = await rpc.getAccountInfo(table, { encoding: 'base64' }).send();
217
+ const slot = acc.context.slot;
218
+ if (acc.value) {
219
+ const len = Buffer.from(acc.value.data[0], 'base64').length;
220
+ const count = len >= META ? Math.floor((len - META) / 32) : 0;
221
+ if (count >= expectedCount) {
222
+ if (slotAllPresent === null) {
223
+ slotAllPresent = slot;
224
+ }
225
+ else if (slot > slotAllPresent) {
226
+ return; // all addresses present + a slot has elapsed → warm
227
+ }
228
+ }
229
+ }
230
+ await new Promise((r) => setTimeout(r, 800));
231
+ }
232
+ throw new Error(`lookup table ${table} not active (≥${expectedCount} addresses + 1 slot) within ${maxWaitMs}ms`);
233
+ }
234
+ /**
235
+ * Reclaim rent from the ephemeral Address Lookup Tables `signer` created for
236
+ * prescribe (see {@link sendWithEphemeralLookupTable}). Each prescribe leaves a
237
+ * single-use table allocated (~0.0126 SOL of rent); reclaiming needs a
238
+ * deactivate → ~513-slot cooldown → close sequence, so it can't run inline — a
239
+ * throttled permissionless cleanup pass (cranker / observer) calls this.
240
+ *
241
+ * Discovery is RPC-portable. `getProgramAccounts` on the Address Lookup Table
242
+ * program is rejected by Agave RPCs (`Invalid param: WrongSize`, on public
243
+ * devnet/mainnet-beta and dedicated providers alike — the ALT program can't be
244
+ * enumerated), so instead we read the signer's own transaction history
245
+ * (`getSignaturesForAddress` + `getTransaction`) and collect the tables it
246
+ * referenced via `message.addressTableLookups` — a prescribe ALT is used in
247
+ * exactly one transaction.
248
+ *
249
+ * Safety fingerprint: a candidate is only touched when EVERY one of its entries
250
+ * is owned by a program in `allowedEntryOwners` (the GAR + ArNS programs — i.e.
251
+ * observer Gateway PDAs + the ArNS NameRegistry). That composition uniquely
252
+ * identifies a prescribe ephemeral, so the pass never deactivates/closes an
253
+ * unrelated table even if `signer` is also used to author Address Lookup Tables
254
+ * for other purposes.
255
+ *
256
+ * DEACTIVATES still-active matches (starts the cooldown) and CLOSES deactivated
257
+ * matches past the cooldown (refunding rent to `signer`). At most `maxTables`
258
+ * submissions per call; scans at most `scanLimit` recent signatures. Best-effort:
259
+ * per-table failures are skipped and retried on the next pass.
260
+ */
261
+ export async function reclaimLookupTablesForSigner({ rpc, rpcSubscriptions, signer, allowedEntryOwners, commitment = 'confirmed', maxTables = 10, scanLimit = 500, }) {
262
+ const ALT_META = 56; // metadata header before the 32-byte address array
263
+ const ACTIVE = 0xffffffffffffffffn; // u64::MAX = not yet deactivated
264
+ const COOLDOWN_SLOTS = 513n; // deactivation_slot must age out of SlotHashes
265
+ const allowed = new Set(allowedEntryOwners);
266
+ const addressDecoder = getAddressDecoder();
267
+ // getTransaction only honours 'confirmed' | 'finalized'.
268
+ const historyCommitment = commitment === 'finalized' ? 'finalized' : 'confirmed';
269
+ // --- Discover candidate tables from the signer's transaction history -------
270
+ const sigs = await rpc
271
+ .getSignaturesForAddress(signer.address, { limit: scanLimit })
272
+ .send();
273
+ const candidates = new Set();
274
+ for (const { signature } of sigs) {
275
+ // A little headroom over maxTables so already-closed candidates don't
276
+ // starve the budget; the rest get picked up next pass.
277
+ if (candidates.size >= maxTables * 3)
278
+ break;
279
+ const tx = await rpc
280
+ .getTransaction(signature, {
281
+ encoding: 'json',
282
+ maxSupportedTransactionVersion: 0,
283
+ commitment: historyCommitment,
284
+ })
285
+ .send();
286
+ const lookups = tx?.transaction?.message?.addressTableLookups ?? [];
287
+ for (const l of lookups)
288
+ candidates.add(l.accountKey);
289
+ }
290
+ // --- Reclaim ----------------------------------------------------------------
291
+ const currentSlot = await rpc.getSlot().send();
292
+ let deactivated = 0;
293
+ let closed = 0;
294
+ for (const table of candidates) {
295
+ if (deactivated + closed >= maxTables)
296
+ break;
297
+ const address = table;
298
+ try {
299
+ const info = await rpc
300
+ .getAccountInfo(address, { encoding: 'base64' })
301
+ .send();
302
+ const value = info.value;
303
+ if (!value)
304
+ continue; // already closed
305
+ if (value.owner !==
306
+ ADDRESS_LOOKUP_TABLE_PROGRAM_ADDRESS) {
307
+ continue;
308
+ }
309
+ const data = Buffer.from(value.data[0], 'base64');
310
+ if (data.length < ALT_META)
311
+ continue;
312
+ const deactivationSlot = data.readBigUInt64LE(4);
313
+ // Fingerprint: every entry must be owned by an allowed program. A prescribe
314
+ // ALT is exclusively observer Gateway PDAs (GAR) + the NameRegistry (ArNS).
315
+ const entries = [];
316
+ for (let off = ALT_META; off + 32 <= data.length; off += 32) {
317
+ entries.push(addressDecoder.decode(data.subarray(off, off + 32)));
318
+ }
319
+ if (entries.length === 0)
320
+ continue;
321
+ const owners = await rpc
322
+ .getMultipleAccounts(entries, {
323
+ encoding: 'base64',
324
+ dataSlice: { offset: 0, length: 0 },
325
+ })
326
+ .send();
327
+ const allOwned = owners.value.every((a) => a != null && allowed.has(a.owner));
328
+ if (!allOwned)
329
+ continue; // not a prescribe ephemeral — leave it alone
330
+ if (deactivationSlot === ACTIVE) {
331
+ await sendAndConfirm({
332
+ rpc,
333
+ rpcSubscriptions,
334
+ signer,
335
+ commitment,
336
+ computeUnitLimit: 30_000,
337
+ instructions: [
338
+ getDeactivateLookupTableInstruction({ address, authority: signer }),
339
+ ],
340
+ });
341
+ deactivated += 1;
342
+ }
343
+ else if (currentSlot > deactivationSlot + COOLDOWN_SLOTS) {
344
+ await sendAndConfirm({
345
+ rpc,
346
+ rpcSubscriptions,
347
+ signer,
348
+ commitment,
349
+ computeUnitLimit: 30_000,
350
+ instructions: [
351
+ getCloseLookupTableInstruction({
352
+ address,
353
+ authority: signer,
354
+ recipient: signer.address,
355
+ }),
356
+ ],
357
+ });
358
+ closed += 1;
359
+ }
360
+ }
361
+ catch {
362
+ // best-effort: a racing close / not-yet-cooled table just gets retried
363
+ // on the next cleanup pass.
364
+ }
365
+ }
366
+ return {
367
+ deactivated,
368
+ closed,
369
+ candidates: candidates.size,
370
+ scannedSignatures: sigs.length,
371
+ };
372
+ }