@agirails/sdk 2.0.4 → 2.2.0
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 +536 -87
- package/dist/adapters/BasicAdapter.d.ts.map +1 -1
- package/dist/adapters/BasicAdapter.js +8 -0
- package/dist/adapters/BasicAdapter.js.map +1 -1
- package/dist/adapters/StandardAdapter.d.ts +10 -5
- package/dist/adapters/StandardAdapter.d.ts.map +1 -1
- package/dist/adapters/StandardAdapter.js +19 -6
- package/dist/adapters/StandardAdapter.js.map +1 -1
- package/dist/config/networks.d.ts +9 -0
- package/dist/config/networks.d.ts.map +1 -1
- package/dist/config/networks.js +25 -10
- package/dist/config/networks.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/level0/provide.d.ts.map +1 -1
- package/dist/level0/provide.js +2 -1
- package/dist/level0/provide.js.map +1 -1
- package/dist/level1/Agent.d.ts.map +1 -1
- package/dist/level1/Agent.js +11 -3
- package/dist/level1/Agent.js.map +1 -1
- package/dist/protocol/ACTPKernel.d.ts.map +1 -1
- package/dist/protocol/ACTPKernel.js +7 -5
- package/dist/protocol/ACTPKernel.js.map +1 -1
- package/dist/protocol/DIDResolver.js +1 -1
- package/dist/protocol/DIDResolver.js.map +1 -1
- package/dist/protocol/EASHelper.d.ts.map +1 -1
- package/dist/protocol/EASHelper.js +2 -3
- package/dist/protocol/EASHelper.js.map +1 -1
- package/dist/protocol/MessageSigner.d.ts.map +1 -1
- package/dist/protocol/MessageSigner.js +8 -8
- package/dist/protocol/MessageSigner.js.map +1 -1
- package/dist/runtime/BlockchainRuntime.d.ts +7 -0
- package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
- package/dist/runtime/BlockchainRuntime.js +38 -22
- package/dist/runtime/BlockchainRuntime.js.map +1 -1
- package/dist/runtime/IACTPRuntime.d.ts +15 -0
- package/dist/runtime/IACTPRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.d.ts +7 -0
- package/dist/runtime/MockRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.js +15 -4
- package/dist/runtime/MockRuntime.js.map +1 -1
- package/dist/runtime/types/MockState.d.ts +5 -2
- package/dist/runtime/types/MockState.d.ts.map +1 -1
- package/dist/runtime/types/MockState.js.map +1 -1
- package/dist/storage/ArchiveBundleBuilder.d.ts +150 -0
- package/dist/storage/ArchiveBundleBuilder.d.ts.map +1 -0
- package/dist/storage/ArchiveBundleBuilder.js +468 -0
- package/dist/storage/ArchiveBundleBuilder.js.map +1 -0
- package/dist/storage/ArweaveClient.d.ts +271 -0
- package/dist/storage/ArweaveClient.d.ts.map +1 -0
- package/dist/storage/ArweaveClient.js +761 -0
- package/dist/storage/ArweaveClient.js.map +1 -0
- package/dist/storage/FilebaseClient.d.ts +193 -0
- package/dist/storage/FilebaseClient.d.ts.map +1 -0
- package/dist/storage/FilebaseClient.js +643 -0
- package/dist/storage/FilebaseClient.js.map +1 -0
- package/dist/storage/index.d.ts +47 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +64 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/types.d.ts +291 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +18 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/types/state.d.ts +5 -4
- package/dist/types/state.d.ts.map +1 -1
- package/dist/types/state.js +10 -9
- package/dist/types/state.js.map +1 -1
- package/dist/utils/IPFSClient.d.ts.map +1 -1
- package/dist/utils/IPFSClient.js +5 -2
- package/dist/utils/IPFSClient.js.map +1 -1
- package/dist/utils/NonceManager.d.ts.map +1 -1
- package/dist/utils/NonceManager.js +3 -2
- package/dist/utils/NonceManager.js.map +1 -1
- package/dist/utils/UsedAttestationTracker.d.ts.map +1 -1
- package/dist/utils/UsedAttestationTracker.js +3 -3
- package/dist/utils/UsedAttestationTracker.js.map +1 -1
- package/dist/utils/circuitBreaker.d.ts +136 -0
- package/dist/utils/circuitBreaker.d.ts.map +1 -0
- package/dist/utils/circuitBreaker.js +253 -0
- package/dist/utils/circuitBreaker.js.map +1 -0
- package/dist/utils/retry.d.ts +120 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +260 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/validation.d.ts +100 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +248 -1
- package/dist/utils/validation.js.map +1 -1
- package/package.json +14 -2
- package/src/adapters/BasicAdapter.ts +11 -0
- package/src/adapters/StandardAdapter.ts +26 -6
- package/src/config/networks.ts +34 -10
- package/src/index.ts +54 -0
- package/src/level0/provide.ts +2 -1
- package/src/level1/Agent.ts +13 -3
- package/src/protocol/ACTPKernel.ts +7 -5
- package/src/protocol/DIDResolver.ts +1 -1
- package/src/protocol/EASHelper.ts +2 -5
- package/src/protocol/MessageSigner.ts +8 -14
- package/src/runtime/BlockchainRuntime.ts +39 -45
- package/src/runtime/IACTPRuntime.ts +16 -0
- package/src/runtime/MockRuntime.ts +16 -4
- package/src/runtime/types/MockState.ts +5 -2
- package/src/storage/ArchiveBundleBuilder.ts +563 -0
- package/src/storage/ArweaveClient.ts +945 -0
- package/src/storage/FilebaseClient.ts +790 -0
- package/src/storage/index.ts +96 -0
- package/src/storage/types.ts +348 -0
- package/src/types/state.ts +10 -9
- package/src/utils/IPFSClient.ts +5 -4
- package/src/utils/NonceManager.ts +3 -2
- package/src/utils/UsedAttestationTracker.ts +3 -5
- package/src/utils/circuitBreaker.ts +324 -0
- package/src/utils/fsSafe.ts +5 -0
- package/src/utils/retry.ts +365 -0
- package/src/utils/validation.ts +295 -1
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ArweaveClient - Permanent Storage via Irys (AIP-7 §4.3)
|
|
4
|
+
*
|
|
5
|
+
* Provides permanent storage on Arweave via Irys (formerly Bundlr).
|
|
6
|
+
* Used for archiving settled transaction bundles for compliance.
|
|
7
|
+
*
|
|
8
|
+
* Security Features (Post-Audit):
|
|
9
|
+
* - Gateway URL whitelist (SSRF protection)
|
|
10
|
+
* - Download size limits (DoS protection)
|
|
11
|
+
* - Credential sanitization in errors
|
|
12
|
+
* - Retry with exponential backoff
|
|
13
|
+
* - Circuit breaker for gateway health tracking
|
|
14
|
+
*
|
|
15
|
+
* @module storage/ArweaveClient
|
|
16
|
+
*
|
|
17
|
+
* Key Principle: Arweave-First Write Order
|
|
18
|
+
* 1. Write to Arweave FIRST -> Get Arweave TX ID
|
|
19
|
+
* 2. THEN anchor TX ID on-chain
|
|
20
|
+
*
|
|
21
|
+
* @see AIP-7 §4.1 for invariant explanation
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.ArweaveClient = void 0;
|
|
28
|
+
const sdk_1 = __importDefault(require("@irys/sdk"));
|
|
29
|
+
const errors_1 = require("../errors");
|
|
30
|
+
const types_1 = require("./types");
|
|
31
|
+
const validation_1 = require("../utils/validation");
|
|
32
|
+
const retry_1 = require("../utils/retry");
|
|
33
|
+
const circuitBreaker_1 = require("../utils/circuitBreaker");
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Constants
|
|
36
|
+
// ============================================================================
|
|
37
|
+
const DEFAULT_CURRENCY = 'base-eth';
|
|
38
|
+
const DEFAULT_NETWORK = 'mainnet';
|
|
39
|
+
const DEFAULT_TIMEOUT = 60000; // 60 seconds
|
|
40
|
+
const DEFAULT_MAX_DOWNLOAD_SIZE = 10 * 1024 * 1024; // 10MB for archive bundles
|
|
41
|
+
const ARWEAVE_GATEWAY = 'https://arweave.net/';
|
|
42
|
+
// Minimum funding amount (to avoid dust)
|
|
43
|
+
const MIN_FUNDING_AMOUNT = 1000n; // 1000 wei minimum
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// ArweaveClient Class
|
|
46
|
+
// ============================================================================
|
|
47
|
+
/**
|
|
48
|
+
* ArweaveClient - Permanent storage on Arweave via Irys
|
|
49
|
+
*
|
|
50
|
+
* Used for:
|
|
51
|
+
* - Archiving settled transaction bundles (compliance)
|
|
52
|
+
* - 7-year retention requirement (Arweave guarantees 200+ years)
|
|
53
|
+
*
|
|
54
|
+
* IMPORTANT: Uses Base ETH for payments (no bridging required)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const client = await ArweaveClient.create({
|
|
59
|
+
* privateKey: process.env.ARCHIVE_UPLOADER_KEY!,
|
|
60
|
+
* rpcUrl: process.env.BASE_RPC_URL!
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // Check balance
|
|
64
|
+
* const balance = await client.getBalance();
|
|
65
|
+
* console.log('Irys balance:', balance);
|
|
66
|
+
*
|
|
67
|
+
* // Fund if needed
|
|
68
|
+
* if (balance < 10000n) {
|
|
69
|
+
* await client.fund(100000n);
|
|
70
|
+
* }
|
|
71
|
+
*
|
|
72
|
+
* // Upload archive bundle
|
|
73
|
+
* const result = await client.uploadBundle(archiveBundle);
|
|
74
|
+
* console.log('Arweave TX ID:', result.txId);
|
|
75
|
+
*
|
|
76
|
+
* // Download archive bundle
|
|
77
|
+
* const downloaded = await client.downloadBundle(result.txId);
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
class ArweaveClient {
|
|
81
|
+
/**
|
|
82
|
+
* Get initialized Irys instance (throws if not initialized)
|
|
83
|
+
*/
|
|
84
|
+
get irys() {
|
|
85
|
+
if (!this.initialized || !this._irys) {
|
|
86
|
+
throw new errors_1.StorageError('operation', 'ArweaveClient not initialized. Call create() first.');
|
|
87
|
+
}
|
|
88
|
+
return this._irys;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Private constructor - use ArweaveClient.create() factory
|
|
92
|
+
*/
|
|
93
|
+
constructor(config) {
|
|
94
|
+
this._irys = null;
|
|
95
|
+
this.initialized = false;
|
|
96
|
+
// Validate required config
|
|
97
|
+
if (!config.privateKey) {
|
|
98
|
+
throw new errors_1.ValidationError('privateKey', 'Private key is required for Arweave uploads');
|
|
99
|
+
}
|
|
100
|
+
if (!config.rpcUrl) {
|
|
101
|
+
throw new errors_1.ValidationError('rpcUrl', 'RPC URL is required for payment chain');
|
|
102
|
+
}
|
|
103
|
+
// P0-1: Validate default gateway URL against whitelist
|
|
104
|
+
(0, validation_1.validateGatewayURL)(ARWEAVE_GATEWAY, validation_1.ALLOWED_ARWEAVE_GATEWAYS, 'gatewayUrl');
|
|
105
|
+
this.config = {
|
|
106
|
+
privateKey: config.privateKey,
|
|
107
|
+
rpcUrl: config.rpcUrl,
|
|
108
|
+
currency: config.currency || DEFAULT_CURRENCY,
|
|
109
|
+
network: config.network || DEFAULT_NETWORK,
|
|
110
|
+
timeout: config.timeout || DEFAULT_TIMEOUT
|
|
111
|
+
};
|
|
112
|
+
// P1-1: DoS protection
|
|
113
|
+
this.maxDownloadSize = DEFAULT_MAX_DOWNLOAD_SIZE;
|
|
114
|
+
// P1-2: Default retry options
|
|
115
|
+
this.retryOptions = {
|
|
116
|
+
maxAttempts: 3,
|
|
117
|
+
initialDelayMs: 2000,
|
|
118
|
+
maxDelayMs: 30000,
|
|
119
|
+
backoffMultiplier: 2
|
|
120
|
+
};
|
|
121
|
+
// Circuit breaker for gateway health tracking
|
|
122
|
+
this.circuitBreakerEnabled = config.circuitBreaker?.enabled !== false;
|
|
123
|
+
if (this.circuitBreakerEnabled) {
|
|
124
|
+
this.circuitBreaker = new circuitBreaker_1.GatewayCircuitBreaker({
|
|
125
|
+
failureThreshold: config.circuitBreaker?.failureThreshold,
|
|
126
|
+
resetTimeoutMs: config.circuitBreaker?.resetTimeoutMs,
|
|
127
|
+
failureWindowMs: config.circuitBreaker?.failureWindowMs,
|
|
128
|
+
successThreshold: config.circuitBreaker?.successThreshold
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.circuitBreaker = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Create and initialize ArweaveClient
|
|
137
|
+
*
|
|
138
|
+
* @param config - Arweave configuration
|
|
139
|
+
* @returns Initialized ArweaveClient
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const client = await ArweaveClient.create({
|
|
144
|
+
* privateKey: process.env.ARCHIVE_UPLOADER_KEY!,
|
|
145
|
+
* rpcUrl: 'https://mainnet.base.org'
|
|
146
|
+
* });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
static async create(config) {
|
|
150
|
+
const client = new ArweaveClient(config);
|
|
151
|
+
await client.initialize();
|
|
152
|
+
return client;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Initialize Irys SDK connection
|
|
156
|
+
*/
|
|
157
|
+
async initialize() {
|
|
158
|
+
if (this.initialized)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
this._irys = new sdk_1.default({
|
|
162
|
+
network: this.config.network,
|
|
163
|
+
token: this.config.currency,
|
|
164
|
+
key: this.config.privateKey,
|
|
165
|
+
config: {
|
|
166
|
+
providerUrl: this.config.rpcUrl
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Connect to Irys node
|
|
170
|
+
await this._irys.ready();
|
|
171
|
+
this.initialized = true;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
// P0-2: Sanitize error message (may contain credentials)
|
|
175
|
+
throw new errors_1.StorageError('initialization', `Failed to initialize Irys client: ${(0, validation_1.sanitizeErrorMessage)(error)}`, { currency: this.config.currency, network: this.config.network });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ==========================================================================
|
|
179
|
+
// Funding Methods
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
/**
|
|
182
|
+
* Fund the Irys node (required before uploading)
|
|
183
|
+
*
|
|
184
|
+
* @param amount - Amount to fund in payment currency (wei for ETH)
|
|
185
|
+
* @throws {ValidationError} If amount is invalid
|
|
186
|
+
* @throws {StorageError} If funding fails
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* // Fund with 0.001 ETH (1e15 wei)
|
|
191
|
+
* await client.fund(1000000000000000n);
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
async fund(amount) {
|
|
195
|
+
if (amount < MIN_FUNDING_AMOUNT) {
|
|
196
|
+
throw new errors_1.ValidationError('amount', `Funding amount too small. Minimum: ${MIN_FUNDING_AMOUNT} wei`);
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
await this.withTimeout(this.irys.fund(amount), this.config.timeout, 'fund');
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error instanceof errors_1.ArweaveTimeoutError)
|
|
203
|
+
throw error;
|
|
204
|
+
// P0-2: Sanitize error message (may contain credentials)
|
|
205
|
+
throw new errors_1.StorageError('funding', `Failed to fund Irys: ${(0, validation_1.sanitizeErrorMessage)(error)}`, {
|
|
206
|
+
amount: amount.toString(),
|
|
207
|
+
currency: this.config.currency
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Get current Irys balance
|
|
213
|
+
*
|
|
214
|
+
* @returns Balance in payment currency (wei for ETH)
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const balance = await client.getBalance();
|
|
219
|
+
* console.log('Balance:', balance, 'wei');
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
async getBalance() {
|
|
223
|
+
try {
|
|
224
|
+
const balance = await this.irys.getLoadedBalance();
|
|
225
|
+
return BigInt(balance.toString());
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
// P0-2: Sanitize error message
|
|
229
|
+
throw new errors_1.StorageError('balance', `Failed to get Irys balance: ${(0, validation_1.sanitizeErrorMessage)(error)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Estimate cost of uploading data
|
|
234
|
+
*
|
|
235
|
+
* @param sizeBytes - Size of data to upload in bytes
|
|
236
|
+
* @returns Cost in payment currency (wei for ETH)
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const cost = await client.estimateCost(50 * 1024); // 50KB
|
|
241
|
+
* console.log('Upload cost:', cost, 'wei');
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
async estimateCost(sizeBytes) {
|
|
245
|
+
if (sizeBytes <= 0) {
|
|
246
|
+
throw new errors_1.ValidationError('sizeBytes', 'Size must be positive');
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const price = await this.irys.getPrice(sizeBytes);
|
|
250
|
+
return BigInt(price.toString());
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
// P0-2: Sanitize error message
|
|
254
|
+
throw new errors_1.StorageError('estimate', `Failed to estimate cost: ${(0, validation_1.sanitizeErrorMessage)(error)}`, {
|
|
255
|
+
sizeBytes
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ==========================================================================
|
|
260
|
+
// Upload Methods
|
|
261
|
+
// ==========================================================================
|
|
262
|
+
/**
|
|
263
|
+
* Upload archive bundle to Arweave (permanent storage)
|
|
264
|
+
*
|
|
265
|
+
* IMPORTANT: This is the first step in the archive flow.
|
|
266
|
+
* After getting the TX ID, anchor it on-chain via ArchiveTreasury.anchorArchive()
|
|
267
|
+
*
|
|
268
|
+
* @param bundle - Archive bundle to upload
|
|
269
|
+
* @returns Upload result with Arweave TX ID
|
|
270
|
+
* @throws {InsufficientBalanceError} If Irys balance is too low
|
|
271
|
+
* @throws {ArweaveUploadError} If upload fails
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* ```typescript
|
|
275
|
+
* const result = await client.uploadBundle(bundle);
|
|
276
|
+
* console.log('Arweave TX ID:', result.txId);
|
|
277
|
+
*
|
|
278
|
+
* // Then anchor on-chain
|
|
279
|
+
* await archiveTreasury.anchorArchive(bundle.txId, result.txId);
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
async uploadBundle(bundle) {
|
|
283
|
+
// Validate bundle
|
|
284
|
+
this.validateBundle(bundle);
|
|
285
|
+
// Serialize bundle
|
|
286
|
+
const jsonString = JSON.stringify(bundle);
|
|
287
|
+
const buffer = Buffer.from(jsonString, 'utf-8');
|
|
288
|
+
// Check balance
|
|
289
|
+
const cost = await this.estimateCost(buffer.length);
|
|
290
|
+
const balance = await this.getBalance();
|
|
291
|
+
if (balance < cost) {
|
|
292
|
+
throw new errors_1.InsufficientBalanceError(cost.toString(), balance.toString(), this.config.currency.toUpperCase());
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
// Upload with tags for discoverability
|
|
296
|
+
const tx = await this.withTimeout(this.irys.upload(buffer, {
|
|
297
|
+
tags: [
|
|
298
|
+
{ name: 'Content-Type', value: 'application/json' },
|
|
299
|
+
{ name: 'Protocol', value: 'AGIRAILS' },
|
|
300
|
+
{ name: 'Version', value: bundle.protocolVersion },
|
|
301
|
+
{ name: 'Schema', value: bundle.archiveSchemaVersion },
|
|
302
|
+
{ name: 'Type', value: bundle.type },
|
|
303
|
+
{ name: 'ChainId', value: bundle.chainId.toString() },
|
|
304
|
+
{ name: 'TxId', value: bundle.txId }
|
|
305
|
+
]
|
|
306
|
+
}), this.config.timeout, 'upload');
|
|
307
|
+
return {
|
|
308
|
+
txId: tx.id,
|
|
309
|
+
size: buffer.length,
|
|
310
|
+
uploadedAt: new Date(),
|
|
311
|
+
cost: cost.toString()
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error instanceof errors_1.ArweaveTimeoutError)
|
|
316
|
+
throw error;
|
|
317
|
+
if (error instanceof errors_1.InsufficientBalanceError)
|
|
318
|
+
throw error;
|
|
319
|
+
throw new errors_1.ArweaveUploadError(error.message || 'Upload failed', {
|
|
320
|
+
bundleTxId: bundle.txId,
|
|
321
|
+
size: buffer.length
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Upload arbitrary JSON to Arweave
|
|
327
|
+
*
|
|
328
|
+
* For general-purpose permanent storage (not archive bundles).
|
|
329
|
+
*
|
|
330
|
+
* @param data - JSON data to upload
|
|
331
|
+
* @param tags - Optional tags for discoverability
|
|
332
|
+
* @returns Upload result with Arweave TX ID
|
|
333
|
+
*/
|
|
334
|
+
async uploadJSON(data, tags) {
|
|
335
|
+
const jsonString = JSON.stringify(data);
|
|
336
|
+
const buffer = Buffer.from(jsonString, 'utf-8');
|
|
337
|
+
// Check balance
|
|
338
|
+
const cost = await this.estimateCost(buffer.length);
|
|
339
|
+
const balance = await this.getBalance();
|
|
340
|
+
if (balance < cost) {
|
|
341
|
+
throw new errors_1.InsufficientBalanceError(cost.toString(), balance.toString(), this.config.currency.toUpperCase());
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const defaultTags = [
|
|
345
|
+
{ name: 'Content-Type', value: 'application/json' },
|
|
346
|
+
{ name: 'Protocol', value: 'AGIRAILS' }
|
|
347
|
+
];
|
|
348
|
+
const tx = await this.withTimeout(this.irys.upload(buffer, {
|
|
349
|
+
tags: tags ? [...defaultTags, ...tags] : defaultTags
|
|
350
|
+
}), this.config.timeout, 'upload');
|
|
351
|
+
return {
|
|
352
|
+
txId: tx.id,
|
|
353
|
+
size: buffer.length,
|
|
354
|
+
uploadedAt: new Date(),
|
|
355
|
+
cost: cost.toString()
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (error instanceof errors_1.ArweaveTimeoutError)
|
|
360
|
+
throw error;
|
|
361
|
+
if (error instanceof errors_1.InsufficientBalanceError)
|
|
362
|
+
throw error;
|
|
363
|
+
throw new errors_1.ArweaveUploadError(error.message || 'JSON upload failed', {
|
|
364
|
+
size: buffer.length
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ==========================================================================
|
|
369
|
+
// Download Methods
|
|
370
|
+
// ==========================================================================
|
|
371
|
+
/**
|
|
372
|
+
* Download archive bundle from Arweave
|
|
373
|
+
*
|
|
374
|
+
* Security Features:
|
|
375
|
+
* - Size limit enforcement (P1-1: DoS protection)
|
|
376
|
+
* - Retry with exponential backoff (P1-2)
|
|
377
|
+
* - Centralized TX ID validation (P1-3)
|
|
378
|
+
* - Credential sanitization in errors (P0-2)
|
|
379
|
+
*
|
|
380
|
+
* @param txId - Arweave transaction ID
|
|
381
|
+
* @returns Downloaded archive bundle
|
|
382
|
+
* @throws {InvalidArweaveTxIdError} If TX ID format is invalid
|
|
383
|
+
* @throws {FileSizeLimitExceededError} If content exceeds size limit
|
|
384
|
+
* @throws {ArweaveDownloadError} If download fails
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```typescript
|
|
388
|
+
* const result = await client.downloadBundle('h7Xk2...');
|
|
389
|
+
* console.log('Bundle:', result.data);
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
async downloadBundle(txId) {
|
|
393
|
+
// P1-3: Use centralized TX ID validation
|
|
394
|
+
(0, validation_1.validateArweaveTxId)(txId);
|
|
395
|
+
const url = `${ARWEAVE_GATEWAY}${txId}`;
|
|
396
|
+
// Circuit breaker: check gateway health before attempting download
|
|
397
|
+
if (this.circuitBreaker && !this.circuitBreaker.isHealthy(ARWEAVE_GATEWAY)) {
|
|
398
|
+
const state = this.circuitBreaker.getState(ARWEAVE_GATEWAY);
|
|
399
|
+
const failures = this.circuitBreaker.getFailureCount(ARWEAVE_GATEWAY);
|
|
400
|
+
throw new errors_1.ArweaveDownloadError(txId, `Gateway circuit breaker OPEN for ${ARWEAVE_GATEWAY}. ` +
|
|
401
|
+
`State: ${state}, Failures: ${failures}. ` +
|
|
402
|
+
`Please wait for cooldown period before retrying.`);
|
|
403
|
+
}
|
|
404
|
+
// P1-2: Wrap in retry logic
|
|
405
|
+
return (0, retry_1.withRetry)(async () => {
|
|
406
|
+
const controller = new AbortController();
|
|
407
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
408
|
+
try {
|
|
409
|
+
const response = await fetch(url, {
|
|
410
|
+
signal: controller.signal,
|
|
411
|
+
headers: { 'Accept': 'application/json' }
|
|
412
|
+
});
|
|
413
|
+
clearTimeout(timeoutId);
|
|
414
|
+
if (!response.ok) {
|
|
415
|
+
throw new errors_1.ArweaveDownloadError(txId, `HTTP ${response.status}: ${response.statusText}`);
|
|
416
|
+
}
|
|
417
|
+
// P1-1: Check Content-Length header before downloading
|
|
418
|
+
const contentLength = response.headers.get('content-length');
|
|
419
|
+
if (contentLength && parseInt(contentLength, 10) > this.maxDownloadSize) {
|
|
420
|
+
throw new errors_1.FileSizeLimitExceededError(parseInt(contentLength, 10), this.maxDownloadSize);
|
|
421
|
+
}
|
|
422
|
+
// P1-1: Stream response with size limit enforcement
|
|
423
|
+
const reader = response.body?.getReader();
|
|
424
|
+
if (!reader) {
|
|
425
|
+
throw new errors_1.ArweaveDownloadError(txId, 'No response body');
|
|
426
|
+
}
|
|
427
|
+
const chunks = [];
|
|
428
|
+
let totalSize = 0;
|
|
429
|
+
while (true) {
|
|
430
|
+
const { done, value } = await reader.read();
|
|
431
|
+
if (done)
|
|
432
|
+
break;
|
|
433
|
+
totalSize += value.length;
|
|
434
|
+
// P1-1: Enforce size limit during streaming
|
|
435
|
+
if (totalSize > this.maxDownloadSize) {
|
|
436
|
+
reader.cancel();
|
|
437
|
+
throw new errors_1.FileSizeLimitExceededError(totalSize, this.maxDownloadSize);
|
|
438
|
+
}
|
|
439
|
+
chunks.push(value);
|
|
440
|
+
}
|
|
441
|
+
// Combine chunks into text
|
|
442
|
+
const decoder = new TextDecoder();
|
|
443
|
+
const text = chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') +
|
|
444
|
+
decoder.decode();
|
|
445
|
+
const data = JSON.parse(text);
|
|
446
|
+
// Validate it's actually an archive bundle
|
|
447
|
+
if (data.type !== types_1.ARCHIVE_BUNDLE_TYPE) {
|
|
448
|
+
throw new errors_1.ArweaveDownloadError(txId, `Invalid bundle type: ${data.type}. Expected: ${types_1.ARCHIVE_BUNDLE_TYPE}`);
|
|
449
|
+
}
|
|
450
|
+
// Circuit breaker: record success
|
|
451
|
+
this.circuitBreaker?.recordSuccess(ARWEAVE_GATEWAY);
|
|
452
|
+
return {
|
|
453
|
+
data,
|
|
454
|
+
size: totalSize,
|
|
455
|
+
downloadedAt: new Date()
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
clearTimeout(timeoutId);
|
|
460
|
+
// Circuit breaker: record failure only for gateway issues (not content errors)
|
|
461
|
+
if (this.isGatewayFailure(error)) {
|
|
462
|
+
this.circuitBreaker?.recordFailure(ARWEAVE_GATEWAY);
|
|
463
|
+
}
|
|
464
|
+
if (error instanceof errors_1.ArweaveDownloadError)
|
|
465
|
+
throw error;
|
|
466
|
+
if (error instanceof errors_1.FileSizeLimitExceededError)
|
|
467
|
+
throw error;
|
|
468
|
+
if (error.name === 'AbortError') {
|
|
469
|
+
throw new errors_1.ArweaveTimeoutError('download', this.config.timeout);
|
|
470
|
+
}
|
|
471
|
+
if (error instanceof SyntaxError) {
|
|
472
|
+
throw new errors_1.ArweaveDownloadError(txId, 'Invalid JSON content');
|
|
473
|
+
}
|
|
474
|
+
// P0-2: Sanitize error message
|
|
475
|
+
throw new errors_1.ArweaveDownloadError(txId, (0, validation_1.sanitizeErrorMessage)(error));
|
|
476
|
+
}
|
|
477
|
+
}, this.retryOptions);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Download arbitrary JSON from Arweave
|
|
481
|
+
*
|
|
482
|
+
* Security Features:
|
|
483
|
+
* - Size limit enforcement (P1-1: DoS protection)
|
|
484
|
+
* - Retry with exponential backoff (P1-2)
|
|
485
|
+
* - Centralized TX ID validation (P1-3)
|
|
486
|
+
*
|
|
487
|
+
* @param txId - Arweave transaction ID
|
|
488
|
+
* @returns Downloaded JSON data
|
|
489
|
+
*/
|
|
490
|
+
async downloadJSON(txId) {
|
|
491
|
+
// P1-3: Use centralized TX ID validation
|
|
492
|
+
(0, validation_1.validateArweaveTxId)(txId);
|
|
493
|
+
const url = `${ARWEAVE_GATEWAY}${txId}`;
|
|
494
|
+
// Circuit breaker: check gateway health before attempting download
|
|
495
|
+
if (this.circuitBreaker && !this.circuitBreaker.isHealthy(ARWEAVE_GATEWAY)) {
|
|
496
|
+
const state = this.circuitBreaker.getState(ARWEAVE_GATEWAY);
|
|
497
|
+
const failures = this.circuitBreaker.getFailureCount(ARWEAVE_GATEWAY);
|
|
498
|
+
throw new errors_1.ArweaveDownloadError(txId, `Gateway circuit breaker OPEN for ${ARWEAVE_GATEWAY}. ` +
|
|
499
|
+
`State: ${state}, Failures: ${failures}. ` +
|
|
500
|
+
`Please wait for cooldown period before retrying.`);
|
|
501
|
+
}
|
|
502
|
+
// P1-2: Wrap in retry logic
|
|
503
|
+
return (0, retry_1.withRetry)(async () => {
|
|
504
|
+
const controller = new AbortController();
|
|
505
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
506
|
+
try {
|
|
507
|
+
const response = await fetch(url, {
|
|
508
|
+
signal: controller.signal,
|
|
509
|
+
headers: { 'Accept': 'application/json' }
|
|
510
|
+
});
|
|
511
|
+
clearTimeout(timeoutId);
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
throw new errors_1.ArweaveDownloadError(txId, `HTTP ${response.status}: ${response.statusText}`);
|
|
514
|
+
}
|
|
515
|
+
// P1-1: Check Content-Length header before downloading
|
|
516
|
+
const contentLength = response.headers.get('content-length');
|
|
517
|
+
if (contentLength && parseInt(contentLength, 10) > this.maxDownloadSize) {
|
|
518
|
+
throw new errors_1.FileSizeLimitExceededError(parseInt(contentLength, 10), this.maxDownloadSize);
|
|
519
|
+
}
|
|
520
|
+
// P1-1: Stream response with size limit enforcement
|
|
521
|
+
const reader = response.body?.getReader();
|
|
522
|
+
if (!reader) {
|
|
523
|
+
throw new errors_1.ArweaveDownloadError(txId, 'No response body');
|
|
524
|
+
}
|
|
525
|
+
const chunks = [];
|
|
526
|
+
let totalSize = 0;
|
|
527
|
+
while (true) {
|
|
528
|
+
const { done, value } = await reader.read();
|
|
529
|
+
if (done)
|
|
530
|
+
break;
|
|
531
|
+
totalSize += value.length;
|
|
532
|
+
// P1-1: Enforce size limit during streaming
|
|
533
|
+
if (totalSize > this.maxDownloadSize) {
|
|
534
|
+
reader.cancel();
|
|
535
|
+
throw new errors_1.FileSizeLimitExceededError(totalSize, this.maxDownloadSize);
|
|
536
|
+
}
|
|
537
|
+
chunks.push(value);
|
|
538
|
+
}
|
|
539
|
+
// Combine chunks into text
|
|
540
|
+
const decoder = new TextDecoder();
|
|
541
|
+
const text = chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') +
|
|
542
|
+
decoder.decode();
|
|
543
|
+
const data = JSON.parse(text);
|
|
544
|
+
// Circuit breaker: record success
|
|
545
|
+
this.circuitBreaker?.recordSuccess(ARWEAVE_GATEWAY);
|
|
546
|
+
return {
|
|
547
|
+
data,
|
|
548
|
+
size: totalSize,
|
|
549
|
+
downloadedAt: new Date()
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
clearTimeout(timeoutId);
|
|
554
|
+
// Circuit breaker: record failure only for gateway issues (not content errors)
|
|
555
|
+
if (this.isGatewayFailure(error)) {
|
|
556
|
+
this.circuitBreaker?.recordFailure(ARWEAVE_GATEWAY);
|
|
557
|
+
}
|
|
558
|
+
if (error instanceof errors_1.ArweaveDownloadError)
|
|
559
|
+
throw error;
|
|
560
|
+
if (error instanceof errors_1.FileSizeLimitExceededError)
|
|
561
|
+
throw error;
|
|
562
|
+
if (error.name === 'AbortError') {
|
|
563
|
+
throw new errors_1.ArweaveTimeoutError('download', this.config.timeout);
|
|
564
|
+
}
|
|
565
|
+
if (error instanceof SyntaxError) {
|
|
566
|
+
throw new errors_1.ArweaveDownloadError(txId, 'Invalid JSON content');
|
|
567
|
+
}
|
|
568
|
+
// P0-2: Sanitize error message
|
|
569
|
+
throw new errors_1.ArweaveDownloadError(txId, (0, validation_1.sanitizeErrorMessage)(error));
|
|
570
|
+
}
|
|
571
|
+
}, this.retryOptions);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Check if content exists on Arweave
|
|
575
|
+
*
|
|
576
|
+
* @param txId - Arweave transaction ID
|
|
577
|
+
* @returns True if content exists
|
|
578
|
+
*/
|
|
579
|
+
async exists(txId) {
|
|
580
|
+
// P1-3: Use centralized TX ID validation
|
|
581
|
+
(0, validation_1.validateArweaveTxId)(txId);
|
|
582
|
+
const url = `${ARWEAVE_GATEWAY}${txId}`;
|
|
583
|
+
try {
|
|
584
|
+
const controller = new AbortController();
|
|
585
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
586
|
+
const response = await fetch(url, {
|
|
587
|
+
method: 'HEAD',
|
|
588
|
+
signal: controller.signal
|
|
589
|
+
});
|
|
590
|
+
clearTimeout(timeoutId);
|
|
591
|
+
return response.ok;
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// ==========================================================================
|
|
598
|
+
// Getters
|
|
599
|
+
// ==========================================================================
|
|
600
|
+
/**
|
|
601
|
+
* Get configured currency
|
|
602
|
+
*/
|
|
603
|
+
getCurrency() {
|
|
604
|
+
return this.config.currency;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Get configured network
|
|
608
|
+
*/
|
|
609
|
+
getNetwork() {
|
|
610
|
+
return this.config.network;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get wallet address
|
|
614
|
+
*/
|
|
615
|
+
async getAddress() {
|
|
616
|
+
return this.irys.address;
|
|
617
|
+
}
|
|
618
|
+
// ==========================================================================
|
|
619
|
+
// Private Methods
|
|
620
|
+
// ==========================================================================
|
|
621
|
+
/**
|
|
622
|
+
* Validate archive bundle structure
|
|
623
|
+
*/
|
|
624
|
+
validateBundle(bundle) {
|
|
625
|
+
if (!bundle) {
|
|
626
|
+
throw new errors_1.ValidationError('bundle', 'Archive bundle is required');
|
|
627
|
+
}
|
|
628
|
+
if (bundle.type !== types_1.ARCHIVE_BUNDLE_TYPE) {
|
|
629
|
+
throw new errors_1.ValidationError('bundle.type', `Invalid bundle type: ${bundle.type}. Expected: ${types_1.ARCHIVE_BUNDLE_TYPE}`);
|
|
630
|
+
}
|
|
631
|
+
if (!bundle.txId || !/^0x[a-fA-F0-9]{64}$/.test(bundle.txId)) {
|
|
632
|
+
throw new errors_1.ValidationError('bundle.txId', 'Invalid transaction ID format');
|
|
633
|
+
}
|
|
634
|
+
if (![8453, 84532].includes(bundle.chainId)) {
|
|
635
|
+
throw new errors_1.ValidationError('bundle.chainId', `Invalid chain ID: ${bundle.chainId}. Expected 8453 or 84532`);
|
|
636
|
+
}
|
|
637
|
+
if (!bundle.participants?.requester || !bundle.participants?.provider) {
|
|
638
|
+
throw new errors_1.ValidationError('bundle.participants', 'Both requester and provider required');
|
|
639
|
+
}
|
|
640
|
+
if (!bundle.references?.requestCID || !bundle.references?.deliveryCID) {
|
|
641
|
+
throw new errors_1.ValidationError('bundle.references', 'Both requestCID and deliveryCID required');
|
|
642
|
+
}
|
|
643
|
+
if (!bundle.hashes?.requestHash || !bundle.hashes?.deliveryHash || !bundle.hashes?.serviceHash) {
|
|
644
|
+
throw new errors_1.ValidationError('bundle.hashes', 'All hashes required');
|
|
645
|
+
}
|
|
646
|
+
if (!bundle.settlement) {
|
|
647
|
+
throw new errors_1.ValidationError('bundle.settlement', 'Settlement info required');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Wrap promise with timeout
|
|
652
|
+
*/
|
|
653
|
+
async withTimeout(promise, timeoutMs, operation) {
|
|
654
|
+
let timeoutId;
|
|
655
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
656
|
+
timeoutId = setTimeout(() => {
|
|
657
|
+
reject(new errors_1.ArweaveTimeoutError(operation, timeoutMs));
|
|
658
|
+
}, timeoutMs);
|
|
659
|
+
});
|
|
660
|
+
try {
|
|
661
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
clearTimeout(timeoutId);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Determine if an error represents a gateway failure vs content-specific error
|
|
669
|
+
*
|
|
670
|
+
* Gateway failures (should trigger circuit breaker):
|
|
671
|
+
* - 5xx HTTP errors (server errors)
|
|
672
|
+
* - Network timeouts
|
|
673
|
+
* - Connection refused
|
|
674
|
+
* - DNS resolution failures
|
|
675
|
+
*
|
|
676
|
+
* Content-specific errors (should NOT trigger circuit breaker):
|
|
677
|
+
* - 404 (content not found - not gateway's fault)
|
|
678
|
+
* - 400 (bad request - client's fault)
|
|
679
|
+
* - SyntaxError (invalid JSON - content issue)
|
|
680
|
+
* - Invalid bundle type (content validation failed)
|
|
681
|
+
*/
|
|
682
|
+
isGatewayFailure(error) {
|
|
683
|
+
// Network-level errors are always gateway failures
|
|
684
|
+
if (error.name === 'AbortError')
|
|
685
|
+
return true; // Timeout
|
|
686
|
+
if (error.code === 'ECONNREFUSED')
|
|
687
|
+
return true;
|
|
688
|
+
if (error.code === 'ENOTFOUND')
|
|
689
|
+
return true;
|
|
690
|
+
if (error.code === 'ETIMEDOUT')
|
|
691
|
+
return true;
|
|
692
|
+
// ArweaveTimeoutError is a gateway failure
|
|
693
|
+
if (error instanceof errors_1.ArweaveTimeoutError)
|
|
694
|
+
return true;
|
|
695
|
+
// Content-specific errors are NOT gateway failures
|
|
696
|
+
if (error instanceof SyntaxError)
|
|
697
|
+
return false; // Invalid JSON
|
|
698
|
+
if (error instanceof errors_1.FileSizeLimitExceededError)
|
|
699
|
+
return false;
|
|
700
|
+
// Check for HTTP status codes in ArweaveDownloadError
|
|
701
|
+
if (error instanceof errors_1.ArweaveDownloadError) {
|
|
702
|
+
const message = error.message || '';
|
|
703
|
+
// 5xx errors are gateway failures
|
|
704
|
+
if (/HTTP 5\d{2}/.test(message))
|
|
705
|
+
return true;
|
|
706
|
+
// 4xx errors are NOT gateway failures (content/client issues)
|
|
707
|
+
if (/HTTP 4\d{2}/.test(message))
|
|
708
|
+
return false;
|
|
709
|
+
// "Invalid bundle type" is content validation failure
|
|
710
|
+
if (message.includes('Invalid bundle type'))
|
|
711
|
+
return false;
|
|
712
|
+
// "Invalid JSON content" is content error
|
|
713
|
+
if (message.includes('Invalid JSON'))
|
|
714
|
+
return false;
|
|
715
|
+
// Default: treat unknown ArweaveDownloadError as potential gateway issue
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
// Generic errors (TypeError, etc.) could be network issues
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
// ==========================================================================
|
|
722
|
+
// Circuit Breaker Methods
|
|
723
|
+
// ==========================================================================
|
|
724
|
+
/**
|
|
725
|
+
* Get circuit breaker status for Arweave gateway
|
|
726
|
+
*
|
|
727
|
+
* @returns Circuit breaker status or null if disabled
|
|
728
|
+
*
|
|
729
|
+
* @example
|
|
730
|
+
* ```typescript
|
|
731
|
+
* const status = client.getCircuitBreakerStatus();
|
|
732
|
+
* if (status && status.state === 'OPEN') {
|
|
733
|
+
* console.log('Gateway unhealthy, failures:', status.failures);
|
|
734
|
+
* }
|
|
735
|
+
* ```
|
|
736
|
+
*/
|
|
737
|
+
getCircuitBreakerStatus() {
|
|
738
|
+
if (!this.circuitBreaker)
|
|
739
|
+
return null;
|
|
740
|
+
return {
|
|
741
|
+
state: this.circuitBreaker.getState(ARWEAVE_GATEWAY),
|
|
742
|
+
failures: this.circuitBreaker.getFailureCount(ARWEAVE_GATEWAY)
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Reset circuit breaker for Arweave gateway
|
|
747
|
+
*
|
|
748
|
+
* Use with caution - only reset when you're confident the gateway is healthy.
|
|
749
|
+
* Useful for testing or after manual verification.
|
|
750
|
+
*
|
|
751
|
+
* @example
|
|
752
|
+
* ```typescript
|
|
753
|
+
* client.resetCircuitBreaker();
|
|
754
|
+
* ```
|
|
755
|
+
*/
|
|
756
|
+
resetCircuitBreaker() {
|
|
757
|
+
this.circuitBreaker?.reset(ARWEAVE_GATEWAY);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
exports.ArweaveClient = ArweaveClient;
|
|
761
|
+
//# sourceMappingURL=ArweaveClient.js.map
|