@tjamescouch/agentchat 0.1.0
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/.claude/settings.local.json +12 -0
- package/.github/workflows/fly-deploy.yml +18 -0
- package/Dockerfile +12 -0
- package/README.md +296 -0
- package/ROADMAP.md +88 -0
- package/SPEC.md +279 -0
- package/bin/agentchat.js +702 -0
- package/fly.toml +21 -0
- package/lib/client.js +362 -0
- package/lib/deploy/akash.js +811 -0
- package/lib/deploy/config.js +128 -0
- package/lib/deploy/index.js +149 -0
- package/lib/identity.js +166 -0
- package/lib/protocol.js +236 -0
- package/lib/server.js +526 -0
- package/package.json +44 -0
- package/quick-test.sh +45 -0
- package/test/integration.test.js +536 -0
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Akash Network Deployment Module
|
|
3
|
+
*
|
|
4
|
+
* Enables self-service deployment to Akash decentralized cloud.
|
|
5
|
+
*
|
|
6
|
+
* DISCLAIMER: This is infrastructure tooling, not a cryptocurrency product.
|
|
7
|
+
* AKT tokens are used solely to pay for compute resources.
|
|
8
|
+
* You are responsible for your own wallet security and funds.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs/promises';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import yaml from 'js-yaml';
|
|
15
|
+
|
|
16
|
+
// Default paths
|
|
17
|
+
const AKASH_DIR = path.join(os.homedir(), '.agentchat');
|
|
18
|
+
const WALLET_PATH = path.join(AKASH_DIR, 'akash-wallet.json');
|
|
19
|
+
const DEPLOYMENTS_PATH = path.join(AKASH_DIR, 'akash-deployments.json');
|
|
20
|
+
const CERTIFICATE_PATH = path.join(AKASH_DIR, 'akash-cert.json');
|
|
21
|
+
|
|
22
|
+
// Network configuration
|
|
23
|
+
const NETWORKS = {
|
|
24
|
+
mainnet: {
|
|
25
|
+
chainId: 'akashnet-2',
|
|
26
|
+
rpcEndpoint: 'https://rpc.akashnet.net:443',
|
|
27
|
+
restEndpoint: 'https://api.akashnet.net:443',
|
|
28
|
+
prefix: 'akash'
|
|
29
|
+
},
|
|
30
|
+
testnet: {
|
|
31
|
+
chainId: 'sandbox-01',
|
|
32
|
+
rpcEndpoint: 'https://rpc.sandbox-01.aksh.pw:443',
|
|
33
|
+
restEndpoint: 'https://api.sandbox-01.aksh.pw:443',
|
|
34
|
+
prefix: 'akash'
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Default deposit amount (5 AKT in uakt)
|
|
39
|
+
const DEFAULT_DEPOSIT = '5000000';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Akash Wallet - manages keypair and signing
|
|
43
|
+
*/
|
|
44
|
+
export class AkashWallet {
|
|
45
|
+
constructor(data) {
|
|
46
|
+
this.mnemonic = data.mnemonic;
|
|
47
|
+
this.address = data.address;
|
|
48
|
+
this.pubkey = data.pubkey;
|
|
49
|
+
this.network = data.network || 'testnet';
|
|
50
|
+
this.created = data.created || new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate a new wallet
|
|
55
|
+
*/
|
|
56
|
+
static async generate(network = 'testnet') {
|
|
57
|
+
const { DirectSecp256k1HdWallet } = await import('@cosmjs/proto-signing');
|
|
58
|
+
|
|
59
|
+
const wallet = await DirectSecp256k1HdWallet.generate(24, {
|
|
60
|
+
prefix: NETWORKS[network].prefix
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const [account] = await wallet.getAccounts();
|
|
64
|
+
|
|
65
|
+
return new AkashWallet({
|
|
66
|
+
mnemonic: wallet.mnemonic,
|
|
67
|
+
address: account.address,
|
|
68
|
+
pubkey: Buffer.from(account.pubkey).toString('base64'),
|
|
69
|
+
network,
|
|
70
|
+
created: new Date().toISOString()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Load wallet from mnemonic
|
|
76
|
+
*/
|
|
77
|
+
static async fromMnemonic(mnemonic, network = 'testnet') {
|
|
78
|
+
const { DirectSecp256k1HdWallet } = await import('@cosmjs/proto-signing');
|
|
79
|
+
|
|
80
|
+
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
|
|
81
|
+
prefix: NETWORKS[network].prefix
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const [account] = await wallet.getAccounts();
|
|
85
|
+
|
|
86
|
+
return new AkashWallet({
|
|
87
|
+
mnemonic,
|
|
88
|
+
address: account.address,
|
|
89
|
+
pubkey: Buffer.from(account.pubkey).toString('base64'),
|
|
90
|
+
network
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get signing wallet instance
|
|
96
|
+
*/
|
|
97
|
+
async getSigningWallet() {
|
|
98
|
+
const { DirectSecp256k1HdWallet } = await import('@cosmjs/proto-signing');
|
|
99
|
+
return DirectSecp256k1HdWallet.fromMnemonic(this.mnemonic, {
|
|
100
|
+
prefix: NETWORKS[this.network].prefix
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Save wallet to file
|
|
106
|
+
*/
|
|
107
|
+
async save(filePath = WALLET_PATH) {
|
|
108
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
109
|
+
|
|
110
|
+
const data = {
|
|
111
|
+
version: 1,
|
|
112
|
+
network: this.network,
|
|
113
|
+
address: this.address,
|
|
114
|
+
pubkey: this.pubkey,
|
|
115
|
+
mnemonic: this.mnemonic,
|
|
116
|
+
created: this.created
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
120
|
+
return filePath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load wallet from file
|
|
125
|
+
*/
|
|
126
|
+
static async load(filePath = WALLET_PATH) {
|
|
127
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
128
|
+
const data = JSON.parse(content);
|
|
129
|
+
|
|
130
|
+
if (data.version !== 1) {
|
|
131
|
+
throw new Error(`Unsupported wallet version: ${data.version}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return new AkashWallet(data);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if wallet file exists
|
|
139
|
+
*/
|
|
140
|
+
static async exists(filePath = WALLET_PATH) {
|
|
141
|
+
try {
|
|
142
|
+
await fs.access(filePath);
|
|
143
|
+
return true;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get wallet info for display (no sensitive data)
|
|
151
|
+
*/
|
|
152
|
+
getInfo() {
|
|
153
|
+
return {
|
|
154
|
+
address: this.address,
|
|
155
|
+
network: this.network,
|
|
156
|
+
created: this.created
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate SDL (Stack Definition Language) for agentchat server
|
|
163
|
+
*/
|
|
164
|
+
export function generateSDL(options = {}) {
|
|
165
|
+
const config = {
|
|
166
|
+
name: options.name || 'agentchat',
|
|
167
|
+
port: options.port || 6667,
|
|
168
|
+
cpu: options.cpu || 0.5,
|
|
169
|
+
memory: options.memory || 512,
|
|
170
|
+
storage: options.storage || 1,
|
|
171
|
+
logMessages: options.logMessages || false
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const sdl = {
|
|
175
|
+
version: '2.0',
|
|
176
|
+
services: {
|
|
177
|
+
agentchat: {
|
|
178
|
+
image: options.image || 'ghcr.io/anthropics/agentchat:latest',
|
|
179
|
+
expose: [
|
|
180
|
+
{
|
|
181
|
+
port: config.port,
|
|
182
|
+
as: 80,
|
|
183
|
+
to: [{ global: true }]
|
|
184
|
+
}
|
|
185
|
+
],
|
|
186
|
+
env: [
|
|
187
|
+
`PORT=${config.port}`,
|
|
188
|
+
'HOST=0.0.0.0',
|
|
189
|
+
`SERVER_NAME=${config.name}`,
|
|
190
|
+
`LOG_MESSAGES=${config.logMessages}`
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
profiles: {
|
|
195
|
+
compute: {
|
|
196
|
+
agentchat: {
|
|
197
|
+
resources: {
|
|
198
|
+
cpu: { units: config.cpu },
|
|
199
|
+
memory: { size: `${config.memory}Mi` },
|
|
200
|
+
storage: { size: `${config.storage}Gi` }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
placement: {
|
|
205
|
+
dcloud: {
|
|
206
|
+
pricing: {
|
|
207
|
+
agentchat: {
|
|
208
|
+
denom: 'uakt',
|
|
209
|
+
amount: 1000
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
deployment: {
|
|
216
|
+
agentchat: {
|
|
217
|
+
dcloud: {
|
|
218
|
+
profile: 'agentchat',
|
|
219
|
+
count: 1
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return yaml.dump(sdl, { lineWidth: -1 });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Akash deployment client
|
|
230
|
+
*/
|
|
231
|
+
export class AkashClient {
|
|
232
|
+
constructor(wallet) {
|
|
233
|
+
this.wallet = wallet;
|
|
234
|
+
this.network = NETWORKS[wallet.network];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get signing client for transactions
|
|
239
|
+
*/
|
|
240
|
+
async getSigningClient() {
|
|
241
|
+
const { SigningStargateClient } = await import('@cosmjs/stargate');
|
|
242
|
+
const { getAkashTypeRegistry } = await import('@akashnetwork/akashjs/build/stargate/index.js');
|
|
243
|
+
const { Registry } = await import('@cosmjs/proto-signing');
|
|
244
|
+
|
|
245
|
+
const signingWallet = await this.wallet.getSigningWallet();
|
|
246
|
+
const registry = new Registry(getAkashTypeRegistry());
|
|
247
|
+
|
|
248
|
+
return SigningStargateClient.connectWithSigner(
|
|
249
|
+
this.network.rpcEndpoint,
|
|
250
|
+
signingWallet,
|
|
251
|
+
{ registry }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Query account balance
|
|
257
|
+
*/
|
|
258
|
+
async getBalance() {
|
|
259
|
+
const { StargateClient } = await import('@cosmjs/stargate');
|
|
260
|
+
|
|
261
|
+
let client;
|
|
262
|
+
try {
|
|
263
|
+
client = await StargateClient.connect(this.network.rpcEndpoint);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Failed to connect to ${this.wallet.network} RPC endpoint.\n` +
|
|
267
|
+
`Network: ${this.network.rpcEndpoint}\n` +
|
|
268
|
+
`The network may be temporarily unavailable. Try again later.`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const balance = await client.getBalance(this.wallet.address, 'uakt');
|
|
273
|
+
const akt = parseInt(balance.amount) / 1_000_000;
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
uakt: balance.amount,
|
|
277
|
+
akt: akt.toFixed(6),
|
|
278
|
+
sufficient: parseInt(balance.amount) >= 5_000_000
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a deployment on Akash
|
|
284
|
+
*/
|
|
285
|
+
async createDeployment(sdlContent, options = {}) {
|
|
286
|
+
const { SDL } = await import('@akashnetwork/akashjs/build/sdl/SDL/SDL.js');
|
|
287
|
+
const { MsgCreateDeployment } = await import('@akashnetwork/akash-api/v1beta3');
|
|
288
|
+
const { Message } = await import('@akashnetwork/akashjs/build/stargate/index.js');
|
|
289
|
+
|
|
290
|
+
// Parse SDL
|
|
291
|
+
const sdl = SDL.fromString(sdlContent, 'beta3');
|
|
292
|
+
|
|
293
|
+
// Get signing client
|
|
294
|
+
const client = await this.getSigningClient();
|
|
295
|
+
const blockHeight = await client.getHeight();
|
|
296
|
+
|
|
297
|
+
// Create deployment ID
|
|
298
|
+
const dseq = options.dseq || blockHeight.toString();
|
|
299
|
+
|
|
300
|
+
// Build deployment message
|
|
301
|
+
const groups = sdl.groups();
|
|
302
|
+
const manifestVersion = await sdl.manifestVersion();
|
|
303
|
+
|
|
304
|
+
const deploymentMsg = {
|
|
305
|
+
id: {
|
|
306
|
+
owner: this.wallet.address,
|
|
307
|
+
dseq: dseq
|
|
308
|
+
},
|
|
309
|
+
groups: groups,
|
|
310
|
+
deposit: {
|
|
311
|
+
denom: 'uakt',
|
|
312
|
+
amount: options.deposit || DEFAULT_DEPOSIT
|
|
313
|
+
},
|
|
314
|
+
version: manifestVersion,
|
|
315
|
+
depositor: this.wallet.address
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const msg = {
|
|
319
|
+
typeUrl: Message.MsgCreateDeployment,
|
|
320
|
+
value: MsgCreateDeployment.fromPartial(deploymentMsg)
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Broadcast transaction
|
|
324
|
+
const fee = {
|
|
325
|
+
amount: [{ denom: 'uakt', amount: '25000' }],
|
|
326
|
+
gas: '500000'
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
console.log('Broadcasting deployment transaction...');
|
|
330
|
+
const tx = await client.signAndBroadcast(
|
|
331
|
+
this.wallet.address,
|
|
332
|
+
[msg],
|
|
333
|
+
fee,
|
|
334
|
+
'agentchat deployment'
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (tx.code !== 0) {
|
|
338
|
+
throw new Error(`Deployment failed: ${tx.rawLog}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
console.log(`Deployment created: dseq=${dseq}, tx=${tx.transactionHash}`);
|
|
342
|
+
|
|
343
|
+
// Save deployment record
|
|
344
|
+
await this.saveDeployment({
|
|
345
|
+
dseq,
|
|
346
|
+
owner: this.wallet.address,
|
|
347
|
+
txHash: tx.transactionHash,
|
|
348
|
+
status: 'pending_bids',
|
|
349
|
+
createdAt: new Date().toISOString(),
|
|
350
|
+
sdl: sdlContent
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
dseq,
|
|
355
|
+
txHash: tx.transactionHash,
|
|
356
|
+
status: 'pending_bids',
|
|
357
|
+
manifest: sdl.manifest()
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Query bids for a deployment
|
|
363
|
+
*/
|
|
364
|
+
async queryBids(dseq) {
|
|
365
|
+
const url = `${this.network.restEndpoint}/akash/market/v1beta4/bids/list?filters.owner=${this.wallet.address}&filters.dseq=${dseq}`;
|
|
366
|
+
|
|
367
|
+
const response = await fetch(url);
|
|
368
|
+
if (!response.ok) {
|
|
369
|
+
throw new Error(`Failed to query bids: ${response.statusText}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const data = await response.json();
|
|
373
|
+
return data.bids || [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Accept a bid and create a lease
|
|
378
|
+
*/
|
|
379
|
+
async createLease(dseq, provider, gseq = 1, oseq = 1) {
|
|
380
|
+
const { MsgCreateLease } = await import('@akashnetwork/akash-api/v1beta3');
|
|
381
|
+
const { Message } = await import('@akashnetwork/akashjs/build/stargate/index.js');
|
|
382
|
+
|
|
383
|
+
const client = await this.getSigningClient();
|
|
384
|
+
|
|
385
|
+
const leaseMsg = {
|
|
386
|
+
bidId: {
|
|
387
|
+
owner: this.wallet.address,
|
|
388
|
+
dseq: dseq,
|
|
389
|
+
gseq: gseq,
|
|
390
|
+
oseq: oseq,
|
|
391
|
+
provider: provider
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const msg = {
|
|
396
|
+
typeUrl: Message.MsgCreateLease,
|
|
397
|
+
value: MsgCreateLease.fromPartial(leaseMsg)
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const fee = {
|
|
401
|
+
amount: [{ denom: 'uakt', amount: '25000' }],
|
|
402
|
+
gas: '500000'
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
console.log('Creating lease...');
|
|
406
|
+
const tx = await client.signAndBroadcast(
|
|
407
|
+
this.wallet.address,
|
|
408
|
+
[msg],
|
|
409
|
+
fee,
|
|
410
|
+
'create lease'
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
if (tx.code !== 0) {
|
|
414
|
+
throw new Error(`Lease creation failed: ${tx.rawLog}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(`Lease created: provider=${provider}, tx=${tx.transactionHash}`);
|
|
418
|
+
|
|
419
|
+
// Update deployment record
|
|
420
|
+
await this.updateDeployment(dseq, {
|
|
421
|
+
status: 'active',
|
|
422
|
+
provider,
|
|
423
|
+
leaseCreatedAt: new Date().toISOString()
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
dseq,
|
|
428
|
+
provider,
|
|
429
|
+
gseq,
|
|
430
|
+
oseq,
|
|
431
|
+
txHash: tx.transactionHash
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Send manifest to provider
|
|
437
|
+
*/
|
|
438
|
+
async sendManifest(dseq, provider, manifest) {
|
|
439
|
+
const { certificate } = await import('@akashnetwork/akashjs/build/certificates/index.js');
|
|
440
|
+
|
|
441
|
+
// Load or create certificate
|
|
442
|
+
let cert;
|
|
443
|
+
try {
|
|
444
|
+
const certData = await fs.readFile(CERTIFICATE_PATH, 'utf-8');
|
|
445
|
+
cert = JSON.parse(certData);
|
|
446
|
+
} catch {
|
|
447
|
+
// Generate new certificate
|
|
448
|
+
console.log('Generating deployment certificate...');
|
|
449
|
+
const generated = await certificate.create(this.wallet.address);
|
|
450
|
+
cert = {
|
|
451
|
+
cert: generated.cert,
|
|
452
|
+
privateKey: generated.privateKey,
|
|
453
|
+
publicKey: generated.publicKey
|
|
454
|
+
};
|
|
455
|
+
await fs.writeFile(CERTIFICATE_PATH, JSON.stringify(cert, null, 2), { mode: 0o600 });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Query provider info to get hostUri
|
|
459
|
+
const providerUrl = `${this.network.restEndpoint}/akash/provider/v1beta3/providers/${provider}`;
|
|
460
|
+
const providerResponse = await fetch(providerUrl);
|
|
461
|
+
if (!providerResponse.ok) {
|
|
462
|
+
throw new Error(`Failed to get provider info: ${providerResponse.statusText}`);
|
|
463
|
+
}
|
|
464
|
+
const providerInfo = await providerResponse.json();
|
|
465
|
+
const hostUri = providerInfo.provider?.hostUri;
|
|
466
|
+
|
|
467
|
+
if (!hostUri) {
|
|
468
|
+
throw new Error('Provider hostUri not found');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Send manifest
|
|
472
|
+
const manifestUrl = `${hostUri}/deployment/${dseq}/manifest`;
|
|
473
|
+
console.log(`Sending manifest to ${manifestUrl}...`);
|
|
474
|
+
|
|
475
|
+
const response = await fetch(manifestUrl, {
|
|
476
|
+
method: 'PUT',
|
|
477
|
+
headers: {
|
|
478
|
+
'Content-Type': 'application/json'
|
|
479
|
+
},
|
|
480
|
+
body: JSON.stringify(manifest)
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (!response.ok) {
|
|
484
|
+
const text = await response.text();
|
|
485
|
+
throw new Error(`Failed to send manifest: ${response.statusText} - ${text}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log('Manifest sent successfully');
|
|
489
|
+
|
|
490
|
+
// Update deployment record
|
|
491
|
+
await this.updateDeployment(dseq, {
|
|
492
|
+
manifestSent: true,
|
|
493
|
+
manifestSentAt: new Date().toISOString()
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
return { success: true };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get lease status from provider
|
|
501
|
+
*/
|
|
502
|
+
async getLeaseStatus(dseq, provider, gseq = 1, oseq = 1) {
|
|
503
|
+
// Query provider info
|
|
504
|
+
const providerUrl = `${this.network.restEndpoint}/akash/provider/v1beta3/providers/${provider}`;
|
|
505
|
+
const providerResponse = await fetch(providerUrl);
|
|
506
|
+
if (!providerResponse.ok) {
|
|
507
|
+
throw new Error(`Failed to get provider info: ${providerResponse.statusText}`);
|
|
508
|
+
}
|
|
509
|
+
const providerInfo = await providerResponse.json();
|
|
510
|
+
const hostUri = providerInfo.provider?.hostUri;
|
|
511
|
+
|
|
512
|
+
if (!hostUri) {
|
|
513
|
+
throw new Error('Provider hostUri not found');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Query lease status
|
|
517
|
+
const statusUrl = `${hostUri}/lease/${dseq}/${gseq}/${oseq}/status`;
|
|
518
|
+
const response = await fetch(statusUrl);
|
|
519
|
+
|
|
520
|
+
if (!response.ok) {
|
|
521
|
+
throw new Error(`Failed to get lease status: ${response.statusText}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return response.json();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Close a deployment
|
|
529
|
+
*/
|
|
530
|
+
async closeDeployment(dseq) {
|
|
531
|
+
const { MsgCloseDeployment } = await import('@akashnetwork/akash-api/v1beta3');
|
|
532
|
+
const { Message } = await import('@akashnetwork/akashjs/build/stargate/index.js');
|
|
533
|
+
|
|
534
|
+
const client = await this.getSigningClient();
|
|
535
|
+
|
|
536
|
+
const closeMsg = {
|
|
537
|
+
id: {
|
|
538
|
+
owner: this.wallet.address,
|
|
539
|
+
dseq: dseq
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const msg = {
|
|
544
|
+
typeUrl: Message.MsgCloseDeployment,
|
|
545
|
+
value: MsgCloseDeployment.fromPartial(closeMsg)
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const fee = {
|
|
549
|
+
amount: [{ denom: 'uakt', amount: '25000' }],
|
|
550
|
+
gas: '500000'
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
console.log('Closing deployment...');
|
|
554
|
+
const tx = await client.signAndBroadcast(
|
|
555
|
+
this.wallet.address,
|
|
556
|
+
[msg],
|
|
557
|
+
fee,
|
|
558
|
+
'close deployment'
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
if (tx.code !== 0) {
|
|
562
|
+
throw new Error(`Failed to close deployment: ${tx.rawLog}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Update deployment record
|
|
566
|
+
await this.updateDeployment(dseq, {
|
|
567
|
+
status: 'closed',
|
|
568
|
+
closedAt: new Date().toISOString()
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
return { dseq, txHash: tx.transactionHash, status: 'closed' };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Save deployment to local records
|
|
576
|
+
*/
|
|
577
|
+
async saveDeployment(deployment) {
|
|
578
|
+
let deployments = [];
|
|
579
|
+
try {
|
|
580
|
+
const content = await fs.readFile(DEPLOYMENTS_PATH, 'utf-8');
|
|
581
|
+
deployments = JSON.parse(content);
|
|
582
|
+
} catch {
|
|
583
|
+
// File doesn't exist yet
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
deployments.push(deployment);
|
|
587
|
+
await fs.writeFile(DEPLOYMENTS_PATH, JSON.stringify(deployments, null, 2));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Update deployment in local records
|
|
592
|
+
*/
|
|
593
|
+
async updateDeployment(dseq, updates) {
|
|
594
|
+
let deployments = [];
|
|
595
|
+
try {
|
|
596
|
+
const content = await fs.readFile(DEPLOYMENTS_PATH, 'utf-8');
|
|
597
|
+
deployments = JSON.parse(content);
|
|
598
|
+
} catch {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const index = deployments.findIndex(d => d.dseq === dseq);
|
|
603
|
+
if (index !== -1) {
|
|
604
|
+
deployments[index] = { ...deployments[index], ...updates };
|
|
605
|
+
await fs.writeFile(DEPLOYMENTS_PATH, JSON.stringify(deployments, null, 2));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* List local deployment records
|
|
611
|
+
*/
|
|
612
|
+
async listDeployments() {
|
|
613
|
+
try {
|
|
614
|
+
const content = await fs.readFile(DEPLOYMENTS_PATH, 'utf-8');
|
|
615
|
+
return JSON.parse(content);
|
|
616
|
+
} catch {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* High-level deployment functions for CLI
|
|
624
|
+
*/
|
|
625
|
+
|
|
626
|
+
export async function generateWallet(network = 'testnet', walletPath = WALLET_PATH) {
|
|
627
|
+
if (await AkashWallet.exists(walletPath)) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Wallet already exists at ${walletPath}\n` +
|
|
630
|
+
'Use --force to overwrite (WARNING: This will destroy your existing wallet!)'
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const wallet = await AkashWallet.generate(network);
|
|
635
|
+
await wallet.save(walletPath);
|
|
636
|
+
|
|
637
|
+
return wallet;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export async function checkBalance(walletPath = WALLET_PATH) {
|
|
641
|
+
const wallet = await AkashWallet.load(walletPath);
|
|
642
|
+
const client = new AkashClient(wallet);
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
wallet: wallet.getInfo(),
|
|
646
|
+
balance: await client.getBalance()
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export async function createDeployment(options = {}) {
|
|
651
|
+
const walletPath = options.walletPath || WALLET_PATH;
|
|
652
|
+
const wallet = await AkashWallet.load(walletPath);
|
|
653
|
+
const client = new AkashClient(wallet);
|
|
654
|
+
|
|
655
|
+
// Check balance first
|
|
656
|
+
const balance = await client.getBalance();
|
|
657
|
+
if (!balance.sufficient) {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`Insufficient balance: ${balance.akt} AKT\n` +
|
|
660
|
+
`Need at least 5 AKT for deployment.\n` +
|
|
661
|
+
`Fund your wallet: ${wallet.address}`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Generate SDL
|
|
666
|
+
const sdl = generateSDL(options);
|
|
667
|
+
|
|
668
|
+
// Create deployment
|
|
669
|
+
const deployment = await client.createDeployment(sdl, options);
|
|
670
|
+
|
|
671
|
+
// Wait for bids
|
|
672
|
+
console.log('Waiting for bids (30 seconds)...');
|
|
673
|
+
await new Promise(resolve => setTimeout(resolve, 30000));
|
|
674
|
+
|
|
675
|
+
// Query bids
|
|
676
|
+
const bids = await client.queryBids(deployment.dseq);
|
|
677
|
+
|
|
678
|
+
if (bids.length === 0) {
|
|
679
|
+
console.log('No bids received. Deployment is pending.');
|
|
680
|
+
console.log(`Check status with: agentchat deploy --provider akash --status`);
|
|
681
|
+
return deployment;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Select best bid (lowest price)
|
|
685
|
+
const sortedBids = bids
|
|
686
|
+
.filter(b => b.bid?.state === 'open')
|
|
687
|
+
.sort((a, b) => parseInt(a.bid?.price?.amount || 0) - parseInt(b.bid?.price?.amount || 0));
|
|
688
|
+
|
|
689
|
+
if (sortedBids.length === 0) {
|
|
690
|
+
console.log('No open bids available.');
|
|
691
|
+
return deployment;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const bestBid = sortedBids[0];
|
|
695
|
+
const provider = bestBid.bid?.bidId?.provider;
|
|
696
|
+
|
|
697
|
+
console.log(`Accepting bid from provider: ${provider}`);
|
|
698
|
+
|
|
699
|
+
// Create lease
|
|
700
|
+
const lease = await client.createLease(
|
|
701
|
+
deployment.dseq,
|
|
702
|
+
provider,
|
|
703
|
+
bestBid.bid?.bidId?.gseq || 1,
|
|
704
|
+
bestBid.bid?.bidId?.oseq || 1
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Send manifest
|
|
708
|
+
await client.sendManifest(deployment.dseq, provider, deployment.manifest);
|
|
709
|
+
|
|
710
|
+
// Get status
|
|
711
|
+
console.log('Waiting for deployment to start (15 seconds)...');
|
|
712
|
+
await new Promise(resolve => setTimeout(resolve, 15000));
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
const status = await client.getLeaseStatus(deployment.dseq, provider);
|
|
716
|
+
const services = status.services || {};
|
|
717
|
+
const service = Object.values(services)[0];
|
|
718
|
+
const uris = service?.uris || [];
|
|
719
|
+
|
|
720
|
+
if (uris.length > 0) {
|
|
721
|
+
console.log(`\nDeployment ready!`);
|
|
722
|
+
console.log(`Endpoint: ${uris[0]}`);
|
|
723
|
+
return { ...deployment, ...lease, endpoint: uris[0], status: 'active' };
|
|
724
|
+
}
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.log('Status check failed, deployment may still be starting.');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return { ...deployment, ...lease, status: 'active' };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export async function listDeployments(walletPath = WALLET_PATH) {
|
|
733
|
+
const wallet = await AkashWallet.load(walletPath);
|
|
734
|
+
const client = new AkashClient(wallet);
|
|
735
|
+
|
|
736
|
+
return client.listDeployments();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export async function closeDeployment(dseq, walletPath = WALLET_PATH) {
|
|
740
|
+
const wallet = await AkashWallet.load(walletPath);
|
|
741
|
+
const client = new AkashClient(wallet);
|
|
742
|
+
|
|
743
|
+
return client.closeDeployment(dseq);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export async function acceptBid(dseq, provider, walletPath = WALLET_PATH) {
|
|
747
|
+
const wallet = await AkashWallet.load(walletPath);
|
|
748
|
+
const client = new AkashClient(wallet);
|
|
749
|
+
|
|
750
|
+
// Get deployment from local records
|
|
751
|
+
const deployments = await client.listDeployments();
|
|
752
|
+
const deployment = deployments.find(d => d.dseq === dseq);
|
|
753
|
+
|
|
754
|
+
if (!deployment) {
|
|
755
|
+
throw new Error(`Deployment ${dseq} not found in local records`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Create lease
|
|
759
|
+
const lease = await client.createLease(dseq, provider);
|
|
760
|
+
|
|
761
|
+
// Parse SDL and send manifest
|
|
762
|
+
const { SDL } = await import('@akashnetwork/akashjs/build/sdl/SDL/SDL.js');
|
|
763
|
+
const sdl = SDL.fromString(deployment.sdl, 'beta3');
|
|
764
|
+
await client.sendManifest(dseq, provider, sdl.manifest());
|
|
765
|
+
|
|
766
|
+
return lease;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export async function queryBids(dseq, walletPath = WALLET_PATH) {
|
|
770
|
+
const wallet = await AkashWallet.load(walletPath);
|
|
771
|
+
const client = new AkashClient(wallet);
|
|
772
|
+
|
|
773
|
+
return client.queryBids(dseq);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export async function getDeploymentStatus(dseq, walletPath = WALLET_PATH) {
|
|
777
|
+
const wallet = await AkashWallet.load(walletPath);
|
|
778
|
+
const client = new AkashClient(wallet);
|
|
779
|
+
|
|
780
|
+
// Get deployment from local records
|
|
781
|
+
const deployments = await client.listDeployments();
|
|
782
|
+
const deployment = deployments.find(d => d.dseq === dseq);
|
|
783
|
+
|
|
784
|
+
if (!deployment) {
|
|
785
|
+
throw new Error(`Deployment ${dseq} not found`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (!deployment.provider) {
|
|
789
|
+
// No lease yet, check for bids
|
|
790
|
+
const bids = await client.queryBids(dseq);
|
|
791
|
+
return {
|
|
792
|
+
...deployment,
|
|
793
|
+
bids: bids.map(b => ({
|
|
794
|
+
provider: b.bid?.bidId?.provider,
|
|
795
|
+
price: b.bid?.price?.amount,
|
|
796
|
+
state: b.bid?.state
|
|
797
|
+
}))
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Has a lease, get status from provider
|
|
802
|
+
try {
|
|
803
|
+
const status = await client.getLeaseStatus(dseq, deployment.provider);
|
|
804
|
+
return { ...deployment, leaseStatus: status };
|
|
805
|
+
} catch (err) {
|
|
806
|
+
return { ...deployment, leaseStatusError: err.message };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Export for CLI
|
|
811
|
+
export { NETWORKS, WALLET_PATH, DEPLOYMENTS_PATH, CERTIFICATE_PATH };
|