- eth_sendTransaction dropped the calldata, gas and fee fields, so an ERC-20 transfer went out as a 0-value send to the token contract and any contract call was broadcast as something else. plan() now carries data/gas/fee caps/nonce, estimates gas for calls, and the overlay decodes transfer/approve/permit/setApprovalForAll, flags unlimited approvals and calldata to a non-contract, and shows estimated vs max fee. - personal_sign signed the hex string ethers/viem send as literal text; it now signs the decoded bytes. - wallet_switchEthereumChain flipped the global selected wallet, so any site could move every connected dapp to another chain. Chain is now per origin; the sidebar selection no longer redirects a dapp. - wallet_addEthereumChain silently persisted a connection for chains Aegis already had, handing the address to any site. Adding a chain no longer grants anything, and RPC URLs must be https. - eth_sign (blind hash signing) is disabled, as in MetaMask.
614 lines
29 KiB
JavaScript
614 lines
29 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), everywhere -----------------------
|
|
//
|
|
// This used to be gated on a `.x` hostname, inherited from the
|
|
// pre-multi-wallet build where every dapp Aegis knew about was a Silent Mode
|
|
// one. It was never a security boundary: the TRX / ETH / SOL bridges have
|
|
// always injected on every page, and every call here still crosses into
|
|
// main, raises an approval overlay and honours per-origin permissions.
|
|
// Nothing in this file can read an address or sign anything on its own.
|
|
//
|
|
// Keeping it meant Cauldron, bch.guru and our own non-.x dapps (potidaea.asm)
|
|
// saw no BCH provider at all, on a wallet whose whole point is BCH.
|
|
// WizardConnect below was never gated this way, so the two halves disagreed.
|
|
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": {
|
|
// Blind signing of an arbitrary 32-byte hash — the same digest can be
|
|
// a transaction. MetaMask ships it disabled; Aegis does not offer it.
|
|
const err = new Error("eth_sign is disabled in Aegis (blind signing). Use personal_sign or eth_signTypedData_v4.");
|
|
err.code = 4200;
|
|
throw err;
|
|
}
|
|
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.
|
|
//
|
|
// Sites that enforce Trusted Types (CSP `require-trusted-types-for 'script'`)
|
|
// refuse a text script AND report the attempt to their CSP endpoint. Google's
|
|
// sign-in pages do exactly that, and a "something injected a script into our
|
|
// login page" report is a fine reason for them to call the browser insecure.
|
|
// No dapp lives on those origins, so the bridge stays out: a static list for
|
|
// the big enforcing sites (no report at all), plus a per-origin memory for
|
|
// any other site that rejected us once (one report, never again).
|
|
const NO_BRIDGE_HOSTS = /(^|\.)(google\.[a-z.]+|googleapis\.com|gstatic\.com|youtube\.com|googleusercontent\.com|microsoftonline\.com|microsoft\.com|live\.com|office\.com|github\.com|apple\.com|icloud\.com)$/i;
|
|
//
|
|
// The memory used to be "aegis:bridge-blocked", set on ANY failure. The
|
|
// commonest failure was ours: this preload often runs before the page has an
|
|
// <html> element, so the append threw on null and the origin lost the bridge
|
|
// for good. That key is ignored now; only a Trusted Types refusal is kept.
|
|
const BLOCKED_KEY = "aegis:bridge-blocked-tt";
|
|
function bridgeAllowedHere() {
|
|
try {
|
|
if (NO_BRIDGE_HOSTS.test(location.hostname)) return false;
|
|
if (localStorage.getItem(BLOCKED_KEY) === "1") return false;
|
|
} catch {}
|
|
return true;
|
|
}
|
|
function installBridge() {
|
|
try {
|
|
let src = mainWorldSource;
|
|
// Where Trusted Types exist but are not enforced, a policy keeps the
|
|
// assignment clean; where they are enforced with an allow-list the
|
|
// policy call itself throws and we fall through to the plain string.
|
|
if (window.trustedTypes && typeof window.trustedTypes.createPolicy === "function") {
|
|
try { src = window.trustedTypes.createPolicy("aegis-bridge", { createScript: (x) => x }).createScript(mainWorldSource); } catch {}
|
|
}
|
|
const s = document.createElement("script");
|
|
s.textContent = src;
|
|
(document.head || document.documentElement).appendChild(s);
|
|
s.remove();
|
|
} catch (e) {
|
|
const msg = String(e && e.message || e);
|
|
if (/trusted/i.test(msg)) {
|
|
try { localStorage.setItem(BLOCKED_KEY, "1"); } catch {}
|
|
console.warn("[aegis] this site enforces Trusted Types; the wallet bridge stays off here:", msg);
|
|
} else {
|
|
console.warn("[aegis] main-world bridge install failed:", msg);
|
|
}
|
|
}
|
|
}
|
|
if (bridgeAllowedHere()) {
|
|
if (document.documentElement) installBridge();
|
|
else {
|
|
// The parser inserts <html> before it runs any page script, and
|
|
// observer callbacks run at the microtask checkpoint that precedes each
|
|
// parser-inserted script, so the bridge is still first.
|
|
const mo = new MutationObserver(() => {
|
|
if (!document.documentElement) return;
|
|
mo.disconnect();
|
|
installBridge();
|
|
});
|
|
mo.observe(document, { childList: true });
|
|
}
|
|
}
|