@sequence0/sdk 1.0.0 → 1.0.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 (52) hide show
  1. package/README.md +36 -5
  2. package/dist/chains/bitcoin.d.ts +15 -3
  3. package/dist/chains/bitcoin.d.ts.map +1 -1
  4. package/dist/chains/bitcoin.js +23 -3
  5. package/dist/chains/bitcoin.js.map +1 -1
  6. package/dist/chains/ethereum.d.ts +3 -0
  7. package/dist/chains/ethereum.d.ts.map +1 -1
  8. package/dist/chains/ethereum.js +108 -16
  9. package/dist/chains/ethereum.js.map +1 -1
  10. package/dist/core/client.d.ts +117 -4
  11. package/dist/core/client.d.ts.map +1 -1
  12. package/dist/core/client.js +416 -36
  13. package/dist/core/client.js.map +1 -1
  14. package/dist/core/types.d.ts +39 -2
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +11 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +31 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/utils/discovery.d.ts +15 -1
  21. package/dist/utils/discovery.d.ts.map +1 -1
  22. package/dist/utils/discovery.js +36 -9
  23. package/dist/utils/discovery.js.map +1 -1
  24. package/dist/utils/errors.d.ts +26 -0
  25. package/dist/utils/errors.d.ts.map +1 -1
  26. package/dist/utils/errors.js +32 -1
  27. package/dist/utils/errors.js.map +1 -1
  28. package/dist/utils/fee.d.ts +107 -0
  29. package/dist/utils/fee.d.ts.map +1 -0
  30. package/dist/utils/fee.js +220 -0
  31. package/dist/utils/fee.js.map +1 -0
  32. package/dist/utils/http.d.ts +97 -1
  33. package/dist/utils/http.d.ts.map +1 -1
  34. package/dist/utils/http.js +238 -6
  35. package/dist/utils/http.js.map +1 -1
  36. package/dist/utils/logger.d.ts +43 -0
  37. package/dist/utils/logger.d.ts.map +1 -0
  38. package/dist/utils/logger.js +129 -0
  39. package/dist/utils/logger.js.map +1 -0
  40. package/dist/utils/rate-limiter.d.ts +43 -0
  41. package/dist/utils/rate-limiter.d.ts.map +1 -0
  42. package/dist/utils/rate-limiter.js +99 -0
  43. package/dist/utils/rate-limiter.js.map +1 -0
  44. package/dist/utils/validation.d.ts +74 -0
  45. package/dist/utils/validation.d.ts.map +1 -0
  46. package/dist/utils/validation.js +380 -0
  47. package/dist/utils/validation.js.map +1 -0
  48. package/dist/wallet/wallet.d.ts +12 -1
  49. package/dist/wallet/wallet.d.ts.map +1 -1
  50. package/dist/wallet/wallet.js +76 -11
  51. package/dist/wallet/wallet.js.map +1 -1
  52. package/package.json +8 -2
@@ -25,13 +25,18 @@
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
27
  exports.Sequence0 = void 0;
28
+ const ethers_1 = require("ethers");
28
29
  const wallet_1 = require("../wallet/wallet");
29
30
  const http_1 = require("../utils/http");
30
31
  const websocket_1 = require("../utils/websocket");
31
32
  const discovery_1 = require("../utils/discovery");
33
+ const fee_1 = require("../utils/fee");
32
34
  const errors_1 = require("../utils/errors");
35
+ const validation_1 = require("../utils/validation");
33
36
  /** Default agent URL for testnet (we run these ourselves) */
34
37
  const TESTNET_AGENT_URL = 'http://3.140.248.117:8080';
38
+ /** HTTP status codes that warrant trying a different agent */
39
+ const FAILOVER_STATUS_CODES = new Set([502, 503, 504]);
35
40
  /** Sequence0 chain RPC URLs for on-chain agent discovery */
