Skip to content

Commit 70e3c17

Browse files
committed
feat: synapse integration
1 parent 1409490 commit 70e3c17

16 files changed

+766
-60
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@ node_modules/
22
boxo/
33
kubo/
44
dist/
5+
.vite/
6+
*.car
7+
test-output/
8+
test-car-output/
9+
test-e2e-cars/
10+
test-simple-car-output/

README.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ An IPFS Pinning Service API implementation that pins to Filecoin's PDP service,
66

77
Filecoin Pin is a TypeScript daemon that implements the [IPFS Pinning Service API](https://ipfs.github.io/pinning-services-api-spec/) to enable users to pin IPFS content to Filecoin using familiar IPFS tooling like Kubo's `ipfs pin remote` commands.
88

9+
### How It Works
10+
11+
1. **Serve Pin Service**: The daemon runs an HTTP server that implements the IPFS Pinning Service API
12+
2. **Receive Pin Requests**: When you run `ipfs pin remote add`, Kubo sends a pin request to the service
13+
3. **Fetch Blocks from IPFS**: The service connects to the IPFS network and fetches blocks for the requested CID (usually from the requesting node itself)
14+
4. **Store in CAR**: As blocks arrive, they're written directly to a CAR (Content Addressable aRchive) file on disk
15+
5. **Upload to PDP Provider**: Once all blocks are collected, the CAR file is uploaded to a Proof of Data Possession (PDP) service provider
16+
6. **Commit to Filecoin**: The PDP provider commits the data to the Filecoin blockchain
17+
7. **Start Proving**: The storage provider begins generating ongoing proofs that they still possess your data
18+
19+
This bridges the gap between IPFS's content-addressed storage and Filecoin's incentivized persistence layer, giving you the best of both worlds - easy pinning with long-term storage guarantees.
20+
921
**⚠️ Alpha Software**: This is currently alpha software, only deploying on Filecoin's Calibration Test network with storage providers participating in network testing, not dedicating long-term persistence.
1022

1123
You need a Filecoin calibration network wallet funded with USDFC. See the [USDFC documentation](https://docs.secured.finance/usdfc-stablecoin/getting-started) which has a "Testnet Resources" section for getting USDFC on calibnet.
@@ -48,8 +60,8 @@ Configuration is managed through environment variables. The service uses platfor
4860
#### Environment Variables
4961

5062
```bash
51-
# Required for Filecoin operations
52-
export PRIVATE_KEY="your-filecoin-private-key" # Ethereum private key (must be funded with USDFC)
63+
# REQUIRED - Without this, the service will not start
64+
export PRIVATE_KEY="your-filecoin-private-key" # Ethereum private key (must be funded with USDFC on calibration network)
5365

5466
# Optional configuration with defaults
5567
export PORT=3456 # API server port (default: 3456)
@@ -71,12 +83,14 @@ When `DATABASE_PATH` and `CAR_STORAGE_PATH` are not specified, the service uses
7183

7284
### Running the Daemon
7385

86+
⚠️ **PRIVATE_KEY is required** - The service will not start without it.
87+
7488
```bash
7589
# Start the daemon
76-
npm start
90+
PRIVATE_KEY=0x... npm start
7791

7892
# Or with custom configuration
79-
PORT=8080 npm start
93+
PRIVATE_KEY=0x... PORT=8080 RPC_URL=wss://... npm start
8094
```
8195

8296
### CLI Usage
@@ -115,9 +129,11 @@ ipfs pin remote ls --service=filecoin-pin --status=pinning,queued,pinned
115129
- `npm run build` - Compile TypeScript to JavaScript
116130
- `npm run dev` - Start development server with hot reload
117131
- `npm start` - Run compiled output
118-
- `npm test` - Run tests with type checking
132+
- `npm test` - Run linting, type checking, unit tests, and integration tests
133+
- `npm run test:unit` - Run unit tests only
134+
- `npm run test:integration` - Run integration tests only
119135
- `npm run test:watch` - Run tests in watch mode
120-
- `npm run lint` - Check code style
136+
- `npm run lint` - Check code style with ts-standard
121137
- `npm run lint:fix` - Auto-fix code style issues
122138
- `npm run typecheck` - Type check without emitting files
123139

package.json

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,31 @@
88
"bin": {
99
"filecoin-pin": "dist/cli.js"
1010
},
11+
"files": [
12+
"dist/**/*.js",
13+
"dist/**/*.d.ts",
14+
"dist/**/*.js.map",
15+
"dist/**/*.d.ts.map",
16+
"src/**/*.ts",
17+
"README.md",
18+
"LICENSE.md"
19+
],
1120
"scripts": {
1221
"build": "tsc",
13-
"dev": "tsx watch src/index.ts",
14-
"start": "node dist/index.js",
15-
"test": "npm run typecheck && npm run test:unit && npm run test:e2e",
16-
"test:unit": "vitest run test/unit",
17-
"test:e2e": "vitest run test/e2e",
22+
"dev": "tsx watch src/cli.ts daemon",
23+
"start": "node dist/cli.js daemon",
24+
"test": "npm run lint && npm run typecheck && npm run test:unit && npm run test:integration",
25+
"test:unit": "vitest run src/test/unit",
26+
"test:integration": "vitest run src/test/integration",
1827
"test:watch": "vitest",
1928
"lint": "ts-standard 'src/**/*.ts'",
2029
"lint:fix": "ts-standard 'src/**/*.ts' --fix",
21-
"typecheck": "tsc --noEmit"
30+
"typecheck": "tsc --noEmit",
31+
"prepublishOnly": "npm run build"
32+
},
33+
"repository": {
34+
"type": "git",
35+
"url": "git+https://github.com/FilOzone/filecoin-pin.git"
2236
},
2337
"keywords": [
2438
"ipfs",
@@ -29,10 +43,12 @@
2943
],
3044
"author": "Rod Vagg <[email protected]>",
3145
"license": "Apache-2.0 OR MIT",
32-
"engines": {
33-
"node": ">=24.0.0"
46+
"bugs": {
47+
"url": "https://github.com/FilOzone/filecoin-pin/issues"
3448
},
49+
"homepage": "https://github.com/FilOzone/filecoin-pin#readme",
3550
"dependencies": {
51+
"@filoz/synapse-sdk": "^0.20.0",
3652
"@ipld/car": "^5.4.2",
3753
"fastify": "^5.4.0",
3854
"helia": "^5.5.0",
@@ -47,5 +63,8 @@
4763
"tsx": "^4.20.3",
4864
"typescript": "^5.8.3",
4965
"vitest": "^3.2.4"
66+
},
67+
"publishConfig": {
68+
"access": "public"
5069
}
5170
}

src/cli.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,18 @@ Usage:
2323
filecoin-pin help Show this help message
2424
2525
Environment Variables:
26-
PORT API server port (default: 3456)
27-
HOST API server host (default: localhost)
28-
PRIVATE_KEY Ethereum private key for Filecoin transactions
26+
PRIVATE_KEY Private key for Filecoin transactions (required)
27+
PORT API server port (default: 3456)
28+
HOST API server host (default: localhost)
2929
RPC_URL Filecoin RPC endpoint (default: calibration testnet)
30-
DATABASE_PATH SQLite database location (default: ./pins.db)
31-
CAR_STORAGE_PATH Temporary CAR file directory (default: ./cars)
30+
DATABASE_PATH SQLite database location (default: {config}/pins.db)
31+
CAR_STORAGE_PATH Temporary CAR file directory (default: {config}/cars)
3232
LOG_LEVEL Log level (default: info)
33+
PANDORA_ADDRESS Override Pandora contract address (optional)
3334
34-
Example:
35-
filecoin-pin daemon
36-
PORT=8080 PRIVATE_KEY=0x... filecoin-pin daemon
35+
Examples:
36+
PRIVATE_KEY=0x... filecoin-pin daemon
37+
PORT=8080 PRIVATE_KEY=0x... RPC_URL=wss://... filecoin-pin daemon
3738
`)
3839
}
3940

