Skip to content

Commit ef6adc1

Browse files
SgtPookirvagg
andauthored
fix: move gh-action logic to filecoin-pin/core (#143)
* fix: move gh-action logic to filecoin-pin/core * Update src/core/payments/index.ts Co-authored-by: Rod Vagg <rod@vagg.org> * refactor: use explicit names for balances * chore: remove emojis from logger.warn * fix: warnings should not duplicate the message * chore: normalize piece/car/file terminology * refactor: file->piece, car->piece * fix: use reasonCode in TopUpCalculation * chore: payment status requires wallet & filecoinPay balance * chore: remove duplicate topup functions and types * fix: more strict ServiceApprovalStatus props --------- Co-authored-by: Rod Vagg <rod@vagg.org>
1 parent 15c00ee commit ef6adc1

File tree

8 files changed

+474
-272
lines changed

8 files changed

+474
-272
lines changed

src/core/payments/index.ts

Lines changed: 45 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818
import { SIZE_CONSTANTS, type Synapse, TIME_CONSTANTS, TOKENS } from '@filoz/synapse-sdk'
1919
import { ethers } from 'ethers'
2020
import { isSessionKeyMode } from '../synapse/index.js'
21+
import type { PaymentStatus, ServiceApprovalStatus, StorageAllowances, StorageRunwaySummary } from './types.js'
2122

2223
// Constants
2324
export const USDFC_DECIMALS = 18
2425
const MIN_FIL_FOR_GAS = ethers.parseEther('0.1') // Minimum FIL padding for gas
2526
export const DEFAULT_LOCKUP_DAYS = 30 // WarmStorage requires 30 days lockup
2627

28+
export * from './top-up.js'
29+
export * from './types.js'
30+
2731
// Maximum allowances for trusted WarmStorage service
2832
// Using MaxUint256 which MetaMask displays as "Unlimited"
2933
const MAX_RATE_ALLOWANCE = ethers.MaxUint256
@@ -60,52 +64,6 @@ export function getStorageScale(storageTiB: number): number {
6064
return Math.max(1, Math.min(STORAGE_SCALE_MAX, maxScaleBySafe))
6165
}
6266

63-
/**
64-
* Service approval status from the Payments contract
65-
*/
66-
export interface ServiceApprovalStatus {
67-
rateAllowance: bigint
68-
lockupAllowance: bigint
69-
lockupUsed: bigint
70-
maxLockupPeriod?: bigint
71-
rateUsed?: bigint
72-
}
73-
74-
/**
75-
* Complete payment status including balances and approvals
76-
*/
77-
export interface PaymentStatus {
78-
network: string
79-
address: string
80-
filBalance: bigint
81-
/** USDFC tokens sitting in the owner wallet (not yet deposited) */
82-
walletUsdfcBalance: bigint
83-
/** USDFC balance currently deposited into Filecoin Pay (WarmStorage contract) */
84-
filecoinPayBalance: bigint
85-
currentAllowances: ServiceApprovalStatus
86-
}
87-
88-
/**
89-
* Storage allowance calculations
90-
*/
91-
export interface StorageAllowances {
92-
rateAllowance: bigint
93-
lockupAllowance: bigint
94-
storageCapacityTiB: number
95-
}
96-
97-
export type StorageRunwayState = 'unknown' | 'no-spend' | 'active'
98-
99-
export interface StorageRunwaySummary {
100-
state: StorageRunwayState
101-
available: bigint
102-
rateUsed: bigint
103-
perDay: bigint
104-
lockupUsed: bigint
105-
days: number
106-
hours: number
107-
}
108-
10967
/**
11068
* Check FIL balance for gas fees
11169
*
@@ -955,6 +913,40 @@ export interface PaymentCapacityCheck {
955913
suggestions: string[]
956914
}
957915

916+
/**
917+
* Calculate piece upload deposit requirements
918+
*
919+
* @param status - Current payment status
920+
* @param pieceSizeBytes - Size of the piece (CAR, File, etc.) file in bytes
921+
* @param pricePerTiBPerEpoch - Current pricing from storage service
922+
* @returns Piece upload deposit requirements
923+
*/
924+
export function calculatePieceUploadRequirements(
925+
status: PaymentStatus,
926+
pieceSizeBytes: number,
927+
pricePerTiBPerEpoch: bigint
928+
): {
929+
required: StorageAllowances
930+
totalDepositNeeded: bigint
931+
insufficientDeposit: bigint
932+
canUpload: boolean
933+
} {
934+
// Calculate requirements
935+
const required = calculateRequiredAllowances(pieceSizeBytes, pricePerTiBPerEpoch)
936+
const totalDepositNeeded = withBuffer(required.lockupAllowance)
937+
938+
// Check if current deposit can cover the new file's lockup requirement
939+
const insufficientDeposit =
940+
status.filecoinPayBalance < totalDepositNeeded ? totalDepositNeeded - status.filecoinPayBalance : 0n
941+
942+
return {
943+
required,
944+
totalDepositNeeded,
945+
insufficientDeposit,
946+
canUpload: insufficientDeposit === 0n,
947+
}
948+
}
949+
958950
/**
959951
* Validate payment capacity for a specific piece size
960952
*
@@ -992,27 +984,26 @@ export async function validatePaymentCapacity(synapse: Synapse, pieceSizeBytes:
992984
const storageTiB = pieceSizeBytes / Number(SIZE_CONSTANTS.TiB)
993985

994986
// Calculate requirements
995-
const required = calculateRequiredAllowances(pieceSizeBytes, pricePerTiBPerEpoch)
996-
const totalDepositNeeded = withBuffer(required.lockupAllowance)
987+
const uploadRequirements = calculatePieceUploadRequirements(status, pieceSizeBytes, pricePerTiBPerEpoch)
997988

998989
const result: PaymentCapacityCheck = {
999-
canUpload: true,
990+
canUpload: uploadRequirements.canUpload,
1000991
storageTiB,
1001-
required,
992+
required: uploadRequirements.required,
1002993
issues: {},
1003994
suggestions: [],
1004995
}
1005996

1006997
// Only check deposit
1007-
if (status.filecoinPayBalance < totalDepositNeeded) {
998+
if (uploadRequirements.insufficientDeposit > 0n) {
1008999
result.canUpload = false
1009-
result.issues.insufficientDeposit = totalDepositNeeded - status.filecoinPayBalance
1010-
const depositNeeded = ethers.formatUnits(totalDepositNeeded - status.filecoinPayBalance, 18)
1000+
result.issues.insufficientDeposit = uploadRequirements.insufficientDeposit
1001+
const depositNeeded = ethers.formatUnits(uploadRequirements.insufficientDeposit, 18)
10111002
result.suggestions.push(`Deposit at least ${depositNeeded} USDFC`)
10121003
}
10131004

10141005
// Add warning if approaching deposit limit
1015-
const totalLockupAfter = status.currentAllowances.lockupUsed + required.lockupAllowance
1006+
const totalLockupAfter = status.currentAllowances.lockupUsed + uploadRequirements.required.lockupAllowance
10161007
if (totalLockupAfter > withoutBuffer(status.filecoinPayBalance) && result.canUpload) {
10171008
const additionalDeposit = ethers.formatUnits(withBuffer(totalLockupAfter) - status.filecoinPayBalance, 18)
10181009
result.suggestions.push(`Consider depositing ${additionalDeposit} more USDFC for safety margin`)

src/core/payments/top-up.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import type { Synapse } from '@filoz/synapse-sdk'
2+
import type { Logger } from 'pino'
3+
import { formatUSDFC } from '../utils/index.js'
4+
import {
5+
calculatePieceUploadRequirements,
6+
computeAdjustmentForExactDaysWithPiece,
7+
computeTopUpForDuration,
8+
depositUSDFC,
9+
getPaymentStatus,
10+
} from './index.js'
11+
import type { PaymentStatus, TopUpCalculation, TopUpReasonCode, TopUpResult } from './types.js'
12+
13+
/**
14+
* Format a human-readable message for a top-up calculation
15+
*
16+
* @param calc - Top-up calculation result
17+
* @returns Human-readable message explaining the top-up requirement
18+
*/
19+
export function formatTopUpReason(calc: TopUpCalculation): string {
20+
if (calc.requiredTopUp === 0n) {
21+
return 'No top-up required'
22+
}
23+
24+
switch (calc.reasonCode) {
25+
case 'piece-upload':
26+
return 'Required top-up for file upload (lockup requirement)'
27+
case 'required-runway':
28+
return `Required top-up for ${calc.calculation.minStorageDays} days of storage`
29+
case 'required-runway-plus-upload':
30+
return `Required top-up for ${calc.calculation.minStorageDays} days of storage (including upcoming upload)`
31+
default:
32+
return 'Required top-up'
33+
}
34+
}
35+
36+
/**
37+
* Calculate required top-up for a specific storage scenario
38+
*
39+
* This function determines how much USDFC needs to be deposited to meet
40+
* the specified storage requirements, taking into account current usage
41+
* and the size of any upcoming upload.
42+
*
43+
* @param status - Current payment status
44+
* @param options - Storage requirements
45+
* @returns Top-up calculation with reasoning
46+
*/
47+
export function calculateRequiredTopUp(
48+
status: PaymentStatus,
49+
options: {
50+
minStorageDays?: number | undefined
51+
pieceSizeBytes?: number | undefined
52+
pricePerTiBPerEpoch?: bigint | undefined
53+
}
54+
): TopUpCalculation {
55+
const { minStorageDays = 0, pieceSizeBytes, pricePerTiBPerEpoch } = options
56+
const rateUsed = status.currentAllowances.rateUsed ?? 0n
57+
const lockupUsed = status.currentAllowances.lockupUsed ?? 0n
58+
59+
let requiredTopUp = 0n
60+
let reasonCode: TopUpReasonCode = 'none'
61+
62+
// Calculate piece upload requirements if we have piece info
63+
let pieceUploadTopUp = 0n
64+
if (pieceSizeBytes != null && pieceSizeBytes > 0 && pricePerTiBPerEpoch != null) {
65+
const uploadRequirements = calculatePieceUploadRequirements(status, pieceSizeBytes, pricePerTiBPerEpoch)
66+
if (uploadRequirements.insufficientDeposit > 0n) {
67+
pieceUploadTopUp = uploadRequirements.insufficientDeposit
68+
}
69+
}
70+
71+
// Calculate runway requirements if specified
72+
let runwayTopUp = 0n
73+
let isRunwayWithUpload = false
74+
if (minStorageDays > 0) {
75+
if (rateUsed === 0n && pieceSizeBytes != null && pieceSizeBytes > 0 && pricePerTiBPerEpoch != null) {
76+
// New piece upload with runway requirement
77+
const { delta } = computeAdjustmentForExactDaysWithPiece(
78+
status,
79+
minStorageDays,
80+
pieceSizeBytes,
81+
pricePerTiBPerEpoch
82+
)
83+
runwayTopUp = delta
84+
isRunwayWithUpload = true
85+
} else {
86+
// Existing usage with runway requirement
87+
const { topUp } = computeTopUpForDuration(status, minStorageDays)
88+
runwayTopUp = topUp
89+
}
90+
}
91+
92+
// Determine the final top-up requirement (take the maximum of file upload and runway needs)
93+
if (runwayTopUp > pieceUploadTopUp) {
94+
requiredTopUp = runwayTopUp
95+
reasonCode = isRunwayWithUpload ? 'required-runway-plus-upload' : 'required-runway'
96+
} else if (pieceUploadTopUp > 0n) {
97+
requiredTopUp = pieceUploadTopUp
98+
reasonCode = 'piece-upload'
99+
}
100+
101+
return {
102+
requiredTopUp,
103+
reasonCode,
104+
calculation: {
105+
minStorageDays,
106+
pieceSizeBytes,
107+
pricePerTiBPerEpoch,
108+
currentRateUsed: rateUsed,
109+
currentLockupUsed: lockupUsed,
110+
currentDeposited: status.filecoinPayBalance,
111+
},
112+
}
113+
}
114+
115+
/**
116+
* Execute a top-up operation with balance limit checking
117+
*
118+
* This function handles the complete top-up process including:
119+
* - Checking if top-up is needed
120+
* - Validating against balance limits
121+
* - Executing the deposit transaction
122+
* - Providing detailed feedback
123+
*
124+
* @param synapse - Initialized Synapse instance
125+
* @param topUpAmount - Amount of USDFC to deposit
126+
* @param options - Options for top-up execution
127+
* @returns Top-up execution result
128+
*/
129+
export async function executeTopUp(
130+
synapse: Synapse,
131+
topUpAmount: bigint,
132+
options: {
133+
balanceLimit?: bigint | undefined
134+
logger?: Logger | undefined
135+
} = {}
136+
): Promise<TopUpResult> {
137+
const { balanceLimit, logger } = options
138+
const warnings: string[] = []
139+
140+
if (topUpAmount <= 0n) {
141+
return {
142+
success: true,
143+
deposited: 0n,
144+
message: 'No deposit required - sufficient balance available',
145+
warnings,
146+
}
147+
}
148+
149+
// Get current status for limit checking
150+
const currentStatus = await getPaymentStatus(synapse)
151+
152+
// Check if deposit would exceed maximum balance if specified
153+
if (balanceLimit != null && balanceLimit >= 0n) {
154+
// Check if current balance already equals or exceeds limit
155+
if (currentStatus.filecoinPayBalance >= balanceLimit) {
156+
const message = `Current balance (${formatUSDFC(currentStatus.filecoinPayBalance)}) already equals or exceeds the configured balance limit (${formatUSDFC(balanceLimit)}). No additional deposits will be made.`
157+
logger?.warn(`${message}`)
158+
return {
159+
success: true,
160+
deposited: 0n,
161+
message,
162+
warnings,
163+
}
164+
} else {
165+
// Check if required top-up would exceed the limit
166+
const projectedBalance = currentStatus.filecoinPayBalance + topUpAmount
167+
if (projectedBalance > balanceLimit) {
168+
// Calculate the maximum allowed top-up that won't exceed the limit
169+
const maxAllowedTopUp = balanceLimit - currentStatus.filecoinPayBalance
170+
if (maxAllowedTopUp > 0n) {
171+
const warning = `Required top-up (${formatUSDFC(topUpAmount)}) would exceed the configured balance limit (${formatUSDFC(balanceLimit)}). Reducing to ${formatUSDFC(maxAllowedTopUp)}.`
172+
logger?.warn(`${warning}`)
173+
warnings.push(warning)
174+
topUpAmount = maxAllowedTopUp
175+
} else {
176+
return {
177+
success: true,
178+
deposited: 0n,
179+
message: 'Cannot deposit - would exceed balance limit',
180+
warnings,
181+
}
182+
}
183+
}
184+
}
185+
}
186+
187+
// Ensure wallet has sufficient USDFC for the deposit
188+
if (currentStatus.walletUsdfcBalance < topUpAmount) {
189+
const message = `Insufficient USDFC in wallet for deposit. Needed ${formatUSDFC(topUpAmount)}, available ${formatUSDFC(currentStatus.walletUsdfcBalance)}.`
190+
logger?.warn(`${message}`)
191+
return {
192+
success: false,
193+
deposited: 0n,
194+
message,
195+
warnings,
196+
}
197+
}
198+
199+
try {
200+
// Execute the deposit
201+
const result = await depositUSDFC(synapse, topUpAmount)
202+
203+
// Verify the deposit was successful
204+
const newStatus = await getPaymentStatus(synapse)
205+
const depositDifference = newStatus.filecoinPayBalance - currentStatus.filecoinPayBalance
206+
207+
let message = ''
208+
if (depositDifference > 0n) {
209+
message = `Deposit verified: ${formatUSDFC(depositDifference)} USDFC added to Filecoin Pay`
210+
} else {
211+
message = 'Deposit transaction submitted but not yet reflected in balance'
212+
warnings.push('Transaction may take a moment to process')
213+
}
214+
215+
return {
216+
success: true,
217+
deposited: depositDifference,
218+
transactionHash: result.depositTx,
219+
message,
220+
warnings,
221+
}
222+
} catch (error) {
223+
const errorMessage = error instanceof Error ? error.message : String(error)
224+
return {
225+
success: false,
226+
deposited: 0n,
227+
message: `Deposit failed: ${errorMessage}`,
228+
warnings,
229+
}
230+
}
231+
}

0 commit comments

Comments
 (0)