@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,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
|
+
}
|