src/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface Config {
99
databasePath: string
1010
carStoragePath: string
1111
logLevel: string
12+
pandoraAddress: string | undefined
1213
}
1314

1415
function getDataDirectory (): string {
@@ -44,6 +45,7 @@ export function createConfig (): Config {
4445
rpcUrl: process.env.RPC_URL ?? 'https://api.calibration.node.glif.io/rpc/v1',
4546
databasePath: process.env.DATABASE_PATH ?? join(dataDir, 'pins.db'),
4647
carStoragePath: process.env.CAR_STORAGE_PATH ?? join(dataDir, 'cars'),
47-
logLevel: process.env.LOG_LEVEL ?? 'info'
48+
logLevel: process.env.LOG_LEVEL ?? 'info',
49+
pandoraAddress: process.env.PANDORA_ADDRESS
4850
}
4951
}

src/filecoin-pin-store.ts

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { unlink } from 'node:fs/promises'
1+
import { unlink, readFile } from 'node:fs/promises'
22
import { EventEmitter } from 'node:events'
33
import { join } from 'node:path'
44
import { createPinningHeliaNode } from './create-pinning-helia.js'
@@ -7,6 +7,7 @@ import type { Config } from './config.js'
77
import type { Helia } from 'helia'
88
import type { CID } from 'multiformats/cid'
99
import type { Logger } from 'pino'
10+
import type { SynapseService } from './synapse-service.js'
1011

