chunktech
v0.3.0
Published
On-chain file storage via chunked transactions for EVM chains
Maintainers
Readme
ChunkTech
On-chain file storage via chunked transactions for EVM chains.
Store and retrieve files on Ethereum, Base, and Arbitrum using transaction calldata. Includes specialized support for censorship-resistant browser extension distribution.
Features
- Chunk - Split files into 33.3KB pieces for on-chain storage
- Send - Broadcast chunks as self-transfer transactions
- Track - Monitor transaction confirmations
- Reassemble - Reconstruct files from on-chain data
- Encrypt - Optional X3DH encryption for private data
- Cross-Chain - Store data on L2, pointer on mainnet
- Extensions - Distribute Chrome/Firefox extensions via inscriptions
Installation
npm install chunktech viemFor encryption support:
npm install @noble/curves @noble/hashesQuick Start
import { ChunkTech } from 'chunktech';
import { createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const ct = new ChunkTech({ walletClient });
// Upload
const result = await ct.upload(fileData, {
onProgress: (sent, total) => console.log(`${sent}/${total}`),
});
// Download
const downloaded = await ct.download(result.txHashes);Browser Extension Distribution
Distribute Chrome and Firefox extensions as on-chain inscriptions. The inscription itself is the installer - a self-contained HTML page that fetches extension data from L2 and offers downloads.
┌─────────────────────────────────────────────────────────┐
│ Inscription (Ethereum) │
│ │
│ Self-contained HTML that: │
│ ├── Shows extension info + download buttons │
│ ├── Fetches chunks from Base via RPC │
│ ├── Reassembles + verifies SHA256 │
│ └── Downloads as .zip (Chrome) or .xpi (Firefox) │
│ │
└─────────────────────────────────────────────────────────┘Upload Extension
import { ExtensionUploader } from 'chunktech';
import { createWalletClient, http } from 'viem';
import { base, mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import { readFileSync } from 'fs';
const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
const uploader = new ExtensionUploader({
keyChain: 'ethereum', // Inscription lives here (1 tx)
dataChain: 'base', // Extension data lives here (cheap)
keyWalletClient: createWalletClient({
account,
chain: mainnet,
transport: http(),
}),
dataWalletClient: createWalletClient({
account,
chain: base,
transport: http(),
}),
});
const result = await uploader.upload({
name: 'My Extension',
version: '1.0.0',
developer: 'vitalik.eth',
description: 'A censorship-resistant browser extension',
homepage: 'https://myextension.xyz',
chrome: readFileSync('dist/chrome.zip'),
firefox: readFileSync('dist/firefox.xpi'),
}, {
onProgress: (phase, sent, total) => {
console.log(`${phase}: ${sent}/${total}`);
},
});
console.log('Inscription TX:', result.inscriptionTxHash);
// View at: https://ethscriptions.com/ethscriptions/0x...What Users See
The inscription renders as a download page:
╔═══════════════════════════════════════════════════╗
║ My Extension v1.0.0 ║
║ vitalik.eth ║
╚═══════════════════════════════════════════════════╝
A censorship-resistant browser extension
┌─────────────────┐ ┌─────────────────┐
│ Chrome/Brave │ │ Firefox │
│ [Detected] │ │ │
│ [Download] │ │ [Download] │
│ 142 KB │ │ 138 KB │
└─────────────────┘ └─────────────────┘
Installation Instructions:
1. Download the extension
2. Unzip (Chrome) or keep as .xpi (Firefox)
3. Load in developer mode / Install from fileWhy This Matters
- Censorship-resistant - No app store can remove it
- Immutable - Code is permanently on-chain
- Verifiable - SHA256 verified on download
- Self-contained - The inscription IS the installer
- Multi-browser - Chrome, Brave, Edge, Firefox from one inscription
Cross-Chain Upload
Store bulk data on L2 (cheap), pointer on mainnet (durable):
import { CrossChainUploader } from 'chunktech';
const uploader = new CrossChainUploader({
keyChain: 'ethereum',
dataChain: 'base',
keyWalletClient,
dataWalletClient,
});
const result = await uploader.upload(fileData, {
format: 'html',
title: 'My File',
});
// result.keyTxHash = mainnet inscription (self-loading HTML)
// result.dataTxHashes = Base data chunksAPI Reference
ChunkTech
Main class for single-chain uploads.
const ct = new ChunkTech({
walletClient: WalletClient, // viem wallet
publicClient?: PublicClient, // Optional
chain?: Chain, // Auto-detected
rpcUrl?: string, // Custom RPC
});
// Upload
const result = await ct.upload(data, {
encrypt?: boolean,
keys?: EncryptionKeys,
recipients?: Recipient[],
onProgress?: (sent, total) => void,
confirmations?: number,
});
// Download
const result = await ct.download(txHashes, {
keys?: EncryptionKeys,
recipientId?: string,
});ExtensionUploader
Specialized uploader for browser extensions.
const uploader = new ExtensionUploader({
keyChain: 'ethereum' | 'sepolia',
dataChain: 'base' | 'baseSepolia',
keyWalletClient: WalletClient,
dataWalletClient: WalletClient,
keyRpcUrl?: string,
dataRpcUrl?: string,
});
const result = await uploader.upload({
name: string,
version: string,
developer: string,
description?: string,
homepage?: string,
chrome?: Uint8Array,
firefox?: Uint8Array,
}, {
onProgress?: (phase, sent, total) => void,
confirmations?: number,
});CrossChainUploader
General-purpose cross-chain uploader with HTML loader.
const uploader = new CrossChainUploader({
keyChain: ChainName,
dataChain: ChainName,
keyWalletClient: WalletClient,
dataWalletClient: WalletClient,
});
const result = await uploader.upload(data, {
format?: 'html' | 'json',
title?: string,
description?: string,
mimeType?: string,
});Low-Level Utilities
import {
// Chunking
chunkData,
encodeChunk,
decodeChunk,
reassembleChunks,
ChunkTracker,
estimateChunks,
// Sending
sendChunk,
sendChunks,
sendChunksParallel,
// Tracking
waitForTransaction,
waitForTransactions,
TransactionMonitor,
// Fetching
fetchChunk,
fetchChunks,
assembleFromHashes,
StreamingAssembler,
// Encryption
generateEncryptionKeys,
deriveKeysFromSignature,
encryptForRecipients,
decryptForRecipient,
} from 'chunktech';Encryption
Optional X3DH + AES-256-GCM encryption for private data.
import { generateEncryptionKeys } from 'chunktech';
const myKeys = await generateEncryptionKeys();
// Upload encrypted
await ct.upload(data, {
encrypt: true,
keys: myKeys,
recipients: [
{ id: 'alice', bundle: aliceKeys.bundle },
],
});
// Download encrypted
const result = await ct.download(txHashes, {
keys: myKeys,
recipientId: 'sender',
});Supported Chains
| Chain | ID | Use Case | |-------|-----|----------| | Ethereum | 1 | Inscriptions, durability | | Base | 8453 | Cheap data storage | | Arbitrum | 42161 | Cheap data storage | | Sepolia | 11155111 | Testing | | Base Sepolia | 84532 | Testing | | Arbitrum Sepolia | 421614 | Testing |
Cost Estimates
For a 500KB extension on Base:
Chunks: ~15 (at 33KB each)
Cost per chunk: ~$0.002
Total: ~$0.03
+ 1 Ethereum inscription: ~$2-5 (varies with gas)How It Works
- Chunking - Files split into 33.3KB pieces with metadata (ID, part, total)
- Encoding - Each chunk → JSON → base64 → prefixed calldata
- Sending - Self-transfer transactions (to == from, value = 0)
- Storage - Calldata stored permanently in transaction history
- Retrieval - Fetch via
eth_getTransactionByHash, decode, reassemble - Verification - SHA256 hash checked after reassembly
On-Chain Viewer Pages
Create standalone HTML pages that fetch, verify, and display on-chain content. Perfect for:
- NPM packages with auditable source
- Browser extensions with install instructions
- Any file needing public verification
Quick Inscribe
import { readFileSync } from 'fs';
import { createWalletClient, http, toHex } from 'viem';
import { base } from 'viem/chains';
import { createHash } from 'crypto';
const zipData = readFileSync('package.zip');
const base64 = zipData.toString('base64');
const sha256 = createHash('sha256').update(zipData).digest('hex');
const dataUri = `data:application/zip;base64,${base64}`;
const calldata = toHex(new TextEncoder().encode(dataUri));
const hash = await walletClient.sendTransaction({
to: account.address,
data: calldata,
});
console.log(`TX: ${hash}`);
console.log(`SHA256: ${sha256}`);Viewer HTML
The viewer fetches from any RPC, verifies SHA256, and displays source:
┌─────────────────────────────────────────┐
│ MyPackage v1.0.0 │
│ │
│ [Download] [Verify On-Chain] │
│ │
│ ┌─────────────────────────────────┐ │
│ │ README │ index.ts │ package.json│ │
│ ├─────────────────────────────────┤ │
│ │ │ │
│ │ // Source code displayed here │ │
│ │ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘Key features:
- Fetches tx via public RPC (no backend)
- SHA256 verification on download
- Source code tabs for auditing
- Works offline once loaded
See skill.md for full implementation details.
Acknowledgments
- calldata-rpc by @chopperdaddy - Decentralized RPC endpoint discovery via IPFS and ENS, used in the HTML loaders for reliable chain access.
License
MIT
