Skip to content

Commit 6839a9b

Browse files
authored
feat: add dual IPFS pinning with Filebase backup (#49)
1 parent b9325c6 commit 6839a9b

3 files changed

Lines changed: 121 additions & 1 deletion

File tree

dapp/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ PUBLIC_TANSU_OWNER_ID="GCILP4HWE2QGEO4KUMOZ6S6J3A46W47EVCGZW2YPYCPH5CQF6EACNBCN"
1010

1111
# IPFS upload flows
1212
PUBLIC_DELEGATION_API_URL="https://ipfs-testnet.tansu.dev"
13+
14+
# Dual IPFS pinning — Filebase backup (optional but recommended for redundancy)
15+
FILEBASE_API_KEY=""

dapp/src/service/FlowService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { calculateDirectoryCid } from "../utils/ipfsFunctions";
22
import { create } from "@storacha/client";
33
import * as Delegation from "@ucanto/core/delegation";
44
import type { OutcomeContract } from "../types/proposal";
5+
import { pinToFilebase } from "../utils/dualPin";
56

67
//
78
import Tansu from "../contracts/soroban_tansu";
@@ -113,7 +114,14 @@ export async function uploadWithDelegation({
113114
const directoryCid = await client.uploadDirectory(files);
114115
if (!directoryCid) throw new Error("Failed to upload to IPFS");
115116

116-
return directoryCid.toString();
117+
const cid = directoryCid.toString();
118+
119+
// Dual pin: backup to Filebase in background (fire-and-forget)
120+
pinToFilebase(cid).catch((err) => {
121+
console.warn("[DualPin] Filebase backup pin failed:", err.message);
122+
});
123+
124+
return cid;
117125
}
118126

119127
/**

dapp/src/utils/dualPin.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Dual IPFS Pinning Utility
3+
*
4+
* Uploads data to both Storacha (primary) and Filebase (backup) to ensure
5+
* redundancy. If one provider fails, the other still serves as a backup.
6+
*
7+
* Usage: wrap any upload call with `dualPin` to automatically pin to Filebase
8+
* after the primary Storacha upload succeeds.
9+
*/
10+
11+
const FILEBASE_API_URL = "https://api.filebase.io/v1/ipfs";
12+
13+
type PinResult = {
14+
cid: string;
15+
pinned: boolean;
16+
provider: string;
17+
error?: string;
18+
};
19+
20+
/**
21+
* Pin a CID to Filebase as backup.
22+
*
23+
* Requires FILEBASE_API_KEY env var to be set.
24+
* If the key is missing, this is a no-op (graceful degradation).
25+
*/
26+
export async function pinToFilebase(cid: string): Promise<PinResult> {
27+
const apiKey = import.meta.env.FILEBASE_API_KEY;
28+
if (!apiKey) {
29+
console.warn("[DualPin] FILEBASE_API_KEY not set, skipping Filebase pin");
30+
return { cid, pinned: false, provider: "filebase", error: "API key not configured" };
31+
}
32+
33+
try {
34+
const res = await fetch(`${FILEBASE_API_URL}/pins`, {
35+
method: "POST",
36+
headers: {
37+
Authorization: `Bearer ${apiKey}`,
38+
"Content-Type": "application/json",
39+
},
40+
body: JSON.stringify({
41+
cid,
42+
name: `tansu-backup-${cid.slice(0, 12)}`,
43+
}),
44+
signal: AbortSignal.timeout(15000),
45+
});
46+
47+
if (res.ok) {
48+
console.info(`[DualPin] Filebase pin succeeded for CID: ${cid}`);
49+
return { cid, pinned: true, provider: "filebase" };
50+
}
51+
52+
const text = await res.text();
53+
console.warn(`[DualPin] Filebase pin failed: ${res.status} ${text}`);
54+
return { cid, pinned: false, provider: "filebase", error: `HTTP ${res.status}` };
55+
} catch (err: any) {
56+
console.warn(`[DualPin] Filebase pin error: ${err.message}`);
57+
return { cid, pinned: false, provider: "filebase", error: err.message };
58+
}
59+
}
60+
61+
/**
62+
* Check if a CID is pinned on Filebase.
63+
*/
64+
export async function checkFilebasePinStatus(cid: string): Promise<boolean> {
65+
const apiKey = import.meta.env.FILEBASE_API_KEY;
66+
if (!apiKey) return false;
67+
68+
try {
69+
const res = await fetch(`${FILEBASE_API_URL}/pins/${cid}`, {
70+
headers: { Authorization: `Bearer ${apiKey}` },
71+
signal: AbortSignal.timeout(8000),
72+
});
73+
return res.ok;
74+
} catch {
75+
return false;
76+
}
77+
}
78+
79+
/**
80+
* Dual-pin wrapper: executes a primary upload function, then pins the
81+
* resulting CID to Filebase in the background.
82+
*
83+
* @param primaryUpload - The primary upload function (e.g., Storacha)
84+
* @returns The CID from the primary upload (unchanged)
85+
*/
86+
export async function dualPin(
87+
primaryUpload: () => Promise<string>,
88+
): Promise<string> {
89+
const cid = await primaryUpload();
90+
91+
// Fire-and-forget Filebase pin (don't block the user flow)
92+
pinToFilebase(cid).catch((err) => {
93+
console.warn("[DualPin] Background Filebase pin failed:", err.message);
94+
});
95+
96+
return cid;
97+
}
98+
99+
/**
100+
* Dual-pin with verification: waits for both uploads to complete.
101+
* Use this when you need guaranteed dual-pinning before proceeding.
102+
*/
103+
export async function dualPinVerified(
104+
primaryUpload: () => Promise<string>,
105+
): Promise<{ cid: string; filebasePinned: boolean }> {
106+
const cid = await primaryUpload();
107+
const filebaseResult = await pinToFilebase(cid);
108+
return { cid, filebasePinned: filebaseResult.pinned };
109+
}

0 commit comments

Comments
 (0)