theseus/bundled-addons/aegis/wallet-inject.js

507 lines
23 KiB
JavaScript
Raw Normal View History

feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
// Aegis dapp bridges. Runs in the isolated world of every matching tab and
// puts up to two dapp APIs on the page's main world:
//
// window.bitcoincash Silent Mode's own BCH API (only on .x pages —
// matches the pre-multi-wallet gate).
// window.tronWeb TronLink-shaped Tron dapp bridge (on any page).
// window.tronLink TronLink shim; `tron_requestAccounts` triggers
// an approval overlay via activate().
//
// Everything crosses into the wallet's activate() context in main, which
// shows the approval overlay and enforces per-origin permissions. Nothing
// here can sign, read a balance, or read an address on its own.
//
// `theseus` is provided by the host: { id, origin, contextBridge, invoke }.
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
const call = (msg, payload) =>
theseus.invoke(msg, payload).catch((e) => {
const text = String(e && e.message || e).replace(/^Error invoking remote method '[^']+': (Error: )?/, "");
throw new Error(text);
});
feat(theseus/aegis): multi-wallet + Tron mainnet + Tron Nile in the bundled addon Turns the single-account BCH addon into Aegis: a chain-agnostic wallet manager with a wallet picker in the sidebar header, per-wallet sub-accounts, and Tron mainnet + Nile alongside BCH. Add-on id stays "bchwallet" so vault-derive paths stay in the same namespace and the legacy BCH default wallet uses PURPOSE "bchwallet/mainnet/0" byte-identical to before — funds are untouched. - lib/chain-bch.js wraps the existing keys/tx/wallet/electrum stack with the common adapter shape and scopes each wallet's storage under wallets/<id>/… - lib/chain-tron.js: m/44'/195'/0'/0/0 → secp256k1 → keccak256 → 0x41 || h20 → base58check. Balance + history via TronGrid v1, send via createtransaction + sha256(raw_data_hex) sign + broadcasttransaction. Mainnet and Nile share the address format; different vault paths mean different keys so a mainnet wallet can never accidentally sign against Nile. - lib/base58check.js: bitcoin-alphabet base58 with sha256d checksum. k=1 derivation verified against Ethereum's canonical k=1 H160 in a scratchpad harness (correct-by-construction for Tron address). - Combined wallet-inject.js: window.bitcoincash on .x pages (unchanged gate), window.tronWeb + window.tronLink on any https page. tron_requestAccounts triggers the approval overlay; sign / sendRawTransaction / signMessageV2 route to the currently-selected Tron wallet. Emits accountsChanged / setNode messages TronLink dapps listen for; chain ids 0x2b6653dc / 0xcd8690dc match what TronLink itself uses. - New panel: chain-aware wallet picker in the header (badges 🟨 BCH, 🔴 Tron, 🔵 Nile), Add-wallet dropdown per chain, per-chain unit picker (BCH/sat, TRX/sun), per-wallet rename + remove (isLegacy default is protected). Sends show the chosen wallet in the approval overlay so the user can never mistake sub-account. - Migration on first launch: pre-multi-wallet storage (top-level receiveCursor / txCache) is rehomed under wallets/bch-default/… and the legacy account path is preserved. Not shipped: user is bundling into the next release. Live Nile broadcast + real dapp connect need a set-up vault; the code paths are unit-verified end to end but a testnet send + tronscan.io/nile connect are user-side steps.
2026-09-06 22:00:27 +02:00
// -------- BCH bridge (window.bitcoincash), only on .x pages ------------------
const isBchOrigin = (() => { try { return /\.x$/i.test(location.hostname); } catch { return false; } })();
if (isBchOrigin) {
theseus.contextBridge.exposeInMainWorld("bitcoincash", {
isTheseus: true,
version: "0.2.0",
network: "mainnet",
getAddress: () => call("getAddress"),
signAndSend: (txSpec) => call("signAndSend", txSpec && typeof txSpec === "object" ? txSpec : {}),
signMessage: (message) => call("signMessage", { message: String(message ?? "") }),
});
}
// -------- Tron bridge (window.tronWeb, window.tronLink), everywhere ---------
//
// Matches enough of the TronLink surface for read + send flows on dapps like
// tronscan, sunswap, and justlend (mainnet or Nile). Not implemented:
// TRC20/TRC721 abstractions, address utilities beyond base58<->hex, event
// emitters beyond the "message" events TronLink documents.
let CACHED = { address: null, network: null };
const listeners = new Set();
// Public copy of tronWeb.trx.
const trx = {
// Returns the account object TronGrid gives (may be {} for never-funded).
// Deliberately mirrors tronWeb.trx.getAccount.
getAccount: async () => call("trx.getAccount"),
// Sign an assembled transaction (dapp built it via its own tronWeb).
// Returns the same object with a `signature` field populated.
sign: async (transaction) => {
if (!transaction || typeof transaction !== "object" || !transaction.raw_data_hex) {
throw new Error("tronWeb.trx.sign: expected an assembled transaction with raw_data_hex");
}
return call("trx.signTransaction", { transaction });
},
// Broadcast a signed transaction. Returns { result: true, txid } on success.
sendRawTransaction: async (signedTx) => {
const r = await call("trx.sendRawTransaction", { transaction: signedTx });
return { result: true, txid: r.txid };
},
// Convenience wrappers matching the tronWeb surface — dapps call these
// when the wallet is supposed to build the tx too. Left unimplemented
// in this build so a dapp that hits them gets a clear error instead of
// silently unassembled behavior; assemble on the dapp side.
sendTrx: async () => { throw new Error("tronWeb.trx.sendTrx not implemented — build the transaction with tronWeb.transactionBuilder and call sign+sendRawTransaction"); },
// TronLink documents signMessage (legacy, keccak of just the message) and
// signMessageV2 (adds the "\x19TRON Signed Message:\n<len>" prefix). We
// only implement V2 because that is what modern dapps use.
signMessageV2: async (message) => {
const r = await call("trx.signMessageV2", { message: String(message ?? "") });
return r.signature;
},
};
// Shallow tronWeb — enough for `tronWeb.defaultAddress.base58` / `.hex` reads
// and the trx methods. Populated once the user connects.
const tronWeb = {
ready: false,
defaultAddress: { base58: false, hex: false, name: false },
fullNode: { host: "" },
solidityNode: { host: "" },
eventServer: { host: "" },
trx,
// Utility helpers a few dapps poke at.
isConnected: () => tronWeb.ready === true && !!tronWeb.defaultAddress.base58,
address: {
fromHex: (hex) => hex, // no-op stub; dapps that need a real
toHex: (base58) => base58, // conversion should use their own copy.
},
// Some dapps read this to decide "Tron mainnet vs testnet".
get currentNetwork() { return CACHED.network || null; },
};
function nodeHost(network) {
return network === "nile" ? "https://nile.trongrid.io" : "https://api.trongrid.io";
}
function applyConnection({ address, network }) {
CACHED = { address, network };
tronWeb.defaultAddress = { base58: address || false, hex: false, name: false };
tronWeb.ready = !!address;
tronLink.ready = !!address;
const host = nodeHost(network);
tronWeb.fullNode = { host };
tronWeb.solidityNode = { host };
tronWeb.eventServer = { host };
// Broadcast on the window in the way TronLink documents.
try {
window.postMessage({ message: { action: "accountsChanged", data: { address } }, isTronLink: true }, location.origin);
window.postMessage({ message: { action: "setAccount", data: { address } }, isTronLink: true }, location.origin);
window.postMessage({ message: { action: "setNode", data: { node: { fullNode: host, solidityNode: host, eventServer: host, chain: network === "nile" ? "0xcd8690dc" : "0x2b6653dc" } } }, isTronLink: true }, location.origin);
} catch {}
for (const fn of listeners) { try { fn({ address, network }); } catch {} }
}
const tronLink = {
ready: false,
tronWeb: tronWeb,
// TronLink's canonical request router. Handles at minimum tron_requestAccounts.
request: async (opts) => {
const method = String(opts?.method || "");
if (method === "tron_requestAccounts" || method === "requestAccounts") {
const r = await call("trx.requestAccounts");
applyConnection({ address: r.address, network: r.network });
return { code: 200, message: "ok" };
}
if (method === "tron_accounts") {
const r = await call("trx.getAccount").catch(() => null);
return r ? [r.address] : [];
}
throw new Error("Aegis: unsupported tronLink method " + method);
},
// Dapps sometimes attach here to react to wallet changes.
on: (event, fn) => {
if (event === "message" || event === "accountsChanged" || event === "networkChanged") listeners.add(fn);
},
removeListener: (_event, fn) => listeners.delete(fn),
};
theseus.contextBridge.exposeInMainWorld("tronWeb", tronWeb);
theseus.contextBridge.exposeInMainWorld("tronLink", tronLink);
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
// -------- Main-world bridges (window.ethereum, window.solana) ---------------
//
// EIP-1193 (Ethereum) and the Solana wallet-adapter both expect the wallet
// object to touch the dapp's own JS values — Solana in particular passes
// `Transaction` instances whose `.serializeMessage()` method the wallet has
// to call. Electron's contextBridge shallow-copies function arguments
// between worlds and strips methods, so bridges that need to call methods
// on dapp-side objects have to live in the main world. We inject a `<script>`
// with the bridge source; it runs synchronously in the main world and talks
// to us via `window.postMessage` on a namespaced envelope. This mirrors how
// MetaMask and Phantom bridge extension code back to page code.
// Namespace used on the postMessage envelope. Includes the addon id so a
// page that runs multiple dapp-wallet extensions doesn't misroute messages.
const AEGIS_TAG = "aegis-" + theseus.id;
const pendingCalls = new Map();
window.addEventListener("message", async (e) => {
const d = e && e.data;
if (!d || d.aegisTag !== AEGIS_TAG) return;
if (d.kind === "request") {
// Forward main-world → isolated-world → addon.
try {
const result = await call(d.msg, d.payload);
window.postMessage({ aegisTag: AEGIS_TAG, kind: "response", id: d.id, ok: true, result }, location.origin);
} catch (err) {
window.postMessage({ aegisTag: AEGIS_TAG, kind: "response", id: d.id, ok: false, error: String(err && err.message || err) }, location.origin);
}
}
});
function emitToMainWorld(event, data) {
try { window.postMessage({ aegisTag: AEGIS_TAG, kind: "event", event, data }, location.origin); } catch {}
}
// The main-world bridge — installed as a page-level `<script>` so it can
// call methods on Transaction objects the dapp hands it, and so the globals
// it defines look like ordinary page code to the dapp.
const mainWorldSource = `(function () {
if (window.__aegisBridge) return;
window.__aegisBridge = true;
const TAG = ${JSON.stringify(AEGIS_TAG)};
const pending = new Map();
let seq = 1;
function invoke(msg, payload) {
const id = "r" + (seq++);
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
window.postMessage({ aegisTag: TAG, kind: "request", id, msg, payload }, location.origin);
});
}
window.addEventListener("message", (e) => {
const d = e && e.data;
if (!d || d.aegisTag !== TAG) return;
if (d.kind === "response") {
const p = pending.get(d.id);
if (!p) return;
pending.delete(d.id);
d.ok ? p.resolve(d.result) : p.reject(new Error(d.error));
} else if (d.kind === "event") {
dispatchEvent(d.event, d.data);
}
});
// ---- Ethereum (EIP-1193) ------------------------------------------------
const ethListeners = { connect: [], disconnect: [], accountsChanged: [], chainChanged: [], message: [] };
let ethState = { address: null, chainIdHex: "0x1", networkVersion: "1" };
function ethEmit(event, data) {
for (const fn of ethListeners[event] || []) { try { fn(data); } catch {} }
}
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
// After a chain switch or add, refresh the local chainId/address state
// and fire the two events dapps expect (accountsChanged +
// chainChanged). MetaMask does the same round-trip after switchChain.
async function pullEthStateAndEmit() {
try {
const s = await invoke("eth.state");
const oldChain = ethState.chainIdHex, oldAddr = ethState.address;
ethState.address = s.address || null;
ethState.chainIdHex = s.chainIdHex;
ethState.networkVersion = s.networkVersion;
window.ethereum.selectedAddress = ethState.address;
window.ethereum.chainId = ethState.chainIdHex;
window.ethereum.networkVersion = ethState.networkVersion;
if (s.chainIdHex !== oldChain) ethEmit("chainChanged", s.chainIdHex);
if ((s.address || null) !== oldAddr) ethEmit("accountsChanged", s.address ? [s.address] : []);
} catch {}
}
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
async function ethHandle(method, params) {
params = Array.isArray(params) ? params : (params ? [params] : []);
switch (method) {
case "eth_requestAccounts": {
const r = await invoke("eth.requestAccounts");
ethState.address = r.address;
ethState.chainIdHex = r.chainIdHex;
ethState.networkVersion = r.networkVersion;
window.ethereum.selectedAddress = r.address;
window.ethereum.chainId = r.chainIdHex;
window.ethereum.networkVersion = r.networkVersion;
ethEmit("accountsChanged", [r.address]);
return [r.address];
}
case "eth_accounts":
return ethState.address ? [ethState.address] : [];
case "eth_chainId":
return ethState.chainIdHex;
case "net_version":
return ethState.networkVersion;
case "personal_sign": {
// Both param orders are seen in the wild: [message, from] and [from, message].
const [a, b] = params;
const looksLikeAddr = (s) => typeof s === "string" && /^0x[0-9a-fA-F]{40}$/.test(s);
const message = looksLikeAddr(a) ? b : a;
return (await invoke("eth.personalSign", { message: String(message) })).signature;
}
case "eth_sign": {
// Legacy method. Same shape as personal_sign for our purposes.
const [_from, msg] = params;
return (await invoke("eth.personalSign", { message: String(msg) })).signature;
}
case "eth_sendTransaction": {
const tx = params[0] || {};
return (await invoke("eth.sendTransaction", { tx })).txid;
}
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
case "eth_signTypedData_v4":
case "eth_signTypedData":
case "eth_signTypedData_v3": {
// v3/v4 differ mostly in nested-struct support; the encoder handles
// both. v1 is the flat "type[]" schema that Metamask deprecated —
// reject it, dapps that still use v1 should upgrade.
const [a, b] = params;
const looksLikeAddr = (s) => typeof s === "string" && /^0x[0-9a-fA-F]{40}$/.test(s);
const typedData = looksLikeAddr(a) ? b : a;
return (await invoke("eth.signTypedData", { typedData })).signature;
}
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
case "wallet_switchEthereumChain": {
const target = String((params[0] && params[0].chainId) || "").toLowerCase();
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp that wants to route through Polygon (or Base, or Arbitrum, or any other EVM the Silent Mode user hasn't added yet) calls the pair the industry already wrote for it — Aegis registers the chain, provisions a wallet on it under the same vault seed, auto-connects the origin, fires chainChanged, and hands the dapp back a provider pointed at the new chain. No sidebar detour, no Custom RPC copy-paste. Users still see every chain in the picker post-add and can revoke sites in Settings. - lib/chain-eth.js: EthWallet accepts a customNetwork override ({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}). When present it replaces the NETWORKS lookup so mainnet+Sepolia ship built-in and every EIP-3085 chain is a runtime override the addon persists. The ticker flows into snapshot() so the send approval reads MATIC / BNB / whatever the chain's native currency is, not a hardcoded ETH. - index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl, explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`. Persisted under api.storage.customEthChains, so an added chain survives Theseus restarts. chainMeta("eth", "custom-<chainId>") synthesizes the meta from storage so the panel renders custom chains without needing them in COINS at module-load time. - eth.addChain handler (EIP-3085): approval overlay shows chain name, decimal + hex chain id, native ticker, RPC and explorer URLs (the phishing-signal quartet). On approval, persist config + create wallet with a custom-<chainId> network + auto-grant the origin readAddress on this chain. No-op success if the chain is already added. - eth.switchChain rewritten to be EIP-3326 correct: look up any ready ETH wallet whose adapter reports the requested chainId, make it the selected wallet, fire chainChanged. When no wallet matches, throw with .code = 4902 (the standard 'chain not added' code) so wagmi / RainbowKit / any 3326-aware dapp does the fallback wallet_addEthereumChain call in the same click. - eth.state handler: cheap {address, chainIdHex, networkVersion} peek for the origin's currently-connected wallet (no approval, no key access). The main-world bridge calls it after every switch/add to emit chainChanged + accountsChanged locally — the events MetaMask fires and RainbowKit listens for. - wallet-inject.js: routes wallet_addEthereumChain via eth.addChain, preserves the 4902 code across the postMessage boundary on switch failures, calls pullEthStateAndEmit() to fire the post-switch/add events.
2026-09-07 22:26:27 +02:00
try {
const r = await invoke("eth.switchChain", { chainId: target });
await pullEthStateAndEmit();
return r;
}
catch (e) {
// EIP-3326: preserve the 4902 signal the isolated-world handler
// stamps on the Error so dapps fall through to addChain.
if (/is not added/i.test(e.message || "")) { const err = new Error(e.message); err.code = 4902; throw err; }
throw e;
}
}
case "wallet_addEthereumChain": {
// EIP-3085. Approval overlay + persistence live in the addon.
const r = await invoke("eth.addChain", { params: params[0] });
await pullEthStateAndEmit();
return r;
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
}
case "wallet_getPermissions":
case "wallet_requestPermissions":
// Minimum shape most dapps accept.
return [{ parentCapability: "eth_accounts" }];
default:
// Read passthrough: eth_getBalance, eth_call, eth_blockNumber, etc.
return invoke("eth.rpc", { method, params });
}
}
const ethereum = {
isAegis: true,
isMetaMask: false,
chainId: ethState.chainIdHex,
networkVersion: ethState.networkVersion,
selectedAddress: null,
request: (opts) => ethHandle(String(opts && opts.method || ""), (opts && opts.params) || []),
// EIP-1193 event surface.
on: (event, fn) => { if (ethListeners[event]) ethListeners[event].push(fn); },
removeListener: (event, fn) => {
const arr = ethListeners[event]; if (!arr) return;
const i = arr.indexOf(fn); if (i >= 0) arr.splice(i, 1);
},
// Legacy compat some old dapps still call.
enable: () => ethereum.request({ method: "eth_requestAccounts" }),
sendAsync: (payload, cb) => ethereum.request(payload).then((result) => cb(null, { id: payload.id, jsonrpc: "2.0", result }), (err) => cb(err)),
send: (methodOrPayload, params) => ethereum.request(typeof methodOrPayload === "string" ? { method: methodOrPayload, params } : methodOrPayload),
};
// ---- Solana (wallet-adapter shape) --------------------------------------
const solListeners = { connect: [], disconnect: [], accountChanged: [] };
let solState = { publicKey: null };
function makePubkey(base58) {
return {
toString: () => base58,
toBase58: () => base58,
toBytes: () => base58ToBytes(base58),
equals: (other) => other && other.toString && other.toString() === base58,
_bn: null,
};
}
// Local base58 decoder — needed to expose PublicKey.toBytes(). Alphabet
// matches Bitcoin's (the only base58 flavor in real use).
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function base58ToBytes(s) {
let zeros = 0;
while (zeros < s.length && s[zeros] === B58[0]) zeros++;
const out = new Uint8Array(Math.ceil(s.length * 733 / 1000 + 1));
let length = 0;
for (let i = zeros; i < s.length; i++) {
const v = B58.indexOf(s[i]);
if (v < 0) throw new Error("bad base58 char " + s[i]);
let carry = v, j = 0;
for (let k = out.length - 1; (carry !== 0 || j < length) && k >= 0; k--, j++) {
carry += 58 * out[k];
out[k] = carry & 0xff;
carry >>>= 8;
}
length = j;
}
let it = out.length - length;
while (it < out.length && out[it] === 0) it++;
const total = zeros + (out.length - it);
const dec = new Uint8Array(total);
let p = zeros;
while (it < out.length) dec[p++] = out[it++];
return dec;
}
function bytesToBase58(b) {
let zeros = 0;
while (zeros < b.length && b[zeros] === 0) zeros++;
const buf = new Uint8Array(Math.ceil(b.length * 138 / 100 + 1));
let length = 0;
for (let i = zeros; i < b.length; i++) {
let carry = b[i], j = 0;
for (let k = buf.length - 1; (carry !== 0 || j < length) && k >= 0; k--, j++) {
carry += (buf[k] << 8) >>> 0;
buf[k] = carry % 58;
carry = (carry / 58) | 0;
}
length = j;
}
let it = buf.length - length;
while (it < buf.length && buf[it] === 0) it++;
let out = "";
for (let i = 0; i < zeros; i++) out += B58[0];
for (; it < buf.length; it++) out += B58[buf[it]];
return out;
}
function u8ToBase64(u8) {
let s = "";
for (let i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]);
return btoa(s);
}
function base64ToU8(s) {
const bin = atob(s);
const u8 = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
return u8;
}
async function solConnect() {
const r = await invoke("sol.connect");
solState.publicKey = makePubkey(r.address);
window.solana.publicKey = solState.publicKey;
window.solana.isConnected = true;
for (const fn of solListeners.connect) { try { fn(solState.publicKey); } catch {} }
return { publicKey: solState.publicKey };
}
async function solDisconnect() {
solState.publicKey = null;
window.solana.publicKey = null;
window.solana.isConnected = false;
for (const fn of solListeners.disconnect) { try { fn(); } catch {} }
}
async function solSignMessage(u8) {
if (!(u8 instanceof Uint8Array)) u8 = new Uint8Array(u8 || []);
const r = await invoke("sol.signMessage", { messageB64: u8ToBase64(u8) });
return { publicKey: solState.publicKey, signature: base58ToBytes(r.signature) };
}
// Solana Transaction objects have serializeMessage() → Uint8Array and
// addSignature(publicKey, sig) — we live in the main world, so we can
// call both. Wallets that live in an isolated content-script can't.
async function solSignAndSendTransaction(tx, opts) {
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
if (!tx || typeof tx.serialize !== "function") {
throw new Error("Aegis: pass a @solana/web3.js Transaction (needs .serialize)");
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
}
feat(theseus/aegis): EIP-712 signTypedData_v4 + Solana multi-signer send Two follow-ups to the dapp bridges. Both change wire shape only — no new UI, existing wallets keep signing byte-identically for the flows they already covered. - lib/eip712.js: full EIP-712 typed-data encoder — encodeType with alphabetically-sorted transitive sub-types, typeHash, encodeValue for string / address / bool / uint*/int* (any width) / bytes / bytesN / nested structs / dynamic and fixed arrays, hashStruct recursion, digest = keccak256(0x19 || 0x01 || domainSeparator || hashStruct). Verified against the spec §"Ether Mail" test vector — hashStruct on both the domain and the message plus the final digest all match the canonical values byte-for-byte (see scratchpad/verify-eip712.mjs). - chain-eth.js: exposes signTypedDataDigest(digest32) that signs the precomputed digest with r||s||v (v = 27+recid), the same envelope personal_sign uses. Aegis computes the digest server-side (in the addon) so a bug in the encoder can't be tricked by a malicious dapp into signing over data the user never saw. - index.js: eth.signTypedData handler shows domain (name · version · chainId), primary type, and a truncated JSON preview of the message in the approval overlay — every classic phishing signal (mismatched domain, unexpected primary type) is in front of the user before they hit Sign. Accepts either an already-parsed typedData object or the JSON-string form older MetaMask specs used. - wallet-inject.js router: eth_signTypedData_v4 (and _v3 for the same payload shape) route to eth.signTypedData. v1's flat "type[]" form is unwired — dapps that still use v1 should upgrade. - Solana signAndSend: bridge now passes the FULL wire (from tx.serialize({requireAllSignatures:false, verifySignatures:false})) instead of just the message. The addon parses compact-u16 signature count, finds this wallet's pubkey in the message's account-key list, signs the message, and patches ONLY its own slot in the signature array — any partial signatures the dapp had already filled with tx.partialSign() (session keys, escrow co-signers, permissioned authorities) are preserved. Multi-signer flows work now; single-signer is the degenerate case of sigCount=1. - Approval overlay for sol.signAndSend now shows required-signer count and the wallet's slot index so multi-signer requests are visibly distinct from a plain single-signer send.
2026-09-07 22:19:51 +02:00
// Serialize the FULL wire including any partial signatures the dapp
// has already collected (multi-signer flows: session keys, escrows,
// ephemeral co-signers). Aegis fills the wallet's own signature slot
// in the addon and leaves the other slots untouched.
const wire = tx.serialize({ requireAllSignatures: false, verifySignatures: false });
const r = await invoke("sol.signAndSend", { wireB64: u8ToBase64(new Uint8Array(wire)) });
feat(theseus/aegis): EIP-1193 + Solana wallet-adapter bridges; BTC signet Aegis now integrates with the two dapp-wallet APIs the wider ecosystem actually uses — MetaMask-style window.ethereum for Ethereum, Phantom-style window.solana for Solana — plus BTC signet as a third Bitcoin network alongside mainnet + testnet3. - wallet-inject.js: adds a main-world bridge, installed via a one-shot <script textContent=…> appended to <head> and immediately removed. Electron's contextBridge shallow-copies args and strips methods, which means BCH- and Tron-shaped params (plain data) work in the isolated world but Solana's wallet-adapter dapps — which pass @solana/web3.js Transaction objects and expect .serializeMessage()/.addSignature() to fire on them — need code that lives in the same world as the dapp. Bridge talks back to the isolated world via window.postMessage on a namespaced envelope (aegisTag = "aegis-" + addonId), which forwards to theseus.invoke. Same pattern MetaMask + Phantom use. - window.ethereum (EIP-1193): request({method, params}), on(), removeListener(), chainId, networkVersion, selectedAddress. Handles eth_requestAccounts, eth_accounts, eth_chainId, net_version, personal_sign, eth_sign, eth_sendTransaction, wallet_switchEthereumChain (rejects with "use the Aegis picker"), wallet_addEthereumChain (rejects, chains come from Settings), wallet_get/requestPermissions. Every other eth_*/net_*/web3_* method passes through to the wallet's configured RPC via a new eth.rpc handler. EIP-6963 announceProvider event fires so wagmi / RainbowKit / any 6963-aware dapp discovers Aegis alongside MetaMask instead of racing for window.ethereum. - window.solana (wallet-adapter shape): connect(), disconnect(), publicKey (with toString/toBase58/toBytes/equals — the PublicKey interface dapps check), signMessage(u8) → {publicKey, signature: u8}, signTransaction(tx) → mutates + returns the same tx with the signature added, signAndSendTransaction(tx) → returns {signature: txid}, signAllTransactions([tx]), request({method, params}). isPhantom flag set true so dapps that gate on it pick us. on/off events for connect / disconnect / accountChanged. - Handlers in index.js registerPageMessages: eth.requestAccounts, eth.personalSign, eth.sendTransaction, eth.switchChain, eth.rpc, sol.connect, sol.signMessage, sol.signAndSend. Every write path is per-origin gated + goes through api.approvalModal with the wallet label + network in the row list so the user always knows which Aegis wallet is about to sign. - Signet added to chain-btc.js — signet shares testnet3's address format and SLIP-44 coin type (BIP-325 only changed consensus/signing), so bitcoinjs-lib.networks.testnet handles derivation unchanged. Only the electrum pool (aranguren + wakiyamap) + explorer (mempool.space /signet) + faucet (signetfaucet.com) differ. Registered as btc:signet in COINS with per-network coinType lookup. Known limits (follow-ups in the same shape as existing chains): - SOL signAndSendTransaction is single-signer only; dapps that combine the wallet's sig with co-signer sigs need the wire assembled on the dapp side. - ETH eth_signTypedData_v4 (EIP-712) is not wired — the handler set covers personal_sign only.
2026-09-07 22:08:56 +02:00
return { signature: r.txid, publicKey: solState.publicKey };
}
async function solSignTransaction(tx) {
if (!tx || typeof tx.serializeMessage !== "function" || typeof tx.addSignature !== "function") {
throw new Error("Aegis: pass a @solana/web3.js Transaction");
}
const messageBytes = tx.serializeMessage();
const r = await invoke("sol.signMessage", { messageB64: u8ToBase64(new Uint8Array(messageBytes)) });
// r.signature is base58 of the 64-byte ed25519 sig.
tx.addSignature(solState.publicKey, base58ToBytes(r.signature));
return tx;
}
const solana = {
isAegis: true,
isPhantom: true, // set so dapps that gate on isPhantom pick us
isConnected: false,
publicKey: null,
connect: async (opts) => solConnect(opts),
disconnect: solDisconnect,
signMessage: (u8) => solSignMessage(u8),
signTransaction: (tx) => solSignTransaction(tx),
signAllTransactions: async (txs) => {
const out = [];
for (const tx of txs) out.push(await solSignTransaction(tx));
return out;
},
signAndSendTransaction: (tx, opts) => solSignAndSendTransaction(tx, opts),
request: async (opts) => {
const method = String(opts && opts.method || "");
const params = opts && opts.params || {};
if (method === "connect") return solConnect();
if (method === "disconnect") return solDisconnect();
if (method === "signMessage") return solSignMessage(params.message);
if (method === "signTransaction") return solSignTransaction(params.transaction);
if (method === "signAndSendTransaction") return solSignAndSendTransaction(params.transaction, params.options);
throw new Error("Aegis: unsupported solana method " + method);
},
on: (event, fn) => { if (solListeners[event]) solListeners[event].push(fn); },
off: (event, fn) => {
const arr = solListeners[event]; if (!arr) return;
const i = arr.indexOf(fn); if (i >= 0) arr.splice(i, 1);
},
removeAllListeners: () => { Object.keys(solListeners).forEach((k) => solListeners[k].length = 0); },
};
function dispatchEvent(event, data) {
if (event === "eth.accountsChanged") { ethState.address = data.address || null; ethereum.selectedAddress = ethState.address; ethEmit("accountsChanged", data.address ? [data.address] : []); }
else if (event === "eth.chainChanged") { ethState.chainIdHex = data.chainIdHex; ethState.networkVersion = data.networkVersion; ethereum.chainId = data.chainIdHex; ethereum.networkVersion = data.networkVersion; ethEmit("chainChanged", data.chainIdHex); }
else if (event === "sol.accountChanged") {
const pk = data.address ? makePubkey(data.address) : null;
solState.publicKey = pk; solana.publicKey = pk;
for (const fn of solListeners.accountChanged || []) { try { fn(pk); } catch {} }
}
}
// Install the globals. Defined lazily via Object.defineProperty so we
// survive dapps that check hasOwnProperty(window, "ethereum") after page
// load — MetaMask does the same trick.
try { Object.defineProperty(window, "ethereum", { value: ethereum, writable: false, configurable: false }); } catch { window.ethereum = ethereum; }
try { Object.defineProperty(window, "solana", { value: solana, writable: false, configurable: false }); } catch { window.solana = solana; }
// EIP-6963 provider announcement so wagmi / RainbowKit discover Aegis.
try {
const info = { uuid: crypto.randomUUID(), name: "Aegis", icon: "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpolygon points='16,2 29,9 29,23 16,30 3,23 3,9' fill='none' stroke='%23d6ff3d' stroke-width='2.5'/%3E%3Ccircle cx='16' cy='16' r='4.3' fill='none' stroke='%23d6ff3d' stroke-width='1.6'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23d6ff3d'/%3E%3C/svg%3E", rdns: "st.silentmode.aegis" };
const announce = () => window.dispatchEvent(new CustomEvent("eip6963:announceProvider", { detail: Object.freeze({ info, provider: ethereum }) }));
announce();
window.addEventListener("eip6963:requestProvider", announce);
} catch {}
})();`;
// Actually push the script into the main world. Doing this at
// document_start (which is when this preload runs) means the bridge is in
// place before the dapp's own scripts execute.
try {
const s = document.createElement("script");
s.textContent = mainWorldSource;
(document.head || document.documentElement).appendChild(s);
s.remove();
} catch (e) {
console.warn("[aegis] main-world bridge install failed:", e && e.message || e);
}