36
41
  const SEQUENCE_RPC = {
37
42
  testnet: 'https://testnet-rpc.sequence0.network',
@@ -40,7 +45,12 @@ const SEQUENCE_RPC = {
40
45
  /** AgentRegistry contract addresses */
41
46
  const REGISTRY_ADDRESSES = {
42
47
  testnet: '0xf8d7cc35a22882bb8ac36b9ccadd08e852404377',
43
- mainnet: '0xb2c34de56df1041887de1d1103f1a6aa439e4980',
48
+ mainnet: '0x8F642dd318C7A7CD2B295dC4d266C3d2e4838B33',
49
+ };
50
+ /** FeeCollector contract addresses */
51
+ const FEE_COLLECTOR_ADDRESSES = {
52
+ testnet: '0x0000000000000000000000000000000000000000', // No fee on testnet
53
+ mainnet: '0x7253ff9f45d1bF8Ed0A23Dfb4E6308adcb1E0eEC',
44
54
  };
45
55
  /** Default chain RPC URLs */
46
56
  const CHAIN_RPCS = {
@@ -62,10 +72,21 @@ const CHAIN_RPCS = {
62
72
  celo: 'https://forno.celo.org',
63
73
  cronos: 'https://evm.cronos.org',
64
74
  moonbeam: 'https://rpc.api.moonbeam.network',
75
+ moonriver: 'https://rpc.api.moonriver.moonbeam.network',
65
76
  harmony: 'https://api.harmony.one',
66
77
  kava: 'https://evm.kava.io',
67
78
  canto: 'https://canto.slingshot.finance',
68
79
  aurora: 'https://mainnet.aurora.dev',
80
+ klaytn: 'https://public-en-cypress.klaytn.net',
81
+ okc: 'https://exchainrpc.okex.org',
82
+ fuse: 'https://rpc.fuse.io',
83
+ evmos: 'https://evmos-evm-rpc.publicnode.com',
84
+ core: 'https://rpc.coredao.org',
85
+ flare: 'https://flare-api.flare.network/ext/C/rpc',
86
+ iotex: 'https://babel-api.mainnet.iotex.io',
87
+ rootstock: 'https://public-node.rsk.co',
88
+ telos: 'https://mainnet.telos.net/evm',
89
+ thundercore: 'https://mainnet-rpc.thundercore.com',
69
90
  // Layer 2s
70
91
  arbitrum: 'https://arb1.arbitrum.io/rpc',
71
92
  optimism: 'https://mainnet.optimism.io',
@@ -81,11 +102,26 @@ const CHAIN_RPCS = {
81
102
  metis: 'https://andromeda.metis.io/?owner=1088',
82
103
  zora: 'https://rpc.zora.energy',
83
104
  sei: 'https://evm-rpc.sei-apis.com',
105
+ boba: 'https://mainnet.boba.network',
106
+ taiko: 'https://rpc.mainnet.taiko.xyz',
107
+ opbnb: 'https://opbnb-mainnet-rpc.bnbchain.org',
108
+ fraxtal: 'https://rpc.frax.com',
109
+ worldchain: 'https://worldchain-mainnet.g.alchemy.com/public',
110
+ lisk: 'https://rpc.api.lisk.com',
111
+ redstone: 'https://rpc.redstonechain.com',
112
+ cyber: 'https://cyber.alt.technology',
113
+ mint: 'https://rpc.mintchain.io',
114
+ bob: 'https://rpc.gobob.xyz',
115
+ xai: 'https://xai-chain.net/rpc',
116
+ morph: 'https://rpc.morphl2.io',
117
+ 'astar-zkevm': 'https://rpc.startale.com/astar-zkevm',
84
118
  // Non-EVM
85
119
  solana: 'https://api.mainnet-beta.solana.com',
86
120
  bitcoin: 'https://mempool.space/api',
87
121
  },
88
122
  };
123
+ /** Duration in ms to exclude a failed agent from selection */
124
+ const AGENT_EXCLUSION_TTL = 60000;
89
125
  class Sequence0 {
90
126
  /**
91
127
  * Create a new Sequence0 SDK client
@@ -95,30 +131,71 @@ class Sequence0 {
95
131
  *
96
132
  * @example
97
133
  * ```typescript
98
- * // Mainnet auto-discovers agents from on-chain registry
134
+ * // Mainnet -- auto-discovers agents from on-chain registry
99
135
  * const s0 = new Sequence0({ network: 'mainnet' });
100
136
  *
101
137
  * // Or specify a specific agent
102
138
  * const s0 = new Sequence0({ network: 'mainnet', agentUrl: 'http://my-agent:8080' });
139
+ *
140
+ * // With debug logging and custom rate limit
141
+ * const s0 = new Sequence0({
142
+ * network: 'mainnet',
143
+ * debug: true,
144
+ * maxRetries: 5,
145
+ * rateLimiter: { maxRequestsPerSecond: 20 },
146
+ * });
103
147
  * ```
104
148
  */
105
149
  constructor(config) {
106
150
  this.http = null;
107
151
  this.ws = null;
108
152
  this.discovery = null;
153
+ this.feeManager = null;
109
154
  this.resolvedAgentUrl = null;
155
+ this.circuitBreaker = new http_1.CircuitBreaker();
156
+ /** Map of agent API URL -> timestamp when the agent was marked as failed */
157
+ this.failedAgents = new Map();
158
+ /** Optional signer for wallet ownership proofs */
159
+ this.ownerSigner = null;
160
+ /** Owner's Ethereum address (derived from private key when provided) */
161
+ this.ownerAddress = null;
110
162
  const network = config.network || 'mainnet';
111
163
  this.config = {
112
164
  ...config,
113
165
  network,
114
166
  };
167
+ // Set up owner signer for ownership proofs on sign requests
168
+ if (config.ownerSigner) {
169
+ this.ownerSigner = config.ownerSigner;
170
+ }
171
+ else if (config.ownerPrivateKey) {
172
+ const key = config.ownerPrivateKey.startsWith('0x')
173
+ ? config.ownerPrivateKey
174
+ : '0x' + config.ownerPrivateKey;
175
+ const signingKey = new ethers_1.SigningKey(key);
176
+ this.ownerAddress = (0, ethers_1.computeAddress)(signingKey.publicKey);
177
+ this.ownerSigner = async (messageHash) => {
178
+ const sig = signingKey.sign(messageHash);
179
+ // Return compact 65-byte signature: r (32) + s (32) + v (1)
180
+ const v = sig.v === 27 ? '1b' : '1c';
181
+ return sig.r + sig.s.slice(2) + v;
182
+ };
183
+ }
184
+ const httpOpts = {
185
+ timeout: config.timeout || 30000,
186
+ headers: config.apiKey ? { 'X-API-Key': config.apiKey } : undefined,
187
+ maxRetries: config.maxRetries,
188
+ rateLimiter: config.rateLimiter,
189
+ debug: config.debug,
190
+ logger: config.logger,
191
+ };
115
192
  // If agent URL provided directly, use it immediately
116
193
  if (config.agentUrl) {
117
194
  this.resolvedAgentUrl = config.agentUrl;
118
195
  this.http = new http_1.HttpClient({
119
196
  baseUrl: config.agentUrl,
120
- timeout: config.timeout || 30000,
121
- headers: config.apiKey ? { 'X-API-Key': config.apiKey } : undefined,
197
+ circuitBreaker: this.circuitBreaker,
198
+ ...httpOpts,
122
199
  });
123
200
  }
124
201
  else if (network === 'testnet') {
@@ -126,8 +203,8 @@ class Sequence0 {
126
203
  this.resolvedAgentUrl = TESTNET_AGENT_URL;
127
204
  this.http = new http_1.HttpClient({
128
205
  baseUrl: TESTNET_AGENT_URL,
129
- timeout: config.timeout || 30000,
130
- headers: config.apiKey ? { 'X-API-Key': config.apiKey } : undefined,
206
+ circuitBreaker: this.circuitBreaker,
207
+ ...httpOpts,
131
208
  });
132
209
  }
133
210
  // Mainnet without agentUrl: lazy discovery on first API call
@@ -135,22 +212,133 @@ class Sequence0 {
135
212
  const seqRpc = config.sequenceRpcUrl || SEQUENCE_RPC[network] || SEQUENCE_RPC.mainnet;
136
213
  const registry = config.registryAddress || REGISTRY_ADDRESSES[network] || REGISTRY_ADDRESSES.mainnet;
137
214
  this.discovery = new discovery_1.AgentDiscovery({ rpcUrl: seqRpc, registryAddress: registry });
215
+ // Set up FeeManager for fee queries and TX building
216
+ const feeCollector = config.feeCollectorAddress || FEE_COLLECTOR_ADDRESSES[network] || FEE_COLLECTOR_ADDRESSES.mainnet;
217
+ if (feeCollector !== '0x0000000000000000000000000000000000000000') {
218
+ this.feeManager = new fee_1.FeeManager({
219
+ rpcUrl: seqRpc,
220
+ feeCollectorAddress: feeCollector,
221
+ registryAddress: registry,
222
+ });
223
+ }
138
224
  }
139
225
  /**
140
- * Resolve an agent URL uses direct URL if set, otherwise discovers from registry
226
+ * Resolve an agent URL -- uses direct URL if set, otherwise discovers from registry.
227
+ * Filters out recently-failed agents during discovery.
141
228
  */
142
229
  async getHttp() {
143
230
  if (this.http)
144
231
  return this.http;
145
- // Auto-discover agent from on-chain registry
146
- const agentUrl = await this.discovery.selectAgent();
232
+ // Auto-discover agent from on-chain registry, excluding failed agents
233
+ const agentUrl = await this.selectAgent();
147
234
  this.resolvedAgentUrl = agentUrl;
148
- this.http = new http_1.HttpClient({
149
- baseUrl: agentUrl,
235
+ this.http = this.createHttpClient(agentUrl);
236
+ return this.http;
237
+ }
238
+ /**
239
+ * Select a healthy agent from the registry, filtering out
240
+ * agents that failed within the last AGENT_EXCLUSION_TTL ms.
241
+ */
242
+ async selectAgent() {
243
+ this.pruneExpiredFailures();
244
+ const healthy = await this.discovery.getHealthyAgents();
245
+ const available = healthy.filter((agent) => !this.failedAgents.has(agent.apiUrl));
246
+ if (available.length === 0) {
247
+ // If all healthy agents have been excluded, clear exclusions and try again
248
+ if (healthy.length > 0) {
249
+ this.failedAgents.clear();
250
+ const idx = Math.floor(Math.random() * healthy.length);
251
+ return healthy[idx].apiUrl;
252
+ }
253
+ throw new errors_1.NetworkError('No healthy agents found in the registry. The network may not have active node runners yet.');
254
+ }
255
+ const idx = Math.floor(Math.random() * available.length);
256
+ return available[idx].apiUrl;
257
+ }
258
+ /**
259
+ * Mark the current agent as failed and switch to a different one.
260
+ * Called internally when a request to the current agent fails
261
+ * after exhausting retries or when the circuit breaker trips.
262
+ */
263
+ async failoverToNextAgent() {
264
+ const failedUrl = this.resolvedAgentUrl;
265
+ if (failedUrl) {
266
+ this.failedAgents.set(failedUrl, Date.now());
267
+ }
268
+ // Destroy the old HTTP client
269
+ if (this.http) {
270
+ this.http.destroy();
271
+ this.http = null;
272
+ }
273
+ // Disconnect WebSocket (bound to old agent)
274
+ if (this.ws) {
275
+ this.ws.disconnect();
276
+ this.ws = null;
277
+ }
278
+ // Select a new agent
279
+ const agentUrl = await this.selectAgent();
280
+ this.resolvedAgentUrl = agentUrl;
281
+ this.http = this.createHttpClient(agentUrl);
282
+ return this.http;
283
+ }
284
+ /**
285
+ * Remove expired entries from the failed agents map.
286
+ */
287
+ pruneExpiredFailures() {
288
+ const now = Date.now();
289
+ for (const [url, timestamp] of this.failedAgents) {
290
+ if (now - timestamp >= AGENT_EXCLUSION_TTL) {
291
+ this.failedAgents.delete(url);
292
+ }
293
+ }
294
+ }
295
+ /**
296
+ * Create a new HttpClient with the shared circuit breaker and current config.
297
+ */
298
+ createHttpClient(baseUrl) {
299
+ return new http_1.HttpClient({
300
+ baseUrl,
150
301
  timeout: this.config.timeout || 30000,
151
302
  headers: this.config.apiKey ? { 'X-API-Key': this.config.apiKey } : undefined,
303
+ maxRetries: this.config.maxRetries,
304
+ rateLimiter: this.config.rateLimiter,
305
+ debug: this.config.debug,
306
+ logger: this.config.logger,
307
+ circuitBreaker: this.circuitBreaker,
152
308
  });
153
- return this.http;
309
+ }
310
+ /**
311
+ * Execute an HTTP request with automatic agent failover.
312
+ * If the current agent's circuit breaker trips or all retries fail,
313
+ * tries to failover to a different agent (up to 2 failover attempts).
314
+ */
315
+ async withFailover(fn) {
316
+ const MAX_FAILOVERS = 2;
317
+ for (let failover = 0; failover <= MAX_FAILOVERS; failover++) {
318
+ const http = await this.getHttp();
319
+ try {
320
+ return await fn(http);
321
+ }
322
+ catch (error) {
323
+ // On circuit breaker error or network error, try failover
324
+ const isFailoverable = error instanceof http_1.CircuitBreakerError ||
325
+ (error instanceof errors_1.NetworkError && FAILOVER_STATUS_CODES.has(error.statusCode ?? 0)) ||
326
+ (error instanceof errors_1.NetworkError && !error.statusCode); // connection-level failures
327
+ if (isFailoverable && failover < MAX_FAILOVERS) {
328
+ try {
329
+ await this.failoverToNextAgent();
330
+ continue;
331
+ }
332
+ catch {
333
+ // If failover itself fails (no agents), throw original error
334
+ throw error;
335
+ }
336
+ }
337
+ throw error;
338
+ }
339
+ }
340
+ // Should be unreachable
341
+ throw new errors_1.NetworkError('All failover attempts exhausted');
154
342
  }
155
343
  // ────────────────────────────────────────────────
156
344
  // Wallet Management
@@ -159,21 +347,32 @@ class Sequence0 {
159
347
  * Create a new threshold wallet via DKG ceremony
160
348
  *
161
349
  * Initiates Distributed Key Generation with the agent network.
162
- * The private key is never assembled each agent holds a share.
350
+ * The private key is never assembled -- each agent holds a share.
163
351
  *
164
352
  * @example
165
353
  * ```typescript
166
354
  * const wallet = await s0.createWallet({ chain: 'ethereum' });
167
355
  * console.log(wallet.address); // 0x...
168
- * console.log(wallet.threshold); // { t: 17, n: 24 }
356
+ * console.log(wallet.threshold); // { t: 16, n: 24 }
169
357
  * ```
170
358
  */
171
359
  async createWallet(options) {
172
- const chain = options.chain || 'ethereum';
173
- const threshold = options.threshold || { t: 17, n: 24 };
360
+ // Validate inputs
361
+ const chain = (0, validation_1.validateChain)(options.chain || 'ethereum');
362
+ const threshold = options.threshold
363
+ ? (0, validation_1.validateThreshold)(options.threshold)
364
+ : { t: 16, n: 24 };
174
365
  const curve = options.curve || this.getCurveForChain(chain);
175
- const walletId = options.walletId || this.generateWalletId();
366
+ const walletId = options.walletId
367
+ ? (0, validation_1.validateWalletId)(options.walletId)
368
+ : this.generateWalletId();
176
369
  const timeout = options.timeout || 60000;
370
+ const creator = options.creator || this.ownerAddress;
371
+ // Validate creator address
372
+ if (!creator || !/^0x[0-9a-fA-F]{40}$/.test(creator)) {
373
+ throw new errors_1.Sequence0Error('creator must be a valid 0x-prefixed Ethereum address (40 hex chars). ' +
374
+ 'Either pass it in CreateWalletOptions or provide ownerPrivateKey in NetworkConfig.');
375
+ }
177
376
  // Get agent peers to form committee
178
377
  const status = await this.getStatus();
179
378
  const participants = status.connected_peer_ids.slice(0, threshold.n);
@@ -186,12 +385,15 @@ class Sequence0 {
186
385
  const dkgComplete = ws.waitFor('WalletCreated', (e) => e.wallet_id === walletId, timeout);
187
386
  // Initiate DKG via REST API
188
387
  const http = await this.getHttp();
189
- const dkgResponse = await http.post('/dkg/initiate', {
388
+ const rawDkgResponse = await http.post('/dkg/initiate', {
190
389
  wallet_id: walletId,
191
390
  participants,
192
391
  threshold: threshold.t,
193
392
  curve,
393
+ creator,
194
394
  });
395
+ // Validate the DKG response
396
+ const dkgResponse = (0, validation_1.validateDkgResponse)(rawDkgResponse, '/dkg/initiate');
195
397
  if (dkgResponse.status !== 'initiated') {
196
398
  throw new errors_1.DkgError(`DKG initiation failed: ${dkgResponse.status}`);
197
399
  }
@@ -199,9 +401,12 @@ class Sequence0 {
199
401
  let publicKey;
200
402
  try {
201
403
  const result = await dkgComplete;
202
- publicKey = result.public_key || '';
404
+ const validated = (0, validation_1.validateDkgCompletion)(result, 'ws:WalletCreated');
405
+ publicKey = validated.public_key || '';
203
406
  }
204
407
  catch (e) {
408
+ if (e instanceof errors_1.DkgError)
409
+ throw e;
205
410
  throw new errors_1.DkgError(`DKG ceremony timed out after ${timeout}ms. The agent network may be busy.`);
206
411
  }
207
412
  // Derive address from public key based on chain
@@ -215,6 +420,7 @@ class Sequence0 {
215
420
  agentUrl: this.resolvedAgentUrl,
216
421
  rpcUrl: this.getRpcUrl(chain),
217
422
  curve,
423
+ ownerSigner: this.ownerSigner || undefined,
218
424
  });
219
425
  }
220
426
  /**
@@ -229,6 +435,8 @@ class Sequence0 {
229
435
  * ```
230
436
  */
231
437
  async getWallet(walletId) {
438
+ // Validate input
439
+ (0, validation_1.validateWalletId)(walletId);
232
440
  const wallets = await this.listWallets();
233
441
  const detail = wallets.find((w) => w.wallet_id === walletId);
234
442
  if (!detail) {
@@ -244,15 +452,18 @@ class Sequence0 {
244
452
  agentUrl: this.resolvedAgentUrl,
245
453
  rpcUrl: this.getRpcUrl(chain),
246
454
  curve: this.getCurveForChain(chain),
455
+ ownerSigner: this.ownerSigner || undefined,
247
456
  });
248
457
  }
249
458
  /**
250
459
  * List all wallets managed by the agent network
251
460
  */
252
461
  async listWallets() {
253
- const http = await this.getHttp();
254
- const response = await http.get('/wallets');
255
- return response.wallets;
462
+ return this.withFailover(async (http) => {
463
+ const rawResponse = await http.get('/wallets');
464
+ const response = (0, validation_1.validateWalletsResponse)(rawResponse, '/wallets');
465
+ return response.wallets;
466
+ });
256
467
  }
257
468
  // ────────────────────────────────────────────────
258
469
  // Signing (Low-Level)
@@ -265,12 +476,24 @@ class Sequence0 {
265
476
  * @returns request ID for polling
266
477
  */
267
478
  async requestSignature(walletId, message) {
268
- const http = await this.getHttp();
269
- const response = await http.post('/sign', {
270
- wallet_id: walletId,
271
- message,
479
+ // Validate inputs
480
+ (0, validation_1.validateWalletId)(walletId);
481
+ (0, validation_1.validateHexMessage)(message);
482
+ // Build ownership proof if signer is configured
483
+ const proof = await this.signOwnershipProof(walletId, message);
484
+ return this.withFailover(async (http) => {
485
+ const body = {
486
+ wallet_id: walletId,
487
+ message,
488
+ };
489
+ if (proof) {
490
+ body.owner_signature = proof.owner_signature;
491
+ body.timestamp = proof.timestamp;
492
+ }
493
+ const rawResponse = await http.post('/sign', body);
494
+ const response = (0, validation_1.validateSignResponse)(rawResponse, '/sign');
495
+ return response.request_id;
272
496
  });
273
- return response.request_id;
274
497
  }
275
498
  /**
276
499
  * Poll for a signature result
@@ -279,8 +502,14 @@ class Sequence0 {
279
502
  * @returns The signature when ready, null if still pending
280
503
  */
281
504
  async getSignatureResult(requestId) {
282
- const http = await this.getHttp();
283
- return http.get(`/sign/${requestId}`);
505
+ if (!requestId || typeof requestId !== 'string') {
506
+ throw new errors_1.Sequence0Error('requestId must be a non-empty string');
507
+ }
508
+ return this.withFailover(async (http) => {
509
+ const endpoint = `/sign/${requestId}`;
510
+ const rawResponse = await http.get(endpoint);
511
+ return (0, validation_1.validateSignResultResponse)(rawResponse, endpoint);
512
+ });
284
513
  }
285
514
  /**
286
515
  * Request a signature and wait for completion
@@ -291,6 +520,9 @@ class Sequence0 {
291
520
  * @returns hex-encoded signature
292
521
  */
293
522
  async signAndWait(walletId, message, timeoutMs = 30000) {
523
+ // Validate inputs
524
+ (0, validation_1.validateWalletId)(walletId);
525
+ (0, validation_1.validateHexMessage)(message);
294
526
  const requestId = await this.requestSignature(walletId, message);
295
527
  // Try WebSocket first for real-time notification
296
528
  try {
@@ -319,22 +551,28 @@ class Sequence0 {
319
551
  * Get agent network status
320
552
  */
321
553
  async getStatus() {
322
- const http = await this.getHttp();
323
- return http.get('/status');
554
+ return this.withFailover(async (http) => {
555
+ const rawResponse = await http.get('/status');
556
+ return (0, validation_1.validateStatusResponse)(rawResponse, '/status');
557
+ });
324
558
  }
325
559
  /**
326
560
  * Health check
327
561
  */
328
562
  async health() {
329
- const http = await this.getHttp();
330
- return http.get('/health');
563
+ return this.withFailover(async (http) => {
564
+ const rawResponse = await http.get('/health');
565
+ return (0, validation_1.validateHealthResponse)(rawResponse, '/health');
566
+ });
331
567
  }
332
568
  /**
333
569
  * Request key refresh for a wallet (proactive security)
334
570
  */
335
571
  async refreshKeys(walletId) {
336
- const http = await this.getHttp();
337
- await http.post('/refresh', { wallet_id: walletId });
572
+ (0, validation_1.validateWalletId)(walletId);
573
+ return this.withFailover(async (http) => {
574
+ await http.post('/refresh', { wallet_id: walletId });
575
+ });
338
576
  }
339
577
  /**
340
578
  * Discover active agents from the on-chain registry
@@ -342,6 +580,88 @@ class Sequence0 {
342
580
  async discoverAgents() {
343
581
  return this.discovery.discoverAgents();
344
582
  }
583
+ /**
584
+ * Discover all active agents from the on-chain registry (paginated).
585
+ * Fetches all pages when there are more than 100 agents.
586
+ */
587
+ async discoverAllAgents() {
588
+ return this.discovery.discoverAllAgents();
589
+ }
590
+ // ────────────────────────────────────────────────
591
+ // Fees
592
+ // ────────────────────────────────────────────────
593
+ /**
594
+ * Get the current per-signature fee from the FeeCollector contract.
595
+ *
596
+ * The fee is paid on the Sequence0 chain (chain ID 800801) in native S0
597
+ * tokens. 80% goes to the signing agents, 10% to protocol treasury,
598
+ * 10% to the reserve fund.
599
+ *
600
+ * Returns 0n on testnet (no fees).
601
+ *
602
+ * @returns Fee in wei as bigint
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * const fee = await s0.getSignatureFee();
607
+ * console.log(`Fee: ${fee} wei`);
608
+ * ```
609
+ */
610
+ async getSignatureFee() {
611
+ if (!this.feeManager)
612
+ return 0n;
613
+ return this.feeManager.getSignatureFee();
614
+ }
615
+ /**
616
+ * Build an unsigned fee-payment transaction for a wallet's signing committee.
617
+ *
618
+ * This looks up the wallet's committee from the agent network, resolves
619
+ * each committee member's Ethereum payment address from the on-chain
620
+ * AgentRegistry, and builds an unsigned `collectFee()` transaction for
621
+ * the FeeCollector contract.
622
+ *
623
+ * **The app developer must sign and send this transaction themselves** on the
624
+ * Sequence0 chain (RPC: https://rpc.sequence0.network, chain ID: 800801).
625
+ * They need S0 native tokens to cover the fee.
626
+ *
627
+ * Returns null on testnet (no fees).
628
+ *
629
+ * @param walletId - The wallet ID that will be signed with
630
+ * @returns Unsigned transaction `{ to, data, value }` or null if no fee required
631
+ *
632
+ * @example
633
+ * ```typescript
634
+ * const feeTx = await s0.buildFeeTx('my-wallet-id');
635
+ * if (feeTx) {
636
+ * // Sign and send with your own wallet on the Sequence0 chain
637
+ * const tx = await signer.sendTransaction(feeTx);
638
+ * await tx.wait();
639
+ * }
640
+ * // Now request the signature
641
+ * const sig = await s0.signAndWait('my-wallet-id', messageHex);
642
+ * ```
643
+ */
644
+ async buildFeeTx(walletId) {
645
+ if (!this.feeManager)
646
+ return null;
647
+ (0, validation_1.validateWalletId)(walletId);
648
+ // Fetch wallet details to get the committee peer IDs
649
+ const wallets = await this.listWallets();
650
+ const detail = wallets.find((w) => w.wallet_id === walletId);
651
+ if (!detail) {
652
+ throw new errors_1.Sequence0Error(`Wallet '${walletId}' not found on ${this.config.network}`);
653
+ }
654
+ if (!detail.committee || detail.committee.length === 0) {
655
+ throw new errors_1.Sequence0Error(`Wallet '${walletId}' has no committee members`);
656
+ }
657
+ // Resolve peer IDs to Ethereum payment addresses via the AgentRegistry
658
+ const agentAddresses = await this.feeManager.resolveAgentPaymentAddresses(detail.committee);
659
+ if (agentAddresses.length === 0) {
660
+ throw new errors_1.Sequence0Error(`Could not resolve payment addresses for wallet '${walletId}' committee. ` +
661
+ 'Agents may not be registered on-chain.');
662
+ }
663
+ return this.feeManager.buildCollectFeeTx(walletId, agentAddresses);
664
+ }
345
665
  // ────────────────────────────────────────────────
346
666
  // WebSocket Events
347
667
  // ────────────────────────────────────────────────
@@ -356,12 +676,31 @@ class Sequence0 {
356
676
  * ```
357
677
  */
358
678
  async subscribe(walletId) {
679
+ if (walletId) {
680
+ (0, validation_1.validateWalletId)(walletId);
681
+ }
359
682
  await this.getHttp(); // Ensure agent URL is resolved
360
683
  const wsUrl = this.resolvedAgentUrl.replace(/^http/, 'ws') + '/ws';
361
684
  const ws = new websocket_1.WsClient({ url: wsUrl, walletId });
362
685
  await ws.connect();
363
686
  return ws;
364
687
  }
688
+ /**
689
+ * Clean up all resources (HTTP client, rate limiter, WebSocket, circuit breaker).
690
+ * Call this when you are done using the SDK.
691
+ */
692
+ destroy() {
693
+ if (this.http) {
694
+ this.http.destroy();
695
+ this.http = null;
696
+ }
697
+ if (this.ws) {
698
+ this.ws.disconnect();
699
+ this.ws = null;
700
+ }
701
+ this.circuitBreaker.resetAll();
702
+ this.failedAgents.clear();
703
+ }
365
704
  // ────────────────────────────────────────────────
366
705
  // Internals
367
706
  // ────────────────────────────────────────────────
@@ -381,12 +720,53 @@ class Sequence0 {
381
720
  return networkRpcs[chain] || networkRpcs.ethereum;
382
721
  }
383
722
  getCurveForChain(chain) {
384
- const ed25519Chains = ['solana', 'near', 'sui', 'aptos', 'cosmos', 'polkadot', 'cardano'];
723
+ // Ed25519 chains — Solana, Near, Sui, Aptos, Polkadot, Cardano, Stellar, TON, Algorand
724
+ // Note: Cosmos uses secp256k1 by default (same as Ethereum)
725
+ // Tezos supports both but secp256k1 is more common
726
+ // Polkadot primarily uses sr25519, but ed25519 is supported
727
+ const ed25519Chains = [
728
+ 'solana', 'near', 'sui', 'aptos', 'polkadot', 'cardano',
729
+ 'stellar', 'ton', 'algorand',
730
+ ];
385
731
  if (ed25519Chains.includes(chain)) {
386
732
  return 'ed25519';
387
733
  }
388
734
  return 'secp256k1';
389
735
  }
736
+ /**
737
+ * Build an ownership proof for a sign request.
738
+ *
739
+ * The proof is: sign( keccak256( wallet_id_bytes + message_hex_bytes + timestamp_be_bytes ) )
740
+ *
741
+ * - wallet_id_bytes: UTF-8 encoding of the wallet ID string
742
+ * - message_hex_bytes: the raw bytes of the hex-encoded message (i.e. the message as passed to /sign)
743
+ * - timestamp_be_bytes: 8-byte big-endian encoding of the Unix epoch second
744
+ *
745
+ * Returns null when no ownerSigner is configured (backwards compatible).
746
+ */
747
+ async signOwnershipProof(walletId, messageHex) {
748
+ if (!this.ownerSigner)
749
+ return null;
750
+ const timestamp = Math.floor(Date.now() / 1000);
751
+ // wallet_id as UTF-8 bytes
752
+ const walletIdBytes = new TextEncoder().encode(walletId);
753
+ // message as UTF-8 string bytes (must match Rust: message.as_bytes())
754
+ const messageBytes = new TextEncoder().encode(messageHex);
755
+ // timestamp as 8-byte big-endian
756
+ const timestampBytes = new Uint8Array(8);
757
+ const view = new DataView(timestampBytes.buffer);
758
+ // DataView setBigUint64 writes big-endian by default
759
+ view.setBigUint64(0, BigInt(timestamp), false);
760
+ // Concatenate and hash: keccak256(walletId + message + timestamp)
761
+ const payload = (0, ethers_1.concat)([walletIdBytes, messageBytes, timestampBytes]);
762
+ const digest = (0, ethers_1.getBytes)((0, ethers_1.keccak256)(payload));
763
+ // Sign the digest
764
+ const signature = await this.ownerSigner(digest);
765
+ return {
766
+ owner_signature: signature.startsWith('0x') ? signature : '0x' + signature,
767
+ timestamp,
768
+ };
769
+ }
390
770
  generateWalletId() {
391
771
  const timestamp = Date.now().toString(36);
392
772
  const random = Math.random().toString(36).slice(2, 8);