filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
423 lines (352 loc) • 16 kB
text/typescript
import { ethers } from 'ethers'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
calculateActualCapacity,
calculateStorageAllowances,
calculateStorageFromUSDFC,
checkFILBalance,
checkUSDFCBalance,
depositUSDFC,
getPaymentStatus,
setServiceApprovals,
} from '../../core/payments/index.js'
import { formatFIL, formatUSDFC } from '../../core/utils/format.js'
import { assertPriceNonZero } from '../../core/utils/validate-pricing.js'
import { parseStorageAllowance } from '../../payments/setup.js'
// Mock Synapse SDK
vi.mock('@filoz/synapse-sdk', () => {
const mockSynapse = {
getProvider: vi.fn(),
getSigner: vi.fn(),
getClient: vi.fn(),
getNetwork: vi.fn(),
getPaymentsAddress: vi.fn(),
getWarmStorageAddress: vi.fn(),
payments: {
walletBalance: vi.fn(),
balance: vi.fn(),
serviceApproval: vi.fn(),
allowance: vi.fn(),
approve: vi.fn(),
deposit: vi.fn(),
approveService: vi.fn(),
},
storage: {
getStorageInfo: vi.fn(),
},
}
return {
Synapse: {
create: vi.fn().mockResolvedValue(mockSynapse),
},
TOKENS: {
USDFC: 'USDFC',
},
TIME_CONSTANTS: {
EPOCHS_PER_DAY: 2880n,
EPOCHS_PER_MONTH: 86400n,
},
SIZE_CONSTANTS: {
MIN_UPLOAD_SIZE: 127,
},
METADATA_KEYS: {
WITH_IPFS_INDEXING: 'withIPFSIndexing',
IPFS_ROOT_CID: 'ipfsRootCid',
},
}
})
describe('assertPriceNonZero', () => {
it('throws when pricePerTiBPerEpoch is zero', () => {
expect(() => assertPriceNonZero(0n)).toThrow('Invalid pricePerTiBPerEpoch: must be positive non-zero value')
})
it('throws when pricePerTiBPerEpoch is negative', () => {
expect(() => assertPriceNonZero(-10n)).toThrow('Invalid pricePerTiBPerEpoch: must be positive non-zero value')
})
it('does not throw for positive price', () => {
expect(() => assertPriceNonZero(1n)).not.toThrow()
})
})
describe('Payment Setup Tests', () => {
let mockSynapse: any
let mockProvider: any
let mockSigner: any
beforeEach(() => {
// Reset mocks
vi.clearAllMocks()
// Create mock instances
mockProvider = {
getBalance: vi.fn().mockResolvedValue(ethers.parseEther('5')),
}
mockSigner = {
getAddress: vi.fn().mockResolvedValue('0x1234567890123456789012345678901234567890'),
}
// Create mock Synapse instance
mockSynapse = {
getProvider: vi.fn().mockReturnValue(mockProvider),
getSigner: vi.fn().mockReturnValue(mockSigner),
getClient: vi.fn().mockReturnValue(mockSigner),
getNetwork: vi.fn().mockReturnValue('calibration'),
getPaymentsAddress: vi.fn().mockReturnValue('0xpayments'),
getWarmStorageAddress: vi.fn().mockReturnValue('0xwarmstorage'),
payments: {
walletBalance: vi.fn().mockResolvedValue(ethers.parseUnits('100', 18)),
balance: vi.fn().mockResolvedValue(ethers.parseUnits('10', 18)),
serviceApproval: vi.fn().mockResolvedValue({
rateAllowance: ethers.parseUnits('0.0001', 18),
lockupAllowance: ethers.parseUnits('2', 18),
rateUsed: 0n,
lockupUsed: 0n,
}),
allowance: vi.fn().mockResolvedValue(ethers.parseUnits('0', 18)),
depositWithPermitAndApproveOperator: vi.fn().mockResolvedValue({
wait: vi.fn(),
hash: '0xdepositWithPermitAndApproveOperator',
}),
deposit: vi.fn().mockResolvedValue({
wait: vi.fn(),
hash: '0xdeposit',
}),
approveService: vi.fn().mockResolvedValue({
wait: vi.fn(),
hash: '0xservice',
}),
},
storage: {
getStorageInfo: vi.fn().mockResolvedValue({
pricing: {
noCDN: {
perTiBPerEpoch: ethers.parseUnits('0.00002893519', 18), // 2.5 USDFC/TiB/month
perTiBPerDay: ethers.parseUnits('0.08333333', 18),
perTiBPerMonth: ethers.parseUnits('2.5', 18),
},
},
}),
},
}
})
describe('checkFILBalance', () => {
it('should check FIL balance and network correctly', async () => {
const result = await checkFILBalance(mockSynapse)
expect(result.balance).toBe(ethers.parseEther('5'))
expect(result.isCalibnet).toBe(true)
expect(result.hasSufficientGas).toBe(true)
})
it('should detect insufficient gas', async () => {
mockProvider.getBalance.mockResolvedValue(ethers.parseEther('0.05'))
const result = await checkFILBalance(mockSynapse)
expect(result.hasSufficientGas).toBe(false)
})
})
describe('checkUSDFCBalance', () => {
it('should return USDFC wallet balance', async () => {
const balance = await checkUSDFCBalance(mockSynapse)
expect(balance).toBe(ethers.parseUnits('100', 18))
expect(mockSynapse.payments.walletBalance).toHaveBeenCalledWith('USDFC')
})
})
describe('getPaymentStatus', () => {
it('should return complete payment status', async () => {
const status = await getPaymentStatus(mockSynapse)
expect(status.network).toBe('calibration')
expect(status.address).toBe('0x1234567890123456789012345678901234567890')
expect(status.filBalance).toBe(ethers.parseEther('5'))
expect(status.walletUsdfcBalance).toBe(ethers.parseUnits('100', 18))
expect(status.filecoinPayBalance).toBe(ethers.parseUnits('10', 18))
expect(status.currentAllowances.rateAllowance).toBe(ethers.parseUnits('0.0001', 18))
})
})
describe('depositUSDFC', () => {
it('should deposit USDFC without approval when allowance sufficient', async () => {
mockSynapse.payments.allowance.mockResolvedValue(ethers.parseUnits('10', 18))
const result = await depositUSDFC(mockSynapse, ethers.parseUnits('5', 18))
expect(result.depositTx).toBe('0xdepositWithPermitAndApproveOperator')
expect(mockSynapse.payments.depositWithPermitAndApproveOperator).toHaveBeenCalled()
})
it('should approve and deposit when allowance insufficient', async () => {
mockSynapse.payments.allowance.mockResolvedValue(ethers.parseUnits('0', 18))
const result = await depositUSDFC(mockSynapse, ethers.parseUnits('5', 18))
expect(result.depositTx).toBe('0xdepositWithPermitAndApproveOperator')
expect(mockSynapse.payments.depositWithPermitAndApproveOperator).toHaveBeenCalled()
})
})
describe('setServiceApprovals', () => {
it('should set service approvals with correct parameters', async () => {
const rateAllowance = ethers.parseUnits('0.0001', 18)
const lockupAllowance = ethers.parseUnits('2', 18)
const txHash = await setServiceApprovals(mockSynapse, rateAllowance, lockupAllowance)
expect(txHash).toBe('0xservice')
expect(mockSynapse.payments.approveService).toHaveBeenCalledWith(
'0xwarmstorage',
rateAllowance,
lockupAllowance,
86400n, // 30 days * 2880 epochs/day (bigint)
'USDFC'
)
})
})
describe('calculateStorageAllowances', () => {
it('should calculate allowances for 1 TiB/month', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
const allowances = calculateStorageAllowances(1, pricePerTiBPerEpoch)
expect(allowances.storageCapacityTiB).toBe(1)
expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.0000565', 18))
expect(allowances.lockupAllowance).toBe(
ethers.parseUnits('0.0000565', 18) * 2880n * 30n // rate * epochs/day * 30 days
)
})
it('should calculate allowances for fractional TiB', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
const allowances = calculateStorageAllowances(0.5, pricePerTiBPerEpoch)
expect(allowances.storageCapacityTiB).toBe(0.5)
// 0.5 TiB
expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.00002825', 18))
})
it('should calculate allowances for 1.5 TiB correctly', async () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
const allowances = calculateStorageAllowances(1.5, pricePerTiBPerEpoch)
expect(allowances.storageCapacityTiB).toBe(1.5)
// 1.5 TiB
expect(allowances.rateAllowance).toBe(ethers.parseUnits('0.00008475', 18))
expect(allowances.lockupAllowance).toBe(
ethers.parseUnits('0.00008475', 18) * 2880n * 30n // rate * epochs/day * 30 days
)
})
it('should calculate allowances for 1 GiB/month (small storage amount)', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
const storageTiB = 1 / 1024 // 1 GiB = 1/1024 TiB ~= 0.0009765625 TiB
const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)
expect(allowances.storageCapacityTiB).toBe(storageTiB)
expect(allowances.rateAllowance).toBeGreaterThan(0n)
expect(allowances.lockupAllowance).toBeGreaterThan(0n)
const roundTripTiB = calculateActualCapacity(allowances.rateAllowance, pricePerTiBPerEpoch)
expect(roundTripTiB).toBeCloseTo(storageTiB, 6)
})
it('should calculate allowances for 512 MiB/month', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
const storageTiB = 512 / (1024 * 1024) // 512 MiB = 512/(1024*1024) TiB ~= 0.00048828125 TiB
const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)
expect(allowances.storageCapacityTiB).toBe(storageTiB)
expect(allowances.rateAllowance).toBeGreaterThan(0n)
expect(allowances.lockupAllowance).toBeGreaterThan(0n)
const roundTripTiB = calculateActualCapacity(allowances.rateAllowance, pricePerTiBPerEpoch)
expect(roundTripTiB).toBeCloseTo(storageTiB, 6)
})
it('should calculate allowances for 1 MiB/month', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
const storageTiB = 1 / (1024 * 1024) // 1 MiB in TiB
const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)
expect(allowances.storageCapacityTiB).toBe(storageTiB)
expect(allowances.rateAllowance).toBeGreaterThan(0n)
expect(allowances.lockupAllowance).toBeGreaterThan(0n)
const roundTripTiB = calculateActualCapacity(allowances.rateAllowance, pricePerTiBPerEpoch)
expect(roundTripTiB).toBeCloseTo(storageTiB, 6)
})
it('should handle very large TiB values without overflow', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
// 900 billion TiB (if we multiplied this by STORAGE_SCALE_MAX, it would overflow)
const storageTiB = 900_000_000_000
const allowances = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch)
// rateAllowance should be price * storageTiB exactly representable via bigint math
const expectedRate = (pricePerTiBPerEpoch * BigInt(storageTiB)) / 1n
expect(allowances.rateAllowance).toBe(expectedRate)
})
})
describe('parseStorageAllowance', () => {
it('should parse TiB/month format', () => {
const tibPerMonth = parseStorageAllowance('2TiB/month')
expect(tibPerMonth).toBe(2)
})
it('should parse GiB/month format', () => {
const tibPerMonth = parseStorageAllowance('512GiB/month')
expect(tibPerMonth).toBe(0.5)
})
it('should parse MiB/month format', () => {
const tibPerMonth = parseStorageAllowance(`524288MiB/month`) // 512 GiB
expect(tibPerMonth).toBe(0.5)
})
it('should return null for direct USDFC/epoch format', () => {
const tibPerMonth = parseStorageAllowance('0.0001')
expect(tibPerMonth).toBeNull()
})
it('should throw on invalid format', () => {
expect(() => parseStorageAllowance('invalid')).toThrow()
})
})
describe('formatUSDFC', () => {
it('should format USDFC amounts correctly', () => {
expect(formatUSDFC(ethers.parseUnits('1.2345', 18))).toBe('1.2345')
expect(formatUSDFC(ethers.parseUnits('1.23456789', 18))).toBe('1.2346')
expect(formatUSDFC(ethers.parseUnits('1000', 18))).toBe('1000.0000')
expect(formatUSDFC(ethers.parseUnits('0.0001', 18), 6)).toBe('0.000100')
})
})
describe('formatFIL', () => {
it('should format FIL amounts with correct unit', () => {
expect(formatFIL(ethers.parseEther('1.5'), false)).toBe('1.5000 FIL')
expect(formatFIL(ethers.parseEther('1.5'), true)).toBe('1.5000 tFIL')
expect(formatFIL(ethers.parseEther('0.0001'), false)).toBe('0.0001 FIL')
})
})
describe('calculateActualCapacity', () => {
it('should calculate capacity from rate allowance with high precision', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
const storageTiB = 1 / 1024 // 1 GiB/month
const rateAllowance = calculateStorageAllowances(storageTiB, pricePerTiBPerEpoch).rateAllowance
const capacityTiB = calculateActualCapacity(rateAllowance, pricePerTiBPerEpoch)
const expectedTiB = 1 / 1024 // ~= 0.0009765625
expect(capacityTiB).toBeCloseTo(expectedTiB, 5)
})
it('throws when pricePerTiBPerEpoch is zero', () => {
expect(() => calculateActualCapacity(ethers.parseUnits('1', 18), 0n)).toThrow(
'Invalid pricePerTiBPerEpoch: must be positive non-zero value'
)
})
})
describe('calculateStorageFromUSDFC', () => {
it('should calculate storage capacity from USDFC amount with high precision', () => {
const pricePerTiBPerEpoch = ethers.parseUnits('0.0000565', 18)
// 30 days worth of 1GiB/month = 0.0047644416 USDFC
const usdfcAmount = ethers.parseUnits('0.0047644416', 18)
const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
const expectedTiB = 1 / 1024 // ~= 0.0009765625
expect(capacityTiB).toBeCloseTo(expectedTiB, 5)
})
it('throws when pricePerTiBPerEpoch is zero', () => {
expect(() => calculateStorageFromUSDFC(ethers.parseUnits('1', 18), 0n)).toThrow(
'Invalid pricePerTiBPerEpoch: must be positive non-zero value'
)
})
it('returns 0 if usdfcAmount is 0', () => {
const usdfcAmount = ethers.parseUnits('0', 18)
const pricePerTiBPerEpoch = ethers.parseUnits('0.0005', 18)
const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
expect(capacityTiB).toBe(0)
})
// Verify pricePerTibPerEpoch needed to get 1TiB/month with 1USDFC given 30-day lockup
// With 30-day lockup: 1 USDFC / (30 days * 2880 epochs/day) = 1 USDFC / 86400 epochs
// For 1 TiB capacity: pricePerTiBPerEpoch = 1 / 86400 = 0.000011574074 USDFC
it('should return capacity of 1 when pricePerTibPerEpoch is low', () => {
const usdfcAmount = ethers.parseUnits('1', 18)
const pricePerTiBPerEpoch = ethers.parseUnits('0.000011574074', 18)
const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
// within 10 decimal places accuracy of 1
expect(capacityTiB).toBeCloseTo(1, 10)
})
it('should return lower capacity as pricePerTibPerEpoch increases', () => {
const usdfcAmount = ethers.parseUnits('1', 18)
const pricePerTiBPerEpoch = ethers.parseUnits('0.00005', 18)
const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
expect(
calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch + ethers.parseUnits('0.00001', 18))
).toBeLessThan(capacityTiB)
})
it('should return higher capacity as pricePerTibPerEpoch decreases', () => {
const usdfcAmount = ethers.parseUnits('1', 18)
const pricePerTiBPerEpoch = ethers.parseUnits('0.00005', 18)
const capacityTiB = calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch)
expect(
calculateStorageFromUSDFC(usdfcAmount, pricePerTiBPerEpoch - ethers.parseUnits('0.00001', 18))
).toBeGreaterThan(capacityTiB)
})
})
})