@sequence0/sdk 1.0.0 → 1.0.2

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