-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathauto.ts
More file actions
219 lines (192 loc) · 8.16 KB
/
Copy pathauto.ts
File metadata and controls
219 lines (192 loc) · 8.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/**
* Automatic payment setup flow
*
* This module provides an automated, non-interactive setup experience for
* configuring payment approvals. It uses default values and command-line
* options to complete the setup without user interaction.
*/
import pc from 'picocolors'
import { parseUnits } from 'viem'
import { CliFatal, isCliFatal } from '../common/cli-errors.js'
import {
calculateDepositCapacity,
checkAllowances,
checkAndSetAllowances,
checkFILBalance,
checkUSDFCBalance,
computeAutoSetupTargetBalance,
depositUSDFC,
getPaymentStatus,
validateGasRequirement,
validatePaymentRequirements,
} from '../core/payments/index.js'
import { DEFAULT_COPIES } from '../core/synapse/constants.js'
import { getClientAddress, initializeSynapse } from '../core/synapse/index.js'
import { formatUSDFC } from '../core/utils/format.js'
import { getCLILogger, parseCLIAuth } from '../utils/cli-auth.js'
import { cancel, createSpinner, intro, outro } from '../utils/cli-helpers.js'
import { log } from '../utils/cli-logger.js'
import { displayAccountInfo, displayDepositWarning } from './setup.js'
import type { PaymentSetupOptions } from './types.js'
/**
* Run automatic payment setup with defaults
*
* @param options - Options from command line
*/
export async function runAutoSetup(options: PaymentSetupOptions): Promise<void> {
intro(pc.bold('Filecoin Onchain Cloud Payment Setup'))
log.message(pc.gray('Running in auto mode...'))
// Parse an explicit --deposit override before the outer try below, throwing
// CliFatal so the CLI wrapper exits without re-printing. When omitted, the
// target balance is derived from live on-chain pricing after connecting (see
// below).
let targetFilecoinPayBalance: bigint | undefined
if (options.deposit != null) {
try {
targetFilecoinPayBalance = parseUnits(options.deposit, 18)
} catch {
log.line(pc.red(`Error: Invalid deposit amount '${options.deposit}'`))
log.flush()
throw new CliFatal(`Invalid deposit amount '${options.deposit}'`)
}
}
const spinner = createSpinner()
spinner.start('Initializing connection...')
try {
// Parse and validate authentication
const authConfig = await parseCLIAuth(options)
const logger = getCLILogger()
const synapse = await initializeSynapse(authConfig, logger)
const network = synapse.chain.name
const address = getClientAddress(synapse)
spinner.stop(`${pc.green('✓')} Connected to ${pc.bold(network)}`)
// Check balances and on-chain payment state. Wallet funding is validated
// later, once the transactions this run needs are known: an
// already-configured account needs none, so it is never rejected for a
// low-gas wallet or for holding all of its USDFC as deposits.
spinner.start('Checking balances...')
const filStatus = await checkFILBalance(synapse)
const walletUsdfcBalance = await checkUSDFCBalance(synapse)
const [status, accountSummary, allowanceCheck] = await Promise.all([
getPaymentStatus(synapse),
synapse.payments.accountSummary(),
checkAllowances(synapse),
])
spinner.stop(`${pc.green('✓')} Balance check complete`)
// Display account and balance info using shared function
displayAccountInfo(
address,
network,
filStatus.balance,
filStatus.isCalibnet,
filStatus.hasSufficientGas,
walletUsdfcBalance,
status.filecoinPayBalance
)
// Get storage pricing for capacity calculation
const storageInfo = await synapse.storage.getStorageInfo()
const pricePerTiBPerEpoch = storageInfo.pricing.noCDN.perTiBPerEpoch
// With no --deposit given, ask current on-chain pricing how much must be
// available to set up DEFAULT_COPIES data sets (including the CDN lockup the
// default FilCDN upload path needs), then deposit enough to cover it.
if (targetFilecoinPayBalance == null) {
const { targetBalance } = computeAutoSetupTargetBalance({
filecoinPayBalance: status.filecoinPayBalance,
availableFunds: accountSummary.availableFunds,
copies: DEFAULT_COPIES,
priceList: storageInfo.pricing.priceList,
})
targetFilecoinPayBalance = targetBalance
log.line(
pc.gray(
`Using default deposit target ${formatUSDFC(targetFilecoinPayBalance)} USDFC ` +
`(covers ${DEFAULT_COPIES} CDN data sets + 1 USDFC runway)`
)
)
log.flush()
}
// Track if any changes were made
let actionsTaken = false
let actualFilecoinPayTopUp = 0n
const needsDeposit = status.filecoinPayBalance < targetFilecoinPayBalance
const needsAllowanceUpdate = allowanceCheck.needsUpdate
// Gate on wallet funding only when this run will send transactions.
// A deposit spends wallet USDFC and gas; an allowance update spends gas
// alone, so wallet USDFC is not required for it.
if (needsDeposit || needsAllowanceUpdate) {
const validation = needsDeposit
? validatePaymentRequirements(filStatus.balance, walletUsdfcBalance, filStatus.isCalibnet)
: validateGasRequirement(filStatus.balance, filStatus.isCalibnet)
if (!validation.isValid) {
const errorMsg = validation.errorMessage ?? 'Payment validation failed'
log.line(`${pc.red('✗')} ${errorMsg}`)
if (validation.helpMessage) {
log.line('')
log.line(` ${pc.cyan(validation.helpMessage)}`)
}
log.flush()
cancel('Please fund your wallet and try again')
throw new CliFatal(errorMsg)
}
}
if (needsDeposit) {
const neededFilecoinPayTopUp = targetFilecoinPayBalance - status.filecoinPayBalance
actualFilecoinPayTopUp = neededFilecoinPayTopUp
if (neededFilecoinPayTopUp > walletUsdfcBalance) {
throw new Error(
`Insufficient USDFC for deposit (need ${formatUSDFC(neededFilecoinPayTopUp)} USDFC, have ${formatUSDFC(walletUsdfcBalance)} USDFC)`
)
}
spinner.start(`Depositing ${formatUSDFC(neededFilecoinPayTopUp)} USDFC...`)
const { depositTx } = await depositUSDFC(synapse, neededFilecoinPayTopUp)
spinner.stop(`${pc.green('✓')} Deposited ${formatUSDFC(neededFilecoinPayTopUp)} USDFC`)
actionsTaken = true
log.line(pc.bold('Transaction details:'))
log.indent(pc.gray(`Deposit: ${depositTx}`))
log.flush()
} else {
// Use a dummy spinner to get consistent formatting
spinner.start('Checking deposit...')
const { updated, transactionHash } = await checkAndSetAllowances(synapse)
if (updated) {
spinner.stop(`${pc.green('✓')} Updated payment allowances, tx: ${transactionHash}`)
} else {
spinner.stop(`${pc.green('✓')} Deposit already sufficient (${formatUSDFC(status.filecoinPayBalance)} USDFC)`)
}
}
// Calculate capacity for final summary
const totalDeposit = status.filecoinPayBalance + actualFilecoinPayTopUp
const capacity = calculateDepositCapacity(totalDeposit, pricePerTiBPerEpoch)
// Final summary
spinner.start('Completing setup...')
spinner.stop('━━━ Configuration Summary ━━━')
log.line(`Network: ${pc.bold(network)}`)
log.line(`Deposit: ${formatUSDFC(totalDeposit)} USDFC`)
if (capacity.gibPerMonth > 0) {
const capacityStr =
capacity.gibPerMonth >= 1024
? `${(capacity.gibPerMonth / 1024).toFixed(1)} TiB`
: `${capacity.gibPerMonth.toFixed(1)} GiB`
log.line(`Storage: ~${capacityStr} for 1 month`)
}
log.line(`Status: ${pc.green('Ready to upload')}`)
log.flush()
// Show deposit warning if needed
displayDepositWarning(totalDeposit, status.currentAllowances.lockupUsage)
// Show appropriate outro message based on whether actions were taken
if (actionsTaken) {
outro('Payment setup completed successfully')
} else {
outro('Payment setup already configured - ready to use')
}
} catch (error) {
if (isCliFatal(error)) {
spinner.stop()
throw error
}
const msg = error instanceof Error ? error.message : String(error)
spinner.stop(`${pc.red('✗')} Setup failed: ${msg}`)
cancel('Setup failed')
throw new CliFatal(msg, { cause: error instanceof Error ? error : undefined })
}
}