1112
export interface PinningServiceUser {
1213
id: string
@@ -37,6 +38,9 @@ export interface FilecoinPinMetadata {
3738
carStats: CARBlockstoreStats
3839
pinStarted: number
3940
pinCompleted?: number
41+
synapseCommP?: string
42+
synapseRootId?: number
43+
synapseProofSetId?: string
4044
}
4145

4246
export interface FilecoinStoredPinStatus extends StoredPinStatus {
@@ -46,6 +50,7 @@ export interface FilecoinStoredPinStatus extends StoredPinStatus {
4650
export interface FilecoinPinStoreInit {
4751
config: Config
4852
logger: Logger
53+
synapseService: SynapseService
4954
}
5055

5156
/**
@@ -54,6 +59,7 @@ export interface FilecoinPinStoreInit {
5459
export class FilecoinPinStore extends EventEmitter {
5560
private readonly config: Config
5661
private readonly logger: Logger
62+
private readonly synapseService: SynapseService
5763
private readonly pins = new Map<string, FilecoinStoredPinStatus>()
5864
private readonly activePins = new Map<string, {
5965
helia: Helia
@@ -67,6 +73,7 @@ export class FilecoinPinStore extends EventEmitter {
6773
super()
6874
this.config = init.config
6975
this.logger = init.logger
76+
this.synapseService = init.synapseService
7077
}
7178

7279
async start (): Promise<void> {
@@ -276,6 +283,86 @@ export class FilecoinPinStore extends EventEmitter {
276283
missingBlocks: finalStats.missingBlocks.size
277284
}, 'CAR file finalized')
278285

286+
// Store on Filecoin
287+
try {
288+
// Read the CAR file (streaming not yet supported in Synapse)
289+
const carData = await readFile(pinStatus.filecoin.carFilePath)
290+
291+
// Upload using Synapse
292+
const synapseResult = await this.synapseService.storage.upload(carData, {
293+
onUploadComplete: (commp) => {
294+
this.logger.info({
295+
event: 'synapse.upload.piece_uploaded',
296+
pinId,
297+
commp: commp.toString()
298+
}, 'Upload to PDP server complete')
299+
},
300+
onRootAdded: (transaction) => {
301+
if (transaction != null) {
302+
this.logger.info({
303+
event: 'synapse.upload.root_added',
304+
pinId,
305+
txHash: transaction.hash
306+
}, 'Root addition transaction submitted')
307+
} else {
308+
this.logger.info({
309+
event: 'synapse.upload.root_added',
310+
pinId
311+
}, 'Root added to proof set')
312+
}
313+
},
314+
onRootConfirmed: (rootIds) => {
315+
this.logger.info({
316+
event: 'synapse.upload.root_confirmed',
317+
pinId,
318+
rootIds
319+
}, 'Root addition confirmed on-chain')
320+
}
321+
})
322+
323+
// Store Synapse metadata
324+
pinStatus.filecoin.synapseCommP = synapseResult.commp.toString()
325+
if (synapseResult.rootId !== undefined) {
326+
pinStatus.filecoin.synapseRootId = synapseResult.rootId
327+
}
328+
pinStatus.filecoin.synapseProofSetId = this.synapseService.storage.proofSetId
329+
330+
// Add to info for API response
331+
pinStatus.info = {
332+
...pinStatus.info,
333+
synapse_commp: synapseResult.commp.toString(),
334+
synapse_root_id: (synapseResult.rootId ?? 0).toString(),
335+
synapse_proof_set_id: this.synapseService.storage.proofSetId
336+
}
337+
338+
this.logger.info({
339+
event: 'synapse.upload.success',
340+
pinId,
341+
commp: synapseResult.commp,
342+
rootId: synapseResult.rootId,
343+
proofSetId: this.synapseService.storage.proofSetId
344+
}, 'Successfully uploaded to Filecoin with Synapse')
345+
} catch (error) {
346+
// Rollback on Synapse failure
347+
this.logger.error({
348+
event: 'synapse.upload.failed',
349+
pinId,
350+
error
351+
}, 'Failed to upload to Filecoin with Synapse, rolling back')
352+
353+
// Clean up the CAR file
354+
try {
355+
await blockstore.cleanup()
356+
await unlink(pinStatus.filecoin.carFilePath)
357+
this.logger.info({ pinId, carFilePath: pinStatus.filecoin.carFilePath }, 'Deleted CAR file after Synapse failure')
358+
} catch (cleanupError) {
359+
this.logger.warn({ pinId, error: cleanupError }, 'Failed to clean up CAR file')
360+
}
361+
362+
// Re-throw to mark pin as failed
363+
throw new Error(`Synapse upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
364+
}
365+
279366
// Update pin status to completed
280367
pinStatus.status = 'pinned'
281368
pinStatus.filecoin.carStats = finalStats
@@ -299,13 +386,6 @@ export class FilecoinPinStore extends EventEmitter {
299386
carFilePath: pinStatus.filecoin.carFilePath
300387
})
301388

302-
// CAR file is ready for upload to Filecoin when needed
303-
this.logger.info({
304-
pinId,
305-
cid: cid.toString(),
306-
carFilePath: pinStatus.filecoin.carFilePath
307-
}, 'CAR file ready for Filecoin upload')
308-
309389
this.logger.info({ pinId, cid: cid.toString() }, 'Pin processing completed successfully')
310390
} catch (error) {
311391
this.logger.error({ pinId, cid: cid.toString(), error }, 'Pin processing failed')

src/filecoin-pinning-server.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Config } from './config.js'
44
import type { Logger } from 'pino'
55
import { CID } from 'multiformats/cid'
66
import type { ServiceInfo } from './index.js'
7+
import { initializeSynapse } from './synapse-service.js'
78

89
declare module 'fastify' {
910
interface FastifyRequest {
@@ -19,11 +20,15 @@ const DEFAULT_USER_INFO = {
1920
name: 'Default User'
2021
}
2122

22-
export async function createFilecoinPinningServer (config: Config, logger: Logger, serviceInfo: ServiceInfo): Promise<any> {
23-
// Create our custom Filecoin pin store
23+
export async function createFilecoinPinningServer (config: Config, logger: Logger, serviceInfo: ServiceInfo): Promise<{ server: FastifyInstance, pinStore: FilecoinPinStore }> {
24+
// Initialize Synapse
25+
const synapseService = await initializeSynapse(config, logger)
26+
27+
// Create our custom Filecoin pin store with Synapse service
2428
const filecoinPinStore = new FilecoinPinStore({
2529
config,
26-
logger
30+
logger,
31+
synapseService
2732
})
2833

2934
// Set up event handlers for monitoring
@@ -122,10 +127,7 @@ export async function createFilecoinPinningServer (config: Config, logger: Logge
122127
logger.info('Filecoin pinning service API server started')
123128

124129
return {
125-
server: {
126-
server: server.server, // Expose underlying HTTP server for address()
127-
close: async () => await server.close()
128-
},
130+
server,
129131
pinStore: filecoinPinStore
130132
}
131133
}

0 commit comments

Comments
 (0)