WalletChan is distributed through two channels.
| GitHub Releases (sideloading) | Chrome Web Store | |
|---|---|---|
| Format | ZIP (load as unpacked in developer mode) | CWS package |
| Update mechanism | Manual (download new zip, refresh) | CWS built-in auto-update |
| Audience | Beta testers, developers | General public |
| Speed | Instant (GitHub Release publishes immediately) | CWS review (hours to days) |
| Listing | GitHub Releases | Chrome Web Store |
CWS rejects uploads containing key or update_url fields in manifest.json. Use pnpm zip:cws to create a CWS-ready zip — it builds the extension and strips these fields from the build output before zipping (via scripts/strip-cws-keys.sh). The plain pnpm zip builds and keeps all fields intact (used for GitHub Releases).
Both zip and zip:cws run pnpm build automatically — no need to build separately first.
pnpm release:patch # 0.2.0 → 0.2.1 (bug fixes)
pnpm release:minor # 0.2.0 → 0.3.0 (new features)
pnpm release:major # 0.2.0 → 1.0.0 (breaking changes)Important: The working tree must be clean (no uncommitted changes) before running a release command.
This automatically (via scripts/release.sh):
- Bumps the version in
apps/extension/package.json - Syncs the version to
apps/extension/public/manifest.json - Commits both files from the repo root (so monorepo paths resolve correctly)
- Creates a git tag (e.g.
v0.2.1) - Pushes to origin with tags
The release workflow triggers on v* tags and:
- Runs
pnpm zip(which builds the extension and creates the zip) - Publishes
walletchan-vX.Y.Z.zipto GitHub Releases
Users can download the zip from GitHub Releases and load it as an unpacked extension in developer mode.
- Create the CWS zip (builds automatically):
pnpm zip:cws
- Go to the CWS Developer Dashboard
- Select the WalletChan extension
- Upload
apps/extension/cws-zip/walletchan-vX.Y.Z.zip(not the GitHub Release zip) - Fill in any release notes
- Submit for review
Once approved, CWS users receive the update.
If you need to create a release without the automated workflow:
pnpm zip # builds + zips with key + update_url (for GitHub Release)
pnpm zip:cws # builds + zips without key + update_url (for CWS upload)Then upload apps/extension/zip/walletchan-vX.Y.Z.zip to a new GitHub release.
GitHub Releases provide a ZIP file for users who want to sideload the extension in developer mode. This is useful for beta testing or trying out the extension before it's approved on CWS.
- Download
walletchan-vX.Y.Z.zipfrom GitHub Releases - Extract the zip
- Go to
chrome://extensions→ enable Developer mode - Click "Load unpacked" → select the extracted folder
- To update: download the new zip, extract, and click the refresh icon on
chrome://extensions
Chrome blocks enabling sideloaded CRX extensions that aren't from the Chrome Web Store. Dragging a .crx file into chrome://extensions will install it, but Chrome disables it with the warning: "This extension is not listed in the Chrome Web Store and may have been added without your knowledge."
This is why we only distribute ZIP files (for unpacked loading) and not CRX files on GitHub Releases. CRX-based auto-update via update_url only works for enterprise/managed installs deployed via group policy.
pnpm release:patch
→ bumps version in package.json + manifest.json
→ creates git tag v0.2.1
→ pushes to GitHub
GitHub Actions (.github/workflows/release.yml)
→ runs pnpm zip (builds + creates ZIP)
→ attaches to GitHub Release
The website still serves update_url XML at https://walletchan.com/api/extension/update.xml for any enterprise installs using the CRX + policy approach. This endpoint fetches the latest GitHub Release version dynamically.
curl https://walletchan.com/api/extension/update.xmlShould return XML with appid='gmfimlibjdfoeoiohiaipblfklgammci' and the latest version.
Reference for if signing key or infrastructure needs to be recreated.
openssl genrsa -out walletchan.pem 2048Calculate the extension ID from your signing key:
node -e "
const crypto = require('crypto');
const fs = require('fs');
const pem = fs.readFileSync('walletchan.pem', 'utf8');
const key = crypto.createPrivateKey(pem);
const pubKey = crypto.createPublicKey(key).export({ type: 'spki', format: 'der' });
const hash = crypto.createHash('sha256').update(pubKey).digest();
const id = Array.from(hash.slice(0, 16))
.map(b => String.fromCharCode((b >> 4) + 97) + String.fromCharCode((b & 0xf) + 97))
.join('');
console.log(id);
"Extract the public key to set as the key field in manifest.json:
node -e "
const crypto = require('crypto');
const fs = require('fs');
const pem = fs.readFileSync('walletchan.pem', 'utf8');
const key = crypto.createPrivateKey(pem);
const pubKey = crypto.createPublicKey(key).export({ type: 'spki', format: 'der' });
console.log(pubKey.toString('base64'));
"In the repository settings, add:
EXTENSION_SIGNING_KEY: Base64 encoded .pem filebase64 -i walletchan.pem | pbcopy # macOS base64 -w 0 walletchan.pem # Linux
Add to your website deployment (Vercel, etc.):
EXTENSION_ID=gmfimlibjdfoeoiohiaipblfklgammci
This is the self-hosted extension ID (from step 2), since only CRX-installed users hit the update endpoint.
Store the walletchan.pem file in a password manager. This key is the extension's identity for self-hosted distribution — losing it means self-hosted users won't receive updates.
Chrome extensions auto-update silently. Users cannot choose to stay on an old version. Every release must work seamlessly for users on any previously released version.
Full storage key reference: See
STORAGE.mdfor every key, its shape, which files touch it, and what each version expects.
background.ts listens for chrome.runtime.onInstalled with reason === "update". When it fires, the migrateFromLegacyStorage() function runs. As a safety net, App.tsx also calls the migrateFromLegacy message handler if it detects no accounts on load.
- Never remove or rename a storage key without a migration. If you rename
footobar, you must readfoo, writebar, and keepfoofor at least one release cycle. - Never change the shape of stored data without handling the old shape. If
accountsgains a new required field, set a default for entries that lack it. - Migrations must be idempotent. They can run more than once (onInstalled + App.tsx fallback). Always check if already migrated before writing.
- Migrations must not require the wallet to be unlocked.
onInstalledfires before the user opens the popup. Only use data fromchrome.storage(no decryption, no cached passwords).
- Write a function in
background.ts(or a dedicated module if complex):async function migrateXxx(): Promise<boolean> { // Check if already migrated — exit early // Read old format // Write new format // Return true if migrated, false if skipped }
- Call it from the
onInstalled"update"handler. - Add a fallback call from
App.tsxinit if needed (for cases where the service worker was inactive during install). - Add a message handler gated with
isExtensionPage(sender)if the fallback needs it.
| Version | Migration | What it does |
|---|---|---|
| v1.0.0 | migrateFromLegacyStorage |
Creates accounts array + activeAccountId from legacy address / encryptedApiKey storage (v0.1.1/v0.2.0 had no multi-account system) |
| v1.0.0 | Vault key (on first unlock) | authHandlers.ts auto-migrates encryptedApiKey → encryptedVaultKeyMaster + encryptedApiKeyVault |
| v1.3.0 | Private key vault-key encryption (on first unlock with master password) | authHandlers.ts auto-migrates pkVault and mnemonicVault entries from password encryption (salt !== "") to vault-key encryption (salt === ""). Enables agent password to sign transactions. Idempotent, dual-format support maintained. |
- Build and load the current extension as unpacked
- Complete onboarding normally
- Open the service worker DevTools console and strip the new storage to simulate an old user:
// Simulate v0.2.0 storage state chrome.storage.local.remove([ "accounts", "encryptedVaultKeyMaster", "encryptedApiKeyVault", "agentPasswordEnabled", ]); chrome.storage.sync.remove(["activeAccountId", "tabAccounts"]);
- Click Reload on
chrome://extensions(firesonInstalledwithreason === "update") - Open the popup — should show unlock screen, not onboarding
- Enter password — vault key migration runs on unlock
- Verify the service worker console shows:
[WalletChan] Legacy storage migration complete: 0x...
Before every release that touches chrome.storage:
- List all storage keys added, removed, or changed
- For each change: does a user on the previous release have data in the old format?
- If yes: is there a migration that converts old → new?
- Is the migration idempotent and does it run without the wallet being unlocked?
- Test the upgrade path locally using the steps above
- Never commit the .pem file to the repository (it's in
.gitignore) - The website API caches GitHub responses for 5 minutes to avoid rate limits
- CWS publishing info and permission justifications are in
CHROME_WEBSTORE.md