@antseed/cli 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/.env.example +15 -0
- package/README.md +169 -0
- package/dist/cli/commands/balance.d.ts +3 -0
- package/dist/cli/commands/balance.d.ts.map +1 -0
- package/dist/cli/commands/balance.js +64 -0
- package/dist/cli/commands/balance.js.map +1 -0
- package/dist/cli/commands/browse.d.ts +7 -0
- package/dist/cli/commands/browse.d.ts.map +1 -0
- package/dist/cli/commands/browse.js +100 -0
- package/dist/cli/commands/browse.js.map +1 -0
- package/dist/cli/commands/config.d.ts +20 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +239 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/connect.d.ts +14 -0
- package/dist/cli/commands/connect.d.ts.map +1 -0
- package/dist/cli/commands/connect.js +298 -0
- package/dist/cli/commands/connect.js.map +1 -0
- package/dist/cli/commands/connect.test.d.ts +2 -0
- package/dist/cli/commands/connect.test.d.ts.map +1 -0
- package/dist/cli/commands/connect.test.js +54 -0
- package/dist/cli/commands/connect.test.js.map +1 -0
- package/dist/cli/commands/dashboard.d.ts +6 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -0
- package/dist/cli/commands/dashboard.js +48 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/commands/deposit.d.ts +3 -0
- package/dist/cli/commands/deposit.d.ts.map +1 -0
- package/dist/cli/commands/deposit.js +48 -0
- package/dist/cli/commands/deposit.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +3 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +94 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +91 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/plugin-create.d.ts +11 -0
- package/dist/cli/commands/plugin-create.d.ts.map +1 -0
- package/dist/cli/commands/plugin-create.js +201 -0
- package/dist/cli/commands/plugin-create.js.map +1 -0
- package/dist/cli/commands/plugin-create.test.d.ts +2 -0
- package/dist/cli/commands/plugin-create.test.d.ts.map +1 -0
- package/dist/cli/commands/plugin-create.test.js +53 -0
- package/dist/cli/commands/plugin-create.test.js.map +1 -0
- package/dist/cli/commands/plugin.d.ts +3 -0
- package/dist/cli/commands/plugin.d.ts.map +1 -0
- package/dist/cli/commands/plugin.js +279 -0
- package/dist/cli/commands/plugin.js.map +1 -0
- package/dist/cli/commands/plugin.test.d.ts +2 -0
- package/dist/cli/commands/plugin.test.d.ts.map +1 -0
- package/dist/cli/commands/plugin.test.js +53 -0
- package/dist/cli/commands/plugin.test.js.map +1 -0
- package/dist/cli/commands/profile.d.ts +10 -0
- package/dist/cli/commands/profile.d.ts.map +1 -0
- package/dist/cli/commands/profile.js +89 -0
- package/dist/cli/commands/profile.js.map +1 -0
- package/dist/cli/commands/seed.d.ts +11 -0
- package/dist/cli/commands/seed.d.ts.map +1 -0
- package/dist/cli/commands/seed.js +397 -0
- package/dist/cli/commands/seed.js.map +1 -0
- package/dist/cli/commands/seed.test.d.ts +2 -0
- package/dist/cli/commands/seed.test.d.ts.map +1 -0
- package/dist/cli/commands/seed.test.js +57 -0
- package/dist/cli/commands/seed.test.js.map +1 -0
- package/dist/cli/commands/status.d.ts +8 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +55 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/types.d.ts +14 -0
- package/dist/cli/commands/types.d.ts.map +1 -0
- package/dist/cli/commands/types.js +41 -0
- package/dist/cli/commands/types.js.map +1 -0
- package/dist/cli/commands/withdraw.d.ts +3 -0
- package/dist/cli/commands/withdraw.d.ts.map +1 -0
- package/dist/cli/commands/withdraw.js +48 -0
- package/dist/cli/commands/withdraw.js.map +1 -0
- package/dist/cli/formatters.d.ts +29 -0
- package/dist/cli/formatters.d.ts.map +1 -0
- package/dist/cli/formatters.js +67 -0
- package/dist/cli/formatters.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +41 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/shutdown.d.ts +11 -0
- package/dist/cli/shutdown.d.ts.map +1 -0
- package/dist/cli/shutdown.js +34 -0
- package/dist/cli/shutdown.js.map +1 -0
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +48 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/effective.d.ts +26 -0
- package/dist/config/effective.d.ts.map +1 -0
- package/dist/config/effective.js +84 -0
- package/dist/config/effective.js.map +1 -0
- package/dist/config/effective.test.d.ts +2 -0
- package/dist/config/effective.test.d.ts.map +1 -0
- package/dist/config/effective.test.js +65 -0
- package/dist/config/effective.test.js.map +1 -0
- package/dist/config/loader.d.ts +12 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +212 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/loader.test.d.ts +2 -0
- package/dist/config/loader.test.d.ts.map +1 -0
- package/dist/config/loader.test.js +77 -0
- package/dist/config/loader.test.js.map +1 -0
- package/dist/config/types.d.ts +133 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validation.d.ts +10 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +50 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/env/load-env.d.ts +6 -0
- package/dist/env/load-env.d.ts.map +1 -0
- package/dist/env/load-env.js +18 -0
- package/dist/env/load-env.js.map +1 -0
- package/dist/plugins/loader.d.ts +7 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +70 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/manager.d.ts +11 -0
- package/dist/plugins/manager.d.ts.map +1 -0
- package/dist/plugins/manager.js +52 -0
- package/dist/plugins/manager.js.map +1 -0
- package/dist/plugins/registry.d.ts +8 -0
- package/dist/plugins/registry.d.ts.map +1 -0
- package/dist/plugins/registry.js +39 -0
- package/dist/plugins/registry.js.map +1 -0
- package/dist/proxy/buyer-proxy.d.ts +30 -0
- package/dist/proxy/buyer-proxy.d.ts.map +1 -0
- package/dist/proxy/buyer-proxy.js +488 -0
- package/dist/proxy/buyer-proxy.js.map +1 -0
- package/dist/status/node-status.d.ts +22 -0
- package/dist/status/node-status.d.ts.map +1 -0
- package/dist/status/node-status.js +83 -0
- package/dist/status/node-status.js.map +1 -0
- package/package.json +39 -0
- package/src/cli/commands/balance.ts +77 -0
- package/src/cli/commands/browse.ts +113 -0
- package/src/cli/commands/config.ts +271 -0
- package/src/cli/commands/connect.test.ts +69 -0
- package/src/cli/commands/connect.ts +342 -0
- package/src/cli/commands/dashboard.ts +59 -0
- package/src/cli/commands/deposit.ts +61 -0
- package/src/cli/commands/dev.ts +107 -0
- package/src/cli/commands/init.ts +99 -0
- package/src/cli/commands/plugin-create.test.ts +60 -0
- package/src/cli/commands/plugin-create.ts +230 -0
- package/src/cli/commands/plugin.test.ts +55 -0
- package/src/cli/commands/plugin.ts +295 -0
- package/src/cli/commands/profile.ts +95 -0
- package/src/cli/commands/seed.test.ts +70 -0
- package/src/cli/commands/seed.ts +447 -0
- package/src/cli/commands/status.ts +73 -0
- package/src/cli/commands/types.ts +56 -0
- package/src/cli/commands/withdraw.ts +61 -0
- package/src/cli/formatters.ts +64 -0
- package/src/cli/index.ts +46 -0
- package/src/cli/shutdown.ts +38 -0
- package/src/config/defaults.ts +49 -0
- package/src/config/effective.test.ts +80 -0
- package/src/config/effective.ts +119 -0
- package/src/config/loader.test.ts +95 -0
- package/src/config/loader.ts +251 -0
- package/src/config/types.ts +139 -0
- package/src/config/validation.ts +78 -0
- package/src/env/load-env.ts +20 -0
- package/src/plugins/loader.ts +96 -0
- package/src/plugins/manager.ts +66 -0
- package/src/plugins/registry.ts +45 -0
- package/src/proxy/buyer-proxy.ts +604 -0
- package/src/status/node-status.ts +105 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { writeFile, unlink } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { getGlobalOptions } from './types.js'
|
|
8
|
+
import { loadConfig } from '../../config/loader.js'
|
|
9
|
+
import type { CLIProviderConfig } from '../../config/types.js'
|
|
10
|
+
import { AntseedNode, type Provider, getInstance } from '@antseed/node'
|
|
11
|
+
import type { PaymentConfig } from '@antseed/node/payments'
|
|
12
|
+
import { parseBootstrapList, toBootstrapConfig } from '@antseed/node/discovery'
|
|
13
|
+
import { setupShutdownHandler } from '../shutdown.js'
|
|
14
|
+
import { loadProviderPlugin, buildPluginConfig } from '../../plugins/loader.js'
|
|
15
|
+
import { resolveEffectiveSellerConfig, type SellerRuntimeOverrides } from '../../config/effective.js'
|
|
16
|
+
import type { SellerCLIConfig } from '../../config/types.js'
|
|
17
|
+
|
|
18
|
+
const STATE_FILE = join(homedir(), '.antseed', 'daemon.state.json')
|
|
19
|
+
|
|
20
|
+
/** Map config file provider entry to env-style key/value pairs for the plugin. */
|
|
21
|
+
function providerConfigToEnv(p: CLIProviderConfig): Record<string, string> {
|
|
22
|
+
const env: Record<string, string> = {}
|
|
23
|
+
if (p.authValue) env['ANTHROPIC_API_KEY'] = p.authValue
|
|
24
|
+
if (p.authType) env['ANTSEED_AUTH_TYPE'] = p.authType
|
|
25
|
+
return env
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseOptionalBoolEnv(value: string | undefined): boolean | null {
|
|
29
|
+
if (value === undefined) return null
|
|
30
|
+
const normalized = value.trim().toLowerCase()
|
|
31
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
|
|
32
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function isRpcReachable(rpcUrl: string, timeoutMs = 1500): Promise<boolean> {
|
|
37
|
+
const controller = new AbortController()
|
|
38
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(rpcUrl, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'content-type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
jsonrpc: '2.0',
|
|
46
|
+
id: 1,
|
|
47
|
+
method: 'eth_chainId',
|
|
48
|
+
params: [],
|
|
49
|
+
}),
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (!response.ok) return false
|
|
54
|
+
|
|
55
|
+
const payload = await response.json() as { result?: unknown }
|
|
56
|
+
return typeof payload.result === 'string' && payload.result.startsWith('0x')
|
|
57
|
+
} catch {
|
|
58
|
+
return false
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timeout)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toUSDCBaseUnits(value: string | undefined, fallbackBaseUnits: string): string {
|
|
65
|
+
if (value === undefined) return fallbackBaseUnits
|
|
66
|
+
const parsed = Number.parseFloat(value.trim())
|
|
67
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallbackBaseUnits
|
|
68
|
+
return String(Math.round(parsed * 1_000_000))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildSellerRuntimeOverridesFromFlags(options: {
|
|
72
|
+
reserve?: number
|
|
73
|
+
inputUsdPerMillion?: number
|
|
74
|
+
outputUsdPerMillion?: number
|
|
75
|
+
}): SellerRuntimeOverrides {
|
|
76
|
+
const overrides: SellerRuntimeOverrides = {}
|
|
77
|
+
if (options.reserve !== undefined) {
|
|
78
|
+
overrides.reserveFloor = options.reserve
|
|
79
|
+
}
|
|
80
|
+
if (options.inputUsdPerMillion !== undefined) {
|
|
81
|
+
overrides.inputUsdPerMillion = options.inputUsdPerMillion
|
|
82
|
+
}
|
|
83
|
+
if (options.outputUsdPerMillion !== undefined) {
|
|
84
|
+
overrides.outputUsdPerMillion = options.outputUsdPerMillion
|
|
85
|
+
}
|
|
86
|
+
return overrides
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildSellerPluginRuntimeEnv(
|
|
90
|
+
sellerConfig: SellerCLIConfig,
|
|
91
|
+
providerName: string,
|
|
92
|
+
): Record<string, string> {
|
|
93
|
+
const providerPricing = sellerConfig.pricing.providers?.[providerName]
|
|
94
|
+
const effectiveDefaults = providerPricing?.defaults ?? sellerConfig.pricing.defaults
|
|
95
|
+
const modelPricing = providerPricing?.models
|
|
96
|
+
const runtimeEnv: Record<string, string> = {
|
|
97
|
+
ANTSEED_INPUT_USD_PER_MILLION: String(effectiveDefaults.inputUsdPerMillion),
|
|
98
|
+
ANTSEED_OUTPUT_USD_PER_MILLION: String(effectiveDefaults.outputUsdPerMillion),
|
|
99
|
+
ANTSEED_MAX_CONCURRENCY: String(sellerConfig.maxConcurrentBuyers),
|
|
100
|
+
}
|
|
101
|
+
if (modelPricing && Object.keys(modelPricing).length > 0) {
|
|
102
|
+
runtimeEnv['ANTSEED_MODEL_PRICING_JSON'] = JSON.stringify(modelPricing)
|
|
103
|
+
}
|
|
104
|
+
return runtimeEnv
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function registerSeedCommand(program: Command): void {
|
|
108
|
+
program
|
|
109
|
+
.command('seed')
|
|
110
|
+
.description('Start seeding your idle LLM capacity to the P2P network')
|
|
111
|
+
.option('--provider <name>', 'provider plugin name (e.g., anthropic)')
|
|
112
|
+
.option('--instance <id>', 'use a configured plugin instance by ID')
|
|
113
|
+
.option('-r, --reserve <number>', 'runtime-only reserve floor override (does not write config file)', parseFloat)
|
|
114
|
+
.option('--input-usd-per-million <number>', 'runtime-only input pricing override in USD per 1M tokens', parseFloat)
|
|
115
|
+
.option('--output-usd-per-million <number>', 'runtime-only output pricing override in USD per 1M tokens', parseFloat)
|
|
116
|
+
.action(async (options) => {
|
|
117
|
+
const globalOpts = getGlobalOptions(program)
|
|
118
|
+
const config = await loadConfig(globalOpts.config)
|
|
119
|
+
const runtimeOverrides = buildSellerRuntimeOverridesFromFlags({
|
|
120
|
+
reserve: options.reserve as number | undefined,
|
|
121
|
+
inputUsdPerMillion: options.inputUsdPerMillion as number | undefined,
|
|
122
|
+
outputUsdPerMillion: options.outputUsdPerMillion as number | undefined,
|
|
123
|
+
})
|
|
124
|
+
const effectiveSellerConfig = resolveEffectiveSellerConfig({
|
|
125
|
+
config,
|
|
126
|
+
sellerOverrides: runtimeOverrides,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
let provider: Provider
|
|
130
|
+
|
|
131
|
+
if (options.instance) {
|
|
132
|
+
const configPath = join(homedir(), '.antseed', 'config.json')
|
|
133
|
+
const instance = await getInstance(configPath, options.instance)
|
|
134
|
+
if (!instance) {
|
|
135
|
+
console.error(chalk.red(`Instance "${options.instance}" not found.`))
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
if (instance.type !== 'provider') {
|
|
139
|
+
console.error(chalk.red(`Instance "${options.instance}" is a ${instance.type}, not a provider.`))
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
const spinner = ora(`Loading provider plugin "${instance.package}"...`).start()
|
|
143
|
+
try {
|
|
144
|
+
const plugin = await loadProviderPlugin(instance.package)
|
|
145
|
+
const runtimeEnv = buildSellerPluginRuntimeEnv(effectiveSellerConfig, plugin.name)
|
|
146
|
+
const pluginConfig = buildPluginConfig(plugin.configSchema ?? plugin.configKeys ?? [], runtimeEnv, instance.config as Record<string, string>)
|
|
147
|
+
provider = await plugin.createProvider(pluginConfig)
|
|
148
|
+
if (provider.init) {
|
|
149
|
+
spinner.text = 'Validating credentials...'
|
|
150
|
+
await provider.init()
|
|
151
|
+
}
|
|
152
|
+
spinner.succeed(chalk.green(`Provider "${plugin.displayName}" loaded`))
|
|
153
|
+
} catch (err) {
|
|
154
|
+
spinner.fail(chalk.red(`Failed to load provider: ${(err as Error).message}`))
|
|
155
|
+
process.exit(1)
|
|
156
|
+
}
|
|
157
|
+
} else if (options.provider) {
|
|
158
|
+
const spinner = ora(`Loading provider plugin "${options.provider}"...`).start()
|
|
159
|
+
try {
|
|
160
|
+
const plugin = await loadProviderPlugin(options.provider)
|
|
161
|
+
const configProvider = config.providers.find(p => p.type === options.provider)
|
|
162
|
+
const fileConfig = configProvider ? providerConfigToEnv(configProvider) : {}
|
|
163
|
+
const runtimeEnv = buildSellerPluginRuntimeEnv(effectiveSellerConfig, options.provider as string)
|
|
164
|
+
const envAndRuntimeConfig = buildPluginConfig(plugin.configSchema ?? plugin.configKeys ?? [], runtimeEnv)
|
|
165
|
+
const pluginConfig = { ...fileConfig, ...envAndRuntimeConfig }
|
|
166
|
+
provider = await plugin.createProvider(pluginConfig)
|
|
167
|
+
if (provider.init) {
|
|
168
|
+
spinner.text = 'Validating credentials...'
|
|
169
|
+
await provider.init()
|
|
170
|
+
}
|
|
171
|
+
spinner.succeed(chalk.green(`Provider "${plugin.displayName}" loaded`))
|
|
172
|
+
} catch (err) {
|
|
173
|
+
spinner.fail(chalk.red(`Failed to load provider: ${(err as Error).message}`))
|
|
174
|
+
process.exit(1)
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.error(chalk.red('Error: No provider specified.'))
|
|
178
|
+
console.error(chalk.dim('Run: antseed seed --provider <name> or antseed seed --instance <id>'))
|
|
179
|
+
process.exit(1)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const bootstrapNodes = config.network.bootstrapNodes.length > 0
|
|
183
|
+
? toBootstrapConfig(parseBootstrapList(config.network.bootstrapNodes))
|
|
184
|
+
: undefined
|
|
185
|
+
|
|
186
|
+
const preferredMethod = config.payments.preferredMethod
|
|
187
|
+
const defaultEscrowAmountUSDC = process.env['ANTSEED_DEFAULT_ESCROW_USDC'] ?? config.payments.crypto?.defaultLockAmountUSDC ?? '1'
|
|
188
|
+
const defaultEscrowAmountUSDCBaseUnits = toUSDCBaseUnits(defaultEscrowAmountUSDC, '1000000')
|
|
189
|
+
const settlementIdleMsRaw = process.env['ANTSEED_SETTLEMENT_IDLE_MS']
|
|
190
|
+
const settlementIdleMs = settlementIdleMsRaw ? parseInt(settlementIdleMsRaw, 10) : 30_000
|
|
191
|
+
const sellerWalletAddress = process.env['ANTSEED_SELLER_WALLET_ADDRESS']
|
|
192
|
+
|
|
193
|
+
let paymentConfig: PaymentConfig | null = null
|
|
194
|
+
if (preferredMethod === 'crypto' && config.payments.crypto) {
|
|
195
|
+
const defaultLockAmountUSDCBaseUnits = toUSDCBaseUnits(
|
|
196
|
+
config.payments.crypto.defaultLockAmountUSDC ?? defaultEscrowAmountUSDC,
|
|
197
|
+
defaultEscrowAmountUSDCBaseUnits,
|
|
198
|
+
)
|
|
199
|
+
const cryptoConfig: NonNullable<PaymentConfig['crypto']> = {
|
|
200
|
+
chainId: config.payments.crypto.chainId,
|
|
201
|
+
rpcUrl: config.payments.crypto.rpcUrl,
|
|
202
|
+
contractAddress: config.payments.crypto.escrowContractAddress,
|
|
203
|
+
usdcAddress: config.payments.crypto.usdcContractAddress,
|
|
204
|
+
defaultLockAmountUSDC: defaultLockAmountUSDCBaseUnits,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
paymentConfig = {
|
|
208
|
+
crypto: cryptoConfig,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const settlementEnv = parseOptionalBoolEnv(process.env['ANTSEED_ENABLE_SETTLEMENT'])
|
|
213
|
+
let paymentsEnabled = settlementEnv ?? paymentConfig !== null
|
|
214
|
+
const cryptoRpcUrl = paymentConfig?.crypto?.rpcUrl
|
|
215
|
+
|
|
216
|
+
if (paymentsEnabled && cryptoRpcUrl && settlementEnv !== true) {
|
|
217
|
+
const rpcUp = await isRpcReachable(cryptoRpcUrl)
|
|
218
|
+
if (!rpcUp) {
|
|
219
|
+
paymentsEnabled = false
|
|
220
|
+
console.log(chalk.yellow(`Payments disabled: RPC node unreachable at ${cryptoRpcUrl}`))
|
|
221
|
+
console.log(chalk.dim('Start your chain node or set ANTSEED_ENABLE_SETTLEMENT=true to force-enable payments.'))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const providerName = options.provider as string | undefined ?? provider.name ?? 'unknown'
|
|
226
|
+
const runtimeProviderPricing = buildSellerPluginRuntimeEnv(effectiveSellerConfig, providerName)
|
|
227
|
+
const runtimeInputUsdPerMillion = Number.parseFloat(runtimeProviderPricing['ANTSEED_INPUT_USD_PER_MILLION'] ?? '')
|
|
228
|
+
const runtimeOutputUsdPerMillion = Number.parseFloat(runtimeProviderPricing['ANTSEED_OUTPUT_USD_PER_MILLION'] ?? '')
|
|
229
|
+
let runtimeModelPricing: Record<string, { inputUsdPerMillion: number; outputUsdPerMillion: number }> | undefined
|
|
230
|
+
if (runtimeProviderPricing['ANTSEED_MODEL_PRICING_JSON']) {
|
|
231
|
+
try {
|
|
232
|
+
const parsed = JSON.parse(runtimeProviderPricing['ANTSEED_MODEL_PRICING_JSON']) as unknown
|
|
233
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
234
|
+
const out: Record<string, { inputUsdPerMillion: number; outputUsdPerMillion: number }> = {}
|
|
235
|
+
for (const [model, pricing] of Object.entries(parsed as Record<string, unknown>)) {
|
|
236
|
+
if (!pricing || typeof pricing !== 'object' || Array.isArray(pricing)) continue
|
|
237
|
+
const input = Number((pricing as Record<string, unknown>)['inputUsdPerMillion'])
|
|
238
|
+
const output = Number((pricing as Record<string, unknown>)['outputUsdPerMillion'])
|
|
239
|
+
if (Number.isFinite(input) && Number.isFinite(output)) {
|
|
240
|
+
out[model] = { inputUsdPerMillion: input, outputUsdPerMillion: output }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(out).length > 0) {
|
|
244
|
+
runtimeModelPricing = out
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// Ignore malformed model pricing; fallback defaults are still emitted.
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
console.log(chalk.bold('Effective seller settings:'))
|
|
252
|
+
console.log(chalk.dim(` provider: ${providerName}`))
|
|
253
|
+
console.log(
|
|
254
|
+
chalk.dim(
|
|
255
|
+
` pricing defaults (USD/1M): input=${runtimeProviderPricing['ANTSEED_INPUT_USD_PER_MILLION']}, output=${runtimeProviderPricing['ANTSEED_OUTPUT_USD_PER_MILLION']}`
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
console.log(
|
|
259
|
+
chalk.dim(
|
|
260
|
+
` enabled providers: ${effectiveSellerConfig.enabledProviders.length > 0 ? effectiveSellerConfig.enabledProviders.join(', ') : '(none)'}`
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
console.log(chalk.dim(` reserve floor: ${effectiveSellerConfig.reserveFloor}`))
|
|
264
|
+
console.log(chalk.dim(` max concurrent buyers: ${effectiveSellerConfig.maxConcurrentBuyers}`))
|
|
265
|
+
console.log('')
|
|
266
|
+
|
|
267
|
+
const nodeSpinner = ora('Starting seeding daemon...').start()
|
|
268
|
+
|
|
269
|
+
const node = new AntseedNode({
|
|
270
|
+
role: 'seller',
|
|
271
|
+
bootstrapNodes,
|
|
272
|
+
dataDir: globalOpts.dataDir,
|
|
273
|
+
payments: {
|
|
274
|
+
enabled: paymentsEnabled,
|
|
275
|
+
paymentMethod: preferredMethod,
|
|
276
|
+
platformFeeRate: config.payments.platformFeeRate,
|
|
277
|
+
settlementIdleMs: Number.isFinite(settlementIdleMs) ? settlementIdleMs : 30_000,
|
|
278
|
+
defaultEscrowAmountUSDC: defaultEscrowAmountUSDCBaseUnits,
|
|
279
|
+
sellerWalletAddress,
|
|
280
|
+
paymentConfig,
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
node.registerProvider(provider)
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await node.start()
|
|
288
|
+
nodeSpinner.succeed(chalk.green('Seeding active'))
|
|
289
|
+
console.log(chalk.dim(` Peer ID: ${node.peerId ?? 'unknown'}`))
|
|
290
|
+
console.log(chalk.dim(` DHT port: ${node.dhtPort}`))
|
|
291
|
+
console.log(chalk.dim(` Signaling port: ${node.signalingPort}`))
|
|
292
|
+
} catch (err) {
|
|
293
|
+
nodeSpinner.fail(chalk.red(`Failed to start seeding: ${(err as Error).message}`))
|
|
294
|
+
process.exit(1)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Write daemon state so dashboard and connect can discover this seeder
|
|
298
|
+
const startedAt = Date.now()
|
|
299
|
+
const syntheticSessionStarts = new Map<string, number>()
|
|
300
|
+
let stateWriteInFlight = false
|
|
301
|
+
let stateWritePending = false
|
|
302
|
+
|
|
303
|
+
function formatUptime(): string {
|
|
304
|
+
const ms = Date.now() - startedAt
|
|
305
|
+
const s = Math.floor(ms / 1000)
|
|
306
|
+
if (s < 60) return `${s}s`
|
|
307
|
+
const m = Math.floor(s / 60)
|
|
308
|
+
if (m < 60) return `${m}m ${s % 60}s`
|
|
309
|
+
const h = Math.floor(m / 60)
|
|
310
|
+
return `${h}h ${m % 60}m`
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildDaemonState() {
|
|
314
|
+
const now = Date.now()
|
|
315
|
+
const cap = provider.getCapacity()
|
|
316
|
+
const trackedSessions = node
|
|
317
|
+
.getActiveSellerSessions()
|
|
318
|
+
.filter((session) => !session.settling)
|
|
319
|
+
.map((session) => ({
|
|
320
|
+
sessionId: session.sessionId,
|
|
321
|
+
buyerPeerId: session.buyerPeerId,
|
|
322
|
+
provider: session.provider,
|
|
323
|
+
startedAt: session.startedAt,
|
|
324
|
+
lastActivityAt: session.lastActivityAt,
|
|
325
|
+
totalRequests: session.totalRequests,
|
|
326
|
+
totalTokens: session.totalTokens,
|
|
327
|
+
avgLatencyMs: session.avgLatencyMs,
|
|
328
|
+
}))
|
|
329
|
+
|
|
330
|
+
const syntheticCount = Math.max(0, cap.current - trackedSessions.length)
|
|
331
|
+
const syntheticIds = new Set<string>()
|
|
332
|
+
const syntheticDetails: Array<{
|
|
333
|
+
sessionId: string
|
|
334
|
+
buyerPeerId: string
|
|
335
|
+
provider: string
|
|
336
|
+
startedAt: number
|
|
337
|
+
lastActivityAt: number
|
|
338
|
+
totalRequests: number
|
|
339
|
+
totalTokens: number
|
|
340
|
+
avgLatencyMs: number
|
|
341
|
+
}> = []
|
|
342
|
+
|
|
343
|
+
for (let i = 0; i < syntheticCount; i += 1) {
|
|
344
|
+
const sessionId = `provider-slot-${i + 1}`
|
|
345
|
+
syntheticIds.add(sessionId)
|
|
346
|
+
|
|
347
|
+
const existingStart = syntheticSessionStarts.get(sessionId)
|
|
348
|
+
const startedAtTs = typeof existingStart === 'number' ? existingStart : now
|
|
349
|
+
syntheticSessionStarts.set(sessionId, startedAtTs)
|
|
350
|
+
|
|
351
|
+
syntheticDetails.push({
|
|
352
|
+
sessionId,
|
|
353
|
+
buyerPeerId: 'unknown',
|
|
354
|
+
provider: providerName,
|
|
355
|
+
startedAt: startedAtTs,
|
|
356
|
+
lastActivityAt: now,
|
|
357
|
+
totalRequests: 0,
|
|
358
|
+
totalTokens: 0,
|
|
359
|
+
avgLatencyMs: 0,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const existingId of syntheticSessionStarts.keys()) {
|
|
364
|
+
if (!syntheticIds.has(existingId)) {
|
|
365
|
+
syntheticSessionStarts.delete(existingId)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const activeSessionDetails = [...trackedSessions, ...syntheticDetails]
|
|
370
|
+
const activeSessionsCount = Math.max(node.getActiveSellerSessionCount(), cap.current)
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
state: 'seeding',
|
|
374
|
+
pid: process.pid,
|
|
375
|
+
peerId: node.peerId,
|
|
376
|
+
dhtPort: node.dhtPort,
|
|
377
|
+
signalingPort: node.signalingPort,
|
|
378
|
+
provider: providerName,
|
|
379
|
+
defaultInputUsdPerMillion: Number.isFinite(runtimeInputUsdPerMillion) ? runtimeInputUsdPerMillion : undefined,
|
|
380
|
+
defaultOutputUsdPerMillion: Number.isFinite(runtimeOutputUsdPerMillion) ? runtimeOutputUsdPerMillion : undefined,
|
|
381
|
+
providerPricing: {
|
|
382
|
+
[providerName]: {
|
|
383
|
+
defaults: {
|
|
384
|
+
inputUsdPerMillion: Number.isFinite(runtimeInputUsdPerMillion) ? runtimeInputUsdPerMillion : 0,
|
|
385
|
+
outputUsdPerMillion: Number.isFinite(runtimeOutputUsdPerMillion) ? runtimeOutputUsdPerMillion : 0,
|
|
386
|
+
},
|
|
387
|
+
...(runtimeModelPricing ? { models: runtimeModelPricing } : {}),
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
startedAt,
|
|
391
|
+
// Fields the dashboard reads
|
|
392
|
+
peerCount: 0,
|
|
393
|
+
activeSessions: activeSessionsCount,
|
|
394
|
+
activeSessionDetails,
|
|
395
|
+
capacityUsedPercent: cap.max > 0 ? Math.round((cap.current / cap.max) * 100) : 0,
|
|
396
|
+
earningsToday: '0',
|
|
397
|
+
tokensToday: 0,
|
|
398
|
+
uptime: formatUptime(),
|
|
399
|
+
updatedAt: now,
|
|
400
|
+
proxyPort: null,
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function writeDaemonState(): Promise<void> {
|
|
405
|
+
if (stateWriteInFlight) {
|
|
406
|
+
stateWritePending = true
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
stateWriteInFlight = true
|
|
411
|
+
try {
|
|
412
|
+
await writeFile(STATE_FILE, JSON.stringify(buildDaemonState(), null, 2))
|
|
413
|
+
} finally {
|
|
414
|
+
stateWriteInFlight = false
|
|
415
|
+
if (stateWritePending) {
|
|
416
|
+
stateWritePending = false
|
|
417
|
+
await writeDaemonState()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function scheduleDaemonStateWrite(): void {
|
|
423
|
+
void writeDaemonState().catch(() => {})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await writeDaemonState()
|
|
427
|
+
node.on('connection', scheduleDaemonStateWrite)
|
|
428
|
+
node.on('session:updated', scheduleDaemonStateWrite)
|
|
429
|
+
node.on('session:finalized', scheduleDaemonStateWrite)
|
|
430
|
+
|
|
431
|
+
// Refresh state file every 1s so the desktop dashboard reflects live counters.
|
|
432
|
+
const stateInterval = setInterval(async () => {
|
|
433
|
+
await writeDaemonState().catch(() => {})
|
|
434
|
+
}, 1_000)
|
|
435
|
+
|
|
436
|
+
setupShutdownHandler(async () => {
|
|
437
|
+
clearInterval(stateInterval)
|
|
438
|
+
node.off('connection', scheduleDaemonStateWrite)
|
|
439
|
+
node.off('session:updated', scheduleDaemonStateWrite)
|
|
440
|
+
node.off('session:finalized', scheduleDaemonStateWrite)
|
|
441
|
+
nodeSpinner.start('Shutting down seeding daemon...')
|
|
442
|
+
await node.stop()
|
|
443
|
+
await unlink(STATE_FILE).catch(() => {})
|
|
444
|
+
nodeSpinner.succeed('Seeding daemon stopped. Sessions finalized.')
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import { getGlobalOptions } from './types.js';
|
|
5
|
+
import { loadConfig } from '../../config/loader.js';
|
|
6
|
+
import { resolveEffectiveSellerConfig } from '../../config/effective.js';
|
|
7
|
+
import { getNodeStatus } from '../../status/node-status.js';
|
|
8
|
+
import type { NodeStatus } from '../../status/node-status.js';
|
|
9
|
+
import { formatEarnings, formatTokens } from '../formatters.js';
|
|
10
|
+
|
|
11
|
+
/** Possible node states */
|
|
12
|
+
export type NodeState = 'seeding' | 'connected' | 'idle';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register the `antseed status` command on the Commander program.
|
|
16
|
+
*/
|
|
17
|
+
export function registerStatusCommand(program: Command): void {
|
|
18
|
+
program
|
|
19
|
+
.command('status')
|
|
20
|
+
.description('Show current Antseed node status')
|
|
21
|
+
.option('--json', 'output as JSON', false)
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
try {
|
|
24
|
+
const globalOpts = getGlobalOptions(program);
|
|
25
|
+
const config = await loadConfig(globalOpts.config);
|
|
26
|
+
const effectiveSeller = resolveEffectiveSellerConfig({ config });
|
|
27
|
+
const status: NodeStatus = await getNodeStatus(config);
|
|
28
|
+
|
|
29
|
+
if (options.json) {
|
|
30
|
+
console.log(JSON.stringify(status, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// State badge
|
|
35
|
+
const stateColors: Record<NodeState, (s: string) => string> = {
|
|
36
|
+
seeding: chalk.green,
|
|
37
|
+
connected: chalk.cyan,
|
|
38
|
+
idle: chalk.gray,
|
|
39
|
+
};
|
|
40
|
+
const colorFn = stateColors[status.state] ?? chalk.white;
|
|
41
|
+
console.log(chalk.bold('Status: ') + colorFn(status.state.toUpperCase()));
|
|
42
|
+
console.log('');
|
|
43
|
+
|
|
44
|
+
// Summary table
|
|
45
|
+
const table = new Table({
|
|
46
|
+
head: [chalk.bold('Metric'), chalk.bold('Value')],
|
|
47
|
+
colWidths: [25, 35],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
table.push(
|
|
51
|
+
['Peers connected', chalk.cyan(String(status.peerCount))],
|
|
52
|
+
['Earnings today', chalk.green(formatEarnings(status.earningsToday))],
|
|
53
|
+
['Tokens today', formatTokens(status.tokensToday)],
|
|
54
|
+
['Active sessions', String(status.activeSessions)],
|
|
55
|
+
['Uptime', status.uptime],
|
|
56
|
+
['Wallet address', status.walletAddress ?? chalk.dim('not configured')],
|
|
57
|
+
['Proxy port', status.proxyPort ? String(status.proxyPort) : chalk.dim('n/a')],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (status.state === 'seeding') {
|
|
61
|
+
table.push([
|
|
62
|
+
'Seller pricing defaults (USD/1M in/out)',
|
|
63
|
+
`${effectiveSeller.pricing.defaults.inputUsdPerMillion} / ${effectiveSeller.pricing.defaults.outputUsdPerMillion}`,
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(table.toString());
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(chalk.red(`Error: ${(err as Error).message}`));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/** All command registration functions follow this signature */
|
|
6
|
+
export type RegisterCommandFn = (program: Command) => void;
|
|
7
|
+
|
|
8
|
+
/** Global CLI options available to all commands */
|
|
9
|
+
export interface GlobalOptions {
|
|
10
|
+
config: string;
|
|
11
|
+
dataDir: string;
|
|
12
|
+
verbose: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const NEW_HOME_DIR = join(homedir(), '.antseed');
|
|
16
|
+
const NEW_CONFIG_PATH = join(NEW_HOME_DIR, 'config.json');
|
|
17
|
+
|
|
18
|
+
function resolvePathWithHome(pathLike: string, fallbackAbsolutePath: string): string {
|
|
19
|
+
const raw = typeof pathLike === 'string' ? pathLike.trim() : '';
|
|
20
|
+
if (raw.length === 0) {
|
|
21
|
+
return fallbackAbsolutePath;
|
|
22
|
+
}
|
|
23
|
+
if (raw === '~') {
|
|
24
|
+
return homedir();
|
|
25
|
+
}
|
|
26
|
+
if (raw.startsWith('~/')) {
|
|
27
|
+
return join(homedir(), raw.slice(2));
|
|
28
|
+
}
|
|
29
|
+
return resolve(raw);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isTruthyEnv(value: string | undefined): boolean {
|
|
33
|
+
if (!value) return false;
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract global options from the Commander program instance.
|
|
40
|
+
*/
|
|
41
|
+
export function getGlobalOptions(program: Command): GlobalOptions {
|
|
42
|
+
const opts = program.opts();
|
|
43
|
+
const verbose = opts['verbose'] ?? false;
|
|
44
|
+
if (verbose || isTruthyEnv(process.env['ANTSEED_DEBUG'])) {
|
|
45
|
+
process.env['ANTSEED_DEBUG'] = '1';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const requestedConfig = resolvePathWithHome(opts['config'] ?? '~/.antseed/config.json', NEW_CONFIG_PATH);
|
|
49
|
+
const requestedDataDir = resolvePathWithHome(opts['dataDir'] ?? '~/.antseed', NEW_HOME_DIR);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
config: requestedConfig,
|
|
53
|
+
dataDir: requestedDataDir,
|
|
54
|
+
verbose,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getGlobalOptions } from './types.js';
|
|
5
|
+
import { loadConfig } from '../../config/loader.js';
|
|
6
|
+
import {
|
|
7
|
+
loadOrCreateIdentity,
|
|
8
|
+
BaseEscrowClient,
|
|
9
|
+
identityToEvmWallet,
|
|
10
|
+
identityToEvmAddress,
|
|
11
|
+
} from '@antseed/node';
|
|
12
|
+
|
|
13
|
+
export function registerWithdrawCommand(program: Command): void {
|
|
14
|
+
program
|
|
15
|
+
.command('withdraw <amount>')
|
|
16
|
+
.description('Withdraw USDC from the escrow contract (amount in human-readable USDC, e.g. "5" = 5 USDC)')
|
|
17
|
+
.action(async (amount: string) => {
|
|
18
|
+
const globalOpts = getGlobalOptions(program);
|
|
19
|
+
const config = await loadConfig(globalOpts.config);
|
|
20
|
+
|
|
21
|
+
const payments = config.payments;
|
|
22
|
+
if (!payments?.crypto) {
|
|
23
|
+
console.error(chalk.red('Error: No crypto payment configuration found.'));
|
|
24
|
+
console.error(chalk.dim('Configure payments.crypto in your config file or run: antseed init'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const amountFloat = parseFloat(amount);
|
|
29
|
+
if (isNaN(amountFloat) || amountFloat <= 0) {
|
|
30
|
+
console.error(chalk.red('Error: Amount must be a positive number.'));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Convert human-readable USDC to base units (6 decimals)
|
|
35
|
+
const amountBaseUnits = BigInt(Math.round(amountFloat * 1_000_000));
|
|
36
|
+
|
|
37
|
+
const identity = await loadOrCreateIdentity(globalOpts.dataDir);
|
|
38
|
+
const wallet = identityToEvmWallet(identity);
|
|
39
|
+
const address = identityToEvmAddress(identity);
|
|
40
|
+
|
|
41
|
+
const escrowClient = new BaseEscrowClient({
|
|
42
|
+
rpcUrl: payments.crypto.rpcUrl,
|
|
43
|
+
contractAddress: payments.crypto.escrowContractAddress,
|
|
44
|
+
usdcAddress: payments.crypto.usdcContractAddress,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.log(chalk.dim(`Wallet: ${address}`));
|
|
48
|
+
console.log(chalk.dim(`Amount: ${amountFloat} USDC (${amountBaseUnits} base units)`));
|
|
49
|
+
|
|
50
|
+
const spinner = ora('Withdrawing USDC from escrow...').start();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const txHash = await escrowClient.withdraw(wallet, amountBaseUnits);
|
|
54
|
+
spinner.succeed(chalk.green(`Withdrew ${amountFloat} USDC from escrow`));
|
|
55
|
+
console.log(chalk.dim(`Transaction: ${txHash}`));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
spinner.fail(chalk.red(`Withdrawal failed: ${(err as Error).message}`));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|