sour.js
v0.1.1
Published
ENS name resolution, decentralized content fetching (IPFS/IPNS/Arweave), and ENS domain registration (Citron) — core logic only, no UI.
Downloads
326
Maintainers
Readme
sour.js
Core logic for ENS name resolution, decentralized content fetching (IPFS / IPNS / Arweave), and ENS domain registration (Citron). No DOM, no UI, no wallet-connect modal — just functions you call from your own app, CLI, or server.
Ships as a real dual package: import gets you ESM, require gets you
CommonJS, both from the same source.
Install
npm install sour.js ethersA few features are optional and only need their package installed if you actually use them:
npm install @ensdomains/content-hash # decodeContenthash / resolveENS
npm install @helia/verified-fetch # fetchIPFS / fetchIPNS
npm install jszip # buildDirectoryZipUsage
import { resolveENS, fetchRoot, resolveSiteRoot } from 'sour.js';
// or: const { resolveENS, fetchRoot, resolveSiteRoot } = require('sour.js');
const resolved = await resolveENS('vitalik.eth');
// { ens, resolver, contenthash, protocol: 'ipfs', codec, value, sourPath }
const { mode, result, sitePath } = await resolveSiteRoot(resolved);
// mode: "site" | "media" | "download"Citron (ENS registration)
import { citron } from 'sour.js';
// or: import * as citron from 'sour.js/citron';
const provider = new ethers.JsonRpcProvider(citron.NETWORKS.mainnet.rpc[0]);
const { label, name } = citron.normalizeLabel('my-new-name');
const available = await citron.isAvailable(provider, 'mainnet', label);
if (available) {
const price = await citron.getPrice(provider, 'mainnet', label, 31536000n); // 1 year
// price: { base, premium, total, unit: "ETH", decimals: 18 }
}Commit/reveal flow:
const secret = citron.generateSecret();
const built = citron.buildRegistration('mainnet', {
label, owner: signerAddress, duration: 31536000n, secret
});
const commitment = await citron.makeCommitment(provider, 'mainnet', built);
await citron.commit(signer, 'mainnet', commitment);
// wait for the on-chain minAge window (see getCommitmentWindow) ...
const price = await citron.getPrice(provider, 'mainnet', label, 31536000n);
await citron.register(signer, 'mainnet', built, { value: price.total });ENSv2 (Sepolia testnet) uses the same functions — pass 'sepolia' as
the network — but pays in an ERC20 instead of ETH, so approve() it
first:
const price = await citron.getPrice(provider, 'sepolia', label, duration);
await citron.approvePaymentToken(signer, 'sepolia', price.total);
await citron.register(signer, 'sepolia', built);If a transaction reverts, citron.decodeRegistrarError(error) tries to
turn it into a friendly message (ENSv1 only — ENSv2's custom errors
aren't published yet); it returns null when it can't decode the
revert, so fall back to error.shortMessage / error.reason.
API
ENS resolution (sour.js)
| Export | Description |
|---|---|
| normalizeENS(name) | Canonicalize a name to label.eth (ENSIP-15 normalized). |
| resolveENS(name, opts?) | Full resolve: name → resolver → contenthash → decoded {protocol, value}. |
| getResolver(node, opts?) | Low-level: resolver address for a namehash node. |
| getContenthash(resolver, node, opts?) | Low-level: raw contenthash bytes. |
| decodeContenthash(hex) | Decode raw contenthash into {protocol, codec, value}. |
| ensToSourPath(name) / parseSourPath(path) | Convert between name.eth and Sour's /name/ URL convention. |
| ethHostToSourPath(url) | Convert a https://name.eth/... URL into a Sour-style path. |
| rpcCall(method, params, opts?) | Race a JSON-RPC call across multiple endpoints. |
opts accepts { rpcEndpoints, timeoutMs, registry }.
Content fetching (sour.js)
| Export | Description |
|---|---|
| fetchContent(resolved, path) | Fetch a path from whatever resolved.protocol is (ipfs/ipns/arweave). |
| fetchRoot(resolved) | Fetch /, falling back to /index.html, /index.htm. |
| resolveSiteRoot(resolved) | Figure out whether a resolved name is a site, a single media file, or a directory — returns {mode, result, sitePath?}. |
| fetchIPFS(cid, path) / fetchIPNS(name, path) | Direct protocol-specific fetch via Helia verified-fetch. |
| fetchArweave(txId, path) | Fetch from Arweave, following path-manifests. |
| buildDirectoryZip(resolved, paths) | Zip a set of paths under a resolved directory (needs jszip). |
| isHTML, isMediaMime, isMediaPath, isMediaResult, mimeType | Small content-sniffing helpers. |
Citron / ENS registration (sour.js/citron or sour.js's citron export)
| Export | Description |
|---|---|
| NETWORKS.mainnet / NETWORKS.sepolia | Registrar addresses, RPC lists, commit-reveal window. |
| normalizeLabel(raw) | Parse+normalize a single .eth label. |
| isAvailable(providerOrSigner, network, label) | Availability check. |
| getPrice(providerOrSigner, network, label, durationSeconds) | Quote a price (ETH on mainnet, USD-pegged ERC20 on Sepolia/ENSv2). |
| getCommitmentWindow(providerOrSigner, network) | Live minAge/maxAge from the contract. |
| generateSecret() | Random 32-byte commit secret. |
| buildRegistration(network, params) | Build the registration struct/args for either registrar version. |
| makeCommitment / commit / getCommitmentTimestamp / register | The commit-reveal transaction steps. |
| approvePaymentToken(signer, network, amount) | ERC20 approve step, ENSv2 only. |
| decodeRegistrarError(error) | Best-effort friendly message from a revert (ENSv1 only). |
What's not included
This package is deliberately UI-free. It does not include: a wallet connect UI, an SPA router, HTML rewriting/site-mounting (for actually serving a resolved site in a browser), or countdown-timer polling for the commit-reveal wait — bring your own for all of that.
License
MIT
