diff --git a/README.md b/README.md index f52fdff8..1a7c64dc 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Use Filecoin Pin programmatically in your Node.js or browser applications. The l Run a localhost IPFS Pinning Service API server that implements the [IPFS Pinning Service API specification](https://ipfs.github.io/pinning-services-api-spec/). This allows you to use standard IPFS tooling (like `ipfs pin remote`) while storing data on Filecoin. - **Repository**: This repo (`filecoin-pin server` command in CLI) -- **Usage**: `PRIVATE_KEY=0x... npx filecoin-pin server` +- **Usage**: `PRIVATE_KEY=0x... npx filecoin-pin server` (or use session key auth — see [Configuration](#configuration)) - **Status**: Works and is tested, but hasn't received as many features as the CLI. If it would benefit your usecase, please comment on [tracking issue](https://github.com/filecoin-project/filecoin-pin/issues/46) so we can be better informed when it comes to prioritizing. ### Management Console GUI @@ -139,7 +139,11 @@ npm install -g filecoin-pin ### Basic Usage ```bash -# 0. Set your PRIVATE_KEY environment variable or pass it via --private-key to each command. +# 0. Set up authentication (choose one): +# Private key: export PRIVATE_KEY=0x... +# (or pass --private-key to each command) +# Session key: export WALLET_ADDRESS=0x... SESSION_KEY=0x... +# (or pass --wallet-address --session-key to each command) # 1. Configure payment permissions (one-time setup) filecoin-pin payments setup --auto @@ -221,7 +225,9 @@ When using `--network devnet`, Filecoin Pin reads connection details from a runn * `-h`, `--help`: Display help information for each command * `-V`, `--version`: Output the version number * `-v`, `--verbose`: Verbose output -* `--private-key`: Ethereum-style (`0x`) private key, funded with USDFC (required) +* `--private-key`: Ethereum-style (`0x`) private key (wallet and signer), funded with USDFC +* `--wallet-address`: Session key mode: owner wallet address +* `--session-key`: Session key mode: scoped signing key registered to the wallet * `--network`: Filecoin network to use: `mainnet`, `calibration`, or `devnet` (default: `calibration`) * `--rpc-url`: Filecoin RPC endpoint (overrides `--network` if specified) diff --git a/src/commands/server.ts b/src/commands/server.ts index 338a6523..38127ef2 100644 --- a/src/commands/server.ts +++ b/src/commands/server.ts @@ -9,6 +9,8 @@ export const serverCommand = new Command('server') .option('--car-storage ', 'path for CAR file storage', './cars') .option('--database ', 'path to SQLite database', './pins.db') .option('--private-key ', 'private key for Synapse (env: PRIVATE_KEY)') + .option('--wallet-address
', 'wallet address for session key auth (env: WALLET_ADDRESS)') + .option('--session-key ', 'session key for session key auth (env: SESSION_KEY)') .option('--access-token ', 'bearer token required on all API requests except GET / (env: ACCESS_TOKEN)') addNetworkOptions(serverCommand) @@ -21,6 +23,12 @@ addNetworkOptions(serverCommand) if (options.privateKey) { process.env.PRIVATE_KEY = options.privateKey } + if (options.walletAddress) { + process.env.WALLET_ADDRESS = options.walletAddress + } + if (options.sessionKey) { + process.env.SESSION_KEY = options.sessionKey + } if (options.accessToken) { process.env.ACCESS_TOKEN = options.accessToken } diff --git a/src/config.ts b/src/config.ts index 948aed3e..5fcd85d2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,10 +29,13 @@ function getDataDirectory(): string { /** * Create configuration from environment variables * - * This demonstrates configuration best practices for Synapse SDK: - * - PRIVATE_KEY: Required for transaction signing (keep secure!) - * - RPC_URL: Filecoin network endpoint (mainnet or calibration) - takes precedence over NETWORK - * - NETWORK: Filecoin network name (mainnet or calibration) - used if RPC_URL not set + * Authentication (choose one): + * - PRIVATE_KEY: Standard private key auth + * - WALLET_ADDRESS + SESSION_KEY: Session key auth + * + * Network: + * - RPC_URL: Filecoin RPC endpoint — takes precedence over NETWORK + * - NETWORK: Filecoin network name (mainnet, calibration, devnet) — used if RPC_URL not set */ export function createConfig(): Config { const dataDir = getDataDirectory() @@ -50,7 +53,9 @@ export function createConfig(): Config { accessToken: process.env.ACCESS_TOKEN, // Synapse SDK configuration - privateKey: process.env.PRIVATE_KEY, // Required: Ethereum-compatible private key + privateKey: process.env.PRIVATE_KEY, + walletAddress: process.env.WALLET_ADDRESS, + sessionKey: process.env.SESSION_KEY, rpcUrl, // Determined from RPC_URL, NETWORK, or default to calibration // Storage paths databasePath: process.env.DATABASE_PATH ?? join(dataDir, 'pins.db'), diff --git a/src/core/payments/index.ts b/src/core/payments/index.ts index 120060c1..18d5eeef 100644 --- a/src/core/payments/index.ts +++ b/src/core/payments/index.ts @@ -16,7 +16,7 @@ */ import { calibration, SIZE_CONSTANTS, type Synapse, TIME_CONSTANTS, TOKENS } from '@filoz/synapse-sdk' -import { formatUnits } from 'viem' +import { formatUnits, type Hash } from 'viem' import { getClientAddress, isSessionKeyMode } from '../synapse/index.js' import { assertPriceNonZero } from '../utils/validate-pricing.js' import { @@ -289,7 +289,7 @@ export async function depositUSDFC( const amountMoreThanCurrentAllowance = (await synapse.payments.allowance({ spender: synapse.chain.contracts.filecoinPay.address })) < amount - let txHash: `0x${string}` + let txHash: Hash if (amountMoreThanCurrentAllowance || needsAllowanceUpdate) { txHash = await synapse.payments.depositWithPermitAndApproveOperator({ diff --git a/src/core/piece/remove-piece.ts b/src/core/piece/remove-piece.ts index e0dfa201..8425b40e 100644 --- a/src/core/piece/remove-piece.ts +++ b/src/core/piece/remove-piece.ts @@ -1,6 +1,7 @@ import type { Synapse } from '@filoz/synapse-sdk' import type { StorageContext } from '@filoz/synapse-sdk/storage' import type { Logger } from 'pino' +import type { Hash } from 'viem' import { getErrorMessage } from '../utils/errors.js' import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js' @@ -18,13 +19,13 @@ import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js' */ export type RemovePieceProgressEvents = | ProgressEvent<'remove-piece:submitting', { pieceCid: string; dataSetId: bigint }> - | ProgressEvent<'remove-piece:submitted', { pieceCid: string; dataSetId: bigint; txHash: `0x${string}` }> - | ProgressEvent<'remove-piece:confirming', { pieceCid: string; dataSetId: bigint; txHash: `0x${string}` }> + | ProgressEvent<'remove-piece:submitted', { pieceCid: string; dataSetId: bigint; txHash: Hash }> + | ProgressEvent<'remove-piece:confirming', { pieceCid: string; dataSetId: bigint; txHash: Hash }> | ProgressEvent< 'remove-piece:confirmation-failed', - { pieceCid: string; dataSetId: bigint; txHash: `0x${string}`; message: string } + { pieceCid: string; dataSetId: bigint; txHash: Hash; message: string } > - | ProgressEvent<'remove-piece:complete', { txHash: `0x${string}`; confirmed: boolean }> + | ProgressEvent<'remove-piece:complete', { txHash: Hash; confirmed: boolean }> /** * Number of block confirmations to wait for when waitForConfirmation=true @@ -79,7 +80,7 @@ export async function removePiece( pieceCid: string, storageContext: StorageContext, options: RemovePieceOptions -): Promise<`0x${string}`> { +): Promise { const { onProgress, waitForConfirmation } = options const dataSetId = storageContext.dataSetId diff --git a/src/core/synapse/index.ts b/src/core/synapse/index.ts index 8bf2e1f8..4e86d5fa 100644 --- a/src/core/synapse/index.ts +++ b/src/core/synapse/index.ts @@ -17,11 +17,22 @@ import { AddPiecesPermission, CreateDataSetPermission, DefaultFwssPermissions, + DeleteDataSetPermission, fromSecp256k1, SchedulePieceRemovalsPermission, } from '@filoz/synapse-core/session-key' import type { Logger } from 'pino' -import { type Account, custom, getAddress, type HttpTransport, http, type WebSocketTransport, webSocket } from 'viem' +import { + type Account, + type Address, + custom, + getAddress, + type Hex, + type HttpTransport, + http, + type WebSocketTransport, + webSocket, +} from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { APPLICATION_SOURCE } from './constants.js' @@ -36,6 +47,8 @@ export interface Config { port: number host: string privateKey: string | undefined + walletAddress: string | undefined + sessionKey: string | undefined accessToken: string | undefined rpcUrl: string databasePath: string @@ -61,22 +74,22 @@ interface BaseSynapseConfig { * Standard authentication with private key */ export interface PrivateKeyConfig extends BaseSynapseConfig { - privateKey: `0x${string}` + privateKey: Hex } /** * Session key authentication with owner address and session key private key */ export interface SessionKeyConfig extends BaseSynapseConfig { - walletAddress: `0x${string}` - sessionKey: `0x${string}` + walletAddress: Address + sessionKey: Hex } /** * Read-only mode using an address (cannot sign transactions) */ export interface ReadOnlyConfig extends BaseSynapseConfig { - walletAddress: `0x${string}` + walletAddress: Address readOnly: true } @@ -125,6 +138,7 @@ function createTransport(rpcUrl: string): HttpTransport | WebSocketTransport { const PERMISSION_NAMES: Record = { [CreateDataSetPermission]: 'CreateDataSet', + [DeleteDataSetPermission]: 'DeleteDataSet', [AddPiecesPermission]: 'AddPieces', [SchedulePieceRemovalsPermission]: 'SchedulePieceRemovals', } @@ -165,7 +179,7 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log const rpcUrl = config.rpcUrl ?? chain.rpcUrls.default.webSocket?.[0] ?? chain.rpcUrls.default.http[0] const transport = rpcUrl ? createTransport(rpcUrl) : undefined - let account: Account | `0x${string}` + let account: Account | Address let sessionKey: SessionKey<'Secp256k1'> | undefined if (isReadOnlyConfig(config)) { @@ -190,6 +204,20 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log account = config.account logger?.info({ event: 'synapse.init', mode: 'account' }, 'Initializing Synapse (pre-created account)') } else { + const hasWallet = 'walletAddress' in config && config.walletAddress != null + const hasSessionKey = 'sessionKey' in config && config.sessionKey != null + if (hasWallet && !hasSessionKey) { + throw new Error( + 'Session key authentication requires both --wallet-address and --session-key. ' + + 'Missing: --session-key / SESSION_KEY.' + ) + } + if (hasSessionKey && !hasWallet) { + throw new Error( + 'Session key authentication requires both --wallet-address and --session-key. ' + + 'Missing: --wallet-address / WALLET_ADDRESS.' + ) + } throw new Error( 'No authentication provided. Supply a private key (--private-key / PRIVATE_KEY), ' + 'wallet address (--wallet-address / WALLET_ADDRESS), or session key (--session-key / SESSION_KEY).' @@ -233,9 +261,9 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log * Handles both string addresses (read-only / session key mode) and * full Account objects (private key mode). */ -export function getClientAddress(synapse: Synapse): `0x${string}` { +export function getClientAddress(synapse: Synapse): Address { const account = synapse.client.account - return (typeof account === 'string' ? account : account.address) as `0x${string}` + return (typeof account === 'string' ? account : account.address) as Address } /** diff --git a/src/core/upload/synapse.ts b/src/core/upload/synapse.ts index 6df74ac5..6da2c393 100644 --- a/src/core/upload/synapse.ts +++ b/src/core/upload/synapse.ts @@ -10,12 +10,13 @@ import { METADATA_KEYS, type PDPProvider } from '@filoz/synapse-sdk' import type { StorageContext, StorageManagerUploadOptions } from '@filoz/synapse-sdk/storage' import type { CID } from 'multiformats/cid' import type { Logger } from 'pino' +import type { Hash } from 'viem' import { APPLICATION_SOURCE } from '../synapse/constants.js' import type { ProgressEvent, ProgressEventHandler } from '../utils/types.js' export type UploadProgressEvents = | ProgressEvent<'onStored', { providerId: bigint; pieceCid: PieceCID }> - | ProgressEvent<'onPiecesAdded', { txHash: `0x${string}`; providerId: bigint }> + | ProgressEvent<'onPiecesAdded', { txHash: Hash; providerId: bigint }> | ProgressEvent<'onPiecesConfirmed', { dataSetId: bigint; providerId: bigint; pieceIds: bigint[] }> | ProgressEvent<'onCopyComplete', { providerId: bigint; pieceCid: PieceCID }> | ProgressEvent<'onCopyFailed', { providerId: bigint; pieceCid: PieceCID; error: Error }> diff --git a/src/filecoin-pinning-server.ts b/src/filecoin-pinning-server.ts index bcc8955b..943c6f08 100644 --- a/src/filecoin-pinning-server.ts +++ b/src/filecoin-pinning-server.ts @@ -1,8 +1,9 @@ import fastify, { type FastifyInstance, type FastifyRequest } from 'fastify' import { CID } from 'multiformats/cid' import type { Logger } from 'pino' +import { isAddress, isHex } from 'viem' import type { Config } from './core/synapse/index.js' -import { initializeSynapse, type PrivateKeyConfig } from './core/synapse/index.js' +import { initializeSynapse, type SynapseSetupConfig } from './core/synapse/index.js' import { FilecoinPinStore, type PinOptions } from './filecoin-pin-store.js' import type { ServiceInfo } from './server.js' @@ -20,20 +21,49 @@ const DEFAULT_USER_INFO = { name: 'Default User', } +function buildSynapseConfig(config: Config): SynapseSetupConfig { + const base = { rpcUrl: config.rpcUrl } + + if (config.walletAddress && config.sessionKey) { + if (!isAddress(config.walletAddress)) { + throw new Error('Wallet address must be an ethereum address') + } + if (!isHex(config.sessionKey)) { + throw new Error('Session key must be 0x-prefixed hexadecimal') + } + if (config.sessionKey.length !== 66) { + throw new Error('Session key must be 32 bytes') + } + return { + ...base, + walletAddress: config.walletAddress, + sessionKey: config.sessionKey, + } + } + + if (config.privateKey) { + if (!isHex(config.privateKey)) { + throw new Error('Private key must be 0x-prefixed hexadecimal') + } + if (config.privateKey.length !== 66) { + throw new Error('Private key must be 32 bytes') + } + return { ...base, privateKey: config.privateKey } + } + + throw new Error( + 'No authentication configured. Provide a private key (--private-key / PRIVATE_KEY) ' + + 'or session key (--wallet-address + --session-key / WALLET_ADDRESS + SESSION_KEY).' + ) +} + export async function createFilecoinPinningServer( config: Config, logger: Logger, serviceInfo: ServiceInfo ): Promise<{ server: FastifyInstance; pinStore: FilecoinPinStore }> { // Set up Synapse service - if (!config.privateKey) { - throw new Error('PRIVATE_KEY environment variable is required to start the pinning server') - } - - const synapseConfig: PrivateKeyConfig = { - privateKey: config.privateKey as `0x${string}`, - rpcUrl: config.rpcUrl, - } + const synapseConfig = buildSynapseConfig(config) const synapse = await initializeSynapse(synapseConfig, logger) const filecoinPinStore = new FilecoinPinStore({ diff --git a/src/payments/interactive.ts b/src/payments/interactive.ts index f23ed8e9..e158b90c 100644 --- a/src/payments/interactive.ts +++ b/src/payments/interactive.ts @@ -9,7 +9,7 @@ import { cancel, confirm, isCancel, password, text } from '@clack/prompts' import { calibration, mainnet } from '@filoz/synapse-sdk' import pc from 'picocolors' -import { parseUnits } from 'viem' +import { type Hex, parseUnits } from 'viem' import { calculateDepositCapacity, checkAndSetAllowances, @@ -84,7 +84,7 @@ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise const rpcUrl = options.rpcUrl || defaultRpcUrl const config: PrivateKeyConfig = { - privateKey: privateKey as `0x${string}`, + privateKey: privateKey as Hex, } if (rpcUrl) { config.rpcUrl = rpcUrl diff --git a/src/server.ts b/src/server.ts index 039d5311..3e041cca 100644 --- a/src/server.ts +++ b/src/server.ts @@ -60,10 +60,11 @@ export async function startServer(): Promise { ) // Also print a user-friendly message to stderr for clarity - if (errorMessage.includes('PRIVATE_KEY')) { - console.error('\n❌ Error: PRIVATE_KEY environment variable is required') - console.error(' Please set your private key: export PRIVATE_KEY=0x...') - console.error(' Or run with: PRIVATE_KEY=0x... filecoin-pin server\n') + if (errorMessage.includes('No authentication')) { + console.error('\n❌ Error: Authentication is required to start the pinning server') + console.error(' Private key: --private-key or PRIVATE_KEY=0x...') + console.error(' Session key: --wallet-address --session-key ') + console.error(' or WALLET_ADDRESS=0x... SESSION_KEY=0x...\n') } process.exit(1) diff --git a/src/test/mocks/synapse-core-session-key.ts b/src/test/mocks/synapse-core-session-key.ts new file mode 100644 index 00000000..eeba5823 --- /dev/null +++ b/src/test/mocks/synapse-core-session-key.ts @@ -0,0 +1,29 @@ +import { type Mock, vi } from 'vitest' + +/** + * Mock implementation of @filoz/synapse-core/session-key for testing + * + * Exports real permission constants (hardcoded to avoid circular mock imports) + * and overrides fromSecp256k1 so tests never hit the real network. + */ + +export const CreateDataSetPermission = '0x25ebf20299107c91b4624d5bac3a16d32cabf0db23b450ee09ab7732983b1dc9' +export const DeleteDataSetPermission = '0xb5d6b3fc97881f05e96958136ac09d7e0bc7cbf17ea92fce7c431d88132d2b58' +export const AddPiecesPermission = '0x954bdc254591a7eab1b73f03842464d9283a08352772737094d710a4428fd183' +export const SchedulePieceRemovalsPermission = '0x5415701e313bb627e755b16924727217bb356574fe20e7061442c200b0822b22' + +export const DefaultFwssPermissions = [ + CreateDataSetPermission, + DeleteDataSetPermission, + AddPiecesPermission, + SchedulePieceRemovalsPermission, +] + +const mockExpirations = Object.fromEntries(DefaultFwssPermissions.map((p) => [p, 0n])) + +export const fromSecp256k1: Mock = vi.fn(() => ({ + syncExpirations: vi.fn().mockResolvedValue(undefined), + hasPermission: vi.fn().mockReturnValue(true), + expirations: mockExpirations, + address: '0x0000000000000000000000000000000000000001', +})) diff --git a/src/test/unit/filecoin-pinning-server.unit.test.ts b/src/test/unit/filecoin-pinning-server.unit.test.ts new file mode 100644 index 00000000..1ae1d4e5 --- /dev/null +++ b/src/test/unit/filecoin-pinning-server.unit.test.ts @@ -0,0 +1,182 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createConfig } from '../../config.js' +import { createFilecoinPinningServer } from '../../filecoin-pinning-server.js' +import { createLogger } from '../../logger.js' + +vi.mock('@filoz/synapse-sdk', async (importOriginal) => { + const actual = await importOriginal() + const mockModule = await import('../mocks/synapse-sdk.js') + return { ...mockModule, SIZE_CONSTANTS: actual.SIZE_CONSTANTS } +}) + +vi.mock('@filoz/synapse-core/session-key', async () => await import('../mocks/synapse-core-session-key.js')) + +const SERVICE_INFO = { service: 'filecoin-pin', version: '0.1.0' } +const TEST_OUTPUT_DIR = './test-unit-pinning-server-cars' + +describe('createFilecoinPinningServer auth selection', () => { + let server: any + let pinStore: any + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(async () => { + if (server != null) { + await server.close() + server = undefined + } + if (pinStore != null) { + await pinStore.stop() + pinStore = undefined + } + }) + + it('should start successfully with session key auth (walletAddress + sessionKey)', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', + } + const logger = createLogger(config) + + const result = await createFilecoinPinningServer(config, logger, SERVICE_INFO) + server = result.server + pinStore = result.pinStore + + expect(server).toBeDefined() + expect(pinStore).toBeDefined() + }) + + it('should throw "No authentication configured" when neither privateKey nor sessionKey provided', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'No authentication configured' + ) + }) + + it('should include --private-key and --session-key hints in the no-auth error message', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + /--private-key.*PRIVATE_KEY|--wallet-address.*--session-key/ + ) + }) + + it('should throw when walletAddress is set but sessionKey is missing', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'No authentication configured' + ) + }) + + it('should throw when walletAddress is not a valid ethereum address', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: 'not-an-address', + sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Wallet address must be an ethereum address' + ) + }) + + it('should throw when sessionKey is not 0x-prefixed hex', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: 'not-hex-at-all', + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Session key must be 0x-prefixed hexadecimal' + ) + }) + + it('should throw when sessionKey is valid hex but not 32 bytes', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: undefined, + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: '0x1234', + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Session key must be 32 bytes' + ) + }) + + it('should throw when privateKey is not valid hex', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: 'not-a-hex-key', + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Private key must be 0x-prefixed hexadecimal' + ) + }) + + it('should throw when privateKey is valid hex but not 32 bytes', async () => { + const config = { + ...createConfig(), + carStoragePath: TEST_OUTPUT_DIR, + port: 0, + privateKey: '0x1234', + walletAddress: undefined, + sessionKey: undefined, + } + const logger = createLogger(config) + + await expect(createFilecoinPinningServer(config, logger, SERVICE_INFO)).rejects.toThrow( + 'Private key must be 32 bytes' + ) + }) +}) diff --git a/src/test/unit/remove-piece.test.ts b/src/test/unit/remove-piece.test.ts index 52ef167d..530c536b 100644 --- a/src/test/unit/remove-piece.test.ts +++ b/src/test/unit/remove-piece.test.ts @@ -1,9 +1,10 @@ +import type { Hash } from 'viem' import { beforeEach, describe, expect, it, vi } from 'vitest' import { removePiece } from '../../core/piece/index.js' const { mockDeletePiece, mockWaitForTransactionReceipt, mockSynapse, storageContext, state } = vi.hoisted(() => { const state = { - txHash: '0xtest-hash' as `0x${string}`, + txHash: '0x7e27000000000000000000000000000000000000000000000000000000000000' as Hash, dataSetId: 99n, } @@ -26,7 +27,7 @@ const { mockDeletePiece, mockWaitForTransactionReceipt, mockSynapse, storageCont describe('removePiece', () => { beforeEach(() => { vi.clearAllMocks() - state.txHash = '0xtest-hash' + state.txHash = '0x7e27000000000000000000000000000000000000000000000000000000000000' state.dataSetId = 99n storageContext.dataSetId = state.dataSetId }) diff --git a/src/test/unit/synapse-service.test.ts b/src/test/unit/synapse-service.test.ts index 0b25164c..81d5a4bb 100644 --- a/src/test/unit/synapse-service.test.ts +++ b/src/test/unit/synapse-service.test.ts @@ -9,6 +9,9 @@ import { MockSynapse } from '../mocks/synapse-mocks.js' // Mock the Synapse SDK vi.mock('@filoz/synapse-sdk', async () => await import('../mocks/synapse-sdk.js')) +// Mock the session key module so tests never hit the real network +vi.mock('@filoz/synapse-core/session-key', async () => await import('../mocks/synapse-core-session-key.js')) + // Test CID for upload tests const TEST_CID = CID.parse('bafkreia5fn4rmshmb7cl7fufkpcw733b5anhuhydtqstnglpkzosqln5kq') @@ -71,6 +74,44 @@ describe('synapse-service', () => { 'Initializing Synapse (read-only)' ) }) + + it('should initialize Synapse in session-key mode', async () => { + const config: SynapseSetupConfig = { + walletAddress: '0x0000000000000000000000000000000000000002', + sessionKey: '0x0000000000000000000000000000000000000000000000000000000000000001', + rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1', + } + + const infoSpy = vi.spyOn(logger, 'info') + const synapse = await initializeSynapse(config, logger) + + expect(synapse).toBeDefined() + expect(infoSpy).toHaveBeenCalledWith( + expect.objectContaining({ event: 'synapse.init', mode: 'session-key' }), + 'Initializing Synapse (session key)' + ) + }) + + it('should throw when no authentication is provided', async () => { + // AccountConfig with null account satisfies the type but triggers the no-auth branch + const config = { rpcUrl: 'wss://wss.calibration.node.glif.io/apigw/lotus/rpc/v1' } as any + + await expect(initializeSynapse(config, logger)).rejects.toThrow('No authentication provided') + }) + + it('should throw when walletAddress is provided without sessionKey', async () => { + const config = { walletAddress: '0x1234567890123456789012345678901234567890' } as any + + await expect(initializeSynapse(config, logger)).rejects.toThrow('Missing: --session-key / SESSION_KEY') + }) + + it('should throw when sessionKey is provided without walletAddress', async () => { + const config = { + sessionKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + } as any + + await expect(initializeSynapse(config, logger)).rejects.toThrow('Missing: --wallet-address / WALLET_ADDRESS') + }) }) describe('uploadToSynapse', () => {