@agirails/sdk 2.0.4 → 2.2.1

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