Setup 5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643 Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4 Bundled since 0.3.27: 32d4b75 - Aegis (bchwallet) gains its own update card in Settings > General beside Ariadne. Check for updates hits the same signed OTA endpoint the boot timer uses; Restart to apply appears when a signed newer version is staged. Uses the existing addons-check-updates + a new app-restart IPC. New Aegis versions ship without a Theseus release. 32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to the right of the tab (mode: 'right') instead of a detached window. Matches stock Chrome. Users who prefer detached can drag out via the DevTools own toolbar. b71c925 - Search-engine favicons in Settings > Search now use Google's /s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough hosts (Brave, Bing, Yandex, etc.) that half the list was falling through to the emoji placeholder. Deployed. Verified LIVE 0.3.28.
473 lines
20 KiB
JavaScript
473 lines
20 KiB
JavaScript
// Solana (SOL) chain adapter — mainnet-beta + devnet. Ed25519 keypair per
|
|
// SLIP-0010 (all-hardened path), base58 address, native SOL transfers via
|
|
// the system program. Sits directly on the JSON-RPC endpoint; no @solana
|
|
// SDK dep so the addon stays lean.
|
|
//
|
|
// Derivation: m/44'/501'/0'/0' — Phantom's default path. Every segment is
|
|
// hardened per SLIP-0010 (ed25519 forbids non-hardened derivation because
|
|
// the point-add trick that BIP32 uses on secp256k1 doesn't exist for
|
|
// Curve25519). Multi-index accounts under one wallet aren't exposed here;
|
|
// each Aegis "sub-account" gets its own vault-derive purpose instead.
|
|
//
|
|
// Not implemented in this rev:
|
|
// - SPL token balances / transfers (needs Associated Token Account math
|
|
// and the SPL Token program's transfer instruction).
|
|
// - Transaction history (getSignaturesForAddress + getTransaction is
|
|
// doable but heavy for a first cut; the panel links to Solana Explorer
|
|
// for now).
|
|
|
|
const NETWORKS = {
|
|
mainnet: {
|
|
id: "mainnet", label: "Mainnet",
|
|
defaultRpc: "https://api.mainnet-beta.solana.com",
|
|
explorerTx: "https://explorer.solana.com/tx/",
|
|
explorerAddr: "https://explorer.solana.com/address/",
|
|
explorerCluster: "",
|
|
faucet: null,
|
|
},
|
|
devnet: {
|
|
id: "devnet", label: "Devnet",
|
|
defaultRpc: "https://api.devnet.solana.com",
|
|
explorerTx: "https://explorer.solana.com/tx/",
|
|
explorerAddr: "https://explorer.solana.com/address/",
|
|
explorerCluster: "?cluster=devnet",
|
|
faucet: "https://faucet.solana.com/",
|
|
},
|
|
};
|
|
|
|
// System program's address is 32 bytes of zeros; base58 is "1111...1111".
|
|
const SYSTEM_PROGRAM = new Uint8Array(32);
|
|
|
|
module.exports = function makeSolAdapter({ ed25519, base58, sha256 }) {
|
|
if (!ed25519 || !base58 || !sha256) throw new Error("chain-sol: missing dep");
|
|
const spl = require("./sol-spl.js")({ ed25519, sha256, base58 });
|
|
|
|
// ---- HMAC-SHA512 (for SLIP-0010) -------------------------------------
|
|
// Not in @noble/hashes/sha2 as a direct helper for SHA-512; @noble/hashes
|
|
// exports `hmac` in ./hmac. If unavailable, use Node's crypto — Electron
|
|
// main is Node, so require("crypto") always works.
|
|
const nodeCrypto = require("node:crypto");
|
|
function hmacSha512(key, msg) {
|
|
return new Uint8Array(nodeCrypto.createHmac("sha512", Buffer.from(key)).update(Buffer.from(msg)).digest());
|
|
}
|
|
|
|
// ---- SLIP-0010 ed25519 derivation ------------------------------------
|
|
const ED25519_MASTER_KEY = new TextEncoder().encode("ed25519 seed");
|
|
function slip10Master(seed32) {
|
|
const I = hmacSha512(ED25519_MASTER_KEY, seed32);
|
|
return { key: I.slice(0, 32), chainCode: I.slice(32) };
|
|
}
|
|
function slip10Derive(parent, indexHardened) {
|
|
// Data: 0x00 || parent.key || uint32BE(0x80000000 | index)
|
|
const idx = 0x80000000 | (indexHardened & 0x7fffffff);
|
|
const data = new Uint8Array(1 + 32 + 4);
|
|
data[0] = 0x00;
|
|
data.set(parent.key, 1);
|
|
// Write index as big-endian u32; JS bitwise is signed so |0 masks correctly.
|
|
data[33] = (idx >>> 24) & 0xff;
|
|
data[34] = (idx >>> 16) & 0xff;
|
|
data[35] = (idx >>> 8) & 0xff;
|
|
data[36] = idx & 0xff;
|
|
const I = hmacSha512(parent.chainCode, data);
|
|
return { key: I.slice(0, 32), chainCode: I.slice(32) };
|
|
}
|
|
function derivePath(seed32, segments) {
|
|
let node = slip10Master(seed32);
|
|
for (const s of segments) node = slip10Derive(node, s);
|
|
return node;
|
|
}
|
|
// "m/44'/501'/0'/0'" → [44, 501, 0, 0]. Every SLIP-0010 ed25519 segment
|
|
// is hardened; the parser accepts either the standard "'" suffix or a
|
|
// bare integer (both mean the same for this curve).
|
|
function parseAllHardened(path) {
|
|
const parts = String(path || "").trim().split("/").filter((p) => p && p !== "m");
|
|
return parts.map((p) => {
|
|
const m = /^(\d+)'?$/.exec(p);
|
|
if (!m) throw new Error("bad SOL derivation path: " + path);
|
|
return Number(m[1]);
|
|
});
|
|
}
|
|
|
|
// ---- Solana short-vec (compact-u16) ----------------------------------
|
|
// Up to 3 bytes; 7 data bits per byte with continuation bit in position 7.
|
|
function encodeCompactU16(n) {
|
|
if (n < 0 || n > 0xffff) throw new Error("compact-u16 out of range");
|
|
const out = [];
|
|
let rem = n;
|
|
while (true) {
|
|
let byte = rem & 0x7f;
|
|
rem >>= 7;
|
|
if (rem === 0) { out.push(byte); break; }
|
|
byte |= 0x80;
|
|
out.push(byte);
|
|
}
|
|
return Uint8Array.from(out);
|
|
}
|
|
|
|
const concat = (...ps) => {
|
|
const n = ps.reduce((a, p) => a + p.length, 0);
|
|
const o = new Uint8Array(n); let k = 0;
|
|
for (const p of ps) { o.set(p, k); k += p.length; }
|
|
return o;
|
|
};
|
|
const u64le = (n) => {
|
|
let v = BigInt(n); const o = new Uint8Array(8);
|
|
for (let i = 0; i < 8; i++) { o[i] = Number(v & 0xffn); v >>= 8n; }
|
|
return o;
|
|
};
|
|
|
|
// ---- transaction assembly --------------------------------------------
|
|
// For a native SOL transfer between two addresses:
|
|
// accounts (writable-signed | readonly-signed | writable-unsigned | readonly-unsigned):
|
|
// [ from (WS), to (WU), systemProgram (RU) ]
|
|
// header = [1 required-sig, 0 readonly-signed, 1 readonly-unsigned]
|
|
// instructions = [{ programIdIndex: 2, accounts: [0, 1], data: u32(2) || u64(lamports) }]
|
|
function buildSolTransferMessage({ fromPub, toPub, lamports, recentBlockhash }) {
|
|
// account keys must be de-duplicated in the message; distinct here.
|
|
const keys = [fromPub, toPub, SYSTEM_PROGRAM];
|
|
const header = Uint8Array.from([1, 0, 1]);
|
|
const keysSection = concat(
|
|
encodeCompactU16(keys.length),
|
|
...keys.map((k) => Uint8Array.from(k)),
|
|
);
|
|
// Instruction data: [2 (u32 LE = system Transfer discriminator), lamports (u64 LE)]
|
|
const instrData = concat(Uint8Array.from([2, 0, 0, 0]), u64le(lamports));
|
|
const instr = concat(
|
|
Uint8Array.from([2]), // programIdIndex
|
|
encodeCompactU16(2), // account key count
|
|
Uint8Array.from([0, 1]), // account indexes (from, to)
|
|
encodeCompactU16(instrData.length), // data length
|
|
instrData,
|
|
);
|
|
const instrSection = concat(encodeCompactU16(1), instr);
|
|
return concat(header, keysSection, recentBlockhash, instrSection);
|
|
}
|
|
|
|
// ---- JSON-RPC --------------------------------------------------------
|
|
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 SolWallet {
|
|
constructor(root32, networkId, {
|
|
walletId, storage, log = () => {}, onChange = () => {}, rpcUrl,
|
|
derivationPath = "m/44'/501'/0'/0'",
|
|
} = {}) {
|
|
if (!walletId) throw new Error("chain-sol: walletId required");
|
|
const net = NETWORKS[networkId];
|
|
if (!net) throw new Error(`chain-sol: unknown network ${networkId}`);
|
|
this.walletId = walletId;
|
|
this.chain = "sol";
|
|
this.network = net.id;
|
|
this._net = net;
|
|
this.log = log;
|
|
this.onChange = onChange;
|
|
this.storage = scopedStorage(storage, `wallets/${walletId}/`);
|
|
this._path = derivationPath;
|
|
const derived = derivePath(root32, parseAllHardened(derivationPath));
|
|
this._priv = derived.key; // 32-byte ed25519 seed
|
|
this._pub = ed25519.getPublicKey(this._priv); // 32 bytes
|
|
this.address = base58.encode(this._pub);
|
|
this._root = new Uint8Array(root32);
|
|
this._client = makeClient(String(rpcUrl || "").trim() || net.defaultRpc);
|
|
this._state = {
|
|
balance: { confirmed: 0, unconfirmed: 0 },
|
|
history: [],
|
|
tokens: [], // [{mint, symbol, name, decimals, balance, tokenAccount, tokenProgram}]
|
|
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 {} }
|
|
|
|
snapshot() {
|
|
return {
|
|
chain: "sol", network: this._net.id, ticker: "SOL", decimals: 9,
|
|
address: this.address, addressIndex: 0, addressPath: this._path,
|
|
balance: this._state.balance,
|
|
height: this._state.height,
|
|
history: this._state.history,
|
|
tokens: this._state.tokens,
|
|
scanning: this._state.scanning,
|
|
error: this._state.error,
|
|
server: this._client.url,
|
|
rpcUrl: this._client.url,
|
|
explorerTx: this._net.explorerTx,
|
|
explorerAddr: this._net.explorerAddr,
|
|
explorerSuffix: this._net.explorerCluster,
|
|
faucet: this._net.faucet,
|
|
};
|
|
}
|
|
|
|
async refresh() {
|
|
if (this._state.scanning) return;
|
|
this._state.scanning = true; this._state.error = null; this._emit();
|
|
try {
|
|
const [balRes, slot, tokens] = await Promise.all([
|
|
this._client.call("getBalance", [this.address]),
|
|
this._client.call("getSlot", []),
|
|
this._fetchTokens().catch((e) => { this.log("tokens fetch failed:", e?.message); return []; }),
|
|
]);
|
|
// getBalance response: { context, value: lamports }
|
|
const lamports = balRes && typeof balRes === "object" ? Number(balRes.value || 0) : Number(balRes || 0);
|
|
this._state.balance = { confirmed: lamports, unconfirmed: 0 };
|
|
this._state.height = Number(slot || 0);
|
|
this._state.tokens = tokens;
|
|
} catch (e) {
|
|
this._state.error = e?.message || String(e);
|
|
this.log("refresh failed:", this._state.error);
|
|
} finally {
|
|
this._state.scanning = false;
|
|
this._emit();
|
|
}
|
|
}
|
|
// getTokenAccountsByOwner + parse. Result shape:
|
|
// {context, value: [{ pubkey, account: {data: {parsed: {info:{mint, tokenAmount:{amount,decimals,uiAmountString}}}, program, space}, executable, ...} }]}
|
|
// We ask for jsonParsed encoding so the RPC does the layout heavy-lift.
|
|
async _fetchTokens() {
|
|
const known = spl.KNOWN_TOKENS[this._net.id] || {};
|
|
const call = (programB58) => this._client.call("getTokenAccountsByOwner", [
|
|
this.address,
|
|
{ programId: programB58 },
|
|
{ encoding: "jsonParsed", commitment: "confirmed" },
|
|
]);
|
|
const results = await Promise.all([
|
|
call(spl.TOKEN_PROGRAM_ID_B58).catch(() => ({ value: [] })),
|
|
call(spl.TOKEN_2022_PROGRAM_ID_B58).catch(() => ({ value: [] })),
|
|
]);
|
|
const out = [];
|
|
for (let p = 0; p < results.length; p++) {
|
|
const list = results[p]?.value || [];
|
|
const isTk22 = p === 1;
|
|
for (const it of list) {
|
|
const info = it?.account?.data?.parsed?.info;
|
|
if (!info) continue;
|
|
const mint = String(info.mint || "");
|
|
const dec = Number(info.tokenAmount?.decimals || 0);
|
|
const rawAmount = String(info.tokenAmount?.amount || "0");
|
|
const meta = known[mint];
|
|
out.push({
|
|
mint, tokenAccount: String(it.pubkey || ""),
|
|
tokenProgram: isTk22 ? spl.TOKEN_2022_PROGRAM_ID_B58 : spl.TOKEN_PROGRAM_ID_B58,
|
|
symbol: meta?.symbol || mint.slice(0, 6) + "…",
|
|
name: meta?.name || null,
|
|
decimals: dec,
|
|
balance: rawAmount, // string (u64) to preserve precision
|
|
isKnown: !!meta,
|
|
isToken2022: isTk22,
|
|
});
|
|
}
|
|
}
|
|
// Sort known tokens first, then by balance desc.
|
|
out.sort((a, b) => (b.isKnown - a.isKnown) || (BigInt(b.balance) > BigInt(a.balance) ? 1 : -1));
|
|
return out;
|
|
}
|
|
schedulePoll(ms = 20_000) {
|
|
clearTimeout(this._pollTimer);
|
|
this._pollTimer = setTimeout(() => this.refresh().finally(() => this.schedulePoll(ms)), ms);
|
|
}
|
|
|
|
// Solana fees are (usually) 5000 lamports per signature; reserve that
|
|
// in max-mode. Real fee comes from the network on broadcast.
|
|
async plan({ to, amount, sendMax }) {
|
|
const toBytes = base58.decode(String(to || "").trim());
|
|
if (!toBytes || toBytes.length !== 32) throw new Error("bad Solana address");
|
|
const bal = this._state.balance.confirmed || 0;
|
|
const FEE = 5000; // lamports per signature
|
|
let lamports;
|
|
if (sendMax) {
|
|
if (bal <= FEE) throw new Error("balance does not cover the fee");
|
|
lamports = bal - FEE;
|
|
} else {
|
|
lamports = Math.round(Number(amount) || 0);
|
|
if (!(lamports > 0)) throw new Error("amount must be > 0 lamports");
|
|
if (lamports + FEE > bal) throw new Error("insufficient funds");
|
|
}
|
|
// Fetch the fresh blockhash at plan time so signing can use it
|
|
// immediately — Solana blockhashes expire quickly (~150 slots ≈ 60s).
|
|
const { blockhash } = (await this._client.call("getLatestBlockhash", [])).value || {};
|
|
if (!blockhash) throw new Error("could not fetch a recent blockhash");
|
|
const recent = base58.decode(String(blockhash));
|
|
return {
|
|
_draft: { toBytes, lamports, recentBlockhash: recent },
|
|
recipients: [{ to: base58.encode(toBytes), value: lamports }],
|
|
fee: FEE, feeRate: FEE,
|
|
inputs: [], change: 0,
|
|
total: lamports + FEE,
|
|
};
|
|
}
|
|
|
|
async signAndBroadcast(plan) {
|
|
const d = plan && plan._draft;
|
|
if (!d) throw new Error("bad plan");
|
|
const message = buildSolTransferMessage({
|
|
fromPub: this._pub, toPub: d.toBytes, lamports: d.lamports,
|
|
recentBlockhash: d.recentBlockhash,
|
|
});
|
|
const sig = ed25519.sign(message, this._priv); // 64 bytes
|
|
// Full transaction wire format: sig-count || sigs... || message
|
|
const sigCount = encodeCompactU16(1);
|
|
const wire = concat(sigCount, sig, message);
|
|
// Solana's sendTransaction accepts base58 (default) or base64 with
|
|
// {encoding:"base64"} in the second arg; we use base58 for parity
|
|
// with the rest of the ecosystem.
|
|
const wireBase58 = base58.encode(wire);
|
|
const txid = await this._client.call("sendTransaction", [wireBase58]);
|
|
if (typeof txid !== "string" || !txid.length) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
|
|
this.log("broadcast", txid);
|
|
setTimeout(() => this.refresh(), 4000);
|
|
return { txid };
|
|
}
|
|
|
|
// ---- SPL token send -----------------------------------------------
|
|
// Build a TransferChecked (+ optional CreateAssociatedTokenAccountIdempotent)
|
|
// transaction moving `amount` (in raw token units) of a given mint to a
|
|
// recipient's ATA. `mintB58` is the mint address as a base58 string;
|
|
// decimals come from the sender's own token account (or the caller
|
|
// passes them explicitly if the sender's ATA is empty).
|
|
async planTokenTransfer({ mint, to, amount, decimals, tokenProgram }) {
|
|
const mintB58 = String(mint || "");
|
|
const mintBytes = base58.decode(mintB58);
|
|
if (mintBytes.length !== 32) throw new Error("bad mint address");
|
|
const recipientBytes = base58.decode(String(to || "").trim());
|
|
if (recipientBytes.length !== 32) throw new Error("bad recipient address");
|
|
// Resolve the token program for this mint from our own token list —
|
|
// Token-2022 mints need the 2022 program in the transfer instruction.
|
|
let tp = tokenProgram ? base58.decode(tokenProgram) : spl.TOKEN_PROGRAM_ID;
|
|
let dec = decimals;
|
|
const mine = (this._state.tokens || []).find((t) => t.mint === mintB58);
|
|
if (mine) {
|
|
tp = base58.decode(mine.tokenProgram);
|
|
if (dec == null) dec = mine.decimals;
|
|
}
|
|
if (dec == null) throw new Error("token decimals unknown — no ATA on this wallet for that mint");
|
|
const amt = BigInt(amount);
|
|
if (amt <= 0n) throw new Error("amount must be > 0");
|
|
if (mine && BigInt(mine.balance) < amt) throw new Error("insufficient token balance");
|
|
const sourceATA = spl.associatedTokenAddress(this._pub, mintBytes, tp);
|
|
const destATA = spl.associatedTokenAddress(recipientBytes, mintBytes, tp);
|
|
// Ask the RPC whether the destination ATA already exists. If not,
|
|
// prepend a CreateIdempotent instruction so the transfer succeeds
|
|
// in one round-trip — the recipient never has to have interacted
|
|
// with this mint before.
|
|
const destATA_B58 = base58.encode(destATA);
|
|
const info = await this._client.call("getAccountInfo", [destATA_B58, { encoding: "base64" }]);
|
|
const destExists = !!(info && info.value);
|
|
const instructions = [];
|
|
if (!destExists) {
|
|
instructions.push(spl.createATAIdempotentInstruction({
|
|
payer: this._pub, ata: destATA, owner: recipientBytes, mint: mintBytes, tokenProgram: tp,
|
|
}));
|
|
}
|
|
instructions.push(spl.transferCheckedInstruction({
|
|
sourceATA, mint: mintBytes, destATA, owner: this._pub,
|
|
amount: amt, decimals: dec, tokenProgram: tp,
|
|
}));
|
|
const { blockhash } = (await this._client.call("getLatestBlockhash", [])).value || {};
|
|
if (!blockhash) throw new Error("could not fetch a recent blockhash");
|
|
const recentBlockhash = base58.decode(String(blockhash));
|
|
return {
|
|
_spl: { instructions, recentBlockhash, destExists, sourceATA, destATA },
|
|
recipients: [{ to: base58.encode(recipientBytes), value: amt.toString() }],
|
|
// Fee estimate: 5000 lamports per signature + ~2039280 rent-exempt
|
|
// if we're creating a new ATA. Real fee still comes from the network.
|
|
fee: destExists ? 5000 : 5000 + 2039280,
|
|
feeRate: 5000,
|
|
inputs: [],
|
|
change: 0,
|
|
total: amt.toString(),
|
|
mint: mintB58,
|
|
decimals: dec,
|
|
};
|
|
}
|
|
async signAndBroadcastToken(plan) {
|
|
const sp = plan && plan._spl;
|
|
if (!sp) throw new Error("bad token plan");
|
|
const message = spl.buildMessage({
|
|
feePayer: this._pub,
|
|
instructions: sp.instructions,
|
|
recentBlockhash: sp.recentBlockhash,
|
|
});
|
|
const sig = ed25519.sign(message, this._priv);
|
|
const encodeCompactU16 = (n) => {
|
|
const out = [];
|
|
let rem = n;
|
|
while (true) {
|
|
let byte = rem & 0x7f; rem >>= 7;
|
|
if (rem === 0) { out.push(byte); break; }
|
|
byte |= 0x80; out.push(byte);
|
|
}
|
|
return Uint8Array.from(out);
|
|
};
|
|
const sigCount = encodeCompactU16(1);
|
|
const wire = new Uint8Array(sigCount.length + 64 + message.length);
|
|
wire.set(sigCount, 0);
|
|
wire.set(sig, sigCount.length);
|
|
wire.set(message, sigCount.length + 64);
|
|
const wireB58 = base58.encode(wire);
|
|
const txid = await this._client.call("sendTransaction", [wireB58]);
|
|
if (typeof txid !== "string" || !txid.length) throw new Error("bad txid from RPC: " + JSON.stringify(txid));
|
|
this.log("SPL broadcast", txid);
|
|
setTimeout(() => this.refresh().catch(() => {}), 4000);
|
|
return { txid };
|
|
}
|
|
|
|
// Solana's convention: ed25519 signature over the raw message bytes,
|
|
// returned as {publicKey, signature} both base58. Dapps that follow
|
|
// the wallet-adapter standard verify against these.
|
|
signMessage(message) {
|
|
const bytes = new TextEncoder().encode(String(message));
|
|
const sig = ed25519.sign(bytes, this._priv);
|
|
return {
|
|
address: this.address,
|
|
publicKey: base58.encode(this._pub),
|
|
signature: base58.encode(sig),
|
|
};
|
|
}
|
|
|
|
recovery() {
|
|
return {
|
|
accountPath: this._path,
|
|
xpub: base58.encode(this._pub),
|
|
xprv: Buffer.from(this._priv).toString("hex"),
|
|
};
|
|
}
|
|
|
|
dispose() {
|
|
clearTimeout(this._pollTimer);
|
|
try { this._priv && this._priv.fill(0); } catch {}
|
|
try { this._root && this._root.fill(0); } catch {}
|
|
}
|
|
}
|
|
|
|
return { SolWallet, NETWORKS };
|
|
};
|