UNPKG

filecoin-pin

Version:

Bridge IPFS content to Filecoin Onchain Cloud using familiar tools

329 lines 17.7 kB
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; let mockProvider; let mockSigner; 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); }); }); }); //# sourceMappingURL=payments-setup.test.js.map