Skip to content

Commit 278ed5a

Browse files
authored
feat: status,deposit,withdraw cmds (#52)
* feat: add deposit command; update status details * feat: withdraw function and cleanup * feat: auto fund to exact days/amount * chore: fix lint * refactor: cleanup fund subcommand logic * feat: runway remaining shows years when large * chore: fix lint * feat: display payment rails info in payments status * chore: fix lint
1 parent 70681de commit 278ed5a

File tree

10 files changed

+1131
-17
lines changed

10 files changed

+1131
-17
lines changed

src/commands/payments.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { Command } from 'commander'
22
import { runAutoSetup } from '../payments/auto.js'
3+
import { runDeposit } from '../payments/deposit.js'
4+
import { runFund } from '../payments/fund.js'
35
import { runInteractiveSetup } from '../payments/interactive.js'
46
import { showPaymentStatus } from '../payments/status.js'
57
import type { PaymentSetupOptions } from '../payments/types.js'
8+
import { runWithdraw } from '../payments/withdraw.js'
69

710
export const paymentsCommand = new Command('payments').description('Manage payment setup for Filecoin Onchain Cloud')
811

@@ -38,6 +41,49 @@ paymentsCommand
3841
}
3942
})
4043

44+
// Adjust funds to an exact runway or deposited total
45+
paymentsCommand
46+
.command('fund')
47+
.description('Adjust funds to an exact runway (days) or total deposit')
48+
.option('--private-key <key>', 'Private key (can also use PRIVATE_KEY env)')
49+
.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)')
52+
.action(async (options) => {
53+
try {
54+
const fundOptions: any = {
55+
privateKey: options.privateKey,
56+
rpcUrl: options.rpcUrl || process.env.RPC_URL,
57+
}
58+
if (options.exactDays != null) fundOptions.exactDays = Number(options.exactDays)
59+
if (options.exactAmount != null) fundOptions.exactAmount = options.exactAmount
60+
await runFund(fundOptions)
61+
} catch (error) {
62+
console.error('Failed to adjust funds:', error instanceof Error ? error.message : error)
63+
process.exit(1)
64+
}
65+
})
66+
67+
// Withdraw funds from the payments contract
68+
paymentsCommand
69+
.command('withdraw')
70+
.description('Withdraw funds from Filecoin Pay to your wallet')
71+
.option('--private-key <key>', 'Private key (can also use PRIVATE_KEY env)')
72+
.option('--rpc-url <url>', 'RPC endpoint (can also use RPC_URL env)')
73+
.requiredOption('--amount <usdfc>', 'USDFC amount to withdraw (e.g., 5)')
74+
.action(async (options) => {
75+
try {
76+
await runWithdraw({
77+
privateKey: options.privateKey,
78+
rpcUrl: options.rpcUrl || process.env.RPC_URL,
79+
amount: options.amount,
80+
})
81+
} catch (error) {
82+
console.error('Failed to withdraw:', error instanceof Error ? error.message : error)
83+
process.exit(1)
84+
}
85+
})
86+
4187
// Add a status subcommand for checking current payment status
4288
paymentsCommand
4389
.command('status')
@@ -55,3 +101,25 @@ paymentsCommand
55101
process.exit(1)
56102
}
57103
})
104+
105+
// Add a deposit/top-up subcommand
106+
paymentsCommand
107+
.command('deposit')
108+
.description('Deposit or top-up funds in Filecoin Pay')
109+
.option('--private-key <key>', 'Private key (can also use PRIVATE_KEY env)')
110+
.option('--rpc-url <url>', 'RPC endpoint (can also use RPC_URL env)')
111+
.option('--amount <usdfc>', 'USDFC amount to deposit (e.g., 10.5)')
112+
.option('--days <n>', 'Fund enough to keep current spend alive for N days')
113+
.action(async (options) => {
114+
try {
115+
await runDeposit({
116+
privateKey: options.privateKey,
117+
rpcUrl: options.rpcUrl || process.env.RPC_URL,
118+
amount: options.amount,
119+
days: options.days != null ? Number(options.days) : 10, // always default to 10 days top-up, otherwise top-up to the specified number of days.
120+
})
121+
} catch (error) {
122+
console.error('Failed to perform deposit:', error instanceof Error ? error.message : error)
123+
process.exit(1)
124+
}
125+
})

