Theseus core:
- addons-host: manifest.category ("plugin") propagates through snapshot(); new
addon API surface checkAndStageSelfUpdate() + restartApp() so a plug-in
can offer in-panel "update now → restart to apply" without pushing the
user to Settings.
- main.js: wires the two new hooks into the AddonHost constructor.
- settings.html: Extensions listing filters out category==="plugin"; those
add-ons live in Plug-ins instead, single source of truth.
Aegis 0.6.31:
- BTC picker trimmed to Signet only; testnet3 hidden (adapter kept so any
existing wallet still loads).
- Wallet strip groups by chain, not chain:network; ticker gets a ▾ chevron
and a dropdown listing every subnetwork with its own totals. Mainnet
reads as the plain ticker; testnets carry a small Chipnet/Signet/Sepolia
pill inline.
- Per-unit price sits directly under the ticker; amount + fiat mirror on
the right — one glance covers name/price/holding/value.
- + Add and ⋯ More promoted from the strip into the header's action row,
next to the new ✎ chip (was the redundant top ⋯). Duplicate "Manage
current wallet" entry removed from the More menu.
- Footer update chip is a two-step flow via the new API: stage → restart.
Falls back to opening Settings on any Theseus that lacks the hooks.
- Manifest declares "category": "plugin".
137 lines
6.3 KiB
JavaScript
137 lines
6.3 KiB
JavaScript
// Generic single-address read-only imported adapter for account-model
|
|
// chains. One config-driven runtime handles ETH-family, Tron, and Solana
|
|
// balance polling — every chain differs only in the RPC verb and the
|
|
// JSON path to the balance number.
|
|
//
|
|
// The adapter mirrors the public shape every Aegis chain runtime exposes
|
|
// (snapshot, refresh, plan, signAndBroadcast, dispose) so mountWallet
|
|
// stays chain-agnostic. planSend/send throw a "read-only" error until
|
|
// M.1b delivers the sign path per chain.
|
|
|
|
module.exports = function makeGenericImportedAdapter() {
|
|
|
|
const CHAIN_CFGS = {
|
|
eth: {
|
|
ticker: "ETH", decimals: 18,
|
|
networks: {
|
|
mainnet: { id: "mainnet", label: "Mainnet", rpc: "https://eth.llamarpc.com", explorerAddr: "https://etherscan.io/address/", explorerTx: "https://etherscan.io/tx/" },
|
|
sepolia: { id: "sepolia", label: "Sepolia", rpc: "https://ethereum-sepolia-rpc.publicnode.com", explorerAddr: "https://sepolia.etherscan.io/address/", explorerTx: "https://sepolia.etherscan.io/tx/", testnet: true, faucet: "https://sepoliafaucet.com/" },
|
|
},
|
|
// JSON-RPC eth_getBalance → hex-string wei.
|
|
async fetchBalance({ rpc, address }) {
|
|
const r = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getBalance", params: [address, "latest"] }) });
|
|
const j = await r.json();
|
|
const hex = String(j?.result || "0x0").replace(/^0x/, "");
|
|
return BigInt("0x" + hex).toString();
|
|
},
|
|
},
|
|
trx: {
|
|
ticker: "TRX", decimals: 6,
|
|
networks: {
|
|
mainnet: { id: "mainnet", label: "Mainnet", rpc: "https://api.trongrid.io", explorerAddr: "https://tronscan.org/#/address/", explorerTx: "https://tronscan.org/#/transaction/" },
|
|
nile: { id: "nile", label: "Nile testnet", rpc: "https://api.nileex.io", explorerAddr: "https://nile.tronscan.org/#/address/", explorerTx: "https://nile.tronscan.org/#/transaction/", testnet: true, faucet: "https://nileex.io/join/getJoinPage" },
|
|
},
|
|
// Tron HTTP API returns account.balance in SUN (10^-6 TRX).
|
|
async fetchBalance({ rpc, address }) {
|
|
const r = await fetch(rpc.replace(/\/+$/, "") + "/wallet/getaccount", { method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ address, visible: true }) });
|
|
const j = await r.json();
|
|
return String(j?.balance || 0);
|
|
},
|
|
},
|
|
sol: {
|
|
ticker: "SOL", decimals: 9,
|
|
networks: {
|
|
mainnet: { id: "mainnet", label: "Mainnet-beta", rpc: "https://api.mainnet-beta.solana.com", explorerAddr: "https://explorer.solana.com/address/", explorerTx: "https://explorer.solana.com/tx/" },
|
|
devnet: { id: "devnet", label: "Devnet", rpc: "https://api.devnet.solana.com", explorerAddr: "https://explorer.solana.com/address/", explorerTx: "https://explorer.solana.com/tx/", explorerSuffix: "?cluster=devnet", testnet: true, faucet: "https://faucet.solana.com/" },
|
|
},
|
|
// Solana JSON-RPC getBalance returns lamports as a number.
|
|
async fetchBalance({ rpc, address }) {
|
|
const r = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getBalance", params: [address] }) });
|
|
const j = await r.json();
|
|
return String(j?.result?.value || 0);
|
|
},
|
|
},
|
|
};
|
|
|
|
class GenericImportedWallet {
|
|
constructor({ chain, network, address, log = () => {}, onChange = () => {}, rpcUrl } = {}) {
|
|
const cfg = CHAIN_CFGS[chain]; if (!cfg) throw new Error(`chain-generic-imported: unknown chain ${chain}`);
|
|
const net = cfg.networks[network]; if (!net) throw new Error(`chain-generic-imported: ${chain} has no network ${network}`);
|
|
if (!address) throw new Error("address required");
|
|
this.chain = chain;
|
|
this.network = network;
|
|
this._cfg = cfg;
|
|
this._net = { ...net, rpc: rpcUrl || net.rpc };
|
|
this.log = log;
|
|
this.onChange = onChange;
|
|
this._address = address;
|
|
this._state = {
|
|
balance: { confirmed: "0", unconfirmed: "0" },
|
|
history: [],
|
|
scanning: false,
|
|
error: null,
|
|
};
|
|
this._pollTimer = null;
|
|
}
|
|
|
|
setServers() { /* no-op: this adapter uses HTTP RPC, not electrum */ }
|
|
schedulePoll(ms) {
|
|
clearTimeout(this._pollTimer);
|
|
this._pollTimer = setTimeout(() => { this.refresh(false).catch(() => {}); this.schedulePoll(ms); }, ms);
|
|
}
|
|
|
|
_emit() { try { this.onChange(); } catch {} }
|
|
|
|
snapshot() {
|
|
return {
|
|
chain: this.chain, network: this.network,
|
|
ticker: this._cfg.ticker, decimals: this._cfg.decimals,
|
|
address: this._address,
|
|
addressIndex: 0,
|
|
addressPath: null,
|
|
balance: this._state.balance,
|
|
history: this._state.history,
|
|
scanning: this._state.scanning,
|
|
error: this._state.error,
|
|
server: this._net.rpc,
|
|
rpcUrl: this._net.rpc,
|
|
imported: true,
|
|
explorerAddr: this._net.explorerAddr,
|
|
explorerTx: this._net.explorerTx,
|
|
explorerSuffix: this._net.explorerSuffix || "",
|
|
faucet: this._net.faucet || null,
|
|
};
|
|
}
|
|
|
|
async refresh() {
|
|
this._state.scanning = true; this._emit();
|
|
try {
|
|
const confirmed = await this._cfg.fetchBalance({ rpc: this._net.rpc, address: this._address });
|
|
this._state.balance = { confirmed: String(confirmed || 0), unconfirmed: "0" };
|
|
this._state.error = null;
|
|
} catch (e) {
|
|
this._state.error = e?.message || String(e);
|
|
} finally {
|
|
this._state.scanning = false;
|
|
this._emit();
|
|
}
|
|
}
|
|
|
|
nextAddress() { return { address: this._address, index: 0 }; }
|
|
current() { return { address: this._address, index: 0, branch: 0, path: null }; }
|
|
|
|
plan() { throw new Error(`Imported ${this.chain.toUpperCase()} wallets are read-only in this build. Spending support ships in the next Aegis update.`); }
|
|
signAndBroadcast() { throw new Error("read-only"); }
|
|
signMessage() { throw new Error("read-only"); }
|
|
|
|
recovery() { return { accountPath: null, xpub: null, xprv: null, note: "Recovery lives in the source of the import." }; }
|
|
|
|
dispose() { clearTimeout(this._pollTimer); }
|
|
}
|
|
|
|
return { GenericImportedWallet, CHAIN_CFGS };
|
|
};
|