4
4
* Adjusts funds to exactly match a target runway (days) or a target deposited amount.
5
5
*/
6
6
7
- import { confirm , isCancel } from '@clack/prompts'
7
+ import { confirm } from '@clack/prompts'
8
8
import { RPC_URLS , Synapse , TIME_CONSTANTS } from '@filoz/synapse-sdk'
9
9
import { ethers } from 'ethers'
10
10
import pc from 'picocolors'
@@ -20,23 +20,17 @@ import {
20
20
getPaymentStatus ,
21
21
withdrawUSDFC ,
22
22
} 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'
30
24
31
25
// Helper: confirm/warn or bail when target implies < 10-day runway
32
26
async function ensureBelowTenDaysAllowed ( opts : {
33
27
isCI : boolean
34
- isInteractive : boolean
28
+ isInteractive ? : boolean | undefined
35
29
spinner : any
36
30
warningLine1 : string
37
31
warningLine2 : string
38
32
} ) : Promise < void > {
39
- const { isCI, isInteractive, spinner, warningLine1, warningLine2 } = opts
33
+ const { isCI, isInteractive = isTTY ( ) , spinner, warningLine1, warningLine2 } = opts
40
34
if ( isCI || ! isInteractive ) {
41
35
spinner . stop ( )
42
36
console . error ( pc . red ( warningLine1 ) )
@@ -54,9 +48,8 @@ async function ensureBelowTenDaysAllowed(opts: {
54
48
message : 'Proceed with reducing runway below 10 days?' ,
55
49
initialValue : false ,
56
50
} )
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' )
60
53
}
61
54
}
62
55
@@ -80,6 +73,16 @@ async function performAdjustment(params: {
80
73
)
81
74
throw new Error ( 'Insufficient USDFC in wallet' )
82
75
}
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
+ }
83
86
spinner . start ( depositMsg )
84
87
const { approvalTx, depositTx } = await depositUSDFC ( synapse , needed )
85
88
spinner . stop ( `${ pc . green ( '✓' ) } Deposit complete` )
@@ -89,6 +92,16 @@ async function performAdjustment(params: {
89
92
log . flush ( )
90
93
} else if ( delta < 0n ) {
91
94
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
+ }
92
105
spinner . start ( withdrawMsg )
93
106
const txHash = await withdrawUSDFC ( synapse , withdrawAmount )
94
107
spinner . stop ( `${ pc . green ( '✓' ) } Withdraw complete` )
@@ -99,13 +112,13 @@ async function performAdjustment(params: {
99
112
}
100
113
101
114
// Helper: summary after adjustment
102
- async function printUpdatedSummary ( synapse : Synapse ) : Promise < void > {
115
+ async function printSummary ( synapse : Synapse , title = 'Updated' ) : Promise < void > {
103
116
const updated = await getPaymentStatus ( synapse )
104
117
const newAvailable = updated . depositedAmount - ( updated . currentAllowances . lockupUsed ?? 0n )
105
118
const newPerDay = ( updated . currentAllowances . rateUsed ?? 0n ) * TIME_CONSTANTS . EPOCHS_PER_DAY
106
119
const newRunway = newPerDay > 0n ? Number ( newAvailable / newPerDay ) : 0
107
120
const newRunwayHours = newPerDay > 0n ? Number ( ( ( newAvailable % newPerDay ) * 24n ) / newPerDay ) : 0
108
- log . section ( 'Updated' , [
121
+ log . section ( title , [
109
122
`Deposited: ${ formatUSDFC ( updated . depositedAmount ) } USDFC` ,
110
123
`Runway: ~${ newRunway } day(s)${ newRunwayHours > 0 ? ` ${ newRunwayHours } hour(s)` : '' } ` ,
111
124
] )
@@ -128,12 +141,16 @@ export async function runFund(options: FundOptions): Promise<void> {
128
141
throw new Error ( 'Invalid private key format' )
129
142
}
130
143
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>' ) )
135
148
throw new Error ( 'Invalid fund options' )
136
149
}
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
+ }
137
154
138
155
const rpcUrl = options . rpcUrl || process . env . RPC_URL || RPC_URLS . calibration . websocket
139
156
@@ -163,31 +180,32 @@ export async function runFund(options: FundOptions): Promise<void> {
163
180
spinner . stop ( `${ pc . green ( '✓' ) } Connected` )
164
181
165
182
const isCI = process . env . CI === 'true' || process . env . GITHUB_ACTIONS === 'true'
166
- const interactive = isTTY ( )
167
183
168
184
// Unified planning: derive delta and target context for both modes
169
185
const rateUsed = status . currentAllowances . rateUsed ?? 0n
170
186
const lockupUsed = status . currentAllowances . lockupUsed ?? 0n
171
187
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
172
192
let delta : bigint
173
- let targetDays : number | null = null
174
193
let clampedTarget : bigint | null = null
175
194
let runwayCheckDays : number | null = null
176
195
let alreadyMessage : string
177
196
let depositMsg : string
178
197
let withdrawMsg : string
179
198
180
- if ( hasExactDays ) {
181
- targetDays = Number ( options . exactDays )
199
+ if ( hasDays ) {
182
200
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' )
185
203
}
186
204
187
205
const adj = computeAdjustmentForExactDays ( status , targetDays )
188
206
if ( adj . rateUsed === 0n ) {
189
207
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.' )
191
209
log . flush ( )
192
210
cancel ( 'Fund adjustment aborted' )
193
211
throw new Error ( 'No active spend' )
@@ -201,17 +219,17 @@ export async function runFund(options: FundOptions): Promise<void> {
201
219
} else {
202
220
let targetDeposit : bigint
203
221
try {
204
- targetDeposit = ethers . parseUnits ( String ( options . exactAmount ) , 18 )
222
+ targetDeposit = ethers . parseUnits ( String ( options . amount ) , 18 )
205
223
} 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' )
208
226
}
209
227
210
228
const adj = computeAdjustmentForExactDeposit ( status , targetDeposit )
211
229
delta = adj . delta
212
230
clampedTarget = adj . clampedTarget
213
231
214
- if ( targetDeposit < lockupUsed ) {
232
+ if ( targetDeposit < lockupUsed && options . mode !== 'minimum' ) {
215
233
log . line ( pc . yellow ( '⚠ Target amount is below locked funds. Clamping to locked amount.' ) )
216
234
log . indent ( `Locked: ${ formatUSDFC ( lockupUsed ) } USDFC` )
217
235
log . flush ( )
@@ -223,39 +241,57 @@ export async function runFund(options: FundOptions): Promise<void> {
223
241
runwayCheckDays = Number ( availableAfter / perDay )
224
242
}
225
243
226
- const targetLabel = clampedTarget != null ? formatUSDFC ( clampedTarget ) : String ( options . exactAmount )
244
+ const targetLabel = clampedTarget != null ? formatUSDFC ( clampedTarget ) : String ( options . amount )
227
245
alreadyMessage = `Already at target deposit of ${ targetLabel } USDFC. No changes needed.`
228
246
depositMsg = `Depositing ${ formatUSDFC ( delta ) } USDFC to reach ${ targetLabel } USDFC total...`
229
247
withdrawMsg = `Withdrawing ${ formatUSDFC ( - delta ) } USDFC to reach ${ targetLabel } USDFC total...`
230
248
}
231
249
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
234
271
? 'Requested runway below 10-day safety baseline.'
235
272
: 'Target deposit implies less than 10 days of runway at current spend.'
236
- const line2 = hasExactDays
273
+ const line2 = hasDays
237
274
? 'WarmStorage reserves 10 days of costs; a shorter runway risks termination.'
238
275
: 'Increase target or accept risk: shorter runway may cause termination.'
239
276
await ensureBelowTenDaysAllowed ( {
240
277
isCI,
241
- isInteractive : interactive ,
242
278
spinner,
243
279
warningLine1 : line1 ,
244
280
warningLine2 : line2 ,
245
281
} )
246
282
}
247
283
248
284
if ( delta === 0n ) {
285
+ await printSummary ( synapse , 'No Changes Needed' )
249
286
outro ( alreadyMessage )
250
287
return
251
288
}
252
289
253
290
await performAdjustment ( { synapse, spinner, delta, depositMsg, withdrawMsg } )
254
291
255
- await printUpdatedSummary ( synapse )
292
+ await printSummary ( synapse )
256
293
outro ( 'Fund adjustment completed' )
257
294
} catch ( error ) {
258
- spinner . stop ( )
259
295
console . error ( pc . red ( '✗ Fund adjustment failed' ) )
260
296
console . error ( pc . red ( 'Error:' ) , error instanceof Error ? error . message : error )
261
297
process . exitCode = 1
0 commit comments