Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/add/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,21 @@ export async function runAdd(options: AddOptions): Promise<AddResult> {
const carSize = carData.length
spinner.stop(`${pc.green('✓')} IPFS content loaded (${formatFileSize(carSize)})`)

const autoFundOptions: Parameters<typeof performAutoFunding>[3] = {
...(dataSetMetadata && { metadata: dataSetMetadata }),
...(options.copies != null && { copies: options.copies }),
}
if (contextSelection.providerIds) {
autoFundOptions.providerIds = contextSelection.providerIds
autoFundOptions.copies = contextSelection.providerIds.length
}
if (contextSelection.dataSetIds) {
autoFundOptions.dataSetIds = contextSelection.dataSetIds
autoFundOptions.copies = contextSelection.dataSetIds.length
}

if (options.autoFund) {
await performAutoFunding(synapse, carSize, spinner)
await performAutoFunding(synapse, carSize, spinner, autoFundOptions)
} else {
spinner.start('Checking payment capacity...')
await validatePaymentSetup(synapse, carSize, spinner)
Expand Down
12 changes: 11 additions & 1 deletion src/common/upload-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,24 @@ export interface UploadFlowResult extends SynapseUploadResult {
* @param synapse - Initialized Synapse instance
* @param fileSize - Size of file being uploaded (in bytes)
* @param spinner - Optional spinner for progress
* @param options - Optional upload targeting inputs used to estimate new data set fees
*/
export async function performAutoFunding(synapse: Synapse, fileSize: number, spinner?: Spinner): Promise<void> {
export async function performAutoFunding(
synapse: Synapse,
fileSize: number,
spinner?: Spinner,
options?: Pick<AutoFundOptions, 'copies' | 'providerIds' | 'dataSetIds' | 'metadata'>
): Promise<void> {
spinner?.start('Checking funding requirements for upload...')

try {
const fundOptions: AutoFundOptions = {
synapse,
fileSize,
...(options?.copies != null ? { copies: options.copies } : {}),
...(options?.providerIds != null ? { providerIds: options.providerIds } : {}),
...(options?.dataSetIds != null ? { dataSetIds: options.dataSetIds } : {}),
...(options?.metadata != null ? { metadata: options.metadata } : {}),
}
if (spinner !== undefined) {
fundOptions.spinner = spinner
Expand Down
5 changes: 5 additions & 0 deletions src/core/payments/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export const DEFAULT_LOCKUP_DAYS = 30
*/
export const FLOOR_PRICE_PER_30_DAYS = parseUnits('0.06', USDFC_DECIMALS)

/**
* One-time sybil fee charged when creating a new WarmStorage data set
*/
export const USDFC_SYBIL_FEE = parseUnits('0.1', USDFC_DECIMALS)

/**
* Number of days the floor price covers
*/
Expand Down
15 changes: 13 additions & 2 deletions src/core/payments/funding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
computeAdjustmentForExactDeposit,
depositUSDFC,
getPaymentStatus,
USDFC_SYBIL_FEE,
validatePaymentRequirements,
withdrawUSDFC,
} from './index.js'
Expand Down Expand Up @@ -139,6 +140,7 @@ export function calculateFilecoinPayFundingPlan(options: FilecoinPayFundingPlanO
targetDeposit,
pieceSizeBytes,
pricePerTiBPerEpoch,
newDataSetCount = 0,
mode = 'exact',
allowWithdraw = true,
} = options
Expand All @@ -155,6 +157,10 @@ export function calculateFilecoinPayFundingPlan(options: FilecoinPayFundingPlanO
throw new Error('pricePerTiBPerEpoch is required when pieceSizeBytes is provided')
}

if (!Number.isInteger(newDataSetCount) || newDataSetCount < 0) {
throw new Error('newDataSetCount must be a non-negative integer')
}

let delta: bigint
let projectedDeposit = status.filecoinPayBalance
let projectedRateUsed = status.currentAllowances.rateUsed ?? 0n
Expand All @@ -175,8 +181,9 @@ export function calculateFilecoinPayFundingPlan(options: FilecoinPayFundingPlanO
pieceSizeBytes,
pricePerTiBPerEpoch
)
delta = adjustment.delta
resolvedTargetDeposit = adjustment.targetDeposit
const dataSetCreationFees = BigInt(newDataSetCount) * USDFC_SYBIL_FEE
delta = adjustment.delta + dataSetCreationFees
resolvedTargetDeposit = adjustment.targetDeposit + dataSetCreationFees
projectedRateUsed = adjustment.newRateUsed
projectedLockupUsed = adjustment.newLockupUsed

Expand Down Expand Up @@ -260,6 +267,7 @@ export function calculateFilecoinPayFundingPlan(options: FilecoinPayFundingPlanO
...(resolvedTargetDeposit != null ? { targetDeposit: resolvedTargetDeposit } : {}),
...(pieceSizeBytes != null ? { pieceSizeBytes } : {}),
...(pricePerTiBPerEpoch != null ? { pricePerTiBPerEpoch } : {}),
...(newDataSetCount > 0 ? { newDataSetCount } : {}),
...(walletShortfall != null ? { walletShortfall } : {}),
}

Expand All @@ -280,6 +288,7 @@ export interface PlanFilecoinPayFundingOptions {
targetDeposit?: bigint | undefined
pieceSizeBytes?: number | undefined
pricePerTiBPerEpoch?: bigint | undefined
newDataSetCount?: number | undefined
mode?: FundingMode | undefined
allowWithdraw?: boolean | undefined
ensureAllowances?: boolean | undefined
Expand Down Expand Up @@ -313,6 +322,7 @@ export async function planFilecoinPayFunding(options: PlanFilecoinPayFundingOpti
targetDeposit,
pieceSizeBytes,
pricePerTiBPerEpoch,
newDataSetCount = 0,
mode = 'exact',
allowWithdraw = true,
ensureAllowances = false,
Expand Down Expand Up @@ -364,6 +374,7 @@ export async function planFilecoinPayFunding(options: PlanFilecoinPayFundingOpti
targetDeposit,
pieceSizeBytes,
pricePerTiBPerEpoch: pricing,
newDataSetCount,
mode,
allowWithdraw,
})
Expand Down
2 changes: 2 additions & 0 deletions src/core/payments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export interface FilecoinPayFundingPlanOptions {
targetDeposit?: bigint | undefined
pieceSizeBytes?: number | undefined
pricePerTiBPerEpoch?: bigint | undefined
newDataSetCount?: number | undefined
mode?: FundingMode | undefined
allowWithdraw?: boolean | undefined
}
Expand All @@ -131,6 +132,7 @@ export interface FilecoinPayFundingPlan {
mode: FundingMode
pieceSizeBytes?: number
pricePerTiBPerEpoch?: bigint
newDataSetCount?: number
projectedDeposit: bigint
projectedRateUsed: bigint
projectedLockupUsed: bigint
Expand Down
15 changes: 14 additions & 1 deletion src/import/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,20 @@ export async function runCarImport(options: ImportOptions): Promise<ImportResult
spinner.stop(`${pc.green('✓')} Connected to ${pc.bold(network)}`)

if (options.autoFund) {
await performAutoFunding(synapse, fileStat.size, spinner)
const autoFundOptions: Parameters<typeof performAutoFunding>[3] = {
...(dataSetMetadata && { metadata: dataSetMetadata }),
...(options.copies != null && { copies: options.copies }),
}
if (contextSelection.providerIds) {
autoFundOptions.providerIds = contextSelection.providerIds
autoFundOptions.copies = contextSelection.providerIds.length
}
if (contextSelection.dataSetIds) {
autoFundOptions.dataSetIds = contextSelection.dataSetIds
autoFundOptions.copies = contextSelection.dataSetIds.length
}

await performAutoFunding(synapse, fileStat.size, spinner, autoFundOptions)
} else {
spinner.start('Checking payment capacity...')
await validatePaymentSetup(synapse, fileStat.size, spinner)
Expand Down
11 changes: 10 additions & 1 deletion src/payments/fund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,23 @@ async function printSummary(synapse: Synapse, title = 'Updated'): Promise<void>
* @throws Error if adjustment fails or target is unsafe
*/
export async function autoFund(options: AutoFundOptions): Promise<FundingAdjustmentResult> {
const { synapse, fileSize, spinner } = options
const { synapse, fileSize, copies, providerIds, dataSetIds, metadata, spinner } = options

spinner?.message('Checking wallet readiness...')

const contexts = await synapse.storage.createContexts({
...(copies != null ? { copies } : {}),
...(providerIds != null ? { providerIds } : {}),
...(dataSetIds != null ? { dataSetIds } : {}),
...(metadata != null ? { metadata } : {}),
})
const newDataSetCount = contexts.filter((context) => context.dataSetId == null).length

const planResult = await planFilecoinPayFunding({
synapse,
targetRunwayDays: MIN_RUNWAY_DAYS,
pieceSizeBytes: fileSize,
newDataSetCount,
ensureAllowances: true,
allowWithdraw: false,
})
Expand Down
8 changes: 8 additions & 0 deletions src/payments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export interface AutoFundOptions {
synapse: Synapse
/** Size of file being uploaded (in bytes) - used to calculate additional funding needed */
fileSize: number
/** Number of storage copies to create */
copies?: number
/** Specific provider IDs to upload to */
providerIds?: bigint[]
/** Specific existing data set IDs to target */
dataSetIds?: bigint[]
/** Data set metadata applied when creating or matching contexts */
metadata?: Record<string, string>
/** Optional spinner for progress updates */
spinner?: Spinner
}
Expand Down
25 changes: 25 additions & 0 deletions src/test/unit/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { runAdd } from '../../add/add.js'
// Mock the external dependencies at module level
vi.mock('../../common/upload-flow.js', () => ({
validatePaymentSetup: vi.fn(),
performAutoFunding: vi.fn(),
performUpload: vi.fn().mockResolvedValue({
pieceCid: 'bafkzcibtest1234567890',
size: 1024,
Expand Down Expand Up @@ -242,6 +243,30 @@ describe('Add Command', () => {
)
})

it('passes upload targeting options through to auto-funding', async () => {
await runAdd({
filePath: testFile,
privateKey: 'test-private-key',
rpcUrl: 'wss://test.rpc.url',
autoFund: true,
providerIds: '7,8',
dataSetMetadata: { purpose: 'erc8004' },
})

const { performAutoFunding } = await import('../../common/upload-flow.js')

expect(vi.mocked(performAutoFunding)).toHaveBeenCalledWith(
expect.anything(),
expect.any(Number),
expect.anything(),
expect.objectContaining({
providerIds: [7n, 8n],
copies: 2,
metadata: { purpose: 'erc8004' },
})
)
})

it('should reject when file does not exist', async () => {
const mockExit = vi.spyOn(process, 'exit')

Expand Down
27 changes: 27 additions & 0 deletions src/test/unit/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,33 @@ describe('CAR Import', () => {
})
)
})

it('passes upload targeting options through to auto-funding', async () => {
const carPath = join(testDir, 'auto-fund.car')
await createTestCarFile(carPath, [], [{ content: 'test content' }])

const options: ImportOptions = {
filePath: carPath,
privateKey: testPrivateKey,
autoFund: true,
dataSetIds: '123',
dataSetMetadata: { erc8004Files: '' },
}

await runCarImport(options)

const { performAutoFunding } = await import('../../common/upload-flow.js')
expect(vi.mocked(performAutoFunding)).toHaveBeenCalledWith(
expect.anything(),
expect.any(Number),
expect.anything(),
expect.objectContaining({
dataSetIds: [123n],
copies: 1,
metadata: { erc8004Files: '' },
})
)
})
})

describe('Upload Result', () => {
Expand Down
28 changes: 28 additions & 0 deletions src/test/unit/payments-funding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TIME_CONSTANTS } from '@filoz/synapse-sdk'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as paymentsIndex from '../../core/payments/index.js'
import {
calculateFilecoinPayFundingPlan,
executeFilecoinPayFunding,
getFilecoinPayFundingInsights,
type PaymentStatus,
Expand Down Expand Up @@ -195,6 +196,33 @@ describe('planFilecoinPayFunding', () => {
expect(plan.delta).toBeGreaterThan(0n)
expect(plan.reasonCode).toBe('runway-with-piece')
})

it('adds sybil fees for new data sets in the shared funding plan', () => {
const status = makeStatus({ filecoinPayBalance: 0n, wallet: 1_000_000_000_000_000_000n })

const basePlan = calculateFilecoinPayFundingPlan({
status,
targetRunwayDays: 30,
pieceSizeBytes: 1024,
pricePerTiBPerEpoch: 1n,
newDataSetCount: 0,
mode: 'minimum',
allowWithdraw: false,
})

const withFeesPlan = calculateFilecoinPayFundingPlan({
status,
targetRunwayDays: 30,
pieceSizeBytes: 1024,
pricePerTiBPerEpoch: 1n,
newDataSetCount: 2,
mode: 'minimum',
allowWithdraw: false,
})

expect(withFeesPlan.delta - basePlan.delta).toBe(200_000_000_000_000_000n)
expect(withFeesPlan.targetDeposit).toBe((basePlan.targetDeposit ?? 0n) + 200_000_000_000_000_000n)
})
})

describe('executeFilecoinPayFunding', () => {
Expand Down
25 changes: 21 additions & 4 deletions upload-action/src/filecoin.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { getErrorMessage } from './errors.js'
* @typedef {import('./types.js').UploadResult} UploadResult
* @typedef {import('./types.js').PaymentStatus} PaymentStatus
* @typedef {import('./types.js').SimplifiedPaymentStatus} SimplifiedPaymentStatus
* @typedef {import('./types.js').PaymentConfig} PaymentConfig
* @typedef {import('./types.js').PaymentFundingConfig} PaymentFundingConfig
* @typedef {import('./types.js').UploadConfig} UploadConfig
* @typedef {import('./types.js').FilecoinPinPaymentStatus} FilecoinPinPaymentStatus
* @typedef {import('./types.js').Synapse} Synapse
Expand Down Expand Up @@ -50,22 +50,31 @@ export async function createCarFile(targetPath, contentPath, logger) {
/**
* Handle payment setup and top-ups using core payment functions
* @param {Synapse} synapse - Synapse service
* @param {PaymentConfig} options - Payment options
* @param {PaymentFundingConfig} options - Payment options
* @param {Logger | undefined} logger - Logger instance
* @returns {Promise<SimplifiedPaymentStatus>} Updated payment status
*/
export async function handlePayments(synapse, options, logger) {
const { minStorageDays, filecoinPayBalanceLimit, pieceSizeBytes } = options
const { minStorageDays, filecoinPayBalanceLimit, pieceSizeBytes, withCDN, providerIds } = options

console.log('Checking current Filecoin Pay account balance...')
const [rawStatus, storageInfo] = await Promise.all([getPaymentStatus(synapse), synapse.storage.getStorageInfo()])
const [rawStatus, storageInfo, contexts] = await Promise.all([
getPaymentStatus(synapse),
synapse.storage.getStorageInfo(),
synapse.storage.createContexts({
...(providerIds != null && providerIds.length > 0 ? { providerIds } : {}),
...(withCDN ? { withCDN } : {}),
}),
])

const initialFilecoinPayBalance = formatUSDFC(rawStatus.filecoinPayBalance)
const initialWalletBalance = formatUSDFC(rawStatus.walletUsdfcBalance)

console.log(`Current Filecoin Pay balance: ${initialFilecoinPayBalance} USDFC`)
console.log(`Wallet USDFC balance: ${initialWalletBalance} USDFC`)

const newDataSetCount = contexts.filter((context) => context.dataSetId == null).length

// Calculate required funding using the comprehensive funding planner
const fundingPlan = calculateFilecoinPayFundingPlan({
status: rawStatus,
Expand All @@ -74,13 +83,21 @@ export async function handlePayments(synapse, options, logger) {
targetRunwayDays: minStorageDays,
pieceSizeBytes,
pricePerTiBPerEpoch: storageInfo.pricing.noCDN.perTiBPerEpoch,
newDataSetCount,
})

if (fundingPlan.delta > 0n) {
const reasonMessage = formatFundingReason(fundingPlan.reasonCode, fundingPlan)
console.log(`\n${reasonMessage}: ${formatUSDFC(fundingPlan.delta)} USDFC`)
}

if (newDataSetCount > 0) {
console.log(
`Additional funding for ${newDataSetCount} new data set${newDataSetCount === 1 ? '' : 's'} ` +
'(sybil fee) is included in the planned top-up'
)
}

// Execute top-up with balance limit checking
const topUpResult = await executeTopUp(synapse, fundingPlan.delta, {
balanceLimit: filecoinPayBalanceLimit,
Expand Down
Loading