feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
|
|
|
// Ethereum (EVM) chain adapter — mainnet + Sepolia testnet. One address per
|
|
|
|
|
// wallet, the same "TronLink shape" as chain-tron.js: BIP44 derivation,
|
|
|
|
|
// secp256k1 → keccak256 address, JSON-RPC backend, EIP-1559 send.
|
|
|
|
|
//
|
|
|
|
|
// Kept intentionally minimal:
|
|
|
|
|
// - Native ETH only. ERC-20 token support is a follow-up: it needs a token
|
|
|
|
|
// registry + eth_call for `balanceOf(address)` per token + a dedicated
|
|
|
|
|
// Send flow that builds an ERC-20 `transfer(to, value)` calldata.
|
|
|
|
|
// - No transaction history: without an indexer (Etherscan V2 / Alchemy)
|
|
|
|
|
// the JSON-RPC alone can't answer "which txs touched this address".
|
|
|
|
|
// The panel shows an empty history with a link to Etherscan.
|
|
|
|
|
// - EIP-1559 only (type 0x02). Legacy type 0x00 works too but isn't
|
|
|
|
|
// needed for mainnet or Sepolia in 2026.
|
|
|
|
|
|
|
|
|
|
const NETWORKS = {
|
|
|
|
|
mainnet: {
|
|
|
|
|
id: "mainnet", label: "Mainnet", chainId: 1,
|
|
|
|
|
// Cloudflare's public Ethereum gateway — no key required, rate-limited
|
|
|
|
|
// but adequate for a per-user wallet. User can override in settings.
|
|
|
|
|
defaultRpc: "https://cloudflare-eth.com",
|
|
|
|
|
explorerTx: "https://etherscan.io/tx/",
|
|
|
|
|
explorerAddr: "https://etherscan.io/address/",
|
|
|
|
|
faucet: null,
|
|
|
|
|
},
|
|
|
|
|
sepolia: {
|
|
|
|
|
id: "sepolia", label: "Sepolia testnet", chainId: 11155111,
|
|
|
|
|
defaultRpc: "https://ethereum-sepolia-rpc.publicnode.com",
|
|
|
|
|
explorerTx: "https://sepolia.etherscan.io/tx/",
|
|
|
|
|
explorerAddr: "https://sepolia.etherscan.io/address/",
|
|
|
|
|
faucet: "https://sepoliafaucet.com/",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
module.exports = function makeEthAdapter({ HDKey, secp256k1, keccak_256 }) {
|
|
|
|
|
if (!HDKey || !secp256k1 || !keccak_256) throw new Error("chain-eth: missing dep");
|
|
|
|
|
|
|
|
|
|
const toHex = (b) => Buffer.from(b).toString("hex");
|
|
|
|
|
const fromHex = (h) => Uint8Array.from(Buffer.from(String(h).replace(/^0x/i, ""), "hex"));
|
|
|
|
|
const stripHex = (h) => String(h).replace(/^0x/i, "");
|
|
|
|
|
const hexToBig = (h) => BigInt("0x" + (stripHex(h) || "0"));
|
|
|
|
|
const bigToHex = (n) => "0x" + BigInt(n).toString(16);
|
|
|
|
|
const zeroBig = 0n;
|
|
|
|
|
|
|
|
|
|
// ---- addresses -------------------------------------------------------
|
|
|
|
|
// EIP-55 mixed-case checksum: lowercase hex, then flip case per keccak256
|
|
|
|
|
// of the lowercase hex string (a-f digits get uppercased where the keccak
|
|
|
|
|
// nibble is >= 8). Never needed for wire format (RPCs accept lowercase),
|
|
|
|
|
// but it's what wallets show, so we return it that way.
|
|
|
|
|
function eip55(addressLowerHex) {
|
|
|
|
|
const lower = stripHex(addressLowerHex).toLowerCase();
|
|
|
|
|
const hash = toHex(keccak_256(Buffer.from(lower, "utf8")));
|
|
|
|
|
let out = "0x";
|
|
|
|
|
for (let i = 0; i < lower.length; i++) {
|
|
|
|
|
const c = lower[i];
|
|
|
|
|
out += /[0-9]/.test(c) ? c : (parseInt(hash[i], 16) >= 8 ? c.toUpperCase() : c);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
function addressFromPubkey(uncompressed) {
|
|
|
|
|
const inner = uncompressed.slice(1);
|
|
|
|
|
const h = keccak_256(inner);
|
|
|
|
|
const h20 = h.slice(h.length - 20);
|
|
|
|
|
return eip55(toHex(h20));
|
|
|
|
|
}
|
|
|
|
|
function decodeAddress(str) {
|
|
|
|
|
const s = String(str || "").trim();
|
|
|
|
|
const hex = stripHex(s);
|
|
|
|
|
if (!/^[0-9a-fA-F]{40}$/.test(hex)) throw new Error("bad Ethereum address");
|
|
|
|
|
// Reject checksum mismatches on mixed-case inputs (all-lower and all-upper
|
|
|
|
|
// pass unconditionally — that's the EIP-55 rule).
|
|
|
|
|
const lower = hex.toLowerCase(), upper = hex.toUpperCase();
|
|
|
|
|
if (hex !== lower && hex !== upper) {
|
|
|
|
|
const want = stripHex(eip55(lower));
|
|
|
|
|
if (hex !== want) throw new Error("EIP-55 checksum failed");
|
|
|
|
|
}
|
|
|
|
|
return "0x" + lower;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- RLP encode ------------------------------------------------------
|
|
|
|
|
// Minimal encoder — enough for EIP-1559 tx encoding. Follows the RLP spec
|
|
|
|
|
// (single byte < 0x80 → self; short string ≤ 55 → 0x80 + len + bytes;
|
|
|
|
|
// long string → 0x80 + 55 + lenOfLen + lenBytes + bytes; lists similarly
|
|
|
|
|
// with 0xc0/0xf7).
|
|
|
|
|
function rlpEncodeBytes(bytes) {
|
|
|
|
|
const b = Uint8Array.from(bytes);
|
|
|
|
|
if (b.length === 1 && b[0] < 0x80) return b;
|
|
|
|
|
if (b.length <= 55) return concat(Uint8Array.from([0x80 + b.length]), b);
|
|
|
|
|
const lenBytes = encodeIntBE(b.length);
|
|
|
|
|
return concat(Uint8Array.from([0xb7 + lenBytes.length]), lenBytes, b);
|
|
|
|
|
}
|
|
|
|
|
function rlpEncodeList(items) {
|
|
|
|
|
const encoded = items.map(rlpEncode);
|
|
|
|
|
const body = concat(...encoded);
|
|
|
|
|
if (body.length <= 55) return concat(Uint8Array.from([0xc0 + body.length]), body);
|
|
|
|
|
const lenBytes = encodeIntBE(body.length);
|
|
|
|
|
return concat(Uint8Array.from([0xf7 + lenBytes.length]), lenBytes, body);
|
|
|
|
|
}
|
|
|
|
|
function rlpEncode(item) {
|
|
|
|
|
if (item instanceof Uint8Array) return rlpEncodeBytes(item);
|
|
|
|
|
if (Array.isArray(item)) return rlpEncodeList(item);
|
|
|
|
|
if (typeof item === "bigint") return rlpEncodeBytes(bigToBytes(item));
|
|
|
|
|
if (typeof item === "number") return rlpEncodeBytes(bigToBytes(BigInt(item)));
|
|
|
|
|
if (typeof item === "string") return rlpEncodeBytes(item.startsWith("0x") ? fromHex(item) : Buffer.from(item, "utf8"));
|
|
|
|
|
throw new Error("rlp: unsupported item type " + typeof item);
|
|
|
|
|
}
|
|
|
|
|
function bigToBytes(v) {
|
|
|
|
|
if (v < 0n) throw new Error("negative bigint");
|
|
|
|
|
if (v === 0n) return new Uint8Array(0);
|
|
|
|
|
let hex = v.toString(16);
|
|
|
|
|
if (hex.length % 2) hex = "0" + hex;
|
|
|
|
|
return fromHex(hex);
|
|
|
|
|
}
|
|
|
|
|
function encodeIntBE(n) {
|
|
|
|
|
let hex = n.toString(16);
|
|
|
|
|
if (hex.length % 2) hex = "0" + hex;
|
|
|
|
|
return fromHex(hex);
|
|
|
|
|
}
|
|
|
|
|
function concat(...ps) {
|
|
|
|
|
const n = ps.reduce((a, p) => a + p.length, 0);
|
|
|
|
|
const out = new Uint8Array(n); let k = 0;
|
|
|
|
|
for (const p of ps) { out.set(p, k); k += p.length; }
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- signing ---------------------------------------------------------
|
|
|
|
|
// EIP-1559 signed tx: 0x02 || RLP([chainId, nonce, maxPriorityFeePerGas,
|
|
|
|
|
// maxFeePerGas, gasLimit, to, value, data, accessList,
|
|
|
|
|
// yParity, r, s])
|
|
|
|
|
// hash-to-sign: keccak256(0x02 || RLP([...same-without-sig-fields]))
|
|
|
|
|
function signTxEip1559(unsignedFields, privKey) {
|
|
|
|
|
const unsignedRlp = rlpEncodeList(unsignedFields);
|
|
|
|
|
const preimage = concat(Uint8Array.from([0x02]), unsignedRlp);
|
|
|
|
|
const hash = keccak_256(preimage);
|
|
|
|
|
const sig = secp256k1.sign(hash, privKey, { prehash: false, lowS: true, format: "recovered" });
|
|
|
|
|
// noble returns [recid || r(32) || s(32)]; EIP-1559 uses yParity as
|
|
|
|
|
// 0 or 1 (recid directly, no +27 shift).
|
|
|
|
|
const yParity = sig[0];
|
|
|
|
|
const r = sig.subarray(1, 33);
|
|
|
|
|
const s = sig.subarray(33, 65);
|
|
|
|
|
const signedFields = [...unsignedFields, yParity, stripLeadingZeros(r), stripLeadingZeros(s)];
|
|
|
|
|
const signedRlp = rlpEncodeList(signedFields);
|
|
|
|
|
return "0x" + toHex(concat(Uint8Array.from([0x02]), signedRlp));
|
|
|
|
|
}
|
|
|
|
|
function stripLeadingZeros(bytes) {
|
|
|
|
|
let i = 0;
|
|
|
|
|
while (i < bytes.length - 1 && bytes[i] === 0) i++;
|
|
|
|
|
return bytes.subarray(i);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- JSON-RPC client -------------------------------------------------
|
|
|
|
|
function makeClient(rpcUrl) {
|
|
|
|
|
let seq = 1;
|
|
|
|
|
async function call(method, params = []) {
|
|
|
|
|
const r = await fetch(rpcUrl, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: seq++, method, params }),
|
|
|
|
|
});
|
|
|
|
|
if (!r.ok) throw new Error(`${method}: HTTP ${r.status}`);
|
|
|
|
|
const j = await r.json();
|
|
|
|
|
if (j.error) throw new Error(`${method}: ${j.error.message || JSON.stringify(j.error)}`);
|
|
|
|
|
return j.result;
|
|
|
|
|
}
|
|
|
|
|
return { url: rpcUrl, call };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scopedStorage(storage, keyPrefix) {
|
|
|
|
|
const k = (key) => keyPrefix + key;
|
|
|
|
|
return {
|
|
|
|
|
get: (key, fallback = null) => storage.get(k(key), fallback),
|
|
|
|
|
set: (key, value) => storage.set(k(key), value),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- wallet ----------------------------------------------------------
|
|
|
|
|
class EthWallet {
|
|
|
|
|
constructor(root32, networkId, {
|
|
|
|
|
walletId, storage, log = () => {}, onChange = () => {}, rpcUrl,
|
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain
Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp
that wants to route through Polygon (or Base, or Arbitrum, or any other
EVM the Silent Mode user hasn't added yet) calls the pair the industry
already wrote for it — Aegis registers the chain, provisions a wallet on
it under the same vault seed, auto-connects the origin, fires
chainChanged, and hands the dapp back a provider pointed at the new
chain. No sidebar detour, no Custom RPC copy-paste. Users still see
every chain in the picker post-add and can revoke sites in Settings.
- lib/chain-eth.js: EthWallet accepts a customNetwork override
({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}).
When present it replaces the NETWORKS lookup so mainnet+Sepolia
ship built-in and every EIP-3085 chain is a runtime override the
addon persists. The ticker flows into snapshot() so the send
approval reads MATIC / BNB / whatever the chain's native currency is,
not a hardcoded ETH.
- index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl,
explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`.
Persisted under api.storage.customEthChains, so an added chain
survives Theseus restarts. chainMeta("eth", "custom-<chainId>")
synthesizes the meta from storage so the panel renders custom
chains without needing them in COINS at module-load time.
- eth.addChain handler (EIP-3085): approval overlay shows chain
name, decimal + hex chain id, native ticker, RPC and explorer
URLs (the phishing-signal quartet). On approval, persist config +
create wallet with a custom-<chainId> network + auto-grant the
origin readAddress on this chain. No-op success if the chain is
already added.
- eth.switchChain rewritten to be EIP-3326 correct: look up any
ready ETH wallet whose adapter reports the requested chainId,
make it the selected wallet, fire chainChanged. When no wallet
matches, throw with .code = 4902 (the standard 'chain not
added' code) so wagmi / RainbowKit / any 3326-aware dapp does
the fallback wallet_addEthereumChain call in the same click.
- eth.state handler: cheap {address, chainIdHex, networkVersion}
peek for the origin's currently-connected wallet (no approval,
no key access). The main-world bridge calls it after every
switch/add to emit chainChanged + accountsChanged locally — the
events MetaMask fires and RainbowKit listens for.
- wallet-inject.js: routes wallet_addEthereumChain via
eth.addChain, preserves the 4902 code across the postMessage
boundary on switch failures, calls pullEthStateAndEmit() to fire
the post-switch/add events.
2026-09-07 22:26:27 +02:00
|
|
|
customNetwork, // { id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker } for EIP-3085 chains
|
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
|
|
|
} = {}) {
|
|
|
|
|
if (!walletId) throw new Error("chain-eth: walletId required");
|
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain
Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp
that wants to route through Polygon (or Base, or Arbitrum, or any other
EVM the Silent Mode user hasn't added yet) calls the pair the industry
already wrote for it — Aegis registers the chain, provisions a wallet on
it under the same vault seed, auto-connects the origin, fires
chainChanged, and hands the dapp back a provider pointed at the new
chain. No sidebar detour, no Custom RPC copy-paste. Users still see
every chain in the picker post-add and can revoke sites in Settings.
- lib/chain-eth.js: EthWallet accepts a customNetwork override
({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}).
When present it replaces the NETWORKS lookup so mainnet+Sepolia
ship built-in and every EIP-3085 chain is a runtime override the
addon persists. The ticker flows into snapshot() so the send
approval reads MATIC / BNB / whatever the chain's native currency is,
not a hardcoded ETH.
- index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl,
explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`.
Persisted under api.storage.customEthChains, so an added chain
survives Theseus restarts. chainMeta("eth", "custom-<chainId>")
synthesizes the meta from storage so the panel renders custom
chains without needing them in COINS at module-load time.
- eth.addChain handler (EIP-3085): approval overlay shows chain
name, decimal + hex chain id, native ticker, RPC and explorer
URLs (the phishing-signal quartet). On approval, persist config +
create wallet with a custom-<chainId> network + auto-grant the
origin readAddress on this chain. No-op success if the chain is
already added.
- eth.switchChain rewritten to be EIP-3326 correct: look up any
ready ETH wallet whose adapter reports the requested chainId,
make it the selected wallet, fire chainChanged. When no wallet
matches, throw with .code = 4902 (the standard 'chain not
added' code) so wagmi / RainbowKit / any 3326-aware dapp does
the fallback wallet_addEthereumChain call in the same click.
- eth.state handler: cheap {address, chainIdHex, networkVersion}
peek for the origin's currently-connected wallet (no approval,
no key access). The main-world bridge calls it after every
switch/add to emit chainChanged + accountsChanged locally — the
events MetaMask fires and RainbowKit listens for.
- wallet-inject.js: routes wallet_addEthereumChain via
eth.addChain, preserves the 4902 code across the postMessage
boundary on switch failures, calls pullEthStateAndEmit() to fire
the post-switch/add events.
2026-09-07 22:26:27 +02:00
|
|
|
const net = customNetwork || NETWORKS[networkId];
|
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
|
|
|
if (!net) throw new Error(`chain-eth: unknown network ${networkId}`);
|
|
|
|
|
this.walletId = walletId;
|
|
|
|
|
this.chain = "eth";
|
|
|
|
|
this.network = net.id;
|
|
|
|
|
this._net = net;
|
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain
Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp
that wants to route through Polygon (or Base, or Arbitrum, or any other
EVM the Silent Mode user hasn't added yet) calls the pair the industry
already wrote for it — Aegis registers the chain, provisions a wallet on
it under the same vault seed, auto-connects the origin, fires
chainChanged, and hands the dapp back a provider pointed at the new
chain. No sidebar detour, no Custom RPC copy-paste. Users still see
every chain in the picker post-add and can revoke sites in Settings.
- lib/chain-eth.js: EthWallet accepts a customNetwork override
({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}).
When present it replaces the NETWORKS lookup so mainnet+Sepolia
ship built-in and every EIP-3085 chain is a runtime override the
addon persists. The ticker flows into snapshot() so the send
approval reads MATIC / BNB / whatever the chain's native currency is,
not a hardcoded ETH.
- index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl,
explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`.
Persisted under api.storage.customEthChains, so an added chain
survives Theseus restarts. chainMeta("eth", "custom-<chainId>")
synthesizes the meta from storage so the panel renders custom
chains without needing them in COINS at module-load time.
- eth.addChain handler (EIP-3085): approval overlay shows chain
name, decimal + hex chain id, native ticker, RPC and explorer
URLs (the phishing-signal quartet). On approval, persist config +
create wallet with a custom-<chainId> network + auto-grant the
origin readAddress on this chain. No-op success if the chain is
already added.
- eth.switchChain rewritten to be EIP-3326 correct: look up any
ready ETH wallet whose adapter reports the requested chainId,
make it the selected wallet, fire chainChanged. When no wallet
matches, throw with .code = 4902 (the standard 'chain not
added' code) so wagmi / RainbowKit / any 3326-aware dapp does
the fallback wallet_addEthereumChain call in the same click.
- eth.state handler: cheap {address, chainIdHex, networkVersion}
peek for the origin's currently-connected wallet (no approval,
no key access). The main-world bridge calls it after every
switch/add to emit chainChanged + accountsChanged locally — the
events MetaMask fires and RainbowKit listens for.
- wallet-inject.js: routes wallet_addEthereumChain via
eth.addChain, preserves the 4902 code across the postMessage
boundary on switch failures, calls pullEthStateAndEmit() to fire
the post-switch/add events.
2026-09-07 22:26:27 +02:00
|
|
|
this._ticker = net.ticker || "ETH";
|
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
|
|
|
this.log = log;
|
|
|
|
|
this.onChange = onChange;
|
|
|
|
|
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
|
|
|
|
const master = HDKey.fromMasterSeed(root32);
|
|
|
|
|
// BIP44 for Ethereum: m/44'/60'/0'/0/0 is the canonical first address.
|
|
|
|
|
const node = master.derive("m/44'/60'/0'/0/0");
|
|
|
|
|
this._priv = node.privateKey;
|
|
|
|
|
this._pubUncompressed = secp256k1.getPublicKey(this._priv, false);
|
|
|
|
|
this.address = addressFromPubkey(this._pubUncompressed);
|
|
|
|
|
this._root = new Uint8Array(root32);
|
|
|
|
|
this._client = makeClient(String(rpcUrl || "").trim() || net.defaultRpc);
|
|
|
|
|
this._state = {
|
|
|
|
|
balance: { confirmed: "0", unconfirmed: "0" },
|
|
|
|
|
history: [],
|
|
|
|
|
height: 0,
|
|
|
|
|
scanning: false,
|
|
|
|
|
error: null,
|
|
|
|
|
};
|
|
|
|
|
this._pollTimer = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setRpcUrl(url) {
|
|
|
|
|
const v = String(url || "").trim() || this._net.defaultRpc;
|
|
|
|
|
this._client = makeClient(v);
|
|
|
|
|
this._emit();
|
|
|
|
|
}
|
|
|
|
|
_emit() { try { this.onChange(); } catch {} }
|
|
|
|
|
|
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain
Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp
that wants to route through Polygon (or Base, or Arbitrum, or any other
EVM the Silent Mode user hasn't added yet) calls the pair the industry
already wrote for it — Aegis registers the chain, provisions a wallet on
it under the same vault seed, auto-connects the origin, fires
chainChanged, and hands the dapp back a provider pointed at the new
chain. No sidebar detour, no Custom RPC copy-paste. Users still see
every chain in the picker post-add and can revoke sites in Settings.
- lib/chain-eth.js: EthWallet accepts a customNetwork override
({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}).
When present it replaces the NETWORKS lookup so mainnet+Sepolia
ship built-in and every EIP-3085 chain is a runtime override the
addon persists. The ticker flows into snapshot() so the send
approval reads MATIC / BNB / whatever the chain's native currency is,
not a hardcoded ETH.
- index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl,
explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`.
Persisted under api.storage.customEthChains, so an added chain
survives Theseus restarts. chainMeta("eth", "custom-<chainId>")
synthesizes the meta from storage so the panel renders custom
chains without needing them in COINS at module-load time.
- eth.addChain handler (EIP-3085): approval overlay shows chain
name, decimal + hex chain id, native ticker, RPC and explorer
URLs (the phishing-signal quartet). On approval, persist config +
create wallet with a custom-<chainId> network + auto-grant the
origin readAddress on this chain. No-op success if the chain is
already added.
- eth.switchChain rewritten to be EIP-3326 correct: look up any
ready ETH wallet whose adapter reports the requested chainId,
make it the selected wallet, fire chainChanged. When no wallet
matches, throw with .code = 4902 (the standard 'chain not
added' code) so wagmi / RainbowKit / any 3326-aware dapp does
the fallback wallet_addEthereumChain call in the same click.
- eth.state handler: cheap {address, chainIdHex, networkVersion}
peek for the origin's currently-connected wallet (no approval,
no key access). The main-world bridge calls it after every
switch/add to emit chainChanged + accountsChanged locally — the
events MetaMask fires and RainbowKit listens for.
- wallet-inject.js: routes wallet_addEthereumChain via
eth.addChain, preserves the 4902 code across the postMessage
boundary on switch failures, calls pullEthStateAndEmit() to fire
the post-switch/add events.
2026-09-07 22:26:27 +02:00
|
|
|
// Wei is 10^18 native units; the panel formats via decimals=18. The
|
|
|
|
|
// ticker follows the chain's nativeCurrency (ETH on mainnet/Sepolia,
|
|
|
|
|
// MATIC on Polygon, etc.) so the send-approval overlay reads correctly.
|
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
|
|
|
snapshot() {
|
|
|
|
|
return {
|
feat(theseus/aegis): EIP-3085 wallet_addEthereumChain + EIP-3326 switchChain
Aegis now handles the standard MetaMask try-switch-then-add flow. A dapp
that wants to route through Polygon (or Base, or Arbitrum, or any other
EVM the Silent Mode user hasn't added yet) calls the pair the industry
already wrote for it — Aegis registers the chain, provisions a wallet on
it under the same vault seed, auto-connects the origin, fires
chainChanged, and hands the dapp back a provider pointed at the new
chain. No sidebar detour, no Custom RPC copy-paste. Users still see
every chain in the picker post-add and can revoke sites in Settings.
- lib/chain-eth.js: EthWallet accepts a customNetwork override
({id, label, chainId, defaultRpc, explorerTx, explorerAddr, ticker}).
When present it replaces the NETWORKS lookup so mainnet+Sepolia
ship built-in and every EIP-3085 chain is a runtime override the
addon persists. The ticker flows into snapshot() so the send
approval reads MATIC / BNB / whatever the chain's native currency is,
not a hardcoded ETH.
- index.js customEthChains storage: `{[chainId]: {chainName, rpcUrl,
explorerTx, explorerAddr, ticker, addedAt, addedByOrigin}}`.
Persisted under api.storage.customEthChains, so an added chain
survives Theseus restarts. chainMeta("eth", "custom-<chainId>")
synthesizes the meta from storage so the panel renders custom
chains without needing them in COINS at module-load time.
- eth.addChain handler (EIP-3085): approval overlay shows chain
name, decimal + hex chain id, native ticker, RPC and explorer
URLs (the phishing-signal quartet). On approval, persist config +
create wallet with a custom-<chainId> network + auto-grant the
origin readAddress on this chain. No-op success if the chain is
already added.
- eth.switchChain rewritten to be EIP-3326 correct: look up any
ready ETH wallet whose adapter reports the requested chainId,
make it the selected wallet, fire chainChanged. When no wallet
matches, throw with .code = 4902 (the standard 'chain not
added' code) so wagmi / RainbowKit / any 3326-aware dapp does
the fallback wallet_addEthereumChain call in the same click.
- eth.state handler: cheap {address, chainIdHex, networkVersion}
peek for the origin's currently-connected wallet (no approval,
no key access). The main-world bridge calls it after every
switch/add to emit chainChanged + accountsChanged locally — the
events MetaMask fires and RainbowKit listens for.
- wallet-inject.js: routes wallet_addEthereumChain via
eth.addChain, preserves the 4902 code across the postMessage
boundary on switch failures, calls pullEthStateAndEmit() to fire
the post-switch/add events.
2026-09-07 22:26:27 +02:00
|
|
|
chain: "eth", network: this._net.id, ticker: this._ticker, decimals: 18,
|
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
|
|
|
address: this.address, addressIndex: 0,
|
|
|
|
|
addressPath: "m/44'/60'/0'/0/0",
|
|
|
|
|
balance: this._state.balance,
|
|
|
|
|
height: this._state.height,
|
|
|
|
|
history: this._state.history,
|
|
|
|
|
scanning: this._state.scanning,
|
|
|
|
|
error: this._state.error,
|
|
|
|
|
server: this._client.url,
|
|
|
|
|
rpcUrl: this._client.url,
|
|
|
|
|
explorerTx: this._net.explorerTx,
|
|
|
|
|
explorerAddr: this._net.explorerAddr,
|
|
|
|
|
faucet: this._net.faucet,
|
|
|
|
|
chainId: this._net.chainId,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async refresh() {
|
|
|
|
|
if (this._state.scanning) return;
|
|
|
|
|
this._state.scanning = true; this._state.error = null; this._emit();
|
|
|
|
|
try {
|
|
|
|
|
const [bal, block] = await Promise.all([
|
|
|
|
|
this._client.call("eth_getBalance", [this.address, "latest"]),
|
|
|
|
|
this._client.call("eth_blockNumber", []),
|
|
|
|
|
]);
|
|
|
|
|
this._state.balance = { confirmed: hexToBig(bal).toString(), unconfirmed: "0" };
|
|
|
|
|
this._state.height = Number(hexToBig(block));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this._state.error = e?.message || String(e);
|
|
|
|
|
this.log("refresh failed:", this._state.error);
|
|
|
|
|
} finally {
|
|
|
|
|
this._state.scanning = false;
|
|
|
|
|
this._emit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
schedulePoll(ms = 20_000) {
|
|
|
|
|
clearTimeout(this._pollTimer);
|
|
|
|
|
this._pollTimer = setTimeout(() => this.refresh().finally(() => this.schedulePoll(ms)), ms);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async plan({ to, amount, sendMax }) {
|
|
|
|
|
const dest = decodeAddress(to);
|
|
|
|
|
const from = this.address.toLowerCase();
|
|
|
|
|
const [nonceHex, priorityHex, gasPriceHex, gasLimitHex] = await Promise.all([
|
|
|
|
|
this._client.call("eth_getTransactionCount", [from, "pending"]),
|
|
|
|
|
this._client.call("eth_maxPriorityFeePerGas", []).catch(() => "0x59682f00"), // fallback: 1.5 gwei
|
|
|
|
|
this._client.call("eth_gasPrice", []),
|
|
|
|
|
Promise.resolve("0x5208"), // 21000 for a plain ETH transfer
|
|
|
|
|
]);
|
|
|
|
|
const nonce = Number(hexToBig(nonceHex));
|
|
|
|
|
const maxPriorityFeePerGas = hexToBig(priorityHex);
|
|
|
|
|
// maxFeePerGas heuristic: 2 * base fee + priority tip. base fee ~=
|
|
|
|
|
// gasPrice - priority tip on EIP-1559 chains; we approximate with the
|
|
|
|
|
// reported gasPrice as an upper bound plus the priority.
|
|
|
|
|
const baseGuess = hexToBig(gasPriceHex);
|
|
|
|
|
const maxFeePerGas = baseGuess * 2n + maxPriorityFeePerGas;
|
|
|
|
|
const gasLimit = hexToBig(gasLimitHex);
|
|
|
|
|
const fee = gasLimit * maxFeePerGas;
|
|
|
|
|
const bal = BigInt(this._state.balance.confirmed || "0");
|
|
|
|
|
let value;
|
|
|
|
|
if (sendMax) {
|
|
|
|
|
if (bal <= fee) throw new Error("balance does not cover the gas fee");
|
|
|
|
|
value = bal - fee;
|
|
|
|
|
} else {
|
|
|
|
|
value = BigInt(Math.round(Number(amount) || 0)); // wei
|
|
|
|
|
if (value <= 0n) throw new Error("amount must be > 0 wei");
|
|
|
|
|
if (value + fee > bal) throw new Error("insufficient funds");
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
_draft: {
|
|
|
|
|
chainId: this._net.chainId, nonce, maxPriorityFeePerGas, maxFeePerGas,
|
|
|
|
|
gasLimit, to: dest, value, data: "0x", accessList: [],
|
|
|
|
|
},
|
|
|
|
|
recipients: [{ to: dest, value: value.toString() }],
|
|
|
|
|
fee: fee.toString(),
|
|
|
|
|
feeRate: maxFeePerGas.toString(),
|
|
|
|
|
inputs: [],
|
|
|
|
|
change: "0",
|
|
|
|
|
total: (value + fee).toString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async signAndBroadcast(plan) {
|
|
|
|
|
const d = plan && plan._draft;
|
|
|
|
|
if (!d) throw new Error("bad plan");
|
|
|
|
|
const unsignedFields = [
|
|
|
|
|
d.chainId, d.nonce, d.maxPriorityFeePerGas, d.maxFeePerGas, d.gasLimit,
|
|
|
|
|
fromHex(d.to.slice(2)), d.value, fromHex(""), [],
|
|
|
|
|
];
|
|
|
|
|
const rawTxHex = signTxEip1559(unsignedFields, this._priv);
|
|
|
|
|
const txid = await this._client.call("eth_sendRawTransaction", [rawTxHex]);
|
|
|
|
|
if (typeof txid !== "string" || !/^0x[0-9a-f]{64}$/i.test(txid)) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
|
|
|
|
|
this.log("broadcast", txid);
|
|
|
|
|
setTimeout(() => this.refresh(), 3000);
|
|
|
|
|
return { 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
|
|
|
// EIP-712: sign a pre-computed typed-data digest with r||s||v (v = 27+recid).
|
|
|
|
|
signTypedDataDigest(digest32) {
|
|
|
|
|
const sig = secp256k1.sign(digest32, this._priv, { prehash: false, lowS: true, format: "recovered" });
|
|
|
|
|
const out = new Uint8Array(65);
|
|
|
|
|
out.set(sig.subarray(1), 0);
|
|
|
|
|
out[64] = sig[0] + 27;
|
|
|
|
|
return { address: this.address, signature: "0x" + toHex(out) };
|
|
|
|
|
}
|
feat(theseus/aegis): Ethereum + Solana adapters, DGB address-family picker, Aegis-branded shield
Multi-currency coverage matches what aegis.x has been advertising: BCH,
TRX, SC, DGB, ETH, SOL — six coins, two-step coin/network picker for
each. Panel logos, favicon and fallback all read as Aegis.
- Ethereum (lib/chain-eth.js): mainnet + Sepolia. BIP44 m/44'/60'/0'/0/0
→ secp256k1 → EIP-55 checksummed hex address (verified against
MetaMask's canonical abandon×11 vector 0x9858EfFD23…4EcaEda94). JSON-RPC
backend (Cloudflare mainnet, PublicNode Sepolia by default; per-wallet
override). EIP-1559 send with an inline RLP encoder + secp256k1
recoverable sign; broadcast via eth_sendRawTransaction. personal_sign
message signing follows the \x19Ethereum Signed Message:\n prefix.
- Solana (lib/chain-sol.js): mainnet-beta + devnet. SLIP-0010 ed25519
derivation at m/44'/501'/0'/0' (all-hardened), base58 address (@noble
ed25519). SLIP-0010 layer verified against spec Test Vector 1 in
scratchpad/verify-slip10.mjs. Native SOL transfer via the system
program with compact-u16 message serialization + ed25519 sign +
sendTransaction. Devnet gets a faucet.solana.com link in Receive; the
panel appends ?cluster=devnet when opening the explorer.
- DGB address family selector (lib/chain-dgb.js already carried the
paths): the Settings block now shows a Native SegWit / Taproot /
Wrapped SegWit / Legacy P2PKH picker. Selecting a family auto-fills
the derivation-path input with that family's default; Apply
rebuilds the wallet against the new path. Address families exposed
via chainMeta.addressFamilies so the panel can render them from data.
- Panel branding: inline SVG shield (hexagonal aspis, same silhouette
as the aegis.x hero) replaces the "?" fallback in logoSvg() and is
what the header shows before a wallet is selected. Data-URI favicon
wired into panel.html so the Theseus sidebar tab icon reads as Aegis
rather than a chain-specific coin mark.
- QR payloads now follow each chain's own URI scheme (BIP21 for BCH/DGB,
EIP-681 for ETH, Solana Pay for SOL) so external scanners route the
scan to the right wallet.
Not shipped: EIP-1193 provider (window.ethereum) and wallet-adapter
protocol (window.solana). The signing paths exist; only the page-inject
bridge glue is missing. History for ETH/SOL is also empty in this rev —
both need indexer plumbing (Etherscan V2 for ETH, getSignaturesForAddress
+ getTransaction pagination for SOL).
2026-09-07 20:31:27 +02:00
|
|
|
// Ethereum personal_sign: keccak256("\x19Ethereum Signed Message:\n" + len + msg).
|
|
|
|
|
signMessage(message) {
|
|
|
|
|
const msg = String(message);
|
|
|
|
|
const enc = new TextEncoder();
|
|
|
|
|
const body = enc.encode(msg);
|
|
|
|
|
const prefix = enc.encode("\x19Ethereum Signed Message:\n" + body.length);
|
|
|
|
|
const buf = concat(prefix, body);
|
|
|
|
|
const hash = keccak_256(buf);
|
|
|
|
|
const sig = secp256k1.sign(hash, this._priv, { prehash: false, lowS: true, format: "recovered" });
|
|
|
|
|
// personal_sign format: r || s || v where v = 27 + recid.
|
|
|
|
|
const out = new Uint8Array(65);
|
|
|
|
|
out.set(sig.subarray(1), 0);
|
|
|
|
|
out[64] = sig[0] + 27;
|
|
|
|
|
return { address: this.address, signature: "0x" + toHex(out) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recovery() {
|
|
|
|
|
// Ethereum wallets typically expose the raw private key hex; we do too,
|
|
|
|
|
// but only when the caller re-confirms in the approval overlay upstream.
|
|
|
|
|
return {
|
|
|
|
|
accountPath: "m/44'/60'/0'/0/0",
|
|
|
|
|
xpub: "0x" + toHex(this._pubUncompressed),
|
|
|
|
|
xprv: "0x" + toHex(this._priv),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
|
clearTimeout(this._pollTimer);
|
|
|
|
|
try { this._priv && this._priv.fill(0); } catch {}
|
|
|
|
|
try { this._root && this._root.fill(0); } catch {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { EthWallet, NETWORKS, addressFromPubkey, decodeAddress, eip55 };
|
|
|
|
|
};
|