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.
2026-09-06 02:56:34 +02:00
//
// `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
2026-09-06 02:56:34 +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 { } }
}
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 ( ) ;
return invoke ( "eth.switchChain" , { chainId : target } ) ;
}
case "wallet_addEthereumChain" :
throw new Error ( "Aegis: chains are managed from the wallet's own settings; add the chain there." ) ;
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 ) ;
}