theseus/bundled-addons/aegis/wallet-inject.js
Local Dev 1b74b10fc5 chore(aegis): 0.8.8 — coin drilldown, per-address assets, WizardConnect from the page
Wallet strip:
- Clicking a coin opens that coin's page (addresses, price, totals, back
  and close) instead of only flipping the selection and leaving the list
  sitting there. The page already existed but was reachable only via the
  small count chip.
- The per-coin second action was a gear that selected the wallet and
  opened the global Settings tab — the same destination for every coin,
  so it read as a per-coin control that wasn't one. It is now Remove,
  behind a confirm, with the default/legacy wallet showing a lock
  instead since it gates legacy funds.
- Each address in the drilldown can expand to show what THAT address
  holds: TRC20/SPL via the adapter's tokens, BCH CashTokens via
  tokenBalances. walletSummary now carries both per wallet, so the view
  no longer has to borrow the selected wallet's assets.

WizardConnect — the Connect pane was effectively unusable:
- The locked-vault branch told the user to unlock and gave them nothing
  to click. It is reachable without the lock screen ever appearing,
  because a mounted imported wallet makes overallPhase read "ready".
  It now carries the same unlock form the lock screen uses.
- Imported BCH wallets were never registered with the WC manager —
  startForWallet ran only in the vault-derived mount branch. They mount
  as ready, so they appeared in the "Sign with" picker and then failed
  on pair. They now register from their stored seed. WC derives a child
  key tree, so single-key (WIF) imports genuinely cannot pair; those are
  disabled in the picker with the reason, rather than failing on click.
- Adds window.wizardconnect so a dapp can hand over the wiz:// URI it
  already generated instead of making the user copy it between tabs.
  The protocol is Nostr-relay pairing designed for phone-scans-QR, and
  the SDK has no in-page discovery at all, so this is our own surface:
  connect() + isReady(), plus a wizardconnect:announceProvider event
  shaped like EIP-6963 so several WC wallets can coexist. Pairing always
  goes through the approval modal; the URI is validated before any UI
  shows, and the wallet never reads the page to find one.
2026-09-22 23:08:16 +02:00

553 lines
26 KiB
JavaScript

// 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 }.
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);
});
// -------- 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);
// -------- WizardConnect bridge (window.wizardconnect), everywhere -----------
//
// WizardConnect is a Nostr-relay pairing protocol built for CROSS-device use:
// the dapp calls initiateDappRelay(), gets a wiz:// uri, and renders it as a
// QR for a phone wallet to scan. On the same device that QR round-trip is
// pure friction — the dapp and Aegis are in the same browser.
//
// The SDK has no in-page wallet discovery, so this is Silent Mode's own
// surface. A dapp keeps its existing initiateDappRelay() call and simply
// hands us the uri it already generated:
//
// const { uri } = initiateDappRelay(onStatus);
// if (window.wizardconnect) await window.wizardconnect.connect(uri);
// else renderQr(uri); // unchanged fallback
//
// connect() resolves once the user approves the pairing in Aegis and the
// key exchange completes, and rejects if they decline. Nothing is paired
// without an explicit approval, and we never read the page to find a uri —
// the dapp hands it to us.
const wizardconnect = {
isAegis: true,
version: "1.0.0",
// Present so a dapp can tell "wallet is installed" from "wallet is
// installed but has no BCH wallet ready to pair with" before it decides
// whether to fall back to a QR.
isReady: () => call("wcPageReady"),
connect: (uri) => call("wcConnectFromPage", { uri: String(uri ?? "") }),
};
theseus.contextBridge.exposeInMainWorld("wizardconnect", wizardconnect);
// -------- 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 {} }
}
// 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 {}
}
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;
}
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;
}
case "wallet_switchEthereumChain": {
const target = String((params[0] && params[0].chainId) || "").toLowerCase();
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;
}
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) {
if (!tx || typeof tx.serialize !== "function") {
throw new Error("Aegis: pass a @solana/web3.js Transaction (needs .serialize)");
}
// 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)) });
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 {}
// Same announce/request handshake for WizardConnect. The WC SDK defines
// no discovery mechanism at all, so we borrow EIP-6963's shape: a dapp
// that wants to support several WC wallets dispatches
// "wizardconnect:requestProvider" and collects the announcements instead
// of reaching for window.wizardconnect and finding whoever won the race.
// window.wizardconnect stays for the simple single-wallet case.
try {
const wcInfo = { 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 announceWc = () => {
const provider = window.wizardconnect;
if (!provider) return;
window.dispatchEvent(new CustomEvent("wizardconnect:announceProvider", { detail: Object.freeze({ info: wcInfo, provider }) }));
};
announceWc();
window.addEventListener("wizardconnect:requestProvider", announceWc);
} 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);
}