@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,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { parseTelegramArgs } from './telegram'
|
|
3
|
+
|
|
4
|
+
describe('parseTelegramArgs', () => {
|
|
5
|
+
it('errors on missing subcommand', () => {
|
|
6
|
+
const r = parseTelegramArgs([])
|
|
7
|
+
expect('error' in r).toBe(true)
|
|
8
|
+
if ('error' in r) expect(r.error).toContain('usage')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('errors on unknown subcommand', () => {
|
|
12
|
+
const r = parseTelegramArgs(['nuke'])
|
|
13
|
+
expect('error' in r).toBe(true)
|
|
14
|
+
if ('error' in r) expect(r.error).toContain('nuke')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('parses setup', () => {
|
|
18
|
+
const r = parseTelegramArgs(['setup'])
|
|
19
|
+
expect('error' in r).toBe(false)
|
|
20
|
+
if (!('error' in r)) expect(r.sub).toBe('setup')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('parses status', () => {
|
|
24
|
+
const r = parseTelegramArgs(['status'])
|
|
25
|
+
if (!('error' in r)) expect(r.sub).toBe('status')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('parses remove without --yes', () => {
|
|
29
|
+
const r = parseTelegramArgs(['remove'])
|
|
30
|
+
if (!('error' in r)) {
|
|
31
|
+
expect(r.sub).toBe('remove')
|
|
32
|
+
expect(r.yes).toBeFalsy()
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('parses remove --yes', () => {
|
|
37
|
+
const r = parseTelegramArgs(['remove', '--yes'])
|
|
38
|
+
if (!('error' in r)) {
|
|
39
|
+
expect(r.sub).toBe('remove')
|
|
40
|
+
expect(r.yes).toBe(true)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('parses remove -y', () => {
|
|
45
|
+
const r = parseTelegramArgs(['remove', '-y'])
|
|
46
|
+
if (!('error' in r)) {
|
|
47
|
+
expect(r.yes).toBe(true)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `promus telegram <subcommand>` — argv dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* setup interactive wizard: validate token, encrypt + persist locally
|
|
6
|
+
* status confirm token still valid + show stored config
|
|
7
|
+
* remove delete the encrypted local blob (does NOT revoke at @BotFather)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface TelegramArgs {
|
|
11
|
+
sub: 'setup' | 'status' | 'remove'
|
|
12
|
+
yes?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VALID_SUBS = ['setup', 'status', 'remove'] as const
|
|
16
|
+
|
|
17
|
+
export function parseTelegramArgs(argv: string[]): TelegramArgs | { error: string } {
|
|
18
|
+
const sub = argv[0]
|
|
19
|
+
if (!sub) return { error: 'usage: promus telegram <setup | status | remove>' }
|
|
20
|
+
const valid = (VALID_SUBS as readonly string[]).includes(sub)
|
|
21
|
+
if (!valid) return { error: `unknown subcommand '${sub}' (expected: ${VALID_SUBS.join(' | ')})` }
|
|
22
|
+
const yes = argv.includes('--yes') || argv.includes('-y')
|
|
23
|
+
return { sub: sub as TelegramArgs['sub'], yes }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runTelegram(args: TelegramArgs): Promise<void> {
|
|
27
|
+
switch (args.sub) {
|
|
28
|
+
case 'setup': {
|
|
29
|
+
const { runTelegramSetup } = await import('./telegram-setup')
|
|
30
|
+
await runTelegramSetup()
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
case 'status': {
|
|
34
|
+
const { runTelegramStatus } = await import('./telegram-status')
|
|
35
|
+
await runTelegramStatus()
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
case 'remove': {
|
|
39
|
+
const { runTelegramRemove } = await import('./telegram-remove')
|
|
40
|
+
await runTelegramRemove({ yes: args.yes })
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { cancel, intro, isCancel, outro, password, select, spinner } from '@clack/prompts'
|
|
2
|
+
import {
|
|
3
|
+
SANDBOX_BURN_RATE_OG_PER_HOUR,
|
|
4
|
+
SANDBOX_PROVIDER_GALILEO,
|
|
5
|
+
SandboxSettlementClient,
|
|
6
|
+
VISION_PROVIDER_DEFAULTS,
|
|
7
|
+
agentPaths,
|
|
8
|
+
depositToLedger,
|
|
9
|
+
explorerTxUrl,
|
|
10
|
+
fetchAndDecryptKeystore,
|
|
11
|
+
getGasPriceWithFloor,
|
|
12
|
+
getLedgerBalance,
|
|
13
|
+
iNFTAgentId,
|
|
14
|
+
transferFundToProvider,
|
|
15
|
+
waitForReceiptResilient,
|
|
16
|
+
} from '@promus/core'
|
|
17
|
+
import { type Address, formatEther, getAddress, parseEther } from 'viem'
|
|
18
|
+
import { findAndLoadConfig } from '../config/load'
|
|
19
|
+
import { withSilencedConsole } from '../util/silence-console'
|
|
20
|
+
import { loadOrPickOperatorSigner } from './init/operator-picker'
|
|
21
|
+
|
|
22
|
+
export interface TopupOpts {
|
|
23
|
+
/** Top up the agent EOA from operator wallet, amount in ETH. */
|
|
24
|
+
agent?: number
|
|
25
|
+
/** Top up the compute ledger from agent EOA, amount in ETH. */
|
|
26
|
+
compute?: number
|
|
27
|
+
/**
|
|
28
|
+
* v0.21.5: top up the Galileo SandboxBilling deposit from operator wallet, amount in ETH.
|
|
29
|
+
* Was: `provider` in v0.17.1+; renamed to disambiguate from "compute provider".
|
|
30
|
+
*/
|
|
31
|
+
sandbox?: number
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Use `sandbox` instead. v0.17.1 named this `provider` (matching
|
|
34
|
+
* the SandboxBilling smart-contract field name) which collided with "compute
|
|
35
|
+
* provider". Kept as an alias for backwards compat with existing runbooks;
|
|
36
|
+
* will be removed in a future release.
|
|
37
|
+
*/
|
|
38
|
+
provider?: number
|
|
39
|
+
/**
|
|
40
|
+
* Transfer N ETH from the main ledger into the vision provider sub-account.
|
|
41
|
+
* Without this, `vision.analyze` + `browser.vision` fail with "Sub-account
|
|
42
|
+
* not found" on fresh agents (init wizard only seeds the inference
|
|
43
|
+
* provider). Mainnet-only — no vision provider exists on testnet.
|
|
44
|
+
*/
|
|
45
|
+
vision?: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runTopup(opts: TopupOpts): Promise<void> {
|
|
49
|
+
intro('promus topup')
|
|
50
|
+
|
|
51
|
+
const loaded = await findAndLoadConfig()
|
|
52
|
+
if (!loaded) {
|
|
53
|
+
cancel('No promus.config.ts found. Run `promus init` first.')
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
const { config } = loaded
|
|
57
|
+
if (!config.identity.iNFT || !config.identity.agent) {
|
|
58
|
+
cancel('Config has no iNFT or agent. Run `promus init` first.')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const agentAddress = config.identity.agent as Address
|
|
63
|
+
const network = config.network
|
|
64
|
+
const finalAgentId = iNFTAgentId({
|
|
65
|
+
contractAddress: config.identity.iNFT.contract as Address,
|
|
66
|
+
tokenId: BigInt(config.identity.iNFT.tokenId),
|
|
67
|
+
})
|
|
68
|
+
const paths = agentPaths.agent(finalAgentId)
|
|
69
|
+
|
|
70
|
+
// 'sandbox' is the canonical mode discriminant (was 'provider' in v0.17.1+).
|
|
71
|
+
// `opts.provider` is accepted as a backwards-compat alias.
|
|
72
|
+
let mode: 'agent' | 'compute' | 'sandbox' | 'vision' | null = null
|
|
73
|
+
let amount = 0
|
|
74
|
+
if (opts.agent !== undefined) {
|
|
75
|
+
mode = 'agent'
|
|
76
|
+
amount = opts.agent
|
|
77
|
+
} else if (opts.compute !== undefined) {
|
|
78
|
+
mode = 'compute'
|
|
79
|
+
amount = opts.compute
|
|
80
|
+
} else if (opts.sandbox !== undefined) {
|
|
81
|
+
mode = 'sandbox'
|
|
82
|
+
amount = opts.sandbox
|
|
83
|
+
} else if (opts.provider !== undefined) {
|
|
84
|
+
mode = 'sandbox'
|
|
85
|
+
amount = opts.provider
|
|
86
|
+
} else if (opts.vision !== undefined) {
|
|
87
|
+
mode = 'vision'
|
|
88
|
+
amount = opts.vision
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!mode) {
|
|
92
|
+
const choice = (await select({
|
|
93
|
+
message: 'What do you want to top up?',
|
|
94
|
+
options: [
|
|
95
|
+
{
|
|
96
|
+
value: 'agent' as const,
|
|
97
|
+
label: 'Agent wallet (infra gas)',
|
|
98
|
+
hint: 'operator sends ETH to agent EOA',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
value: 'compute' as const,
|
|
102
|
+
label: 'Compute ledger (inference credits)',
|
|
103
|
+
hint: 'agent deposits ETH into compute ledger (mainnet)',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
value: 'vision' as const,
|
|
107
|
+
label: 'Vision provider sub-account (vision.analyze + browser.vision)',
|
|
108
|
+
hint: 'transfer from main ledger into vision provider envelope (mainnet)',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
value: 'sandbox' as const,
|
|
112
|
+
label: 'Sandbox billing deposit (Galileo testnet runtime fees)',
|
|
113
|
+
hint: 'operator deposits ETH into SandboxBilling for harness burn',
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
})) as 'agent' | 'compute' | 'sandbox' | 'vision' | symbol
|
|
117
|
+
if (isCancel(choice)) {
|
|
118
|
+
cancel('Aborted.')
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
mode = choice
|
|
122
|
+
|
|
123
|
+
const amtRaw = (await password({
|
|
124
|
+
message: `Amount in ETH to move to ${mode}`,
|
|
125
|
+
validate: v => {
|
|
126
|
+
const n = Number(v)
|
|
127
|
+
if (!Number.isFinite(n) || n <= 0) return 'Positive number required.'
|
|
128
|
+
return undefined
|
|
129
|
+
},
|
|
130
|
+
})) as string | symbol
|
|
131
|
+
if (isCancel(amtRaw)) {
|
|
132
|
+
cancel('Aborted.')
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
amount = Number(amtRaw)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (mode === 'sandbox') {
|
|
139
|
+
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
|
|
140
|
+
if (!operator) return
|
|
141
|
+
const operatorAccount = await operator.account()
|
|
142
|
+
const galileoPub = await operator.publicClient('0g-testnet')
|
|
143
|
+
const galileoWallet = await operator.walletClient('0g-testnet')
|
|
144
|
+
const settle = new SandboxSettlementClient({
|
|
145
|
+
publicClient: galileoPub,
|
|
146
|
+
walletClient: galileoWallet,
|
|
147
|
+
})
|
|
148
|
+
const wei = parseEther(String(amount))
|
|
149
|
+
|
|
150
|
+
const sBefore = spinner()
|
|
151
|
+
sBefore.start('Reading current Galileo deposit')
|
|
152
|
+
let before = 0n
|
|
153
|
+
try {
|
|
154
|
+
before = await settle.getBalance(operatorAccount.address, SANDBOX_PROVIDER_GALILEO)
|
|
155
|
+
sBefore.stop(
|
|
156
|
+
`current deposit ${formatEther(before)} ETH (~${(Number(before) / 1e18 / SANDBOX_BURN_RATE_OG_PER_HOUR).toFixed(1)}h runway)`,
|
|
157
|
+
)
|
|
158
|
+
} catch (e) {
|
|
159
|
+
sBefore.stop(`balance read failed: ${(e as Error).message.slice(0, 120)}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const sDep = spinner()
|
|
163
|
+
sDep.start(`Depositing ${amount} ETH to Galileo provider`)
|
|
164
|
+
try {
|
|
165
|
+
const tx = await settle.deposit({
|
|
166
|
+
recipient: operatorAccount.address,
|
|
167
|
+
provider: SANDBOX_PROVIDER_GALILEO,
|
|
168
|
+
amountWei: wei,
|
|
169
|
+
})
|
|
170
|
+
await waitForReceiptResilient(galileoPub, tx, { tries: 60, delayMs: 2000 })
|
|
171
|
+
const after = await settle.getBalance(operatorAccount.address, SANDBOX_PROVIDER_GALILEO)
|
|
172
|
+
sDep.stop(
|
|
173
|
+
`deposit confirmed → ${explorerTxUrl('0g-testnet', tx)} (new balance ${formatEther(after)} ETH ≈ ${(Number(after) / 1e18 / SANDBOX_BURN_RATE_OG_PER_HOUR).toFixed(1)}h)`,
|
|
174
|
+
)
|
|
175
|
+
outro(`Galileo deposit topped up by ${amount} ETH`)
|
|
176
|
+
} catch (e) {
|
|
177
|
+
sDep.stop(`deposit failed: ${(e as Error).message.slice(0, 120)}`)
|
|
178
|
+
} finally {
|
|
179
|
+
await operator.close?.()
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (mode === 'agent') {
|
|
185
|
+
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
|
|
186
|
+
if (!operator) return
|
|
187
|
+
|
|
188
|
+
const s = spinner()
|
|
189
|
+
s.start(`Sending ${amount} ETH from operator to agent ${agentAddress}`)
|
|
190
|
+
try {
|
|
191
|
+
const opWc = await operator.walletClient(network)
|
|
192
|
+
const opAccount = opWc.account
|
|
193
|
+
if (!opAccount) throw new Error('walletClient is missing default account')
|
|
194
|
+
const pub = await operator.publicClient(network)
|
|
195
|
+
const fundGasPrice = await getGasPriceWithFloor(pub)
|
|
196
|
+
const tx = await withSilencedConsole(() =>
|
|
197
|
+
opWc.sendTransaction({
|
|
198
|
+
to: agentAddress,
|
|
199
|
+
value: parseEther(String(amount)),
|
|
200
|
+
chain: operator.chain(network),
|
|
201
|
+
account: opAccount,
|
|
202
|
+
maxFeePerGas: fundGasPrice,
|
|
203
|
+
maxPriorityFeePerGas: fundGasPrice,
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
await waitForReceiptResilient(pub, tx)
|
|
207
|
+
s.stop(`funded → ${explorerTxUrl(network, tx)}`)
|
|
208
|
+
outro(`agent ${agentAddress} balance refreshed`)
|
|
209
|
+
} catch (e) {
|
|
210
|
+
s.stop(`fund failed: ${(e as Error).message.slice(0, 120)}`)
|
|
211
|
+
} finally {
|
|
212
|
+
await operator.close?.()
|
|
213
|
+
}
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// mode === 'compute' or 'vision' — both need the agent's privkey since
|
|
218
|
+
// they're agent-signed broker calls (depositFund vs transferFund).
|
|
219
|
+
if (mode === 'vision' && network !== '0g-mainnet') {
|
|
220
|
+
cancel('Vision provider is mainnet-only; no testnet provider exists yet.')
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
|
|
224
|
+
if (!operator) return
|
|
225
|
+
|
|
226
|
+
const inftContract = config.identity.iNFT.contract as Address
|
|
227
|
+
const inftTokenId = BigInt(config.identity.iNFT.tokenId)
|
|
228
|
+
|
|
229
|
+
const sUnlock = spinner()
|
|
230
|
+
sUnlock.start('Fetching encrypted keystore + decrypting via operator wallet')
|
|
231
|
+
let agentPrivkey: `0x${string}`
|
|
232
|
+
try {
|
|
233
|
+
const decrypted = await withSilencedConsole(() =>
|
|
234
|
+
fetchAndDecryptKeystore({
|
|
235
|
+
network,
|
|
236
|
+
contractAddress: inftContract,
|
|
237
|
+
tokenId: inftTokenId,
|
|
238
|
+
signer: operator,
|
|
239
|
+
agentAddress,
|
|
240
|
+
cachePath: paths.keystore,
|
|
241
|
+
}),
|
|
242
|
+
)
|
|
243
|
+
agentPrivkey = decrypted.privkeyHex
|
|
244
|
+
sUnlock.stop(`unlocked (keystore source: ${decrypted.source})`)
|
|
245
|
+
} catch (e) {
|
|
246
|
+
sUnlock.stop(`unlock failed: ${(e as Error).message.slice(0, 160)}`)
|
|
247
|
+
await operator.close?.()
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const sBal = spinner()
|
|
252
|
+
sBal.start('Reading current ledger balance')
|
|
253
|
+
try {
|
|
254
|
+
const bal = await withSilencedConsole(() =>
|
|
255
|
+
getLedgerBalance({ network, privkeyHex: agentPrivkey }),
|
|
256
|
+
)
|
|
257
|
+
sBal.stop(
|
|
258
|
+
bal
|
|
259
|
+
? `current ledger ${formatEther(bal.totalBalance)} ETH total / ${formatEther(bal.availableBalance)} ETH available`
|
|
260
|
+
: 'no ledger yet — depositing will open one',
|
|
261
|
+
)
|
|
262
|
+
} catch (e) {
|
|
263
|
+
sBal.stop(`balance read failed: ${(e as Error).message.slice(0, 120)}`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (mode === 'vision') {
|
|
267
|
+
const providerRaw = VISION_PROVIDER_DEFAULTS[network as '0g-mainnet']
|
|
268
|
+
if (!providerRaw) {
|
|
269
|
+
console.error(`Vision provider not configured for network ${network}`)
|
|
270
|
+
await operator.close?.()
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
const provider = getAddress(providerRaw)
|
|
274
|
+
const sVis = spinner()
|
|
275
|
+
sVis.start(`Transferring ${amount} ETH from main ledger to vision provider sub-account`)
|
|
276
|
+
try {
|
|
277
|
+
await withSilencedConsole(() =>
|
|
278
|
+
transferFundToProvider({ network, privkeyHex: agentPrivkey, provider, amount }),
|
|
279
|
+
)
|
|
280
|
+
sVis.stop(`vision sub-account seeded (${provider.slice(0, 8)}...${provider.slice(-4)})`)
|
|
281
|
+
outro(
|
|
282
|
+
`vision provider has ${amount} ETH allocated. vision.analyze + browser.vision should work now.`,
|
|
283
|
+
)
|
|
284
|
+
} catch (e) {
|
|
285
|
+
sVis.stop(`transfer failed: ${(e as Error).message.slice(0, 160)}`)
|
|
286
|
+
} finally {
|
|
287
|
+
await operator.close?.()
|
|
288
|
+
}
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const sDep = spinner()
|
|
293
|
+
sDep.start(`Depositing ${amount} ETH into compute ledger`)
|
|
294
|
+
try {
|
|
295
|
+
await withSilencedConsole(() => depositToLedger({ network, privkeyHex: agentPrivkey, amount }))
|
|
296
|
+
sDep.stop('deposit complete')
|
|
297
|
+
outro(`ledger topped up by ${amount} ETH`)
|
|
298
|
+
} catch (e) {
|
|
299
|
+
sDep.stop(`deposit failed: ${(e as Error).message.slice(0, 120)}`)
|
|
300
|
+
} finally {
|
|
301
|
+
await operator.close?.()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { parseTransferArgs } from './transfer'
|
|
3
|
+
|
|
4
|
+
const REF = 'eip155:16661:0x9e71d79f06f956d4d2666b5c93dafab721c84721:7'
|
|
5
|
+
const TO = '0x06B74fe8070C96D92e3a2A8A871849Ac81e4c09e'
|
|
6
|
+
const KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'
|
|
7
|
+
|
|
8
|
+
describe('parseTransferArgs', () => {
|
|
9
|
+
test('minimum: ref + --to', () => {
|
|
10
|
+
const r = parseTransferArgs([REF, '--to', TO])
|
|
11
|
+
expect('error' in r).toBe(false)
|
|
12
|
+
if ('error' in r) return
|
|
13
|
+
expect(r.ref).toBe(REF)
|
|
14
|
+
expect(r.to.toLowerCase()).toBe(TO.toLowerCase())
|
|
15
|
+
expect(r.dryRun).toBeUndefined()
|
|
16
|
+
expect(r.yes).toBeUndefined()
|
|
17
|
+
expect(r.noPurge).toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('--dry-run flag', () => {
|
|
21
|
+
const r = parseTransferArgs([REF, '--to', TO, '--dry-run'])
|
|
22
|
+
if ('error' in r) throw new Error(r.error)
|
|
23
|
+
expect(r.dryRun).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('--yes shorthand -y', () => {
|
|
27
|
+
const r = parseTransferArgs([REF, '--to', TO, '-y'])
|
|
28
|
+
if ('error' in r) throw new Error(r.error)
|
|
29
|
+
expect(r.yes).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('--no-purge', () => {
|
|
33
|
+
const r = parseTransferArgs([REF, '--to', TO, '--no-purge'])
|
|
34
|
+
if ('error' in r) throw new Error(r.error)
|
|
35
|
+
expect(r.noPurge).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('--recipient-key with 0x prefix', () => {
|
|
39
|
+
const r = parseTransferArgs([REF, '--to', TO, '--recipient-key', KEY])
|
|
40
|
+
if ('error' in r) throw new Error(r.error)
|
|
41
|
+
expect(r.recipientKey).toBe(KEY)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('--recipient-key without 0x prefix gets normalized', () => {
|
|
45
|
+
const stripped = KEY.slice(2)
|
|
46
|
+
const r = parseTransferArgs([REF, '--to', TO, '--recipient-key', stripped])
|
|
47
|
+
if ('error' in r) throw new Error(r.error)
|
|
48
|
+
expect(r.recipientKey).toBe(KEY)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('--oracle-key with 0x prefix', () => {
|
|
52
|
+
const r = parseTransferArgs([REF, '--to', TO, '--oracle-key', KEY])
|
|
53
|
+
if ('error' in r) throw new Error(r.error)
|
|
54
|
+
expect(r.oracleKey).toBe(KEY)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('--oracle-key without 0x prefix gets normalized', () => {
|
|
58
|
+
const stripped = KEY.slice(2)
|
|
59
|
+
const r = parseTransferArgs([REF, '--to', TO, '--oracle-key', stripped])
|
|
60
|
+
if ('error' in r) throw new Error(r.error)
|
|
61
|
+
expect(r.oracleKey).toBe(KEY)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('error: missing ref', () => {
|
|
65
|
+
const r = parseTransferArgs(['--to', TO])
|
|
66
|
+
expect('error' in r).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('error: missing --to', () => {
|
|
70
|
+
const r = parseTransferArgs([REF])
|
|
71
|
+
expect('error' in r).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('error: invalid --to address', () => {
|
|
75
|
+
const r = parseTransferArgs([REF, '--to', '0xnotanaddress'])
|
|
76
|
+
expect('error' in r).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('error: invalid --recipient-key length', () => {
|
|
80
|
+
const r = parseTransferArgs([REF, '--to', TO, '--recipient-key', '0xdeadbeef'])
|
|
81
|
+
expect('error' in r).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('error: invalid --oracle-key length', () => {
|
|
85
|
+
const r = parseTransferArgs([REF, '--to', TO, '--oracle-key', '0xdeadbeef'])
|
|
86
|
+
expect('error' in r).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('error: --oracle-key without value', () => {
|
|
90
|
+
const r = parseTransferArgs([REF, '--to', TO, '--oracle-key'])
|
|
91
|
+
expect('error' in r).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('error: unknown flag', () => {
|
|
95
|
+
const r = parseTransferArgs([REF, '--to', TO, '--bogus'])
|
|
96
|
+
expect('error' in r).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('error: unexpected positional', () => {
|
|
100
|
+
const r = parseTransferArgs([REF, 'bonus-arg', '--to', TO])
|
|
101
|
+
expect('error' in r).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('flag order does not matter', () => {
|
|
105
|
+
const r = parseTransferArgs(['--dry-run', '--to', TO, REF, '--yes'])
|
|
106
|
+
if ('error' in r) throw new Error(r.error)
|
|
107
|
+
expect(r.ref).toBe(REF)
|
|
108
|
+
expect(r.dryRun).toBe(true)
|
|
109
|
+
expect(r.yes).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
})
|