Skip to content

Commit 837879b

Browse files
authored
fix: ux friendly payment funds subcommand (#75)
* fix: payments fund accept days/amount and mode * fix: confirm on withdraw/deposit
1 parent f34c8e5 commit 837879b

File tree

3 files changed

+101
-43
lines changed

3 files changed

+101
-43
lines changed

src/commands/payments.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { runDeposit } from '../payments/deposit.js'
44
import { runFund } from '../payments/fund.js'
55
import { runInteractiveSetup } from '../payments/interactive.js'
66
import { showPaymentStatus } from '../payments/status.js'
7-
import type { PaymentSetupOptions } from '../payments/types.js'
7+
import type { FundOptions, PaymentSetupOptions } from '../payments/types.js'
88
import { runWithdraw } from '../payments/withdraw.js'
99

1010
export const paymentsCommand = new Command('payments').description('Manage payment setup for Filecoin Onchain Cloud')
@@ -47,16 +47,21 @@ paymentsCommand
4747
.description('Adjust funds to an exact runway (days) or total deposit')
4848
.option('--private-key <key>', 'Private key (can also use PRIVATE_KEY env)')
4949
.option('--rpc-url <url>', 'RPC endpoint (can also use RPC_URL env)')
50-
.option('--exact-days <n>', 'Set final runway to exactly N days (deposit or withdraw as needed)')
51-
.option('--exact-amount <usdfc>', 'Set final deposited total to exactly this USDFC amount (deposit or withdraw)')
50+
.option('--days <n>', 'Set final runway to exactly N days (deposit or withdraw as needed)')
51+
.option('--amount <usdfc>', 'Set final deposited total to exactly this USDFC amount (deposit or withdraw)')
52+
.option(
53+
'--mode <mode>',
54+
'Mode to use for funding: "exact" (default) or "minimum". "exact" will withdraw/deposit to exactly match the target. "minimum" will only deposit if below the minimum target.'
55+
)
5256
.action(async (options) => {
5357
try {
54-
const fundOptions: any = {
58+
const fundOptions: FundOptions = {
5559
privateKey: options.privateKey,
5660
rpcUrl: options.rpcUrl || process.env.RPC_URL,
61+
amount: options.amount,
62+
mode: options.mode || 'exact',
5763
}
58-
if (options.exactDays != null) fundOptions.exactDays = Number(options.exactDays)
59-
if (options.exactAmount != null) fundOptions.exactAmount = options.exactAmount
64+
if (options.days != null) fundOptions.days = Number(options.days)
6065
await runFund(fundOptions)
6166
} catch (error) {
6267
console.error('Failed to adjust funds:', error instanceof Error ? error.message : error)

src/payments/fund.ts

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Adjusts funds to exactly match a target runway (days) or a target deposited amount.
55
*/
66

7-
import { confirm, isCancel } from '@clack/prompts'
7+
import { confirm } from '@clack/prompts'
88
import { RPC_URLS, Synapse, TIME_CONSTANTS } from '@filoz/synapse-sdk'
99
import { ethers } from 'ethers'
1010
import pc from 'picocolors'
@@ -20,23 +20,17 @@ import {
2020
getPaymentStatus,
2121
withdrawUSDFC,
2222
} from './setup.js'
23-
24-
export interface FundOptions {
25-
privateKey?: string
26-
rpcUrl?: string
27-
exactDays?: number
28-
exactAmount?: string
29-
}
23+
import type { FundOptions } from './types.js'
3024

3125
// Helper: confirm/warn or bail when target implies < 10-day runway
3226
async function ensureBelowTenDaysAllowed(opts: {
3327
isCI: boolean
34-
isInteractive: boolean
28+
isInteractive?: boolean | undefined
3529
spinner: any
3630
warningLine1: string
3731
warningLine2: string
3832
}): Promise<void> {
39-
const { isCI, isInteractive, spinner, warningLine1, warningLine2 } = opts
33+
const { isCI, isInteractive = isTTY(), spinner, warningLine1, warningLine2 } = opts
4034
if (isCI || !isInteractive) {
4135
spinner.stop()
4236
console.error(pc.red(warningLine1))
@@ -54,9 +48,8 @@ async function ensureBelowTenDaysAllowed(opts: {
5448
message: 'Proceed with reducing runway below 10 days?',
5549
initialValue: false,
5650
})
57-
if (isCancel(proceed)) {
58-
cancel('Fund adjustment cancelled')
59-
throw new Error('Cancelled by user')
51+
if (!proceed) {
52+
throw new Error('Fund adjustment cancelled by user')
6053
}
6154
}
6255

@@ -80,6 +73,16 @@ async function performAdjustment(params: {
8073
)
8174
throw new Error('Insufficient USDFC in wallet')
8275
}
76+
if (isTTY()) {
77+
// we will deposit `needed` USDFC, display confirmation to user unless not TTY or --auto flag was passed
78+
const proceed = await confirm({
79+
message: `Deposit ${formatUSDFC(needed)} USDFC?`,
80+
initialValue: false,
81+
})
82+
if (!proceed) {
83+
throw new Error('Deposit cancelled by user')
84+
}
85+
}
8386
spinner.start(depositMsg)
8487
const { approvalTx, depositTx } = await depositUSDFC(synapse, needed)
8588
spinner.stop(`${pc.green('✓')} Deposit complete`)
@@ -89,6 +92,16 @@ async function performAdjustment(params: {
8992
log.flush()
9093
} else if (delta < 0n) {
9194
const withdrawAmount = -delta
95+
if (isTTY()) {
96+
// we will withdraw `withdrawAmount` USDFC, display confirmation to user unless not TTY or --auto flag was passed
97+
const proceed = await confirm({
98+
message: `Withdraw ${formatUSDFC(withdrawAmount)} USDFC?`,
99+
initialValue: false,
100+
})
101+
if (!proceed) {
102+
throw new Error('Withdraw cancelled by user')
103+
}
104+
}
92105
spinner.start(withdrawMsg)
93106
const txHash = await withdrawUSDFC(synapse, withdrawAmount)
94107
spinner.stop(`${pc.green('✓')} Withdraw complete`)
@@ -99,13 +112,13 @@ async function performAdjustment(params: {
99112
}
100113

101114
// Helper: summary after adjustment
102-
async function printUpdatedSummary(synapse: Synapse): Promise<void> {
115+
async function printSummary(synapse: Synapse, title = 'Updated'): Promise<void> {
103116
const updated = await getPaymentStatus(synapse)
104117
const newAvailable = updated.depositedAmount - (updated.currentAllowances.lockupUsed ?? 0n)
105118
const newPerDay = (updated.currentAllowances.rateUsed ?? 0n) * TIME_CONSTANTS.EPOCHS_PER_DAY
106119
const newRunway = newPerDay > 0n ? Number(newAvailable / newPerDay) : 0
107120
const newRunwayHours = newPerDay > 0n ? Number(((newAvailable % newPerDay) * 24n) / newPerDay) : 0
108-
log.section('Updated', [
121+
log.section(title, [
109122
`Deposited: ${formatUSDFC(updated.depositedAmount)} USDFC`,
110123
`Runway: ~${newRunway} day(s)${newRunwayHours > 0 ? ` ${newRunwayHours} hour(s)` : ''}`,
111124
])
@@ -128,12 +141,16 @@ export async function runFund(options: FundOptions): Promise<void> {
128141
throw new Error('Invalid private key format')
129142
}
130143

131-
const hasExactDays = options.exactDays != null
132-
const hasExactAmount = options.exactAmount != null
133-
if ((hasExactDays && hasExactAmount) || (!hasExactDays && !hasExactAmount)) {
134-
console.error(pc.red('Error: Specify exactly one of --exact-days <N> or --exact-amount <USDFC>'))
144+
const hasDays = options.days != null
145+
const hasAmount = options.amount != null
146+
if ((hasDays && hasAmount) || (!hasDays && !hasAmount)) {
147+
console.error(pc.red('Error: Specify exactly one of --days <N> or --amount <USDFC>'))
135148
throw new Error('Invalid fund options')
136149
}
150+
if (options.mode != null && !['exact', 'minimum'].includes(options.mode)) {
151+
console.error(pc.red('Error: Invalid mode'))
152+
throw new Error(`Invalid mode (must be "exact" or "minimum"), received: '${options.mode}'`)
153+
}
137154

138155
const rpcUrl = options.rpcUrl || process.env.RPC_URL || RPC_URLS.calibration.websocket
139156

@@ -163,31 +180,32 @@ export async function runFund(options: FundOptions): Promise<void> {
163180
spinner.stop(`${pc.green('✓')} Connected`)
164181

165182
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'
166-
const interactive = isTTY()
167183

168184
// Unified planning: derive delta and target context for both modes
169185
const rateUsed = status.currentAllowances.rateUsed ?? 0n
170186
const lockupUsed = status.currentAllowances.lockupUsed ?? 0n
171187

188+
// user provided days or 0
189+
const targetDays: number = hasDays ? Number(options.days) : 0
190+
// user provided amount or 0
191+
const targetDeposit: bigint = hasAmount ? ethers.parseUnits(String(options.amount), 18) : 0n
172192
let delta: bigint
173-
let targetDays: number | null = null
174193
let clampedTarget: bigint | null = null
175194
let runwayCheckDays: number | null = null
176195
let alreadyMessage: string
177196
let depositMsg: string
178197
let withdrawMsg: string
179198

180-
if (hasExactDays) {
181-
targetDays = Number(options.exactDays)
199+
if (hasDays) {
182200
if (!Number.isFinite(targetDays) || targetDays < 0) {
183-
console.error(pc.red('Error: --exact-days must be a non-negative number'))
184-
throw new Error('Invalid --exact-days')
201+
console.error(pc.red('Error: --days must be a non-negative number'))
202+
throw new Error('Invalid --days')
185203
}
186204

187205
const adj = computeAdjustmentForExactDays(status, targetDays)
188206
if (adj.rateUsed === 0n) {
189207
log.line(`${pc.red('✗')} No active spend detected (rateUsed = 0). Cannot compute runway.`)
190-
log.line('Use --exact-amount to set a target deposit instead.')
208+
log.line('Use --amount to set a target deposit instead.')
191209
log.flush()
192210
cancel('Fund adjustment aborted')
193211
throw new Error('No active spend')
@@ -201,17 +219,17 @@ export async function runFund(options: FundOptions): Promise<void> {
201219
} else {
202220
let targetDeposit: bigint
203221
try {
204-
targetDeposit = ethers.parseUnits(String(options.exactAmount), 18)
222+
targetDeposit = ethers.parseUnits(String(options.amount), 18)
205223
} catch {
206-
console.error(pc.red(`Error: Invalid --exact-amount '${options.exactAmount}'`))
207-
throw new Error('Invalid --exact-amount')
224+
console.error(pc.red(`Error: Invalid --amount '${options.amount}'`))
225+
throw new Error('Invalid --amount')
208226
}
209227

210228
const adj = computeAdjustmentForExactDeposit(status, targetDeposit)
211229
delta = adj.delta
212230
clampedTarget = adj.clampedTarget
213231

214-
if (targetDeposit < lockupUsed) {
232+
if (targetDeposit < lockupUsed && options.mode !== 'minimum') {
215233
log.line(pc.yellow('⚠ Target amount is below locked funds. Clamping to locked amount.'))
216234
log.indent(`Locked: ${formatUSDFC(lockupUsed)} USDFC`)
217235
log.flush()
@@ -223,39 +241,57 @@ export async function runFund(options: FundOptions): Promise<void> {
223241
runwayCheckDays = Number(availableAfter / perDay)
224242
}
225243

226-
const targetLabel = clampedTarget != null ? formatUSDFC(clampedTarget) : String(options.exactAmount)
244+
const targetLabel = clampedTarget != null ? formatUSDFC(clampedTarget) : String(options.amount)
227245
alreadyMessage = `Already at target deposit of ${targetLabel} USDFC. No changes needed.`
228246
depositMsg = `Depositing ${formatUSDFC(delta)} USDFC to reach ${targetLabel} USDFC total...`
229247
withdrawMsg = `Withdrawing ${formatUSDFC(-delta)} USDFC to reach ${targetLabel} USDFC total...`
230248
}
231249

232-
if (runwayCheckDays != null && runwayCheckDays < 10) {
233-
const line1 = hasExactDays
250+
if (options.mode === 'minimum') {
251+
// if they have selected minimum mode, we don't need to check the runway
252+
if (delta > 0n) {
253+
if (targetDeposit > 0n) {
254+
depositMsg = `Depositing ${formatUSDFC(delta)} USDFC to reach minimum of ${formatUSDFC(targetDeposit)} USDFC total...`
255+
} else if (targetDays > 0) {
256+
depositMsg = `Depositing ${formatUSDFC(delta)} USDFC to reach minimum of ${targetDays} day(s) runway...`
257+
}
258+
} else {
259+
if (delta < 0n) {
260+
if (targetDeposit > 0n) {
261+
alreadyMessage = `Already above minimum deposit of ${formatUSDFC(targetDeposit)} USDFC. No changes needed.`
262+
} else if (targetDays > 0) {
263+
alreadyMessage = `Already above minimum of ${targetDays} day(s) runway. No changes needed.`
264+
}
265+
}
266+
delta = 0n
267+
}
268+
} else if (runwayCheckDays != null && runwayCheckDays < 10) {
269+
// if they have selected exact mode, we need to check the runway
270+
const line1 = hasDays
234271
? 'Requested runway below 10-day safety baseline.'
235272
: 'Target deposit implies less than 10 days of runway at current spend.'
236-
const line2 = hasExactDays
273+
const line2 = hasDays
237274
? 'WarmStorage reserves 10 days of costs; a shorter runway risks termination.'
238275
: 'Increase target or accept risk: shorter runway may cause termination.'
239276
await ensureBelowTenDaysAllowed({
240277
isCI,
241-
isInteractive: interactive,
242278
spinner,
243279
warningLine1: line1,
244280
warningLine2: line2,
245281
})
246282
}
247283

248284
if (delta === 0n) {
285+
await printSummary(synapse, 'No Changes Needed')
249286
outro(alreadyMessage)
250287
return
251288
}
252289

253290
await performAdjustment({ synapse, spinner, delta, depositMsg, withdrawMsg })
254291

255-
await printUpdatedSummary(synapse)
292+
await printSummary(synapse)
256293
outro('Fund adjustment completed')
257294
} catch (error) {
258-
spinner.stop()
259295
console.error(pc.red('✗ Fund adjustment failed'))
260296
console.error(pc.red('Error:'), error instanceof Error ? error.message : error)
261297
process.exitCode = 1

src/payments/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,20 @@ export interface PaymentSetupOptions {
88
deposit: string
99
rateAllowance: string
1010
}
11+
12+
export type FundMode = 'exact' | 'minimum'
13+
14+
export interface FundOptions {
15+
privateKey?: string
16+
rpcUrl?: string
17+
days?: number
18+
amount?: string
19+
/**
20+
* Mode to use for funding (default: exact)
21+
*
22+
*
23+
* exact: Adjust funds to exactly match a target runway (days) or a target deposited amount.
24+
* minimum: Adjust funds to match a minimum runway (days) or a minimum deposited amount.
25+
*/
26+
mode?: FundMode
27+
}

0 commit comments

Comments
 (0)