src/payments/deposit.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* Deposit/top-up command for Filecoin Pay
3+
*
4+
* Provides two modes:
5+
* - Explicit amount: --amount <USDFC>
6+
* - By duration: --days <N> (fund enough to keep current usage alive for N days)
7+
*/
8+
9+
import { RPC_URLS, Synapse, TIME_CONSTANTS } from '@filoz/synapse-sdk'
10+
import { ethers } from 'ethers'
11+
import pc from 'picocolors'
12+
import { computeTopUpForDuration } from '../synapse/payments.js'
13+
import { cleanupProvider } from '../synapse/service.js'
14+
import { cancel, createSpinner, intro, outro } from '../utils/cli-helpers.js'
15+
import { log } from '../utils/cli-logger.js'
16+
import { checkFILBalance, checkUSDFCBalance, depositUSDFC, formatUSDFC, getPaymentStatus } from './setup.js'
17+
18+
export interface DepositOptions {
19+
privateKey?: string
20+
rpcUrl?: string
21+
amount?: string
22+
days?: number
23+
}
24+
25+
/**
26+
* Run the deposit/top-up flow
27+
*/
28+
export async function runDeposit(options: DepositOptions): Promise<void> {
29+
intro(pc.bold('Filecoin Onchain Cloud Deposit'))
30+
31+
const spinner = createSpinner()
32+
33+
// Validate inputs
34+
const privateKey = options.privateKey || process.env.PRIVATE_KEY
35+
if (!privateKey) {
36+
console.error(pc.red('Error: Private key required via --private-key or PRIVATE_KEY env'))
37+
process.exit(1)
38+
}
39+
40+
try {
41+
new ethers.Wallet(privateKey)
42+
} catch {
43+
console.error(pc.red('Error: Invalid private key format'))
44+
process.exit(1)
45+
}
46+
47+
const rpcUrl = options.rpcUrl || process.env.RPC_URL || RPC_URLS.calibration.websocket
48+
49+
const hasAmount = options.amount != null
50+
const hasDays = options.days != null
51+
52+
if ((hasAmount && hasDays) || (!hasAmount && !hasDays)) {
53+
console.error(pc.red('Error: Specify exactly one of --amount <USDFC> or --days <N>'))
54+
process.exit(1)
55+
}
56+
57+
// Connect
58+
spinner.start('Connecting...')
59+
let provider: any = null
60+
try {
61+
const synapse = await Synapse.create({ privateKey, rpcURL: rpcUrl })
62+
63+
if (rpcUrl.match(/^wss?:\/\//)) {
64+
provider = synapse.getProvider()
65+
}
66+
67+
const [filStatus, usdfcBalance, status] = await Promise.all([
68+
checkFILBalance(synapse),
69+
checkUSDFCBalance(synapse),
70+
getPaymentStatus(synapse),
71+
])
72+
73+
spinner.stop(`${pc.green('✓')} Connected`)
74+
75+
// Validate balances
76+
if (!filStatus.hasSufficientGas) {
77+
log.line(`${pc.red('✗')} Insufficient FIL for gas fees`)
78+
const help = filStatus.isCalibnet
79+
? 'Get test FIL from: https://faucet.calibnet.chainsafe-fil.io/'
80+
: 'Acquire FIL for gas from an exchange'
81+
log.line(` ${pc.cyan(help)}`)
82+
log.flush()
83+
await cleanupProvider(provider)
84+
cancel('Deposit aborted')
85+
process.exit(1)
86+
}
87+
88+
let depositAmount: bigint = 0n
89+
90+
if (hasAmount) {
91+
try {
92+
depositAmount = ethers.parseUnits(String(options.amount), 18)
93+
} catch {
94+
console.error(pc.red(`Error: Invalid amount '${options.amount}'`))
95+
process.exit(1)
96+
}
97+
98+
if (depositAmount <= 0n) {
99+
console.error(pc.red('Error: Amount must be greater than 0'))
100+
process.exit(1)
101+
}
102+
} else if (hasDays) {
103+
const days = Number(options.days)
104+
if (!Number.isFinite(days) || days <= 0) {
105+
console.error(pc.red('Error: --days must be a positive number'))
106+
process.exit(1)
107+
}
108+
109+
const { topUp, rateUsed, perDay } = computeTopUpForDuration(status, days)
110+
111+
if (rateUsed === 0n) {
112+
spinner.stop()
113+
log.line(`${pc.yellow('⚠')} No active storage payments detected (rateUsed = 0)`)
114+
log.line('Use --amount to deposit a specific USDFC value instead.')
115+
log.flush()
116+
await cleanupProvider(provider)
117+
cancel('Nothing to fund by duration')
118+
process.exit(1)
119+
}
120+
121+
depositAmount = topUp
122+
123+
if (depositAmount === 0n) {
124+
spinner.stop()
125+
log.line(`${pc.green('✓')} Already funded for at least ${days} day(s) at current spend rate`)
126+
log.indent(`Current daily spend: ${formatUSDFC(perDay)} USDFC/day`)
127+
log.flush()
128+
await cleanupProvider(provider)
129+
outro('No deposit needed')
130+
return
131+
}
132+
}
133+
134+
// Ensure wallet has enough USDFC
135+
if (depositAmount > usdfcBalance) {
136+
console.error(
137+
pc.red(
138+
`✗ Insufficient USDFC (need ${formatUSDFC(depositAmount)} USDFC, have ${formatUSDFC(usdfcBalance)} USDFC)`
139+
)
140+
)
141+
process.exit(1)
142+
}
143+
144+
spinner.start(`Depositing ${formatUSDFC(depositAmount)} USDFC...`)
145+
const { approvalTx, depositTx } = await depositUSDFC(synapse, depositAmount)
146+
spinner.stop(`${pc.green('✓')} Deposit complete`)
147+
148+
log.line(pc.bold('Transaction details:'))
149+
if (approvalTx) {
150+
log.indent(pc.gray(`Approval: ${approvalTx}`))
151+
}
152+
log.indent(pc.gray(`Deposit: ${depositTx}`))
153+
log.flush()
154+
155+
// Brief post-deposit summary
156+
const updated = await getPaymentStatus(synapse)
157+
const lockupUsed = updated.currentAllowances.lockupUsed ?? 0n
158+
const rateUsed = updated.currentAllowances.rateUsed ?? 0n
159+
const available = updated.depositedAmount > lockupUsed ? updated.depositedAmount - lockupUsed : 0n
160+
const dailySpend = rateUsed * TIME_CONSTANTS.EPOCHS_PER_DAY
161+
const runwayDays = rateUsed > 0n ? Number(available / dailySpend) : 0
162+
163+
log.line('')
164+
log.line(pc.bold('Deposit Summary'))
165+
log.indent(`Total deposit: ${formatUSDFC(updated.depositedAmount)} USDFC`)
166+
if (rateUsed > 0n) {
167+
log.indent(`Current spend: ${formatUSDFC(dailySpend)} USDFC/day`)
168+
log.indent(`Runway: ~${runwayDays} days at current spend`)
169+
}
170+
log.flush()
171+
172+
await cleanupProvider(provider)
173+
outro('Deposit completed')
174+
} catch (error) {
175+
spinner.stop()
176+
console.error(pc.red('✗ Deposit failed'))
177+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error)
178+
process.exitCode = 1
179+
} finally {
180+
await cleanupProvider(provider)
181+
}
182+
}

0 commit comments

Comments
 (0)