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
36 changes: 36 additions & 0 deletions .github/workflows/upload-action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test upload action
on:
push:
pull_request:

jobs:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered using a matrix for the very similar upload and dry-run jobs, but overall it was simpler without, and the repeated code isn't much at all

dry-run:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Upload to Filecoin
uses: ./upload-action
with:
path: ./src
walletPrivateKey: ${{ secrets.WALLET_PRIVATE_KEY }}
network: calibration
dryRun: true
upload:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Upload to Filecoin
uses: ./upload-action
with:
path: ./src
walletPrivateKey: ${{ secrets.WALLET_PRIVATE_KEY }}
network: calibration
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
20 changes: 6 additions & 14 deletions upload-action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,27 +129,19 @@ runs:
REPO_ROOT=$(cd "$REPO_ROOT" && pwd)
echo "path=$REPO_ROOT" >> $GITHUB_OUTPUT

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
package_json_file: ./package.json # relative to repository root

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: pnpm
cache-dependency-path: ${{ steps.repo-root.outputs.path }}/pnpm-lock.yaml

- name: Get pnpm version
id: pnpm-version
shell: bash
working-directory: ${{ steps.repo-root.outputs.path }}
run: |
VERSION=$(node -pe "require('./package.json').packageManager.split('@')[1]")
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
version: ${{ steps.pnpm-version.outputs.version }}

- name: Install workspace dependencies
shell: bash
working-directory: ${{ steps.repo-root.outputs.path }}
Expand Down
Loading
Loading