SPL tokens now show up in the Solana wallet — balances on the Receive
card, an asset picker on Send that flips the amount input into the
token's own units. Sends build a TransferChecked + auto-create the
recipient's Associated Token Account (idempotently) in the same
transaction, so the user never has to fund an ATA by hand.
- lib/sol-spl.js: SPL primitives that don't need @solana/web3.js.
TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID,
findProgramAddress (PDA loop backed by an ed25519 is-on-curve check
via @noble Point.fromBytes), associatedTokenAddress (matches the
spl-token JS seed layout: [owner, tokenProgram, mint]),
transferCheckedInstruction (discriminator 12, u64 amount, decimals
byte), createATAIdempotentInstruction (associated-token program
discriminator 1). A small known-mint registry ships inline for USDC /
USDT / wSOL on mainnet + USDC on devnet — everything else falls back
to a truncated mint address in the UI.
- Message assembler classifies every unique pubkey into writable-signed
/ readonly-signed / writable-unsigned / readonly-unsigned, sorts the
fee payer first, and serializes header + accountKeys + blockhash +
instructions using Solana's compact-u16 short-vec encoding. Same
wire shape @solana/web3.js produces from Transaction.serializeMessage.
- lib/chain-sol.js: snapshot() now carries a tokens[] array of
{mint, symbol, name, decimals, balance, tokenAccount, tokenProgram,
isKnown, isToken2022}. Fetched via getTokenAccountsByOwner against
both the classic Token program and Token-2022. New planTokenTransfer
+ signAndBroadcastToken handle a full send (TransferChecked +
optional CreateATAIdempotent) in one wire.
- Panel: Send tab gained an Asset dropdown (SOL / <each token>) that
only shows for SOL wallets with tokens. Picking a token flips the
unit picker's big-unit to the token symbol, amount goes in the
token's own decimals, planTokenSend + sendToken take over from
planSend/send. Receive tab gained a Tokens card listing each SPL
balance with a per-row Send button that pre-fills the asset picker.
- Verified in scratchpad: ATA derivation runs the PDA loop
correctly (owner pubkey passes isOnCurve, derived ATA does not —
the definitional property of a Program-Derived Address). Cross-check
the ATA for any (owner, mint) on Phantom / Solscan / spl-token JS
and the value matches.
Known limits:
- No token metadata lookup on-chain — mints outside the built-in
registry show up with a truncated mint address as symbol. Wiring
Metaplex Metadata program reads would let unknown tokens show
their real names.
- Send is single-signer only (the wallet is the fee payer, sender
and sole required signer). Multi-sig SPL transfers work via the
dapp bridge (window.solana.signAndSendTransaction, which already
handles partial signatures).
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 };
|
|
};
|