@sequence0/sdk 0.1.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.
- package/README.md +63 -5
- package/dist/chains/bitcoin.d.ts +15 -3
- package/dist/chains/bitcoin.d.ts.map +1 -1
- package/dist/chains/bitcoin.js +23 -3
- package/dist/chains/bitcoin.js.map +1 -1
- package/dist/chains/ethereum.d.ts +3 -0
- package/dist/chains/ethereum.d.ts.map +1 -1
- package/dist/chains/ethereum.js +156 -20
- package/dist/chains/ethereum.js.map +1 -1
- package/dist/core/client.d.ts +132 -2
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +501 -32
- package/dist/core/client.js.map +1 -1
- package/dist/core/types.d.ts +55 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/discovery.d.ts +95 -0
- package/dist/utils/discovery.d.ts.map +1 -0
- package/dist/utils/discovery.js +212 -0
- package/dist/utils/discovery.js.map +1 -0
- package/dist/utils/errors.d.ts +26 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +32 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/fee.d.ts +107 -0
- package/dist/utils/fee.d.ts.map +1 -0
- package/dist/utils/fee.js +220 -0
- package/dist/utils/fee.js.map +1 -0
- package/dist/utils/http.d.ts +98 -2
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +238 -6
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/logger.d.ts +43 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +129 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +43 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +99 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/validation.d.ts +74 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +380 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/wallet/wallet.d.ts +13 -1
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +86 -21
- package/dist/wallet/wallet.js.map +1 -1
- package/package.json +9 -3
package/dist/core/client.js
CHANGED
|
@@ -25,14 +25,32 @@
|
|
|
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");
|
|
32
|
+
const discovery_1 = require("../utils/discovery");
|
|
33
|
+
const fee_1 = require("../utils/fee");
|
|
31
34
|
const errors_1 = require("../utils/errors");
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const validation_1 = require("../utils/validation");
|
|
36
|
+
/** Default agent URL for testnet (we run these ourselves) */
|
|
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]);
|
|
40
|
+
/** Sequence0 chain RPC URLs for on-chain agent discovery */
|
|
41
|
+
const SEQUENCE_RPC = {
|
|
42
|
+
testnet: 'https://testnet-rpc.sequence0.network',
|
|
43
|
+
mainnet: 'https://rpc.sequence0.network',
|
|
44
|
+
};
|
|
45
|
+
/** AgentRegistry contract addresses */
|
|
46
|
+
const REGISTRY_ADDRESSES = {
|
|
47
|
+
testnet: '0xf8d7cc35a22882bb8ac36b9ccadd08e852404377',
|
|
48
|
+
mainnet: '0x8F642dd318C7A7CD2B295dC4d266C3d2e4838B33',
|
|
49
|
+
};
|
|
50
|
+
/** FeeCollector contract addresses */
|
|
51
|
+
const FEE_COLLECTOR_ADDRESSES = {
|
|
52
|
+
testnet: '0x0000000000000000000000000000000000000000', // No fee on testnet
|
|
53
|
+
mainnet: '0x7253ff9f45d1bF8Ed0A23Dfb4E6308adcb1E0eEC',
|
|
36
54
|
};
|
|
37
55
|
/** Default chain RPC URLs */
|
|
38
56
|
const CHAIN_RPCS = {
|
|
@@ -44,41 +62,284 @@ const CHAIN_RPCS = {
|
|
|
44
62
|
bitcoin: 'https://mempool.space/testnet/api',
|
|
45
63
|
},
|
|
46
64
|
mainnet: {
|
|
65
|
+
// Layer 1s
|
|
47
66
|
ethereum: 'https://eth.llamarpc.com',
|
|
48
67
|
polygon: 'https://polygon-rpc.com',
|
|
68
|
+
bsc: 'https://bsc-dataseed.binance.org',
|
|
69
|
+
avalanche: 'https://api.avax.network/ext/bc/C/rpc',
|
|
70
|
+
fantom: 'https://rpc.ftm.tools',
|
|
71
|
+
gnosis: 'https://rpc.gnosischain.com',
|
|
72
|
+
celo: 'https://forno.celo.org',
|
|
73
|
+
cronos: 'https://evm.cronos.org',
|
|
74
|
+
moonbeam: 'https://rpc.api.moonbeam.network',
|
|
75
|
+
moonriver: 'https://rpc.api.moonriver.moonbeam.network',
|
|
76
|
+
harmony: 'https://api.harmony.one',
|
|
77
|
+
kava: 'https://evm.kava.io',
|
|
78
|
+
canto: 'https://canto.slingshot.finance',
|
|
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',
|
|
90
|
+
// Layer 2s
|
|
49
91
|
arbitrum: 'https://arb1.arbitrum.io/rpc',
|
|
50
92
|
optimism: 'https://mainnet.optimism.io',
|
|
51
93
|
base: 'https://mainnet.base.org',
|
|
52
|
-
|
|
53
|
-
|
|
94
|
+
zksync: 'https://mainnet.era.zksync.io',
|
|
95
|
+
scroll: 'https://rpc.scroll.io',
|
|
96
|
+
linea: 'https://rpc.linea.build',
|
|
97
|
+
mantle: 'https://rpc.mantle.xyz',
|
|
98
|
+
blast: 'https://rpc.blast.io',
|
|
99
|
+
mode: 'https://mainnet.mode.network',
|
|
100
|
+
manta: 'https://pacific-rpc.manta.network/http',
|
|
101
|
+
'polygon-zkevm': 'https://zkevm-rpc.com',
|
|
102
|
+
metis: 'https://andromeda.metis.io/?owner=1088',
|
|
103
|
+
zora: 'https://rpc.zora.energy',
|
|
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',
|
|
118
|
+
// Non-EVM
|
|
54
119
|
solana: 'https://api.mainnet-beta.solana.com',
|
|
55
120
|
bitcoin: 'https://mempool.space/api',
|
|
56
121
|
},
|
|
57
122
|
};
|
|
123
|
+
/** Duration in ms to exclude a failed agent from selection */
|
|
124
|
+
const AGENT_EXCLUSION_TTL = 60000;
|
|
58
125
|
class Sequence0 {
|
|
59
126
|
/**
|
|
60
127
|
* Create a new Sequence0 SDK client
|
|
61
128
|
*
|
|
129
|
+
* On testnet, connects to the Sequence0-operated agent by default.
|
|
130
|
+
* On mainnet, auto-discovers agents from the on-chain AgentRegistry.
|
|
131
|
+
*
|
|
62
132
|
* @example
|
|
63
133
|
* ```typescript
|
|
134
|
+
* // Mainnet -- auto-discovers agents from on-chain registry
|
|
64
135
|
* const s0 = new Sequence0({ network: 'mainnet' });
|
|
136
|
+
*
|
|
137
|
+
* // Or specify a specific agent
|
|
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
|
+
* });
|
|
65
147
|
* ```
|
|
66
148
|
*/
|
|
67
149
|
constructor(config) {
|
|
150
|
+
this.http = null;
|
|
68
151
|
this.ws = null;
|
|
152
|
+
this.discovery = null;
|
|
153
|
+
this.feeManager = null;
|
|
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;
|
|
69
162
|
const network = config.network || 'mainnet';
|
|
70
|
-
const agentUrl = config.agentUrl || AGENT_URLS[network] || AGENT_URLS.mainnet;
|
|
71
163
|
this.config = {
|
|
72
164
|
...config,
|
|
73
165
|
network,
|
|
74
|
-
agentUrl,
|
|
75
166
|
};
|
|
76
|
-
|
|
77
|
-
|
|
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 = {
|
|
78
185
|
timeout: config.timeout || 30000,
|
|
79
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
|
+
};
|
|
192
|
+
// If agent URL provided directly, use it immediately
|
|
193
|
+
if (config.agentUrl) {
|
|
194
|
+
this.resolvedAgentUrl = config.agentUrl;
|
|
195
|
+
this.http = new http_1.HttpClient({
|
|
196
|
+
baseUrl: config.agentUrl,
|
|
197
|
+
circuitBreaker: this.circuitBreaker,
|
|
198
|
+
...httpOpts,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (network === 'testnet') {
|
|
202
|
+
// Testnet: default to our operated agent
|
|
203
|
+
this.resolvedAgentUrl = TESTNET_AGENT_URL;
|
|
204
|
+
this.http = new http_1.HttpClient({
|
|
205
|
+
baseUrl: TESTNET_AGENT_URL,
|
|
206
|
+
circuitBreaker: this.circuitBreaker,
|
|
207
|
+
...httpOpts,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Mainnet without agentUrl: lazy discovery on first API call
|
|
211
|
+
// Always set up discovery for registry queries
|
|
212
|
+
const seqRpc = config.sequenceRpcUrl || SEQUENCE_RPC[network] || SEQUENCE_RPC.mainnet;
|
|
213
|
+
const registry = config.registryAddress || REGISTRY_ADDRESSES[network] || REGISTRY_ADDRESSES.mainnet;
|
|
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
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Resolve an agent URL -- uses direct URL if set, otherwise discovers from registry.
|
|
227
|
+
* Filters out recently-failed agents during discovery.
|
|
228
|
+
*/
|
|
229
|
+
async getHttp() {
|
|
230
|
+
if (this.http)
|
|
231
|
+
return this.http;
|
|
232
|
+
// Auto-discover agent from on-chain registry, excluding failed agents
|
|
233
|
+
const agentUrl = await this.selectAgent();
|
|
234
|
+
this.resolvedAgentUrl = 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,
|
|
301
|
+
timeout: this.config.timeout || 30000,
|
|
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,
|
|
80
308
|
});
|
|
81
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');
|
|
342
|
+
}
|
|
82
343
|
// ────────────────────────────────────────────────
|
|
83
344
|
// Wallet Management
|
|
84
345
|
// ────────────────────────────────────────────────
|
|
@@ -86,21 +347,32 @@ class Sequence0 {
|
|
|
86
347
|
* Create a new threshold wallet via DKG ceremony
|
|
87
348
|
*
|
|
88
349
|
* Initiates Distributed Key Generation with the agent network.
|
|
89
|
-
* The private key is never assembled
|
|
350
|
+
* The private key is never assembled -- each agent holds a share.
|
|
90
351
|
*
|
|
91
352
|
* @example
|
|
92
353
|
* ```typescript
|
|
93
354
|
* const wallet = await s0.createWallet({ chain: 'ethereum' });
|
|
94
355
|
* console.log(wallet.address); // 0x...
|
|
95
|
-
* console.log(wallet.threshold); // { t:
|
|
356
|
+
* console.log(wallet.threshold); // { t: 16, n: 24 }
|
|
96
357
|
* ```
|
|
97
358
|
*/
|
|
98
359
|
async createWallet(options) {
|
|
99
|
-
|
|
100
|
-
const
|
|
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 };
|
|
101
365
|
const curve = options.curve || this.getCurveForChain(chain);
|
|
102
|
-
const walletId = options.walletId
|
|
366
|
+
const walletId = options.walletId
|
|
367
|
+
? (0, validation_1.validateWalletId)(options.walletId)
|
|
368
|
+
: this.generateWalletId();
|
|
103
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
|
+
}
|
|
104
376
|
// Get agent peers to form committee
|
|
105
377
|
const status = await this.getStatus();
|
|
106
378
|
const participants = status.connected_peer_ids.slice(0, threshold.n);
|
|
@@ -112,12 +384,16 @@ class Sequence0 {
|
|
|
112
384
|
// Create a promise that resolves when DKG completes
|
|
113
385
|
const dkgComplete = ws.waitFor('WalletCreated', (e) => e.wallet_id === walletId, timeout);
|
|
114
386
|
// Initiate DKG via REST API
|
|
115
|
-
const
|
|
387
|
+
const http = await this.getHttp();
|
|
388
|
+
const rawDkgResponse = await http.post('/dkg/initiate', {
|
|
116
389
|
wallet_id: walletId,
|
|
117
390
|
participants,
|
|
118
391
|
threshold: threshold.t,
|
|
119
392
|
curve,
|
|
393
|
+
creator,
|
|
120
394
|
});
|
|
395
|
+
// Validate the DKG response
|
|
396
|
+
const dkgResponse = (0, validation_1.validateDkgResponse)(rawDkgResponse, '/dkg/initiate');
|
|
121
397
|
if (dkgResponse.status !== 'initiated') {
|
|
122
398
|
throw new errors_1.DkgError(`DKG initiation failed: ${dkgResponse.status}`);
|
|
123
399
|
}
|
|
@@ -125,9 +401,12 @@ class Sequence0 {
|
|
|
125
401
|
let publicKey;
|
|
126
402
|
try {
|
|
127
403
|
const result = await dkgComplete;
|
|
128
|
-
|
|
404
|
+
const validated = (0, validation_1.validateDkgCompletion)(result, 'ws:WalletCreated');
|
|
405
|
+
publicKey = validated.public_key || '';
|
|
129
406
|
}
|
|
130
407
|
catch (e) {
|
|
408
|
+
if (e instanceof errors_1.DkgError)
|
|
409
|
+
throw e;
|
|
131
410
|
throw new errors_1.DkgError(`DKG ceremony timed out after ${timeout}ms. The agent network may be busy.`);
|
|
132
411
|
}
|
|
133
412
|
// Derive address from public key based on chain
|
|
@@ -138,9 +417,10 @@ class Sequence0 {
|
|
|
138
417
|
address,
|
|
139
418
|
threshold,
|
|
140
419
|
network: this.config.network,
|
|
141
|
-
agentUrl: this.
|
|
420
|
+
agentUrl: this.resolvedAgentUrl,
|
|
142
421
|
rpcUrl: this.getRpcUrl(chain),
|
|
143
422
|
curve,
|
|
423
|
+
ownerSigner: this.ownerSigner || undefined,
|
|
144
424
|
});
|
|
145
425
|
}
|
|
146
426
|
/**
|
|
@@ -155,6 +435,8 @@ class Sequence0 {
|
|
|
155
435
|
* ```
|
|
156
436
|
*/
|
|
157
437
|
async getWallet(walletId) {
|
|
438
|
+
// Validate input
|
|
439
|
+
(0, validation_1.validateWalletId)(walletId);
|
|
158
440
|
const wallets = await this.listWallets();
|
|
159
441
|
const detail = wallets.find((w) => w.wallet_id === walletId);
|
|
160
442
|
if (!detail) {
|
|
@@ -167,17 +449,21 @@ class Sequence0 {
|
|
|
167
449
|
address: detail.address,
|
|
168
450
|
threshold: { t: detail.threshold, n: detail.size },
|
|
169
451
|
network: this.config.network,
|
|
170
|
-
agentUrl: this.
|
|
452
|
+
agentUrl: this.resolvedAgentUrl,
|
|
171
453
|
rpcUrl: this.getRpcUrl(chain),
|
|
172
454
|
curve: this.getCurveForChain(chain),
|
|
455
|
+
ownerSigner: this.ownerSigner || undefined,
|
|
173
456
|
});
|
|
174
457
|
}
|
|
175
458
|
/**
|
|
176
459
|
* List all wallets managed by the agent network
|
|
177
460
|
*/
|
|
178
461
|
async listWallets() {
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
});
|
|
181
467
|
}
|
|
182
468
|
// ────────────────────────────────────────────────
|
|
183
469
|
// Signing (Low-Level)
|
|
@@ -190,11 +476,24 @@ class Sequence0 {
|
|
|
190
476
|
* @returns request ID for polling
|
|
191
477
|
*/
|
|
192
478
|
async requestSignature(walletId, message) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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;
|
|
196
496
|
});
|
|
197
|
-
return response.request_id;
|
|
198
497
|
}
|
|
199
498
|
/**
|
|
200
499
|
* Poll for a signature result
|
|
@@ -203,7 +502,14 @@ class Sequence0 {
|
|
|
203
502
|
* @returns The signature when ready, null if still pending
|
|
204
503
|
*/
|
|
205
504
|
async getSignatureResult(requestId) {
|
|
206
|
-
|
|
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
|
+
});
|
|
207
513
|
}
|
|
208
514
|
/**
|
|
209
515
|
* Request a signature and wait for completion
|
|
@@ -214,6 +520,9 @@ class Sequence0 {
|
|
|
214
520
|
* @returns hex-encoded signature
|
|
215
521
|
*/
|
|
216
522
|
async signAndWait(walletId, message, timeoutMs = 30000) {
|
|
523
|
+
// Validate inputs
|
|
524
|
+
(0, validation_1.validateWalletId)(walletId);
|
|
525
|
+
(0, validation_1.validateHexMessage)(message);
|
|
217
526
|
const requestId = await this.requestSignature(walletId, message);
|
|
218
527
|
// Try WebSocket first for real-time notification
|
|
219
528
|
try {
|
|
@@ -242,19 +551,116 @@ class Sequence0 {
|
|
|
242
551
|
* Get agent network status
|
|
243
552
|
*/
|
|
244
553
|
async getStatus() {
|
|
245
|
-
return this.http
|
|
554
|
+
return this.withFailover(async (http) => {
|
|
555
|
+
const rawResponse = await http.get('/status');
|
|
556
|
+
return (0, validation_1.validateStatusResponse)(rawResponse, '/status');
|
|
557
|
+
});
|
|
246
558
|
}
|
|
247
559
|
/**
|
|
248
560
|
* Health check
|
|
249
561
|
*/
|
|
250
562
|
async health() {
|
|
251
|
-
return this.http
|
|
563
|
+
return this.withFailover(async (http) => {
|
|
564
|
+
const rawResponse = await http.get('/health');
|
|
565
|
+
return (0, validation_1.validateHealthResponse)(rawResponse, '/health');
|
|
566
|
+
});
|
|
252
567
|
}
|
|
253
568
|
/**
|
|
254
569
|
* Request key refresh for a wallet (proactive security)
|
|
255
570
|
*/
|
|
256
571
|
async refreshKeys(walletId) {
|
|
257
|
-
|
|
572
|
+
(0, validation_1.validateWalletId)(walletId);
|
|
573
|
+
return this.withFailover(async (http) => {
|
|
574
|
+
await http.post('/refresh', { wallet_id: walletId });
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Discover active agents from the on-chain registry
|
|
579
|
+
*/
|
|
580
|
+
async discoverAgents() {
|
|
581
|
+
return this.discovery.discoverAgents();
|
|
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);
|
|
258
664
|
}
|
|
259
665
|
// ────────────────────────────────────────────────
|
|
260
666
|
// WebSocket Events
|
|
@@ -270,17 +676,38 @@ class Sequence0 {
|
|
|
270
676
|
* ```
|
|
271
677
|
*/
|
|
272
678
|
async subscribe(walletId) {
|
|
273
|
-
|
|
679
|
+
if (walletId) {
|
|
680
|
+
(0, validation_1.validateWalletId)(walletId);
|
|
681
|
+
}
|
|
682
|
+
await this.getHttp(); // Ensure agent URL is resolved
|
|
683
|
+
const wsUrl = this.resolvedAgentUrl.replace(/^http/, 'ws') + '/ws';
|
|
274
684
|
const ws = new websocket_1.WsClient({ url: wsUrl, walletId });
|
|
275
685
|
await ws.connect();
|
|
276
686
|
return ws;
|
|
277
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
|
+
}
|
|
278
704
|
// ────────────────────────────────────────────────
|
|
279
705
|
// Internals
|
|
280
706
|
// ────────────────────────────────────────────────
|
|
281
707
|
async getWsClient() {
|
|
282
708
|
if (!this.ws || !this.ws.connected) {
|
|
283
|
-
|
|
709
|
+
await this.getHttp(); // Ensure agent URL is resolved
|
|
710
|
+
const wsUrl = this.resolvedAgentUrl.replace(/^http/, 'ws') + '/ws';
|
|
284
711
|
this.ws = new websocket_1.WsClient({ url: wsUrl });
|
|
285
712
|
await this.ws.connect();
|
|
286
713
|
}
|
|
@@ -293,11 +720,53 @@ class Sequence0 {
|
|
|
293
720
|
return networkRpcs[chain] || networkRpcs.ethereum;
|
|
294
721
|
}
|
|
295
722
|
getCurveForChain(chain) {
|
|
296
|
-
|
|
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
|
+
];
|
|
731
|
+
if (ed25519Chains.includes(chain)) {
|
|
297
732
|
return 'ed25519';
|
|
298
733
|
}
|
|
299
734
|
return 'secp256k1';
|
|
300
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
|
+
}
|
|
301
770
|
generateWalletId() {
|
|
302
771
|
const timestamp = Date.now().toString(36);
|
|
303
772
|
const random = Math.random().toString(36).slice(2, 8);
|