InterPlanetary CID Mapping
IPCM is a smart contract that points to an IPFS CID which can be updated onchain, providing a simple solution to dynamic offchain data using IPFS
IPFS has been the go-to decentralized network for sharing content and websites. However it's greatest strength, immutability, is also it's greatest weakness. There are cases where you might need to update content on IPFS, but that would result in a new CID. IPCM (InterPlanetary CID Mapping) aims to solve this by storing a simple IPFS string onchain and letting the contract be the point of reference for the latest state. Benefits include:
- Cryptographically secures ownership and updates to an onchain identity
- Events create an onchain history of updates
- Can be deployed to any EVM chain
- Fetching the latest state is simple and fast
A simple example IPCM is for static webhosting on IPFS, where you can update the IPFS reference onchain without having to deal with the compliexities of IPNS. Check out the more detailed example below
sequenceDiagram
participant User
participant IPFS
participant IPCM
participant Reader
User->>IPFS: Upload siteA.html
IPFS-->>User: Return CID1
User->>IPCM: Set state to ipfs://CID1
User->>IPFS: Upload siteB.html
IPFS-->>User: Return CID2
User->>IPCM: Update state to ipfs://CID2
Reader->>IPCM: Query latest state
IPCM-->>Reader: Return ipfs://CID2
Reader->>IPFS: Fetch CID2
IPFS-->>Reader: Return html2.html content
Make sure you have Foundry already installed on your machine, more details here
Clone the GitHub repo and install dependencies
git clone https://github.com/PinataCloud/ipcm
cd ipcm
forge install
Deploy the contract in one command
forge create src/IPCM.sol:IPCM \
--rpc-url <YOUR_CHAIN_RPC_URL> \
--private-key <ACCOUNT_PRIVATE_KEY>
Tip
It is preferable to setup an account with cast or another secure method instead of using the private key in plain text. Please practice safe key management!
Optional: Verify the contract after deployment
forge v <CONTRACT_ADDRESS> \
src/IPCM.sol:IPCM \
-e <ETHERSCAN_API_KEY> \
--rpc-url <YOUR_CHAIN_RPC_URL>
After IPCM is deployed you can update the CID mapping using the updateMapping
function and the library of your choice. Here is an example with Viem
import { account, publicClient, walletClient } from "./config";
import { abi } from "./ipcm_abi";
const { request } = await publicClient.simulateContract({
account,
address: "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2",
abi: abi,
functionName: "updateMapping", // Function to Update
args: ["ipfs://bafkreigfakpjywuxaq57zhnnma6ntvs6u5p6eurgdl5kfkjvuhg6sztmda"], // IPFS CID args
});
await walletClient.writeContract(request);
Once a CID has been written to the contract, it can be fetched using the getMapping
function.
import { publicClient } from "./client";
import { abi } from "./ipcm_abi";
const data = await publicClient.readContract({
address: "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2",
abi: abi,
functionName: "getMapping",
});
// ipfs://bafkreigfakpjywuxaq57zhnnma6ntvs6u5p6eurgdl5kfkjvuhg6sztmda
With the CID reference you can render it into content using a tool like the IPFS SDK
import { publicClient } from "./client";
import { abi } from "./ipcm_abi";
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const cidMapping = await publicClient.readContract({
address: "0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2",
abi: abi,
functionName: "getMapping",
});
const { data, contentType } = await pinata.gateways.get(cidMapping);
The IPCM.dev website is an example of how IPCM can be used for static webhosting. It consists of a simple Cloudflare Worker that fetches the contract state each time a request is made and renders the content through an IPFS Gateway, creating a static proxy which only requires updating the onchian reference to update the site.
import { createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";
import { abi } from "../utils/contract";
import { pinata } from "../utils/pinata";
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
try {
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
});
const cid = await publicClient.readContract({
address: "0x81FBD1886121f8575734808959DF808D19De170D",
abi: abi,
functionName: "getMapping",
});
if (!cid) {
throw new Error(`Failed to fetch latest state: ${cid}`);
}
const url = await pinata.gateways.convert(cid as string);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch from CDN: ${response.statusText}`);
}
const contentType = response.headers.get("content-type");
return new Response(response.body, {
status: 200,
headers: {
"Content-Type": contentType || "text/html",
"Cache-Control": "public, max-age=3600",
},
});
} catch (error) {
console.error("Error:", error);
return new Response(`Error: ${error}`, {
status: 500,
headers: { "Content-Type": "text/plain" },
});
}
},
} satisfies ExportedHandler<Env>;
IPCM is MIT licensed and open source; feedback and contributions are welcome! Please contact [email protected] with any questions you might have.