@promus/cli 0.24.17
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 +18 -0
- package/bin/promus +33 -0
- package/package.json +51 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_inft-ref.ts +43 -0
- package/src/commands/_unlock.ts +74 -0
- package/src/commands/admin-autotopup-tick.ts +73 -0
- package/src/commands/admin.test.ts +34 -0
- package/src/commands/admin.ts +32 -0
- package/src/commands/balance.test.ts +10 -0
- package/src/commands/balance.ts +112 -0
- package/src/commands/chat-sandbox.tsx +520 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1916 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/drain.ts +90 -0
- package/src/commands/gateway-logs.ts +47 -0
- package/src/commands/gateway-run.ts +54 -0
- package/src/commands/gateway-start.ts +218 -0
- package/src/commands/gateway-status.ts +88 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/init/cost.test.ts +169 -0
- package/src/commands/init/cost.ts +154 -0
- package/src/commands/init/funding-gate.ts +67 -0
- package/src/commands/init/model-picker.ts +81 -0
- package/src/commands/init/operator-picker.ts +263 -0
- package/src/commands/init/resume.ts +136 -0
- package/src/commands/init/sandbox-provision.test.ts +497 -0
- package/src/commands/init/sandbox-provision.ts +1177 -0
- package/src/commands/init/telegram-step.ts +229 -0
- package/src/commands/init/wizard-state.ts +95 -0
- package/src/commands/init.ts +612 -0
- package/src/commands/inspect.ts +529 -0
- package/src/commands/ledger.ts +176 -0
- package/src/commands/logs.ts +86 -0
- package/src/commands/migrate-keystore.ts +155 -0
- package/src/commands/model.ts +48 -0
- package/src/commands/pairing-approve.ts +114 -0
- package/src/commands/pairing-clear.ts +42 -0
- package/src/commands/pairing-list.ts +58 -0
- package/src/commands/pairing-revoke.ts +52 -0
- package/src/commands/pairing.test.ts +88 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/pause.ts +99 -0
- package/src/commands/profile.ts +184 -0
- package/src/commands/restore.ts +221 -0
- package/src/commands/resume.ts +181 -0
- package/src/commands/status.ts +119 -0
- package/src/commands/sync.ts +147 -0
- package/src/commands/telegram-remove.ts +65 -0
- package/src/commands/telegram-setup.ts +74 -0
- package/src/commands/telegram-status.ts +89 -0
- package/src/commands/telegram.test.ts +50 -0
- package/src/commands/telegram.ts +44 -0
- package/src/commands/topup.ts +303 -0
- package/src/commands/transfer.test.ts +111 -0
- package/src/commands/transfer.ts +520 -0
- package/src/commands/upgrade.test.ts +137 -0
- package/src/commands/upgrade.ts +690 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.test.ts +96 -0
- package/src/config/render.ts +110 -0
- package/src/index.ts +378 -0
- package/src/sandbox/client.test.ts +251 -0
- package/src/sandbox/client.ts +424 -0
- package/src/ui/app.tsx +677 -0
- package/src/ui/approval-summary.test.ts +154 -0
- package/src/ui/approval-summary.ts +34 -0
- package/src/ui/markdown-parse.ts +219 -0
- package/src/ui/markdown.test.ts +146 -0
- package/src/ui/markdown.tsx +37 -0
- package/src/ui/state.test.ts +74 -0
- package/src/ui/state.ts +198 -0
- package/src/util/bootstrap-mode.test.ts +40 -0
- package/src/util/bootstrap-mode.ts +25 -0
- package/src/util/bootstrap-progress-box.test.ts +190 -0
- package/src/util/bootstrap-progress-box.ts +378 -0
- package/src/util/brain-secrets.ts +96 -0
- package/src/util/cli-version.ts +28 -0
- package/src/util/format.test.ts +16 -0
- package/src/util/format.ts +11 -0
- package/src/util/gateway-spawn.test.ts +86 -0
- package/src/util/gateway-spawn.ts +128 -0
- package/src/util/gateway-version.test.ts +113 -0
- package/src/util/gateway-version.ts +154 -0
- package/src/util/github-releases.test.ts +116 -0
- package/src/util/github-releases.ts +79 -0
- package/src/util/profile-key.test.ts +60 -0
- package/src/util/profile-key.ts +25 -0
- package/src/util/ref-resolver.test.ts +77 -0
- package/src/util/ref-resolver.ts +55 -0
- package/src/util/silence-console.test.ts +53 -0
- package/src/util/silence-console.ts +40 -0
- package/src/util/telegram-secrets.test.ts +227 -0
- package/src/util/telegram-secrets.ts +223 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `promus gateway status` — show PID, uptime, socket path, lock state,
|
|
3
|
+
* operator-session freshness.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import {
|
|
11
|
+
agentPaths,
|
|
12
|
+
iNFTAgentId,
|
|
13
|
+
isOperatorSessionFresh,
|
|
14
|
+
readOperatorSession,
|
|
15
|
+
} from '@promus/core'
|
|
16
|
+
import { type Address, getAddress } from 'viem'
|
|
17
|
+
import { findAndLoadConfig } from '../config/load'
|
|
18
|
+
|
|
19
|
+
export interface GatewayStatusOpts {
|
|
20
|
+
agentId?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fmtAge(ms: number): string {
|
|
24
|
+
const s = Math.floor(ms / 1000)
|
|
25
|
+
if (s < 60) return `${s}s`
|
|
26
|
+
const m = Math.floor(s / 60)
|
|
27
|
+
if (m < 60) return `${m}m`
|
|
28
|
+
const h = Math.floor(m / 60)
|
|
29
|
+
return `${h}h${m % 60}m`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runGatewayStatus(opts: GatewayStatusOpts): Promise<void> {
|
|
33
|
+
let agentId = opts.agentId
|
|
34
|
+
if (!agentId) {
|
|
35
|
+
const found = await findAndLoadConfig()
|
|
36
|
+
if (!found?.config) {
|
|
37
|
+
console.error('promus gateway status: no promus.config.ts and no --agent provided')
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
const contractAddress = getAddress(found.config.identity.iNFT!.contract as Address)
|
|
41
|
+
const tokenId = BigInt(found.config.identity.iNFT!.tokenId)
|
|
42
|
+
agentId = iNFTAgentId({ contractAddress, tokenId })
|
|
43
|
+
}
|
|
44
|
+
const paths = agentPaths.agent(agentId)
|
|
45
|
+
const socketPath = join(paths.dir, 'gateway.sock')
|
|
46
|
+
const identityHash = createHash('sha256').update(agentId).digest('hex').slice(0, 16)
|
|
47
|
+
const lockFile = join(homedir(), '.promus', 'locks', `@promus/gateway-${identityHash}.lock`)
|
|
48
|
+
|
|
49
|
+
console.log(`agent: ${agentId}`)
|
|
50
|
+
console.log(`socket: ${socketPath} ${existsSync(socketPath) ? '(present)' : '(absent)'}`)
|
|
51
|
+
console.log(`lock: ${lockFile} ${existsSync(lockFile) ? '(present)' : '(absent)'}`)
|
|
52
|
+
|
|
53
|
+
// PID + uptime via lock file.
|
|
54
|
+
if (existsSync(lockFile)) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(readFileSync(lockFile, 'utf8')) as { pid?: number }
|
|
57
|
+
if (typeof parsed.pid === 'number') {
|
|
58
|
+
let alive = false
|
|
59
|
+
try {
|
|
60
|
+
process.kill(parsed.pid, 0)
|
|
61
|
+
alive = true
|
|
62
|
+
} catch {
|
|
63
|
+
/* dead */
|
|
64
|
+
}
|
|
65
|
+
const stat = statSync(lockFile)
|
|
66
|
+
const ageMs = Date.now() - stat.mtimeMs
|
|
67
|
+
console.log(`pid: ${parsed.pid} ${alive ? '(alive)' : '(dead — stale lock)'}`)
|
|
68
|
+
console.log(`lock-age: ${fmtAge(ageMs)}`)
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
console.log('pid: (lock file unreadable)')
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log('pid: (not running)')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Operator-session freshness.
|
|
78
|
+
const fresh = isOperatorSessionFresh(agentId)
|
|
79
|
+
console.log(`session: ${fresh ? 'fresh' : 'absent or expired'}`)
|
|
80
|
+
if (fresh) {
|
|
81
|
+
const sess = readOperatorSession(agentId)
|
|
82
|
+
if (sess) {
|
|
83
|
+
const remaining = sess.expiresAt - Date.now()
|
|
84
|
+
const scopes = Object.keys(sess.keys).filter(k => sess.keys[k as keyof typeof sess.keys])
|
|
85
|
+
console.log(`session-ttl: ${fmtAge(remaining)} remaining (scopes: ${scopes.join(', ')})`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `promus gateway stop` — SIGTERM the running gateway daemon via the lock
|
|
3
|
+
* file's PID. Falls through to SIGKILL after a 5s grace period.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, unlinkSync } from 'node:fs'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { agentPaths, iNFTAgentId } from '@promus/core'
|
|
10
|
+
import { type Address, getAddress } from 'viem'
|
|
11
|
+
import { findAndLoadConfig } from '../config/load'
|
|
12
|
+
|
|
13
|
+
export interface GatewayStopOpts {
|
|
14
|
+
agentId?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function lockPath(_agentId: string): string {
|
|
18
|
+
// Mirror packages/core/src/locks.ts — `~/.promus/locks/<scope>-<sha256(identity).slice(0,16)>.lock`
|
|
19
|
+
// For '@promus/gateway' scope. We compute the same hash as the lock module.
|
|
20
|
+
// Easiest: read all lock files and find one matching the agent.
|
|
21
|
+
return join(homedir(), '.promus', 'locks')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findGatewayLock(agentId: string): string | null {
|
|
25
|
+
// The lock filename embeds sha256(agentId).slice(0, 16). Compute it.
|
|
26
|
+
const { createHash } = require('node:crypto')
|
|
27
|
+
const identityHash = createHash('sha256').update(agentId).digest('hex').slice(0, 16)
|
|
28
|
+
const lockFile = join(lockPath(agentId), `@promus/gateway-${identityHash}.lock`)
|
|
29
|
+
return existsSync(lockFile) ? lockFile : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runGatewayStop(opts: GatewayStopOpts): Promise<void> {
|
|
33
|
+
let agentId = opts.agentId
|
|
34
|
+
if (!agentId) {
|
|
35
|
+
const found = await findAndLoadConfig()
|
|
36
|
+
if (!found?.config) {
|
|
37
|
+
console.error('promus gateway stop: no promus.config.ts and no --agent provided')
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
const contractAddress = getAddress(found.config.identity.iNFT!.contract as Address)
|
|
41
|
+
const tokenId = BigInt(found.config.identity.iNFT!.tokenId)
|
|
42
|
+
agentId = iNFTAgentId({ contractAddress, tokenId })
|
|
43
|
+
const subname = found.config.subname ?? null
|
|
44
|
+
const agentEoa = (found.config.identity?.agent as string | undefined) ?? null
|
|
45
|
+
const label = subname ? `${subname}.promus.0g` : `agent ${agentId.slice(0, 8)}…`
|
|
46
|
+
const eoaLabel = agentEoa ? ` (EOA ${agentEoa.slice(0, 6)}…${agentEoa.slice(-4)})` : ''
|
|
47
|
+
const configPath = found.path ?? '<unknown>'
|
|
48
|
+
console.log(`promus gateway stop → ${label}${eoaLabel}`)
|
|
49
|
+
console.log(` config: ${configPath}`)
|
|
50
|
+
console.log(
|
|
51
|
+
' if this is not the agent you meant, set PROMUS_ROOT or pass --agent <id> before re-running.',
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
const lockFile = findGatewayLock(agentId)
|
|
55
|
+
if (!lockFile) {
|
|
56
|
+
console.log(`gateway not running (no lock at ${lockPath(agentId)})`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
let pid: number
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(lockFile, 'utf8').trim()
|
|
62
|
+
// Lock files are JSON with shape `{pid, scope, identityHash, expiresAt}`.
|
|
63
|
+
const parsed = JSON.parse(raw) as { pid?: number }
|
|
64
|
+
if (typeof parsed.pid !== 'number') {
|
|
65
|
+
console.error('promus gateway stop: lock file has no pid field')
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
pid = parsed.pid
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`promus gateway stop: lock file unreadable — ${(e as Error).message}`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Verify the PID is alive.
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 0)
|
|
77
|
+
} catch {
|
|
78
|
+
console.log(`gateway not running (stale lock pid=${pid}); cleaning up`)
|
|
79
|
+
try {
|
|
80
|
+
unlinkSync(lockFile)
|
|
81
|
+
} catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Send SIGTERM, wait up to 5s, then SIGKILL.
|
|
88
|
+
console.log(`stopping gateway pid=${pid} ...`)
|
|
89
|
+
try {
|
|
90
|
+
process.kill(pid, 'SIGTERM')
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(`promus gateway stop: SIGTERM failed — ${(e as Error).message}`)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const start = Date.now()
|
|
97
|
+
while (Date.now() - start < 5_000) {
|
|
98
|
+
try {
|
|
99
|
+
process.kill(pid, 0)
|
|
100
|
+
} catch {
|
|
101
|
+
console.log(`gateway stopped pid=${pid}`)
|
|
102
|
+
// Lock file is auto-removed by daemon's shutdown handler. Belt + suspenders:
|
|
103
|
+
try {
|
|
104
|
+
if (existsSync(lockFile)) unlinkSync(lockFile)
|
|
105
|
+
} catch {
|
|
106
|
+
/* ignore */
|
|
107
|
+
}
|
|
108
|
+
// Also clean up the socket file in case the daemon didn't.
|
|
109
|
+
const socketPath = join(agentPaths.agent(agentId).dir, 'gateway.sock')
|
|
110
|
+
try {
|
|
111
|
+
if (existsSync(socketPath)) unlinkSync(socketPath)
|
|
112
|
+
} catch {
|
|
113
|
+
/* ignore */
|
|
114
|
+
}
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
await new Promise(r => setTimeout(r, 200))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('gateway did not exit in 5s; sending SIGKILL')
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 'SIGKILL')
|
|
123
|
+
} catch (e) {
|
|
124
|
+
console.error(`promus gateway stop: SIGKILL failed — ${(e as Error).message}`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
if (existsSync(lockFile)) unlinkSync(lockFile)
|
|
129
|
+
} catch {
|
|
130
|
+
/* ignore */
|
|
131
|
+
}
|
|
132
|
+
console.log(`gateway force-killed pid=${pid}`)
|
|
133
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `promus gateway <sub>` argv dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Subs:
|
|
5
|
+
* run — foreground daemon (blocks; Ctrl+C to stop). Uses operator-session.
|
|
6
|
+
* start — interactive: prompt Touch ID, write operator-session, fork daemon.
|
|
7
|
+
* stop — SIGTERM the running daemon via the lock file's PID.
|
|
8
|
+
* restart — stop + start.
|
|
9
|
+
* status — show PID, uptime, socket path, lock state.
|
|
10
|
+
* logs — tail the gateway log (--tail N, --follow).
|
|
11
|
+
*
|
|
12
|
+
* v0.19.x scope: run + start + stop + status. restart/logs/install/setup ship
|
|
13
|
+
* in v0.19.3 alongside launchd plist generator.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type GatewaySub = 'run' | 'start' | 'stop' | 'restart' | 'status' | 'logs'
|
|
17
|
+
|
|
18
|
+
export interface ParsedGatewayArgs {
|
|
19
|
+
sub: GatewaySub
|
|
20
|
+
/** Optional --agent <id> override; defaults to config-derived. */
|
|
21
|
+
agentId?: string
|
|
22
|
+
/** --tail N for logs; default 100. */
|
|
23
|
+
tail?: number
|
|
24
|
+
/** --follow / -f for logs. */
|
|
25
|
+
follow?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ParseResult = ParsedGatewayArgs | { error: string }
|
|
29
|
+
|
|
30
|
+
export function parseGatewayArgs(argv: string[]): ParseResult {
|
|
31
|
+
const sub = argv[0]
|
|
32
|
+
if (!sub) {
|
|
33
|
+
return { error: 'usage: promus gateway <run | start | stop | restart | status | logs>' }
|
|
34
|
+
}
|
|
35
|
+
if (!['run', 'start', 'stop', 'restart', 'status', 'logs'].includes(sub)) {
|
|
36
|
+
return { error: `unknown gateway sub: ${sub}` }
|
|
37
|
+
}
|
|
38
|
+
let agentId: string | undefined
|
|
39
|
+
let tail: number | undefined
|
|
40
|
+
let follow = false
|
|
41
|
+
for (let i = 1; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i]
|
|
43
|
+
if (a === '--agent') {
|
|
44
|
+
const v = argv[++i]
|
|
45
|
+
if (!v) return { error: '--agent requires a value' }
|
|
46
|
+
agentId = v
|
|
47
|
+
} else if (a === '--tail') {
|
|
48
|
+
const v = argv[++i]
|
|
49
|
+
if (!v) return { error: '--tail requires a value' }
|
|
50
|
+
const n = Number.parseInt(v, 10)
|
|
51
|
+
if (!Number.isFinite(n) || n < 0) return { error: '--tail must be a positive integer' }
|
|
52
|
+
tail = n
|
|
53
|
+
} else if (a === '--follow' || a === '-f') {
|
|
54
|
+
follow = true
|
|
55
|
+
} else {
|
|
56
|
+
return { error: `unknown flag: ${a}` }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { sub: sub as GatewaySub, agentId, tail, follow }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runGateway(parsed: ParsedGatewayArgs): Promise<void> {
|
|
63
|
+
switch (parsed.sub) {
|
|
64
|
+
case 'run': {
|
|
65
|
+
const { runGatewayForeground } = await import('./gateway-run')
|
|
66
|
+
await runGatewayForeground({ agentId: parsed.agentId })
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
case 'start': {
|
|
70
|
+
const { runGatewayStart } = await import('./gateway-start')
|
|
71
|
+
await runGatewayStart({ agentId: parsed.agentId })
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
case 'stop': {
|
|
75
|
+
const { runGatewayStop } = await import('./gateway-stop')
|
|
76
|
+
await runGatewayStop({ agentId: parsed.agentId })
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
case 'restart': {
|
|
80
|
+
const { runGatewayStop } = await import('./gateway-stop')
|
|
81
|
+
const { runGatewayStart } = await import('./gateway-start')
|
|
82
|
+
await runGatewayStop({ agentId: parsed.agentId })
|
|
83
|
+
await runGatewayStart({ agentId: parsed.agentId })
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
case 'status': {
|
|
87
|
+
const { runGatewayStatus } = await import('./gateway-status')
|
|
88
|
+
await runGatewayStatus({ agentId: parsed.agentId })
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
case 'logs': {
|
|
92
|
+
const { runGatewayLogs } = await import('./gateway-logs')
|
|
93
|
+
await runGatewayLogs({
|
|
94
|
+
agentId: parsed.agentId,
|
|
95
|
+
tail: parsed.tail ?? 100,
|
|
96
|
+
follow: parsed.follow ?? false,
|
|
97
|
+
})
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { parseEther } from 'viem'
|
|
3
|
+
import {
|
|
4
|
+
SANDBOX_BURN_RATE_OG_PER_HOUR,
|
|
5
|
+
SANDBOX_DEFAULT_INITIAL_DEPOSIT_OG,
|
|
6
|
+
estimateCosts,
|
|
7
|
+
renderCostSummary,
|
|
8
|
+
} from './cost'
|
|
9
|
+
|
|
10
|
+
describe('estimateCosts (0G stack)', () => {
|
|
11
|
+
// NOTE: the 0G network path still exists in @promus/core but the display
|
|
12
|
+
// strings have been migrated to ETH / Arbitrum branding.
|
|
13
|
+
it('local target: zero sandbox fields', () => {
|
|
14
|
+
const c = estimateCosts({
|
|
15
|
+
ledgerSizeOg: 3,
|
|
16
|
+
withSubname: true,
|
|
17
|
+
deployTarget: 'local',
|
|
18
|
+
network: '0g-mainnet',
|
|
19
|
+
})
|
|
20
|
+
expect(c.sandboxInitialDepositTestnet).toBe(0n)
|
|
21
|
+
expect(c.sandboxBurnRatePerHourTestnet).toBe(0n)
|
|
22
|
+
expect(c.deployTarget).toBe('local')
|
|
23
|
+
expect(c.totalOperator).toBe(parseEther('3.115'))
|
|
24
|
+
expect(c.currency).toBe('0G')
|
|
25
|
+
expect(c.lean).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('sandbox target: populates testnet fields', () => {
|
|
29
|
+
const c = estimateCosts({
|
|
30
|
+
ledgerSizeOg: 3,
|
|
31
|
+
withSubname: true,
|
|
32
|
+
deployTarget: 'sandbox',
|
|
33
|
+
network: '0g-mainnet',
|
|
34
|
+
})
|
|
35
|
+
expect(c.sandboxInitialDepositTestnet).toBe(
|
|
36
|
+
parseEther(String(SANDBOX_DEFAULT_INITIAL_DEPOSIT_OG)),
|
|
37
|
+
)
|
|
38
|
+
expect(c.sandboxBurnRatePerHourTestnet).toBe(parseEther(String(SANDBOX_BURN_RATE_OG_PER_HOUR)))
|
|
39
|
+
expect(c.deployTarget).toBe('sandbox')
|
|
40
|
+
// mainnet totalOperator UNCHANGED by deploy target (testnet is a separate pool)
|
|
41
|
+
expect(c.totalOperator).toBe(parseEther('3.115'))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('burn rate equals topup.ts canonical 0.09 0G/hour', () => {
|
|
45
|
+
expect(SANDBOX_BURN_RATE_OG_PER_HOUR).toBe(0.09)
|
|
46
|
+
const c = estimateCosts({
|
|
47
|
+
ledgerSizeOg: 3,
|
|
48
|
+
withSubname: false,
|
|
49
|
+
deployTarget: 'sandbox',
|
|
50
|
+
network: '0g-mainnet',
|
|
51
|
+
})
|
|
52
|
+
expect(c.sandboxBurnRatePerHourTestnet).toBe(parseEther('0.09'))
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('estimateCosts (lean stack: Arbitrum/Claude/IPFS)', () => {
|
|
57
|
+
it('only real L2 gas: mint + small ETH agent float, no ledger or subname', () => {
|
|
58
|
+
const c = estimateCosts({
|
|
59
|
+
ledgerSizeOg: 0,
|
|
60
|
+
withSubname: false,
|
|
61
|
+
deployTarget: 'local',
|
|
62
|
+
network: 'arbitrum-sepolia',
|
|
63
|
+
})
|
|
64
|
+
expect(c.lean).toBe(true)
|
|
65
|
+
expect(c.currency).toBe('ETH')
|
|
66
|
+
expect(c.agentFloat).toBeGreaterThan(0n) // small ETH float for the agent EOA
|
|
67
|
+
expect(c.computeLedgerDeposit).toBe(0n)
|
|
68
|
+
expect(c.subnameAndRecords).toBe(0n)
|
|
69
|
+
expect(c.storageUploadGas).toBe(0n) // anchor paid from the agent float
|
|
70
|
+
expect(c.sandboxInitialDepositTestnet).toBe(0n)
|
|
71
|
+
// mint + agent float only — well under a faucet-funded testnet wallet
|
|
72
|
+
expect(c.totalOperator).toBe(c.mintAndApproveGas + c.agentFloat)
|
|
73
|
+
expect(c.totalOperator).toBeLessThan(parseEther('0.002'))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('Robinhood Orbit L2 is also lean / ETH', () => {
|
|
77
|
+
const c = estimateCosts({
|
|
78
|
+
ledgerSizeOg: 10,
|
|
79
|
+
withSubname: true,
|
|
80
|
+
deployTarget: 'local',
|
|
81
|
+
network: 'robinhood-testnet',
|
|
82
|
+
})
|
|
83
|
+
expect(c.lean).toBe(true)
|
|
84
|
+
expect(c.currency).toBe('ETH')
|
|
85
|
+
// ledger size + subname requests are ignored on the lean stack
|
|
86
|
+
expect(c.computeLedgerDeposit).toBe(0n)
|
|
87
|
+
expect(c.subnameAndRecords).toBe(0n)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('renderCostSummary (0G stack)', () => {
|
|
92
|
+
it('local target: omits sandbox section', () => {
|
|
93
|
+
const c = estimateCosts({
|
|
94
|
+
ledgerSizeOg: 3,
|
|
95
|
+
withSubname: true,
|
|
96
|
+
deployTarget: 'local',
|
|
97
|
+
network: '0g-mainnet',
|
|
98
|
+
})
|
|
99
|
+
const out = renderCostSummary(c)
|
|
100
|
+
expect(out).toContain('operator spend (Arbitrum mainnet)')
|
|
101
|
+
expect(out).toContain('mint + setApprovalForAll')
|
|
102
|
+
expect(out).toContain('compute ledger deposit')
|
|
103
|
+
expect(out).not.toContain('sandbox spend')
|
|
104
|
+
expect(out).not.toContain('Galileo testnet')
|
|
105
|
+
expect(out).not.toContain('faucet')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('sandbox target: includes Galileo testnet section with runway + faucet', () => {
|
|
109
|
+
const c = estimateCosts({
|
|
110
|
+
ledgerSizeOg: 3,
|
|
111
|
+
withSubname: true,
|
|
112
|
+
deployTarget: 'sandbox',
|
|
113
|
+
network: '0g-mainnet',
|
|
114
|
+
})
|
|
115
|
+
const out = renderCostSummary(c)
|
|
116
|
+
expect(out).toContain('sandbox spend (Arbitrum testnet, free via faucet):')
|
|
117
|
+
expect(out).toContain('initial provider deposit')
|
|
118
|
+
expect(out).toContain('runtime burn')
|
|
119
|
+
expect(out).toContain('1 ETH')
|
|
120
|
+
expect(out).toContain('0.09 ETH/h')
|
|
121
|
+
expect(out).toContain('auto-topup')
|
|
122
|
+
expect(out).toContain('runway')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('sandbox target: runway expressed in hours for ~1 0G default', () => {
|
|
126
|
+
const c = estimateCosts({
|
|
127
|
+
ledgerSizeOg: 3,
|
|
128
|
+
withSubname: false,
|
|
129
|
+
deployTarget: 'sandbox',
|
|
130
|
+
network: '0g-mainnet',
|
|
131
|
+
})
|
|
132
|
+
const out = renderCostSummary(c)
|
|
133
|
+
// 1 ETH / 0.09 ETH/h = 11.11h
|
|
134
|
+
expect(out).toMatch(/~11\.[0-9]h runway/)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('still shows USD $0.00 for testnet line', () => {
|
|
138
|
+
const c = estimateCosts({
|
|
139
|
+
ledgerSizeOg: 3,
|
|
140
|
+
withSubname: false,
|
|
141
|
+
deployTarget: 'sandbox',
|
|
142
|
+
network: '0g-mainnet',
|
|
143
|
+
})
|
|
144
|
+
const out = renderCostSummary(c)
|
|
145
|
+
// Testnet ETH is free — USD col should read ($0.00)
|
|
146
|
+
expect(out).toMatch(/initial provider deposit\s+1 ETH\s+\(\$0\.00\)/)
|
|
147
|
+
expect(out).not.toMatch(/initial provider deposit\s+1 ETH\s+\(\$0\.50\)/)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('renderCostSummary (lean stack)', () => {
|
|
152
|
+
it('ETH gas only: no economy lines', () => {
|
|
153
|
+
const c = estimateCosts({
|
|
154
|
+
ledgerSizeOg: 0,
|
|
155
|
+
withSubname: false,
|
|
156
|
+
deployTarget: 'local',
|
|
157
|
+
network: 'arbitrum-sepolia',
|
|
158
|
+
})
|
|
159
|
+
const out = renderCostSummary(c)
|
|
160
|
+
expect(out).toContain('operator spend (ETH L2 gas)')
|
|
161
|
+
expect(out).toContain('mint + setApprovalForAll')
|
|
162
|
+
expect(out).toContain('agent gas float')
|
|
163
|
+
expect(out).toContain('total operator spend')
|
|
164
|
+
expect(out).not.toContain('0G')
|
|
165
|
+
expect(out).not.toContain('agent infra float')
|
|
166
|
+
expect(out).not.toContain('compute ledger deposit')
|
|
167
|
+
expect(out).not.toContain('sandbox spend')
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type PromusNetwork,
|
|
3
|
+
NETWORK_CURRENCY,
|
|
4
|
+
SANDBOX_BURN_RATE_OG_PER_HOUR,
|
|
5
|
+
SANDBOX_DEFAULT_INITIAL_DEPOSIT_OG,
|
|
6
|
+
isOgNetwork,
|
|
7
|
+
} from '@promus/core'
|
|
8
|
+
import { formatEther } from 'viem'
|
|
9
|
+
|
|
10
|
+
export { SANDBOX_BURN_RATE_OG_PER_HOUR, SANDBOX_DEFAULT_INITIAL_DEPOSIT_OG }
|
|
11
|
+
|
|
12
|
+
/** ETH spot price used for USD estimates. Not authoritative, just a hint. */
|
|
13
|
+
const ETH_USD = 3500
|
|
14
|
+
|
|
15
|
+
export type DeployTarget = 'local' | 'sandbox'
|
|
16
|
+
|
|
17
|
+
export interface CostBreakdown {
|
|
18
|
+
mintAndApproveGas: bigint
|
|
19
|
+
agentFloat: bigint
|
|
20
|
+
computeLedgerDeposit: bigint
|
|
21
|
+
storageUploadGas: bigint
|
|
22
|
+
subnameAndRecords: bigint
|
|
23
|
+
totalOperator: bigint
|
|
24
|
+
/** Galileo testnet — present only when deployTarget === 'sandbox'. */
|
|
25
|
+
sandboxInitialDepositTestnet: bigint
|
|
26
|
+
/** Galileo testnet burn rate per hour, in wei. */
|
|
27
|
+
sandboxBurnRatePerHourTestnet: bigint
|
|
28
|
+
deployTarget: DeployTarget
|
|
29
|
+
/** Native gas-token symbol for the chosen network ('ETH'). */
|
|
30
|
+
currency: string
|
|
31
|
+
/** Lean stack = Claude brain + IPFS memory + local runtime. */
|
|
32
|
+
lean: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function estimateCosts(opts: {
|
|
36
|
+
ledgerSizeOg: number
|
|
37
|
+
withSubname: boolean
|
|
38
|
+
deployTarget: DeployTarget
|
|
39
|
+
network: PromusNetwork
|
|
40
|
+
}): CostBreakdown {
|
|
41
|
+
const currency = NETWORK_CURRENCY[opts.network]
|
|
42
|
+
const lean = !isOgNetwork(opts.network)
|
|
43
|
+
|
|
44
|
+
if (lean) {
|
|
45
|
+
// Arbitrum-family: only real L2 gas, in ETH. Memory is IPFS (off-chain) and
|
|
46
|
+
// the brain is Claude (off-chain API key), so there is no compute ledger or
|
|
47
|
+
// storage cost, and no subname. The operator pays the mint, then seeds a
|
|
48
|
+
// small ETH float to the agent EOA — the agent spends it on its own
|
|
49
|
+
// keystore-CID anchor and the per-turn memory-sync anchors. Both numbers
|
|
50
|
+
// are generous L2 buffers that stay well under a faucet-funded testnet wallet.
|
|
51
|
+
const mintAndApproveGas = 200_000_000_000_000n // ~0.0002 ETH (mint + setApprovalForAll; ~6x the real L2 cost)
|
|
52
|
+
const agentFloat = 300_000_000_000_000n // ~0.0003 ETH — agent EOA gas for anchors + ~60 memory syncs
|
|
53
|
+
const totalOperator = mintAndApproveGas + agentFloat
|
|
54
|
+
return {
|
|
55
|
+
mintAndApproveGas,
|
|
56
|
+
agentFloat,
|
|
57
|
+
computeLedgerDeposit: 0n,
|
|
58
|
+
storageUploadGas: 0n,
|
|
59
|
+
subnameAndRecords: 0n,
|
|
60
|
+
totalOperator,
|
|
61
|
+
sandboxInitialDepositTestnet: 0n,
|
|
62
|
+
sandboxBurnRatePerHourTestnet: 0n,
|
|
63
|
+
deployTarget: opts.deployTarget,
|
|
64
|
+
currency,
|
|
65
|
+
lean,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const mintAndApproveGas = 10_000_000_000_000_000n // ~0.01 ETH (mint + setApprovalForAll bundle)
|
|
70
|
+
const agentFloat = 100_000_000_000_000_000n // 0.1 ETH — infra float for the agent
|
|
71
|
+
const computeLedgerDeposit = BigInt(Math.round(opts.ledgerSizeOg * 1e18))
|
|
72
|
+
const storageUploadGas = 5_000_000_000_000_000n // ~0.005 ETH (storage anchor tx)
|
|
73
|
+
const subnameAndRecords = opts.withSubname
|
|
74
|
+
? 30_000_000_000_000_000n // ~0.03 ETH (claim + 2 text records, paid from agent float)
|
|
75
|
+
: 0n
|
|
76
|
+
const totalOperator = mintAndApproveGas + agentFloat + computeLedgerDeposit + storageUploadGas
|
|
77
|
+
const sandboxInitialDepositTestnet =
|
|
78
|
+
opts.deployTarget === 'sandbox'
|
|
79
|
+
? BigInt(Math.round(SANDBOX_DEFAULT_INITIAL_DEPOSIT_OG * 1e18))
|
|
80
|
+
: 0n
|
|
81
|
+
const sandboxBurnRatePerHourTestnet =
|
|
82
|
+
opts.deployTarget === 'sandbox' ? BigInt(Math.round(SANDBOX_BURN_RATE_OG_PER_HOUR * 1e18)) : 0n
|
|
83
|
+
return {
|
|
84
|
+
mintAndApproveGas,
|
|
85
|
+
agentFloat,
|
|
86
|
+
computeLedgerDeposit,
|
|
87
|
+
storageUploadGas,
|
|
88
|
+
subnameAndRecords,
|
|
89
|
+
totalOperator,
|
|
90
|
+
sandboxInitialDepositTestnet,
|
|
91
|
+
sandboxBurnRatePerHourTestnet,
|
|
92
|
+
deployTarget: opts.deployTarget,
|
|
93
|
+
currency,
|
|
94
|
+
lean,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatUsd(valueWei: bigint): string {
|
|
99
|
+
const eth = Number(formatEther(valueWei))
|
|
100
|
+
return `$${(eth * ETH_USD).toFixed(2)}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatRunway(depositWei: bigint, burnPerHourWei: bigint): string {
|
|
104
|
+
if (burnPerHourWei === 0n) return ''
|
|
105
|
+
const hours = Number(depositWei) / Number(burnPerHourWei)
|
|
106
|
+
if (hours < 1) return `${Math.round(hours * 60)} min runway`
|
|
107
|
+
if (hours < 48) return `~${hours.toFixed(1)}h runway`
|
|
108
|
+
const days = hours / 24
|
|
109
|
+
return `~${days.toFixed(1)}d runway`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function renderCostSummary(c: CostBreakdown): string {
|
|
113
|
+
// On the lean stack the gas token is testnet ETH (no market price) — show $0.00.
|
|
114
|
+
const usd = (wei: bigint): string => (c.lean ? '$0.00' : formatUsd(wei))
|
|
115
|
+
const line = (label: string, wei: bigint): string =>
|
|
116
|
+
` ${label.padEnd(32)}${formatEther(wei).padStart(8)} ${c.currency} (${usd(wei)})`
|
|
117
|
+
|
|
118
|
+
if (c.lean) {
|
|
119
|
+
// Claude + IPFS + local: operator pays the mint, then seeds the agent EOA
|
|
120
|
+
// float that covers the keystore-CID anchor + per-turn memory-sync anchors.
|
|
121
|
+
return [
|
|
122
|
+
` operator spend (${c.currency} L2 gas):`,
|
|
123
|
+
line('mint + setApprovalForAll', c.mintAndApproveGas),
|
|
124
|
+
line('agent gas float', c.agentFloat),
|
|
125
|
+
` ${'─'.repeat(32)}${'─'.repeat(18)}`,
|
|
126
|
+
line('total operator spend', c.totalOperator),
|
|
127
|
+
].join('\n')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const lines = [
|
|
131
|
+
' operator spend (Arbitrum mainnet):',
|
|
132
|
+
line('mint + setApprovalForAll', c.mintAndApproveGas),
|
|
133
|
+
line('storage upload (keystore)', c.storageUploadGas),
|
|
134
|
+
line('agent infra float', c.agentFloat),
|
|
135
|
+
line('compute ledger deposit', c.computeLedgerDeposit),
|
|
136
|
+
` ${'─'.repeat(32)}${'─'.repeat(18)}`,
|
|
137
|
+
line('total operator spend', c.totalOperator),
|
|
138
|
+
'',
|
|
139
|
+
' agent spend (from the float):',
|
|
140
|
+
line('subname + text records', c.subnameAndRecords),
|
|
141
|
+
]
|
|
142
|
+
if (c.deployTarget === 'sandbox') {
|
|
143
|
+
const runway = formatRunway(c.sandboxInitialDepositTestnet, c.sandboxBurnRatePerHourTestnet)
|
|
144
|
+
lines.push(
|
|
145
|
+
'',
|
|
146
|
+
' sandbox spend (Arbitrum testnet, free via faucet):',
|
|
147
|
+
` ${'initial provider deposit'.padEnd(32)}${formatEther(c.sandboxInitialDepositTestnet).padStart(8)} ETH ($0.00)`,
|
|
148
|
+
` ${'runtime burn'.padEnd(32)}${formatEther(c.sandboxBurnRatePerHourTestnet).padStart(8)} ETH/h (${runway})`,
|
|
149
|
+
' fund via faucet → paste operator address',
|
|
150
|
+
' auto-topup agent EOA refills sandbox billing from compute ledger',
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
return lines.join('\n')
|
|
154
|
+
}
|