@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,67 @@
|
|
|
1
|
+
import { cancel, isCancel, select } from '@clack/prompts'
|
|
2
|
+
import qrcode from 'qrcode-terminal'
|
|
3
|
+
import { type Address, type PublicClient, formatEther } from 'viem'
|
|
4
|
+
|
|
5
|
+
export interface FundingGateOpts {
|
|
6
|
+
publicClient: PublicClient
|
|
7
|
+
operatorAddress: Address
|
|
8
|
+
requiredOg: bigint
|
|
9
|
+
/** Native gas-token symbol to display ('ETH' or '0G'). Defaults to 'ETH'. */
|
|
10
|
+
currency?: string
|
|
11
|
+
pollIntervalMs?: number
|
|
12
|
+
maxWaitMs?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type FundingGateOutcome =
|
|
16
|
+
| { kind: 'funded'; balance: bigint }
|
|
17
|
+
| { kind: 'skip-ledger' }
|
|
18
|
+
| { kind: 'cancel' }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Show operator address as a QR and poll balance until it meets required
|
|
22
|
+
* threshold. User can cancel or choose to proceed with minimum-only (skip
|
|
23
|
+
* full compute ledger) at any point.
|
|
24
|
+
*
|
|
25
|
+
* Console prints the QR once; the polling loop updates a single line
|
|
26
|
+
* using `process.stdout.write` so the display doesn't scroll.
|
|
27
|
+
*/
|
|
28
|
+
export async function fundingGate(opts: FundingGateOpts): Promise<FundingGateOutcome> {
|
|
29
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 10_000
|
|
30
|
+
const maxWaitMs = opts.maxWaitMs ?? 30 * 60_000 // 30 minutes
|
|
31
|
+
const ccy = opts.currency ?? 'ETH'
|
|
32
|
+
|
|
33
|
+
console.log('')
|
|
34
|
+
console.log(` Send at least ${formatEther(opts.requiredOg)} ${ccy} to:`)
|
|
35
|
+
console.log(` ${opts.operatorAddress}`)
|
|
36
|
+
console.log('')
|
|
37
|
+
qrcode.generate(opts.operatorAddress, { small: true })
|
|
38
|
+
console.log('')
|
|
39
|
+
|
|
40
|
+
const start = Date.now()
|
|
41
|
+
while (Date.now() - start < maxWaitMs) {
|
|
42
|
+
const balance = await opts.publicClient.getBalance({ address: opts.operatorAddress })
|
|
43
|
+
if (balance >= opts.requiredOg) {
|
|
44
|
+
process.stdout.write('\r')
|
|
45
|
+
return { kind: 'funded', balance }
|
|
46
|
+
}
|
|
47
|
+
process.stdout.write(
|
|
48
|
+
`\r polling... current balance ${formatEther(balance)} ${ccy} (need ${formatEther(opts.requiredOg)}) `,
|
|
49
|
+
)
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.stdout.write('\n\n')
|
|
54
|
+
const choice = await select({
|
|
55
|
+
message: 'Balance still insufficient. What now?',
|
|
56
|
+
options: [
|
|
57
|
+
{ value: 'skip' as const, label: 'Skip compute ledger for now (mint + subname only)' },
|
|
58
|
+
{ value: 'cancel' as const, label: 'Cancel init' },
|
|
59
|
+
],
|
|
60
|
+
initialValue: 'cancel',
|
|
61
|
+
})
|
|
62
|
+
if (isCancel(choice)) {
|
|
63
|
+
cancel('Aborted.')
|
|
64
|
+
return { kind: 'cancel' }
|
|
65
|
+
}
|
|
66
|
+
return choice === 'skip' ? { kind: 'skip-ledger' } : { kind: 'cancel' }
|
|
67
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { cancel, isCancel, select, spinner } from '@clack/prompts'
|
|
2
|
+
import { type PromusNetwork, NETWORK_RPC, OGComputeBrain } from '@promus/core'
|
|
3
|
+
import { formatEther } from 'viem'
|
|
4
|
+
import { shortAddr } from '../../util/format'
|
|
5
|
+
import { withSilencedConsole } from '../../util/silence-console'
|
|
6
|
+
|
|
7
|
+
export interface ModelPick {
|
|
8
|
+
provider: string
|
|
9
|
+
model: string | null
|
|
10
|
+
inputPricePerTokenWei: bigint
|
|
11
|
+
outputPricePerTokenWei: bigint
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetch the live 0G Compute provider catalog and prompt the user to pick
|
|
16
|
+
* one. Uses a throwaway privkey for the read-only listService call so no
|
|
17
|
+
* wallet or funds are needed at this stage.
|
|
18
|
+
*
|
|
19
|
+
* Returns `null` if the user cancels, catalog fetch fails, or the list is
|
|
20
|
+
* empty. Caller should treat `null` as "don't block init, set provider=null
|
|
21
|
+
* and let chat.tsx prompt later if needed."
|
|
22
|
+
*/
|
|
23
|
+
export async function pickBrainModel(opts: {
|
|
24
|
+
network: PromusNetwork
|
|
25
|
+
}): Promise<ModelPick | null> {
|
|
26
|
+
const s = spinner()
|
|
27
|
+
s.start('Fetching live 0G Compute catalog')
|
|
28
|
+
type Svc = {
|
|
29
|
+
provider: string
|
|
30
|
+
model?: string
|
|
31
|
+
serviceType?: string
|
|
32
|
+
inputPrice?: string | bigint
|
|
33
|
+
outputPrice?: string | bigint
|
|
34
|
+
}
|
|
35
|
+
let services: Svc[] = []
|
|
36
|
+
try {
|
|
37
|
+
// Throwaway key — listService is a read; no funds consumed.
|
|
38
|
+
const throwawayKey = `0x${'1'.repeat(64)}`
|
|
39
|
+
services = (await withSilencedConsole(() =>
|
|
40
|
+
OGComputeBrain.listServicesFor({
|
|
41
|
+
privkeyHex: throwawayKey as `0x${string}`,
|
|
42
|
+
rpcUrl: NETWORK_RPC[opts.network],
|
|
43
|
+
}),
|
|
44
|
+
)) as unknown as Svc[]
|
|
45
|
+
s.stop(`Fetched ${services.length} providers`)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
s.stop(`Catalog fetch failed: ${(e as Error).message.slice(0, 120)}`)
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
if (services.length === 0) return null
|
|
51
|
+
|
|
52
|
+
const picked = await select({
|
|
53
|
+
message: 'Pick a brain (model)',
|
|
54
|
+
options: services.map(svc => {
|
|
55
|
+
const input = svc.inputPrice ? BigInt(svc.inputPrice) : 0n
|
|
56
|
+
const output = svc.outputPrice ? BigInt(svc.outputPrice) : 0n
|
|
57
|
+
const priceLine =
|
|
58
|
+
input > 0n || output > 0n
|
|
59
|
+
? `in ${formatEther(input)}/tok · out ${formatEther(output)}/tok`
|
|
60
|
+
: undefined
|
|
61
|
+
return {
|
|
62
|
+
value: svc.provider,
|
|
63
|
+
label: `${svc.model ?? 'unknown'} ${svc.serviceType ? `[${svc.serviceType}]` : ''} ${shortAddr(svc.provider)}`,
|
|
64
|
+
hint: priceLine,
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
if (isCancel(picked) || typeof picked !== 'string') {
|
|
69
|
+
cancel('Aborted.')
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
const svc = services.find(s => s.provider === picked)
|
|
73
|
+
if (!svc) return null
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
provider: picked,
|
|
77
|
+
model: svc.model ?? null,
|
|
78
|
+
inputPricePerTokenWei: svc.inputPrice ? BigInt(svc.inputPrice) : 0n,
|
|
79
|
+
outputPricePerTokenWei: svc.outputPrice ? BigInt(svc.outputPrice) : 0n,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { cancel, confirm, isCancel, note, password, select, text } from '@clack/prompts'
|
|
6
|
+
import {
|
|
7
|
+
type PromusNetwork,
|
|
8
|
+
KeychainOperatorSigner,
|
|
9
|
+
KeystoreFileOperatorSigner,
|
|
10
|
+
type OperatorSigner,
|
|
11
|
+
type OperatorSourceHint,
|
|
12
|
+
type OperatorSourceKind,
|
|
13
|
+
RawPrivkeyOperatorSigner,
|
|
14
|
+
WalletConnectOperatorSigner,
|
|
15
|
+
} from '@promus/core'
|
|
16
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
17
|
+
|
|
18
|
+
interface PickerOptions {
|
|
19
|
+
network: PromusNetwork
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OperatorPickResult {
|
|
23
|
+
signer: OperatorSigner
|
|
24
|
+
hint: OperatorSourceHint
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Prompt the user for their operator wallet source and return both the
|
|
29
|
+
* connected `OperatorSigner` and the metadata needed to reconstruct it
|
|
30
|
+
* later (`OperatorSourceHint`). The hint is saved to `promus.config.ts` by
|
|
31
|
+
* the wizard so subsequent commands (chat, topup, restore) can re-attach
|
|
32
|
+
* to the same source without re-prompting.
|
|
33
|
+
*
|
|
34
|
+
* Platform-aware: on macOS, all four sources are offered. On Linux/Windows
|
|
35
|
+
* the OS keychain option is hidden because libsecret/Credential-Manager
|
|
36
|
+
* support is post-MVP.
|
|
37
|
+
*/
|
|
38
|
+
export async function pickOperatorSigner(opts: PickerOptions): Promise<OperatorPickResult | null> {
|
|
39
|
+
const isMac = process.platform === 'darwin'
|
|
40
|
+
const choices: { value: OperatorSourceKind | 'generate'; label: string; hint?: string }[] = [
|
|
41
|
+
{
|
|
42
|
+
value: 'generate',
|
|
43
|
+
label: 'Generate new key',
|
|
44
|
+
hint: 'creates a fresh testnet wallet (save the key!)',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
value: 'walletconnect',
|
|
48
|
+
label: 'WalletConnect',
|
|
49
|
+
hint: 'scan QR with any WC-compatible mobile wallet',
|
|
50
|
+
},
|
|
51
|
+
...(isMac
|
|
52
|
+
? ([
|
|
53
|
+
{
|
|
54
|
+
value: 'keychain',
|
|
55
|
+
label: 'macOS Keychain',
|
|
56
|
+
hint: 'stored in login keychain',
|
|
57
|
+
},
|
|
58
|
+
] as const)
|
|
59
|
+
: []),
|
|
60
|
+
{
|
|
61
|
+
value: 'keystore-file',
|
|
62
|
+
label: 'Keystore file',
|
|
63
|
+
hint: 'encrypted JSON, geth format',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
value: 'raw-privkey',
|
|
67
|
+
label: 'Raw private key',
|
|
68
|
+
hint: 'stdin prompt, for CI/scripting',
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
const source = (await select({
|
|
72
|
+
message: 'Connect your operator wallet (owns the iNFT)',
|
|
73
|
+
options: choices,
|
|
74
|
+
initialValue: choices[0]!.value,
|
|
75
|
+
})) as OperatorSourceKind | 'generate' | symbol
|
|
76
|
+
if (isCancel(source)) {
|
|
77
|
+
cancel('Aborted.')
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch (source) {
|
|
82
|
+
case 'generate': {
|
|
83
|
+
const privkey = generatePrivateKey()
|
|
84
|
+
const account = privateKeyToAccount(privkey)
|
|
85
|
+
note(
|
|
86
|
+
`Generated testnet wallet:\n address: ${account.address}\n private key: ${privkey}\n\nSAVE THIS KEY — you'll need it to reconnect later.`,
|
|
87
|
+
'new wallet',
|
|
88
|
+
)
|
|
89
|
+
const saveToFile = await confirm({
|
|
90
|
+
message: 'Save private key to ~/.promus/operator-key?',
|
|
91
|
+
initialValue: true,
|
|
92
|
+
})
|
|
93
|
+
if (!isCancel(saveToFile) && saveToFile) {
|
|
94
|
+
const keyPath = join(homedir(), '.promus', 'operator-key')
|
|
95
|
+
await writeFile(keyPath, privkey, 'utf8')
|
|
96
|
+
note(`Key saved to ${keyPath}`, 'saved')
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
signer: new RawPrivkeyOperatorSigner({ privkey, sourceLabel: 'generated' }),
|
|
100
|
+
hint: { source: 'raw-privkey' },
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
case 'walletconnect':
|
|
104
|
+
return {
|
|
105
|
+
signer: new WalletConnectOperatorSigner({ networks: [opts.network] }),
|
|
106
|
+
hint: { source: 'walletconnect' },
|
|
107
|
+
}
|
|
108
|
+
case 'keychain': {
|
|
109
|
+
const service = await text({
|
|
110
|
+
message: 'Keychain service name',
|
|
111
|
+
placeholder: 'promus.operator',
|
|
112
|
+
validate: v => {
|
|
113
|
+
if (!v || v.length === 0) return 'Required.'
|
|
114
|
+
if (!/^[a-zA-Z0-9._-]{1,128}$/.test(v))
|
|
115
|
+
return 'Allowed characters: a-z, A-Z, 0-9, dot, underscore, hyphen (max 128).'
|
|
116
|
+
return undefined
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
if (isCancel(service)) {
|
|
120
|
+
cancel('Aborted.')
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
const svc = service as string
|
|
124
|
+
return {
|
|
125
|
+
signer: new KeychainOperatorSigner(svc),
|
|
126
|
+
hint: { source: 'keychain', keychainService: svc },
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
case 'keystore-file': {
|
|
130
|
+
const path = await text({
|
|
131
|
+
message: 'Path to encrypted JSON keystore',
|
|
132
|
+
placeholder: '~/wallets/operator.json',
|
|
133
|
+
validate: v => {
|
|
134
|
+
if (!v) return 'Required.'
|
|
135
|
+
const expanded = v.replace(/^~/, process.env.HOME ?? '~')
|
|
136
|
+
if (!existsSync(expanded)) return `File not found: ${expanded}`
|
|
137
|
+
return undefined
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
if (isCancel(path)) {
|
|
141
|
+
cancel('Aborted.')
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
const expanded = (path as string).replace(/^~/, process.env.HOME ?? '~')
|
|
145
|
+
const pass = await password({
|
|
146
|
+
message: 'Passphrase for the keystore',
|
|
147
|
+
validate: v => (v && v.length > 0 ? undefined : 'Required.'),
|
|
148
|
+
})
|
|
149
|
+
if (isCancel(pass)) {
|
|
150
|
+
cancel('Aborted.')
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
signer: new KeystoreFileOperatorSigner({ path: expanded, passphrase: pass as string }),
|
|
155
|
+
hint: { source: 'keystore-file', keystorePath: path as string },
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
case 'raw-privkey': {
|
|
159
|
+
if (process.env.PROMUS_OPERATOR_PRIVKEY) {
|
|
160
|
+
note('Using PROMUS_OPERATOR_PRIVKEY from env.', 'raw-privkey')
|
|
161
|
+
return {
|
|
162
|
+
signer: new RawPrivkeyOperatorSigner({
|
|
163
|
+
privkey: process.env.PROMUS_OPERATOR_PRIVKEY,
|
|
164
|
+
sourceLabel: 'env:PROMUS_OPERATOR_PRIVKEY',
|
|
165
|
+
}),
|
|
166
|
+
hint: { source: 'raw-privkey' },
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const pk = await password({
|
|
170
|
+
message: 'Operator private key (hex, 0x prefix optional)',
|
|
171
|
+
validate: v => {
|
|
172
|
+
if (!v) return 'Required.'
|
|
173
|
+
const clean = v.trim().replace(/^0x/, '')
|
|
174
|
+
if (!/^[0-9a-fA-F]{64}$/.test(clean)) return 'Must be 32 bytes hex.'
|
|
175
|
+
return undefined
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
if (isCancel(pk)) {
|
|
179
|
+
cancel('Aborted.')
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
signer: new RawPrivkeyOperatorSigner({ privkey: pk as string, sourceLabel: 'stdin' }),
|
|
184
|
+
hint: { source: 'raw-privkey' },
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Reload an `OperatorSigner` from a previously persisted hint in
|
|
192
|
+
* `promus.config.ts`. Used by chat / topup / restore / resume so the user
|
|
193
|
+
* doesn't re-pick a source every session — they only re-supply per-session
|
|
194
|
+
* secrets (passphrases / QR scans / env vars).
|
|
195
|
+
*
|
|
196
|
+
* Returns null when the hint is missing or unusable; the caller falls back
|
|
197
|
+
* to `pickOperatorSigner` for an interactive choice.
|
|
198
|
+
*/
|
|
199
|
+
export async function loadOperatorFromHint(
|
|
200
|
+
hint: OperatorSourceHint,
|
|
201
|
+
network: PromusNetwork,
|
|
202
|
+
): Promise<OperatorSigner | null> {
|
|
203
|
+
switch (hint.source) {
|
|
204
|
+
case 'walletconnect':
|
|
205
|
+
return new WalletConnectOperatorSigner({ networks: [network] })
|
|
206
|
+
case 'keychain': {
|
|
207
|
+
if (!hint.keychainService) return null
|
|
208
|
+
return new KeychainOperatorSigner(hint.keychainService)
|
|
209
|
+
}
|
|
210
|
+
case 'keystore-file': {
|
|
211
|
+
if (!hint.keystorePath) return null
|
|
212
|
+
const expanded = hint.keystorePath.replace(/^~/, process.env.HOME ?? '~')
|
|
213
|
+
if (!existsSync(expanded)) {
|
|
214
|
+
note(`Operator keystore not found at ${expanded}; pick a new source.`, 'keystore missing')
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
const pass = await password({
|
|
218
|
+
message: `Passphrase for operator keystore ${expanded}`,
|
|
219
|
+
validate: v => (v && v.length > 0 ? undefined : 'Required.'),
|
|
220
|
+
})
|
|
221
|
+
if (isCancel(pass)) return null
|
|
222
|
+
return new KeystoreFileOperatorSigner({
|
|
223
|
+
path: expanded,
|
|
224
|
+
passphrase: pass as string,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
case 'raw-privkey': {
|
|
228
|
+
if (process.env.PROMUS_OPERATOR_PRIVKEY) {
|
|
229
|
+
return new RawPrivkeyOperatorSigner({
|
|
230
|
+
privkey: process.env.PROMUS_OPERATOR_PRIVKEY,
|
|
231
|
+
sourceLabel: 'env:PROMUS_OPERATOR_PRIVKEY',
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
const pk = await password({
|
|
235
|
+
message: 'Operator private key (hex, 0x prefix optional)',
|
|
236
|
+
validate: v => {
|
|
237
|
+
if (!v) return 'Required.'
|
|
238
|
+
const clean = v.trim().replace(/^0x/, '')
|
|
239
|
+
if (!/^[0-9a-fA-F]{64}$/.test(clean)) return 'Must be 32 bytes hex.'
|
|
240
|
+
return undefined
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
if (isCancel(pk)) return null
|
|
244
|
+
return new RawPrivkeyOperatorSigner({ privkey: pk as string, sourceLabel: 'stdin' })
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* High-level helper: load operator from config hint when available, fall
|
|
251
|
+
* back to interactive picker otherwise. Used by all post-init commands.
|
|
252
|
+
*/
|
|
253
|
+
export async function loadOrPickOperatorSigner(opts: {
|
|
254
|
+
network: PromusNetwork
|
|
255
|
+
hint?: OperatorSourceHint | null
|
|
256
|
+
}): Promise<OperatorSigner | null> {
|
|
257
|
+
if (opts.hint) {
|
|
258
|
+
const signer = await loadOperatorFromHint(opts.hint, opts.network)
|
|
259
|
+
if (signer) return signer
|
|
260
|
+
}
|
|
261
|
+
const picked = await pickOperatorSigner({ network: opts.network })
|
|
262
|
+
return picked?.signer ?? null
|
|
263
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { cancel, intro, note, outro, spinner } from '@clack/prompts'
|
|
2
|
+
import {
|
|
3
|
+
type PromusConfig,
|
|
4
|
+
NETWORK_CHAIN_ID,
|
|
5
|
+
agentPaths,
|
|
6
|
+
explorerTxUrl,
|
|
7
|
+
fetchAndDecryptKeystore,
|
|
8
|
+
iNFTAgentId,
|
|
9
|
+
openComputeLedger,
|
|
10
|
+
} from '@promus/core'
|
|
11
|
+
import type { Address, Hex } from 'viem'
|
|
12
|
+
import { loadOrPickOperatorSigner } from './operator-picker'
|
|
13
|
+
import { readWizardState, updateWizardState } from './wizard-state'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resume a partial `promus init` that crashed after mint + funding. Phase 6.6
|
|
17
|
+
* requires that the keystore was uploaded to 0G Storage before resume can
|
|
18
|
+
* proceed — otherwise the agent privkey is lost (it only existed in the
|
|
19
|
+
* original wizard's RAM).
|
|
20
|
+
*/
|
|
21
|
+
export async function runResumeInit(opts: {
|
|
22
|
+
config: PromusConfig
|
|
23
|
+
configPath: string
|
|
24
|
+
}): Promise<void> {
|
|
25
|
+
intro('promus init --resume')
|
|
26
|
+
|
|
27
|
+
const { config } = opts
|
|
28
|
+
if (!config.identity.iNFT || !config.identity.agent) {
|
|
29
|
+
cancel('No iNFT or agent address in config. Nothing to resume — run `promus init` fresh.')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const network = config.network
|
|
33
|
+
const contractAddress = config.identity.iNFT.contract as Address
|
|
34
|
+
const tokenId = BigInt(config.identity.iNFT.tokenId)
|
|
35
|
+
const agentAddress = config.identity.agent as Address
|
|
36
|
+
const finalAgentId = iNFTAgentId({ contractAddress, tokenId })
|
|
37
|
+
const paths = agentPaths.agent(finalAgentId)
|
|
38
|
+
|
|
39
|
+
const state = await readWizardState(paths.dir)
|
|
40
|
+
if (!state) {
|
|
41
|
+
cancel(
|
|
42
|
+
`No state file at ${paths.dir}. If init was never started, run \`promus init\` without --resume.`,
|
|
43
|
+
)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!state.steps.mintTx || !state.steps.agentFundedTx) {
|
|
48
|
+
cancel(
|
|
49
|
+
'Mint or agent-funding did not complete. Resume only supports steps after funding. Start fresh with `promus init` (pick Overwrite) and re-mint.',
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!state.steps.keystorePersistedTx) {
|
|
55
|
+
cancel(
|
|
56
|
+
[
|
|
57
|
+
'Keystore was never uploaded to 0G Storage. The agent privkey only',
|
|
58
|
+
"existed in the original wizard's RAM, so it is unrecoverable now.",
|
|
59
|
+
'Start fresh with `promus init` and re-mint into a new iNFT.',
|
|
60
|
+
].join(' '),
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const operator = await loadOrPickOperatorSigner({ network, hint: config.operator })
|
|
66
|
+
if (!operator) {
|
|
67
|
+
cancel('No operator wallet available; cannot decrypt keystore to resume.')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sUnlock = spinner()
|
|
72
|
+
sUnlock.start('Fetching keystore from 0G Storage + decrypting via operator')
|
|
73
|
+
let agentPrivkey: Hex
|
|
74
|
+
try {
|
|
75
|
+
const decrypted = await fetchAndDecryptKeystore({
|
|
76
|
+
network,
|
|
77
|
+
contractAddress,
|
|
78
|
+
tokenId,
|
|
79
|
+
signer: operator,
|
|
80
|
+
agentAddress,
|
|
81
|
+
cachePath: paths.keystore,
|
|
82
|
+
})
|
|
83
|
+
agentPrivkey = decrypted.privkeyHex
|
|
84
|
+
sUnlock.stop(`unlocked (keystore source: ${decrypted.source})`)
|
|
85
|
+
} catch (e) {
|
|
86
|
+
sUnlock.stop(`unlock failed: ${(e as Error).message.slice(0, 160)}`)
|
|
87
|
+
await operator.close?.()
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!state.steps.ledgerOpenedTx) {
|
|
92
|
+
const s = spinner()
|
|
93
|
+
s.start('Opening 0G Compute ledger (3 0G minimum, top up later)')
|
|
94
|
+
try {
|
|
95
|
+
const status = await openComputeLedger({
|
|
96
|
+
network,
|
|
97
|
+
privkeyHex: agentPrivkey,
|
|
98
|
+
initialBalance: 3,
|
|
99
|
+
providerAddress: config.brain.provider ?? undefined,
|
|
100
|
+
})
|
|
101
|
+
await updateWizardState(paths.dir, draft => {
|
|
102
|
+
draft.steps.ledgerOpenedTx = true
|
|
103
|
+
})
|
|
104
|
+
s.stop(
|
|
105
|
+
status.alreadyExisted ? 'ledger already existed, topped up' : 'ledger opened with 3 0G',
|
|
106
|
+
)
|
|
107
|
+
} catch (e) {
|
|
108
|
+
s.stop(`ledger open failed: ${(e as Error).message.slice(0, 120)}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Subname records are not resumable: the state file intentionally doesn't
|
|
113
|
+
// persist the requested label, so if text records are incomplete we tell
|
|
114
|
+
// the user to re-run `promus init` and pick the same label manually.
|
|
115
|
+
if (!state.steps.subnameClaimedTx) {
|
|
116
|
+
note(
|
|
117
|
+
'If you wanted a subname, re-run `promus init` (it can re-pick the same label).',
|
|
118
|
+
'subname not resumable',
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await operator.close?.()
|
|
123
|
+
|
|
124
|
+
outro(
|
|
125
|
+
[
|
|
126
|
+
'',
|
|
127
|
+
` agent ${agentAddress}`,
|
|
128
|
+
` iNFT #${tokenId.toString()} at ${contractAddress}`,
|
|
129
|
+
` tx ${explorerTxUrl(network, state.steps.mintTx as Hex)}`,
|
|
130
|
+
` keystore ${paths.keystore} (cache of 0G Storage blob)`,
|
|
131
|
+
` chain id ${NETWORK_CHAIN_ID[network]}`,
|
|
132
|
+
'',
|
|
133
|
+
'Resume finished. `promus` to chat, `promus status` for health.',
|
|
134
|
+
].join('\n'),
|
|
135
|
+
)
|
|
136
|
+
}
|