@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.
- package/README.md +536 -87
- package/dist/ACTPClient.d.ts +200 -0
- package/dist/ACTPClient.d.ts.map +1 -1
- package/dist/ACTPClient.js +266 -2
- package/dist/ACTPClient.js.map +1 -1
- package/dist/abi/ACTPKernel.json +16 -0
- package/dist/adapters/AdapterRegistry.d.ts +140 -0
- package/dist/adapters/AdapterRegistry.d.ts.map +1 -0
- package/dist/adapters/AdapterRegistry.js +166 -0
- package/dist/adapters/AdapterRegistry.js.map +1 -0
- package/dist/adapters/AdapterRouter.d.ts +165 -0
- package/dist/adapters/AdapterRouter.d.ts.map +1 -0
- package/dist/adapters/AdapterRouter.js +350 -0
- package/dist/adapters/AdapterRouter.js.map +1 -0
- package/dist/adapters/BaseAdapter.d.ts +17 -0
- package/dist/adapters/BaseAdapter.d.ts.map +1 -1
- package/dist/adapters/BaseAdapter.js +21 -0
- package/dist/adapters/BaseAdapter.js.map +1 -1
- package/dist/adapters/BasicAdapter.d.ts +72 -3
- package/dist/adapters/BasicAdapter.d.ts.map +1 -1
- package/dist/adapters/BasicAdapter.js +178 -2
- package/dist/adapters/BasicAdapter.js.map +1 -1
- package/dist/adapters/IAdapter.d.ts +230 -0
- package/dist/adapters/IAdapter.d.ts.map +1 -0
- package/dist/adapters/IAdapter.js +44 -0
- package/dist/adapters/IAdapter.js.map +1 -0
- package/dist/adapters/StandardAdapter.d.ts +80 -6
- package/dist/adapters/StandardAdapter.d.ts.map +1 -1
- package/dist/adapters/StandardAdapter.js +203 -6
- package/dist/adapters/StandardAdapter.js.map +1 -1
- package/dist/adapters/X402Adapter.d.ts +208 -0
- package/dist/adapters/X402Adapter.d.ts.map +1 -0
- package/dist/adapters/X402Adapter.js +423 -0
- package/dist/adapters/X402Adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +19 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +146 -4
- package/dist/cli/commands/init.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 +27 -12
- package/dist/config/networks.js.map +1 -1
- package/dist/erc8004/ERC8004Bridge.d.ts +155 -0
- package/dist/erc8004/ERC8004Bridge.d.ts.map +1 -0
- package/dist/erc8004/ERC8004Bridge.js +325 -0
- package/dist/erc8004/ERC8004Bridge.js.map +1 -0
- package/dist/erc8004/ReputationReporter.d.ts +223 -0
- package/dist/erc8004/ReputationReporter.d.ts.map +1 -0
- package/dist/erc8004/ReputationReporter.js +266 -0
- package/dist/erc8004/ReputationReporter.js.map +1 -0
- package/dist/erc8004/index.d.ts +36 -0
- package/dist/erc8004/index.d.ts.map +1 -0
- package/dist/erc8004/index.js +46 -0
- package/dist/erc8004/index.js.map +1 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -2
- 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 +1 -1
- package/dist/protocol/ACTPKernel.d.ts.map +1 -1
- package/dist/protocol/ACTPKernel.js +23 -12
- 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 +40 -22
- package/dist/runtime/BlockchainRuntime.js.map +1 -1
- package/dist/runtime/IACTPRuntime.d.ts +21 -0
- package/dist/runtime/IACTPRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.d.ts +19 -0
- package/dist/runtime/MockRuntime.d.ts.map +1 -1
- package/dist/runtime/MockRuntime.js +56 -4
- package/dist/runtime/MockRuntime.js.map +1 -1
- package/dist/runtime/types/MockState.d.ts +11 -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/adapter.d.ts +359 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +115 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/erc8004.d.ts +184 -0
- package/dist/types/erc8004.d.ts.map +1 -0
- package/dist/types/erc8004.js +132 -0
- package/dist/types/erc8004.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- 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/types/transaction.d.ts +12 -0
- package/dist/types/transaction.d.ts.map +1 -1
- package/dist/types/x402.d.ts +162 -0
- package/dist/types/x402.d.ts.map +1 -0
- package/dist/types/x402.js +162 -0
- package/dist/types/x402.js.map +1 -0
- 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 +16 -3
- package/src/ACTPClient.ts +318 -2
- package/src/abi/ACTPKernel.json +16 -0
- package/src/adapters/AdapterRegistry.ts +173 -0
- package/src/adapters/AdapterRouter.ts +417 -0
- package/src/adapters/BaseAdapter.ts +25 -0
- package/src/adapters/BasicAdapter.ts +210 -3
- package/src/adapters/IAdapter.ts +292 -0
- package/src/adapters/StandardAdapter.ts +246 -7
- package/src/adapters/X402Adapter.ts +653 -0
- package/src/adapters/index.ts +27 -0
- package/src/cli/commands/init.ts +166 -3
- package/src/config/networks.ts +36 -12
- package/src/erc8004/ERC8004Bridge.ts +461 -0
- package/src/erc8004/ReputationReporter.ts +472 -0
- package/src/erc8004/index.ts +61 -0
- package/src/index.ts +97 -0
- package/src/level0/provide.ts +2 -1
- package/src/level1/Agent.ts +13 -3
- package/src/protocol/ACTPKernel.ts +33 -12
- 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 +41 -45
- package/src/runtime/IACTPRuntime.ts +22 -0
- package/src/runtime/MockRuntime.ts +58 -4
- package/src/runtime/types/MockState.ts +12 -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/adapter.ts +296 -0
- package/src/types/erc8004.ts +293 -0
- package/src/types/index.ts +3 -0
- package/src/types/state.ts +10 -9
- package/src/types/transaction.ts +12 -0
- package/src/types/x402.ts +219 -0
- 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,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
|
+
}
|