EXTENSION for TreeOS
solana
Every node in the tree can own a Solana wallet. The extension generates keypairs on demand, encrypts private keys with AES-256-GCM using a server-side master key, and stores them in node metadata. No private key ever leaves the server unencrypted. Wallets are per-node: a Finance branch holds SOL, a Rewards node holds tokens, a project node accepts payments. Each wallet is a real Solana address on mainnet. Three operations: create a wallet on any node, send SOL or SPL tokens to a Solana address or another node (auto-creates the destination wallet if needed), and swap tokens through Jupiter's Ultra API with configurable slippage. Sends are fee-aware and auto-adjust amounts to cover rent exemption and transaction fees. Swaps handle the full Jupiter flow: order creation, server-side transaction signing, and execution. Balance sync reads on-chain SOL balance and Jupiter token holdings, then writes them to the values namespace using the _auto__ prefix convention so they appear alongside user-set values. USD prices fetch from Jupiter's price API. Stale token entries auto-cleanup when holdings change. CLI commands expose wallet info, send, and swap without touching the AI conversation. HTML views available when html-rendering is installed.
v1.0.1 by TreeOS Site 0 downloads 8 files 2,927 lines 83.8 KB published 38d ago
treeos ext install solana
View changelog

Manifest

Provides

  • routes
  • 1 CLI commands

Requires

  • models: Node

Optional

  • services: energy
  • extensions: html-rendering, treeos-base
SHA256: dbbae5d493be2b6291e47a592494bdc298f235a782eca84d0fc27c2b9d7270d1

CLI Commands

CommandMethodDescription
walletGETSolana wallet. Actions: create, send, swap. No action shows info.
wallet createPOSTCreate a wallet on this node
wallet sendPOSTSend SOL. Usage: wallet send <amount> <address or nodeId>
wallet swapPOSTSwap tokens via Jupiter. Usage: wallet swap <fromMint> <toMint> <amount>. Type 'sol' instead of SOL's full mint address.

Environment Variables

KeyRequiredDescription
NODE_WALLET_MASTER_KEY secret Yes 32-byte hex key for server-side wallets. Back this up.
SOLANA_RPC_URL Yes Solana RPC endpoint (default: https://api.mainnet-beta.solana.com)
JUP_API_KEY No Jupiter API key for swaps and price data

Source Code

1import log from "../../seed/log.js";
2import {
3  Keypair,
4  Connection,
5  Transaction,
6  SystemProgram,
7  PublicKey,
8  sendAndConfirmTransaction,
9  VersionedTransaction,
10} from "@solana/web3.js";
11import {
12  getAssociatedTokenAddress,
13  createTransferInstruction,
14} from "@solana/spl-token";
15import crypto from "crypto";
16
17// Node model wired from init via setModels
18let Node = null;
19let _metadata = null;
20export function setModels(models) { Node = models.Node; }
21export function setMetadata(metadata) { _metadata = metadata; }
22
23/* ------------------------------------------------------------------ */
24/*  Wallet metadata helpers                                            */
25/* ------------------------------------------------------------------ */
26
27function getWallet(node, versionIndex) {
28  const meta = _metadata.getExtMeta(node, "solana");
29  return meta.wallets?.[versionIndex] || null;
30}
31
32async function setWallet(node, versionIndex, walletData) {
33  const meta = _metadata.getExtMeta(node, "solana");
34  if (!meta.wallets) meta.wallets = {};
35  meta.wallets[versionIndex] = walletData;
36  await _metadata.setExtMeta(node, "solana", meta);
37}
38
39const JUP_BASE = "https://api.jup.ag/ultra/v1";
40const SOL_MINT = "So11111111111111111111111111111111111111112";
41
42/* ------------------------------------------------------------------ */
43/*  Config                                                            */
44/* ------------------------------------------------------------------ */
45
46const SOLANA_ENABLED = !!(process.env.SOLANA_RPC_URL && process.env.NODE_WALLET_MASTER_KEY);
47
48const connection = SOLANA_ENABLED ? new Connection(process.env.SOLANA_RPC_URL, "confirmed") : null;
49
50const MASTER_KEY = SOLANA_ENABLED ? Buffer.from(process.env.NODE_WALLET_MASTER_KEY, "hex") : null;
51if (MASTER_KEY && MASTER_KEY.length !== 32) {
52  throw new Error("NODE_WALLET_MASTER_KEY must be 32 bytes (hex)");
53}
54
55function requireSolana() {
56  if (!SOLANA_ENABLED) throw new Error("Solana is not configured (missing SOLANA_RPC_URL or NODE_WALLET_MASTER_KEY)");
57}
58
59/* ------------------------------------------------------------------ */
60/*  Encryption helpers (INTERNAL)                                      */
61/* ------------------------------------------------------------------ */
62
63function encryptSecretKey(secretKey) {
64  const iv = crypto.randomBytes(12);
65  const cipher = crypto.createCipheriv("aes-256-gcm", MASTER_KEY, iv);
66
67  const encrypted = Buffer.concat([
68    cipher.update(Buffer.from(secretKey)),
69    cipher.final(),
70  ]);
71
72  return {
73    iv: iv.toString("base64"),
74    tag: cipher.getAuthTag().toString("base64"),
75    data: encrypted.toString("base64"),
76  };
77}
78
79function decryptSecretKey(enc) {
80  const decipher = crypto.createDecipheriv(
81    "aes-256-gcm",
82    MASTER_KEY,
83    Buffer.from(enc.iv, "base64"),
84  );
85
86  decipher.setAuthTag(Buffer.from(enc.tag, "base64"));
87
88  const decrypted = Buffer.concat([
89    decipher.update(Buffer.from(enc.data, "base64")),
90    decipher.final(),
91  ]);
92
93  return Uint8Array.from(decrypted);
94}
95
96/* ------------------------------------------------------------------ */
97/*  Wallet creation (PUBLIC, SAFE)                                     */
98/* ------------------------------------------------------------------ */
99
100export async function ensureVersionWallet(nodeId, versionIndex) {
101  requireSolana();
102  const node = await Node.findById(nodeId);
103  if (!node) throw new Error("Node not found");
104
105  const version = { values: {} };
106  if (!version) throw new Error("Version not found");
107
108  const existing = getWallet(node, versionIndex);
109  if (existing?.publicKey) {
110    return { publicKey: existing.publicKey, created: false };
111  }
112
113  const keypair = Keypair.generate();
114  const encryptedPrivateKey = encryptSecretKey(keypair.secretKey);
115
116  setWallet(node, versionIndex, {
117    publicKey: keypair.publicKey.toBase58(),
118    encryptedPrivateKey,
119    createdAt: new Date(),
120  });
121
122  return {
123    publicKey: keypair.publicKey.toBase58(),
124    created: true,
125  };
126}
127
128/* ------------------------------------------------------------------ */
129/*  INTERNAL signing access                                            */
130/* ------------------------------------------------------------------ */
131
132async function getVersionKeypair(node, versionIndex) {
133  const wallet = getWallet(node, versionIndex);
134
135  if (!wallet?.encryptedPrivateKey) {
136    throw new Error("Version wallet does not exist");
137  }
138
139  const secretKey = decryptSecretKey(wallet.encryptedPrivateKey);
140  return Keypair.fromSecretKey(secretKey);
141}
142
143/* ------------------------------------------------------------------ */
144/*  Balance sync (SOL only)                                            */
145/* ------------------------------------------------------------------ */
146
147export async function syncVersionSOLBalance(node, versionIndex) {
148  requireSolana();
149  const wallet = getWallet(node, versionIndex);
150
151  if (!wallet?.publicKey) {
152    return null;
153  }
154
155  const pubkey = new PublicKey(wallet.publicKey);
156  const lamports = await connection.getBalance(pubkey);
157
158  // Store SOL balance in own namespace
159  const solData = _metadata.getExtMeta(node, "solana") || {};
160  solData.sol = lamports;
161  await _metadata.setExtMeta(node, "solana", solData);
162
163  return lamports;
164}
165
166/* ------------------------------------------------------------------ */
167/*  Public wallet info (READ ONLY)                                     */
168/* ------------------------------------------------------------------ */
169
170export async function getVersionWalletInfo(nodeId, versionIndex) {
171  requireSolana();
172  if (!Number.isInteger(versionIndex) || versionIndex < 0) {
173    throw new Error("Invalid version index");
174  }
175
176  const node = await Node.findById(nodeId);
177  const wallet = getWallet(node, versionIndex);
178
179  if (!wallet?.publicKey) {
180    return { exists: false };
181  }
182
183  // Read from own namespace
184  const solData = _metadata.getExtMeta(node, "solana") || {};
185
186  const tokens = [];
187  const tokenData = solData.tokens || {};
188
189  for (const [mint, data] of Object.entries(tokenData)) {
190    tokens.push({
191      mint,
192      uiAmount: data.balance ?? 0,
193      usd: data.usd ?? null,
194    });
195  }
196
197  return {
198    exists: true,
199    publicKey: wallet.publicKey,
200    solBalance: solData.sol ?? 0,
201    tokens,
202  };
203}
204
205/* ------------------------------------------------------------------ */
206/*  Send SOL                                                          */
207/* ------------------------------------------------------------------ */
208
209async function resolveDestinationPublicKey({ toAddress, toNodeId }) {
210  // Case 1: direct Solana address
211  if (toAddress) {
212    return {
213      pubkey: new PublicKey(toAddress),
214      node: null,
215      versionIndex: null,
216    };
217  }
218
219  // Case 2: node → latest version (auto-create wallet if missing)
220  if (toNodeId) {
221    const node = await Node.findById(toNodeId);
222    if (!node) throw new Error("Destination node not found");
223
224    const versionIndex = 0;
225
226    // Ensure destination wallet exists
227    await ensureVersionWallet(toNodeId, versionIndex);
228
229    // Reload to get the wallet
230    const updated = await Node.findById(toNodeId);
231    const destWallet = getWallet(updated, versionIndex);
232    const walletPubkey = destWallet?.publicKey;
233
234    if (!walletPubkey) {
235      throw new Error("Failed to resolve destination wallet");
236    }
237
238    return {
239      pubkey: new PublicKey(walletPubkey),
240      node: updated,
241      versionIndex,
242    };
243  }
244
245  throw new Error("Must provide either toAddress or toNodeId");
246}
247
248export async function sendSOLFromVersion({
249  nodeId,
250  versionIndex,
251  toAddress,
252  toNodeId,
253  lamports,
254}) {
255  requireSolana();
256  if (!Number.isInteger(versionIndex) || versionIndex < 0) {
257    throw new Error("Invalid version index");
258  }
259
260  if (toNodeId && toNodeId === nodeId) {
261    throw new Error("Cannot send SOL to the same node");
262  }
263
264  if (!Number.isSafeInteger(lamports) || lamports <= 0) {
265    throw new Error("Invalid lamports");
266  }
267
268  // Ensure sender wallet exists
269  await ensureVersionWallet(nodeId, versionIndex);
270
271  const node = await Node.findById(nodeId);
272  const signer = await getVersionKeypair(node, versionIndex);
273
274  const dest = await resolveDestinationPublicKey({
275    toAddress,
276    toNodeId,
277  });
278  const destinationPubkey = dest.pubkey;
279
280  /* -------------------------------------------------- */
281  /*  Fee + rent aware auto-adjust                      */
282  /* -------------------------------------------------- */
283
284  const [senderBalance, destInfo] = await Promise.all([
285    connection.getBalance(signer.publicKey),
286    connection.getAccountInfo(destinationPubkey),
287  ]);
288
289  let overhead = 0;
290
291  // Destination account doesn't exist → must be rent exempt
292  if (!destInfo) {
293    overhead += await connection.getMinimumBalanceForRentExemption(0);
294  }
295
296  // Conservative fee buffer
297  const FEE_BUFFER = 10_000; // ~0.00001 SOL
298  overhead += FEE_BUFFER;
299
300  const maxSendable = senderBalance - overhead;
301
302  if (maxSendable <= 0) {
303    throw new Error("Insufficient SOL to cover fees and rent");
304  }
305
306  // Reject if requested amount exceeds available balance after fees
307  if (lamports > maxSendable) {
308    throw new Error(
309      `Requested ${lamports} lamports but only ${maxSendable} sendable after fees and rent. ` +
310      `Reduce the amount or add more SOL.`
311    );
312  }
313  const finalLamports = lamports;
314
315  /* -------------------------------------------------- */
316  /*  Build + send transaction                          */
317  /* -------------------------------------------------- */
318
319  const tx = new Transaction().add(
320    SystemProgram.transfer({
321      fromPubkey: signer.publicKey,
322      toPubkey: destinationPubkey,
323      lamports: finalLamports,
324    }),
325  );
326
327  let signature;
328
329  try {
330    signature = await sendAndConfirmTransaction(connection, tx, [signer]);
331  } catch (err) {
332    // Surface Solana logs if available
333    if (typeof err?.getLogs === "function") {
334      const logs = await err.getLogs();
335 log.error("Solana", "Solana tx logs:", logs);
336    }
337    throw err;
338  }
339
340  /* -------------------------------------------------- */
341  /*  Sync + persist                                   */
342  /* -------------------------------------------------- */
343
344  await syncVersionSOLBalance(node, versionIndex);
345  if (dest.node) {
346    await syncVersionSOLBalance(dest.node, dest.versionIndex);
347  }
348
349  return {
350    signature,
351    from: signer.publicKey.toBase58(),
352    to: destinationPubkey.toBase58(),
353    lamports: finalLamports,
354  };
355}
356
357/* ------------------------------------------------------------------ */
358/*  Send SPL token                                                     */
359/* ------------------------------------------------------------------ */
360
361export async function sendSPLTokenFromVersion({
362  nodeId,
363  versionIndex,
364  mintAddress,
365  toAddress,
366  amount,
367}) {
368  requireSolana();
369  if (!Number.isInteger(versionIndex) || versionIndex < 0) {
370    throw new Error("Invalid version index");
371  }
372
373  await ensureVersionWallet(nodeId, versionIndex);
374
375  const node = await Node.findById(nodeId);
376  const signer = await getVersionKeypair(node, versionIndex);
377
378  const mint = new PublicKey(mintAddress);
379  const owner = signer.publicKey;
380  const recipient = new PublicKey(toAddress);
381
382  const fromATA = await getAssociatedTokenAddress(mint, owner);
383  const toATA = await getAssociatedTokenAddress(mint, recipient);
384
385  const ix = createTransferInstruction(fromATA, toATA, owner, amount);
386
387  const tx = new Transaction().add(ix);
388
389  const sig = await sendAndConfirmTransaction(connection, tx, [signer]);
390
391  await syncVersionSOLBalance(node, versionIndex);
392  await syncVersionTokenHoldings(node, versionIndex);
393
394  return {
395    signature: sig,
396    tokenMint: mintAddress,
397    amount,
398  };
399}
400
401//==============JUPITER===================//
402
403const JUP_HOLDINGS_URL = "https://api.jup.ag/ultra/v1/holdings";
404const JUP_PRICE_URL = "https://api.jup.ag/price/v3";
405
406async function fetchHoldings(address) {
407  const res = await fetch(`${JUP_HOLDINGS_URL}/${address}`, {
408    headers: {
409      "x-api-key": process.env.JUP_API_KEY,
410    },
411  });
412
413  const bodyText = await res.text(); // read ONCE
414
415  if (!res.ok) {
416    throw new Error(`Jupiter holdings error: ${res.status} ${bodyText}`);
417  }
418
419  const data = JSON.parse(bodyText);
420  return data;
421}
422
423export async function syncVersionTokenHoldings(node, versionIndex) {
424  requireSolana();
425  const wallet = getWallet(node, versionIndex);
426  if (!wallet?.publicKey) return null;
427
428  const solData = _metadata.getExtMeta(node, "solana") || {};
429
430  const pubkey = wallet.publicKey;
431  const holdings = await fetchHoldings(pubkey);
432
433  const seenMints = [];
434  const balances = {};
435
436  /* ---------------------------------- */
437  /* 1. Aggregate SPL balances           */
438  /* ---------------------------------- */
439
440  for (const [mint, accounts] of Object.entries(holdings.tokens)) {
441    let totalUi = 0;
442    let decimals = null;
443
444    for (const acct of accounts) {
445      totalUi += acct.uiAmount;
446      decimals ??= acct.decimals;
447    }
448
449    if (totalUi <= 0) continue;
450
451    balances[mint] = { uiAmount: totalUi, decimals };
452    seenMints.push(mint);
453  }
454
455  /* ---------------------------------- */
456  /* 2. Fetch USD prices                 */
457  /* ---------------------------------- */
458
459  let prices = {};
460  try {
461    prices = await fetchPrices(seenMints);
462  } catch (err) {
463    log.warn("Solana", "Price fetch failed, skipping USD valuation:", err.message);
464  }
465
466  /* ---------------------------------- */
467  /* 3. Build clean token data           */
468  /* ---------------------------------- */
469
470  const tokenData = {};
471  for (const mint of seenMints) {
472    const price = prices[mint]?.usdPrice;
473    tokenData[mint] = {
474      balance: balances[mint].uiAmount,
475      decimals: balances[mint].decimals,
476      usd: price != null ? Number((balances[mint].uiAmount * price).toFixed(6)) : null,
477    };
478  }
479
480  solData.tokens = tokenData;
481  await _metadata.setExtMeta(node, "solana", solData);
482
483  return {
484    tokens: seenMints.length,
485  };
486}
487
488async function createJupiterOrder({
489  inputMint,
490  outputMint,
491  amount, // RAW units (string or number)
492  taker,
493  slippageBps = 50, // 0.5%
494}) {
495  const params = new URLSearchParams({
496    inputMint,
497    outputMint,
498    amount: amount.toString(),
499    taker,
500    slippageBps: slippageBps.toString(),
501  });
502
503  const res = await fetch(`${JUP_BASE}/order?${params}`, {
504    headers: {
505      "x-api-key": process.env.JUP_API_KEY,
506    },
507  });
508
509  const text = await res.text();
510  if (!res.ok) {
511    throw new Error(`Jupiter order error: ${text}`);
512  }
513
514  return JSON.parse(text);
515}
516function signJupiterTransaction(base64Tx, signer) {
517  const tx = VersionedTransaction.deserialize(Buffer.from(base64Tx, "base64"));
518
519  tx.sign([signer]);
520
521  return Buffer.from(tx.serialize()).toString("base64");
522}
523async function executeJupiterSwap({ signedTransaction, requestId }) {
524  const res = await fetch(`${JUP_BASE}/execute`, {
525    method: "POST",
526    headers: {
527      "Content-Type": "application/json",
528      "x-api-key": process.env.JUP_API_KEY,
529    },
530    body: JSON.stringify({
531      signedTransaction,
532      requestId,
533    }),
534  });
535
536  const text = await res.text();
537  if (!res.ok) {
538    throw new Error(`Jupiter execute error: ${text}`);
539  }
540
541  return JSON.parse(text);
542}
543
544export async function swapFromVersion({
545  nodeId,
546  versionIndex,
547  inputMint,
548  outputMint,
549  amountUi,
550  slippageBps,
551}) {
552  requireSolana();
553  await ensureVersionWallet(nodeId, versionIndex);
554
555  if (inputMint === SOL_MINT && outputMint === SOL_MINT) {
556    throw new Error("SOL to SOL swap is not allowed");
557  }
558
559  const node = await Node.findById(nodeId);
560  const solData = _metadata.getExtMeta(node, "solana") || {};
561  const signer = await getVersionKeypair(node, versionIndex);
562  const taker = signer.publicKey.toBase58();
563
564  /* ------------------------------ */
565  /* 1. Resolve raw amount           */
566  /* ------------------------------ */
567
568  const decimals = getStoredDecimals(solData, inputMint);
569  const amountRaw = uiToRaw(amountUi, decimals);
570
571  const availableUi = getAvailableUiBalance(solData, inputMint);
572
573  if (amountUi > availableUi) {
574    throw new Error("Insufficient balance");
575  }
576
577  if (amountRaw <= 0) {
578    throw new Error("Amount too small after conversion");
579  }
580
581  /* ------------------------------ */
582  /* 2. Create Jupiter order         */
583  /* ------------------------------ */
584
585  const order = await createJupiterOrder({
586    inputMint,
587    outputMint,
588    amount: amountRaw,
589    taker,
590    slippageBps,
591  });
592
593  if (!order.transaction) {
594    throw new Error(order.errorMessage || "No transaction returned");
595  }
596
597  /* ------------------------------ */
598  /* 3. Sign transaction             */
599  /* ------------------------------ */
600
601  const signedTx = signJupiterTransaction(order.transaction, signer);
602
603  /* ------------------------------ */
604  /* 4. Execute                      */
605  /* ------------------------------ */
606
607  const result = await executeJupiterSwap({
608    signedTransaction: signedTx,
609    requestId: order.requestId,
610  });
611
612  if (result.status !== "Success") {
613    throw new Error(result.error || "Swap failed");
614  }
615
616  /* ------------------------------ */
617  /* 5. Sync balances                */
618  /* ------------------------------ */
619
620  await syncVersionSOLBalance(node, versionIndex);
621  await syncVersionTokenHoldings(node, versionIndex);
622
623  return {
624    signature: result.signature,
625    inputMint,
626    outputMint,
627    inputAmountRaw: result.totalInputAmount,
628    outputAmountRaw: result.totalOutputAmount,
629  };
630}
631
632function getAvailableUiBalance(solData, mint) {
633  if (isSolMint(mint)) {
634    const lamports = solData.sol ?? 0;
635    return lamports / 1e9;
636  }
637
638  return solData.tokens?.[mint]?.balance ?? 0;
639}
640
641async function fetchPrices(mints) {
642  if (!mints.length) return {};
643
644  const params = new URLSearchParams({
645    ids: mints.join(","),
646  });
647
648  const res = await fetch(`${JUP_PRICE_URL}?${params}`, {
649    headers: {
650      "x-api-key": process.env.JUP_API_KEY,
651    },
652  });
653
654  const text = await res.text();
655  if (!res.ok) {
656    throw new Error(`Jupiter price error: ${text}`);
657  }
658
659  return JSON.parse(text);
660}
661const SOL_DECIMALS = 9;
662
663function isSolMint(mint) {
664  return mint === SOL_MINT;
665}
666
667function getStoredDecimals(solData, mint) {
668  if (isSolMint(mint)) return SOL_DECIMALS;
669
670  const dec = solData.tokens?.[mint]?.decimals;
671  if (typeof dec !== "number") {
672    throw new Error(`Missing decimals for token ${mint}`);
673  }
674  return dec;
675}
676
677function uiToRaw(uiAmount, decimals) {
678  if (typeof uiAmount !== "number" || uiAmount <= 0) {
679    throw new Error("Invalid UI amount");
680  }
681
682  const [whole, frac = ""] = uiAmount.toString().split(".");
683  const fracPadded = frac.padEnd(decimals, "0").slice(0, decimals);
684
685  return Number(BigInt(whole) * BigInt(10 ** decimals) + BigInt(fracPadded));
686}
687
1/* ------------------------------------------------- */
2/* HTML renderers for solana wallet pages              */
3/* ------------------------------------------------- */
4
5import { baseStyles } from "../html-rendering/html/baseStyles.js";
6
7export function renderSolanaNoWallet({
8  nodeId,
9  parsedVersion,
10  queryString,
11  token,
12}) {
13  return `
14<!DOCTYPE html>
15<html lang="en">
16<head>
17  <meta charset="UTF-8">
18  <meta name="viewport" content="width=device-width, initial-scale=1.0">
19  <meta name="theme-color" content="#667eea">
20  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
21  <title>Solana Wallet — Version ${parsedVersion}</title>
22  <style>
23${baseStyles}
24
25/* =========================================================
26   UNIFIED GLASS BUTTON SYSTEM
27   ========================================================= */
28
29.glass-btn,
30button,
31.back-link,
32.create-button {
33  position: relative;
34  overflow: hidden;
35
36  padding: 10px 20px;
37  border-radius: 980px;
38
39  display: inline-flex;
40  align-items: center;
41  justify-content: center;
42  white-space: nowrap;
43
44  background: rgba(var(--glass-water-rgb), var(--glass-alpha));
45  backdrop-filter: blur(22px) saturate(140%);
46  -webkit-backdrop-filter: blur(22px) saturate(140%);
47
48  color: white;
49  text-decoration: none;
50  font-family: inherit;
51
52  font-size: 15px;
53  font-weight: 500;
54  letter-spacing: -0.2px;
55
56  border: 1px solid rgba(255, 255, 255, 0.28);
57
58  box-shadow:
59    0 8px 24px rgba(0, 0, 0, 0.12),
60    inset 0 1px 0 rgba(255, 255, 255, 0.25);
61
62  cursor: pointer;
63
64  transition:
65    background 0.3s cubic-bezier(0.4, 0, 0.2, 1),
66    transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
67    box-shadow 0.3s ease;
68}
69
70/* Liquid light layer */
71.glass-btn::before,
72button::before,
73.back-link::before,
74.create-button::before {
75  content: "";
76  position: absolute;
77  inset: -40%;
78
79  background:
80    radial-gradient(
81      120% 60% at 0% 0%,
82      rgba(255, 255, 255, 0.35),
83      transparent 60%
84    ),
85    linear-gradient(
86      120deg,
87      transparent 30%,
88      rgba(255, 255, 255, 0.25),
89      transparent 70%
90    );
91
92  opacity: 0;
93  transform: translateX(-30%) translateY(-10%);
94  transition:
95    opacity 0.35s ease,
96    transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
97
98  pointer-events: none;
99}
100
101/* Hover motion */
102.glass-btn:hover,
103button:hover,
104.back-link:hover,
105.create-button:hover {
106  background: rgba(var(--glass-water-rgb), var(--glass-alpha-hover));
107  transform: translateY(-1px);
108  animation: waterDrift 2.2s ease-in-out infinite alternate;
109}
110
111.glass-btn:hover::before,
112button:hover::before,
113.back-link:hover::before,
114.create-button:hover::before {
115  opacity: 1;
116  transform: translateX(30%) translateY(10%);
117}
118
119/* Active press */
120.glass-btn:active,
121button:active,
122.create-button:active {
123  background: rgba(var(--glass-water-rgb), 0.45);
124  transform: translateY(0);
125  animation: none;
126}
127
128@keyframes waterDrift {
129  0% { transform: translateY(-1px); }
130  100% { transform: translateY(1px); }
131}
132
133/* Button variants */
134.create-button {
135  --glass-alpha: 0.34;
136  --glass-alpha-hover: 0.46;
137  font-weight: 600;
138  padding: 14px 32px;
139  font-size: 16px;
140}
141
142/* =========================================================
143   CONTENT CARDS
144   ========================================================= */
145
146.header,
147.empty-state {
148  background: rgba(255, 255, 255, 0.15);
149  backdrop-filter: blur(22px) saturate(140%);
150  -webkit-backdrop-filter: blur(22px) saturate(140%);
151  border-radius: 14px;
152  padding: 28px;
153  border: 1px solid rgba(255, 255, 255, 0.28);
154  color: white;
155  margin-bottom: 24px;
156}
157
158.header h1 {
159  font-size: 28px;
160  font-weight: 600;
161  letter-spacing: -0.5px;
162  line-height: 1.3;
163  margin-bottom: 8px;
164  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
165  color: white;
166}
167
168.version-badge {
169  display: inline-block;
170  padding: 6px 14px;
171  background: rgba(255, 255, 255, 0.25);
172  backdrop-filter: blur(10px);
173  color: white;
174  border-radius: 20px;
175  font-size: 14px;
176  font-weight: 600;
177  margin-top: 8px;
178  border: 1px solid rgba(255, 255, 255, 0.3);
179}
180
181.empty-state {
182  padding: 60px 40px;
183  text-align: center;
184}
185
186.empty-state-icon {
187  font-size: 64px;
188  margin-bottom: 16px;
189}
190
191.empty-state-text {
192  font-size: 18px;
193  color: rgba(255, 255, 255, 0.9);
194  margin-bottom: 24px;
195}
196
197/* =========================================================
198   NAV
199   ========================================================= */
200
201.back-nav {
202  display: flex;
203  gap: 12px;
204  margin-bottom: 20px;
205  flex-wrap: wrap;
206}
207
208/* =========================================================
209   RESPONSIVE
210   ========================================================= */
211
212@media (max-width: 640px) {
213  body {
214    padding: 16px;
215  }
216
217  .container {
218    max-width: 100%;
219  }
220
221  .header,
222  .empty-state {
223    padding: 20px;
224  }
225
226  .header h1 {
227    font-size: 24px;
228  }
229
230  .empty-state {
231    padding: 40px 24px;
232  }
233
234  .back-nav {
235    flex-direction: column;
236  }
237
238  .back-link {
239    width: 100%;
240    justify-content: center;
241  }
242}
243  </style>
244</head>
245<body>
246  <div class="container">
247    <div class="back-nav">
248      <a href="/api/v1/root/${nodeId}${queryString}" class="back-link">← Back to Tree</a>
249      <a href="/api/v1/node/${nodeId}/${parsedVersion}/values${queryString}" class="back-link">Back to Values</a>
250    </div>
251
252    <div class="header">
253      <h1>🪙 Solana Wallet</h1>
254      <span class="version-badge">Version ${parsedVersion}</span>
255    </div>
256
257    <div class="empty-state">
258      <div class="empty-state-icon">👛</div>
259      <div class="empty-state-text">No wallet exists for this version</div>
260      <form method="POST" action="/api/v1/node/${nodeId}/${parsedVersion}/values/solana?token=${token}&html">
261        <button type="submit" class="create-button">Create Wallet</button>
262      </form>
263    </div>
264  </div>
265</body>
266</html>
267`;
268}
269
270export function renderSolanaWallet({
271  nodeId,
272  parsedVersion,
273  queryString,
274  token,
275  walletInfo,
276  successMsg,
277  errorMsg,
278}) {
279  const alertsHtml = [
280    successMsg
281      ? `
282      <div class="alert alert-success">
283        <strong>Swap successful!</strong><br/>
284        ${
285          successMsg.sig
286            ? `<a href="https://solscan.io/tx/${successMsg.sig}" target="_blank">
287                View transaction
288              </a>`
289            : ""
290        }
291      </div>
292    `
293      : "",
294    errorMsg
295      ? `
296      <div class="alert alert-error">
297        <strong>Transaction failed:</strong><br/>
298        ${errorMsg}
299      </div>
300    `
301      : "",
302  ].join("");
303
304  const tokensHtml = walletInfo.tokens?.length
305    ? `
306        <table>
307          <thead>
308            <tr>
309              <th>Token</th>
310              <th>Amount</th>
311              <th>USD Value</th>
312            </tr>
313          </thead>
314          <tbody>
315            ${walletInfo.tokens
316              .map(
317                (t) => `
318              <tr>
319                <td>
320                  <a href="https://solscan.io/token/${
321                    t.mint
322                  }" target="_blank" rel="noopener noreferrer" style="text-decoration: none;">
323                    <span class="token-mint">${t.mint.slice(
324                      0,
325                      6,
326                    )}…${t.mint.slice(-4)}</span>
327                  </a>
328                </td>
329                <td>${Number(t.uiAmount)
330                  .toFixed(6)
331                  .replace(/\.?0+$/, "")}</td>
332                <td>${t.usd != null ? `$${t.usd.toFixed(2)}` : "—"}</td>
333              </tr>
334            `,
335              )
336              .join("")}
337          </tbody>
338        </table>
339      `
340    : `<div class="empty-tokens">No SPL tokens found</div>`;
341
342  return `
343<!DOCTYPE html>
344<html lang="en">
345<head>
346  <meta charset="UTF-8">
347  <meta name="viewport" content="width=device-width, initial-scale=1.0">
348  <meta name="theme-color" content="#667eea">
349  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
350  <title>Solana Wallet — Version ${parsedVersion}</title>
351  <style>
352${baseStyles}
353
354/* =========================================================
355   UNIFIED GLASS BUTTON SYSTEM
356   ========================================================= */
357
358.glass-btn,
359button,
360.back-link,
361.external-link {
362  position: relative;
363  overflow: hidden;
364
365  padding: 10px 20px;
366  border-radius: 980px;
367
368  display: inline-flex;
369  align-items: center;
370  justify-content: center;
371  white-space: nowrap;
372
373  background: rgba(var(--glass-water-rgb), var(--glass-alpha));
374  backdrop-filter: blur(22px) saturate(140%);
375  -webkit-backdrop-filter: blur(22px) saturate(140%);
376
377  color: white;
378  text-decoration: none;
379  font-family: inherit;
380
381  font-size: 15px;
382  font-weight: 500;
383  letter-spacing: -0.2px;
384
385  border: 1px solid rgba(255, 255, 255, 0.28);
386
387  box-shadow:
388    0 8px 24px rgba(0, 0, 0, 0.12),
389    inset 0 1px 0 rgba(255, 255, 255, 0.25);
390
391  cursor: pointer;
392
393  transition:
394    background 0.3s cubic-bezier(0.4, 0, 0.2, 1),
395    transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
396    box-shadow 0.3s ease;
397}
398
399/* Liquid light layer */
400.glass-btn::before,
401button::before,
402.back-link::before,
403.external-link::before {
404  content: "";
405  position: absolute;
406  inset: -40%;
407
408  background:
409    radial-gradient(
410      120% 60% at 0% 0%,
411      rgba(255, 255, 255, 0.35),
412      transparent 60%
413    ),
414    linear-gradient(
415      120deg,
416      transparent 30%,
417      rgba(255, 255, 255, 0.25),
418      transparent 70%
419    );
420
421  opacity: 0;
422  transform: translateX(-30%) translateY(-10%);
423  transition:
424    opacity 0.35s ease,
425    transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
426
427  pointer-events: none;
428}
429
430/* Hover motion */
431.glass-btn:hover,
432button:hover,
433.back-link:hover,
434.external-link:hover {
435  background: rgba(var(--glass-water-rgb), var(--glass-alpha-hover));
436  transform: translateY(-1px);
437  animation: waterDrift 2.2s ease-in-out infinite alternate;
438}
439
440.glass-btn:hover::before,
441button:hover::before,
442.back-link:hover::before,
443.external-link:hover::before {
444  opacity: 1;
445  transform: translateX(30%) translateY(10%);
446}
447
448/* Active press */
449.glass-btn:active,
450button:active,
451.external-link:active {
452  background: rgba(var(--glass-water-rgb), 0.45);
453  transform: translateY(0);
454  animation: none;
455}
456
457@keyframes waterDrift {
458  0% { transform: translateY(-1px); }
459  100% { transform: translateY(1px); }
460}
461
462/* Button variants */
463button[type="submit"] {
464  --glass-alpha: 0.34;
465  --glass-alpha-hover: 0.46;
466  font-weight: 600;
467  padding: 14px 24px;
468  width: 100%;
469  margin-top: 8px;
470}
471
472.external-link {
473  padding: 8px 16px;
474  font-size: 13px;
475}
476
477/* =========================================================
478   CONTENT CARDS
479   ========================================================= */
480
481.header,
482.card {
483  background: rgba(255, 255, 255, 0.15);
484  backdrop-filter: blur(22px) saturate(140%);
485  -webkit-backdrop-filter: blur(22px) saturate(140%);
486  border-radius: 14px;
487  padding: 28px;
488  border: 1px solid rgba(255, 255, 255, 0.28);
489  color: white;
490  margin-bottom: 24px;
491}
492
493.card {
494  padding: 24px;
495  margin-bottom: 16px;
496}
497
498.header h1 {
499  font-size: 28px;
500  font-weight: 600;
501  letter-spacing: -0.5px;
502  line-height: 1.3;
503  margin-bottom: 8px;
504  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
505  color: white;
506}
507
508.card h3 {
509  font-size: 18px;
510  font-weight: 600;
511  margin-bottom: 16px;
512  color: white;
513  letter-spacing: -0.3px;
514  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
515}
516
517.version-badge {
518  display: inline-block;
519  padding: 6px 14px;
520  background: rgba(255, 255, 255, 0.25);
521  backdrop-filter: blur(10px);
522  color: white;
523  border-radius: 20px;
524  font-size: 14px;
525  font-weight: 600;
526  margin-top: 8px;
527  border: 1px solid rgba(255, 255, 255, 0.3);
528}
529
530/* =========================================================
531   NAV
532   ========================================================= */
533
534.back-nav {
535  display: flex;
536  gap: 12px;
537  margin-bottom: 20px;
538  flex-wrap: wrap;
539}
540
541/* =========================================================
542   WALLET COMPONENTS
543   ========================================================= */
544
545.address-box {
546  display: flex;
547  align-items: center;
548  gap: 12px;
549  margin-top: 12px;
550  flex-wrap: wrap;
551}
552
553.address-code {
554  flex: 1;
555  min-width: 0;
556  background: rgba(255, 255, 255, 0.2);
557  padding: 12px 16px;
558  border-radius: 10px;
559  font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
560  font-size: 13px;
561  color: white;
562  word-break: break-all;
563  font-weight: 600;
564  border: 1px solid rgba(255, 255, 255, 0.3);
565}
566
567.balance-display {
568  margin-top: 20px;
569  padding: 20px;
570  background: rgba(255, 255, 255, 0.15);
571  border-radius: 12px;
572  border: 2px solid rgba(255, 255, 255, 0.3);
573}
574
575.balance-label {
576  font-size: 14px;
577  color: rgba(255, 255, 255, 0.8);
578  margin-bottom: 6px;
579}
580
581.balance-amount {
582  font-size: 32px;
583  font-weight: 700;
584  color: white;
585}
586
587/* =========================================================
588   TABLES
589   ========================================================= */
590
591table {
592  width: 100%;
593  margin-top: 16px;
594  border-collapse: separate;
595  border-spacing: 0;
596  background: rgba(255, 255, 255, 0.1);
597  border-radius: 8px;
598  overflow: hidden;
599}
600
601th {
602  text-align: left;
603  font-size: 13px;
604  font-weight: 600;
605  color: rgba(255, 255, 255, 0.8);
606  padding: 12px 8px;
607  border-bottom: 2px solid rgba(255, 255, 255, 0.2);
608  background: rgba(255, 255, 255, 0.15);
609}
610
611td {
612  padding: 14px 8px;
613  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
614  font-size: 14px;
615  color: white;
616}
617
618tr:last-child td {
619  border-bottom: none;
620}
621
622tbody tr {
623  transition: background 0.2s;
624}
625
626tbody tr:hover {
627  background: rgba(255, 255, 255, 0.05);
628}
629
630.token-mint {
631  font-family: 'SF Mono', Monaco, monospace;
632  font-size: 12px;
633  color: white;
634  font-weight: 600;
635}
636
637.token-mint:hover {
638  opacity: 0.8;
639}
640
641.empty-tokens {
642  text-align: center;
643  padding: 40px 20px;
644  color: rgba(255, 255, 255, 0.7);
645  font-style: italic;
646}
647
648/* =========================================================
649   FORMS
650   ========================================================= */
651
652.form-group {
653  display: flex;
654  flex-direction: column;
655  gap: 12px;
656}
657
658.form-row {
659  display: flex;
660  gap: 10px;
661  align-items: stretch;
662}
663
664input,
665select {
666  padding: 12px 16px;
667  border-radius: 10px;
668  border: 1px solid rgba(255, 255, 255, 0.3);
669  font-size: 15px;
670  font-family: inherit;
671  transition: all 0.2s;
672  background: rgba(255, 255, 255, 0.2);
673  color: white;
674}
675
676input::placeholder {
677  color: rgba(255, 255, 255, 0.6);
678}
679
680input:focus,
681select:focus {
682  outline: none;
683  border-color: rgba(255, 255, 255, 0.5);
684  background: rgba(255, 255, 255, 0.25);
685  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
686}
687
688select {
689  appearance: none;
690  background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L6 6L11 1' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
691  background-repeat: no-repeat;
692  background-position: right 12px center;
693  padding-right: 36px;
694}
695
696.swap-arrow {
697  display: flex;
698  align-items: center;
699  justify-content: center;
700  padding: 12px;
701  font-size: 20px;
702  color: white;
703  font-weight: 700;
704}
705
706.info-box {
707  margin-top: 16px;
708  padding: 16px;
709  background: rgba(255, 193, 7, 0.2);
710  border-left: 4px solid #ffa500;
711  border-radius: 8px;
712  font-size: 14px;
713  line-height: 1.6;
714  color: rgba(255, 255, 255, 0.9);
715}
716
717.info-box strong {
718  color: white;
719}
720
721.swap-container {
722  background: rgba(255, 255, 255, 0.1);
723  border-radius: 12px;
724  padding: 20px;
725  margin-top: 8px;
726}
727
728.swap-input-group {
729  background: rgba(255, 255, 255, 0.15);
730  padding: 16px;
731  border-radius: 10px;
732  margin-bottom: 12px;
733  border: 1px solid rgba(255, 255, 255, 0.2);
734}
735
736.swap-label {
737  font-size: 12px;
738  font-weight: 600;
739  color: rgba(255, 255, 255, 0.8);
740  margin-bottom: 8px;
741  text-transform: uppercase;
742  letter-spacing: 0.5px;
743}
744
745/* =========================================================
746   ALERTS
747   ========================================================= */
748
749.alert {
750  padding: 12px 14px;
751  border-radius: 10px;
752  margin-bottom: 16px;
753  font-size: 14px;
754  border: 1px solid;
755}
756
757.alert-success {
758  background: rgba(16, 185, 129, 0.2);
759  border-color: rgba(16, 185, 129, 0.5);
760  color: white;
761}
762
763.alert-error {
764  background: rgba(239, 68, 68, 0.2);
765  border-color: rgba(239, 68, 68, 0.5);
766  color: white;
767}
768
769.alert strong {
770  font-weight: 600;
771}
772
773.alert a {
774  color: white;
775  text-decoration: underline;
776}
777
778/* =========================================================
779   RESPONSIVE
780   ========================================================= */
781
782@media (max-width: 640px) {
783  body {
784    padding: 16px;
785  }
786
787  .container {
788    max-width: 100%;
789  }
790
791  .header,
792  .card {
793    padding: 20px;
794  }
795
796  .header h1 {
797    font-size: 24px;
798  }
799
800  .balance-amount {
801    font-size: 24px;
802  }
803
804  .back-nav {
805    flex-direction: column;
806  }
807
808  .back-link {
809    width: 100%;
810    justify-content: center;
811  }
812
813  .address-box {
814    flex-direction: column;
815  }
816
817  .external-link {
818    width: 100%;
819    text-align: center;
820  }
821
822  .form-row {
823    flex-direction: column;
824  }
825
826  .swap-arrow {
827    transform: rotate(90deg);
828  }
829
830  table {
831    font-size: 12px;
832  }
833
834  th,
835  td {
836    padding: 10px 6px;
837  }
838}
839  </style>
840</head>
841<body>
842  <div class="container">
843${alertsHtml}
844
845    <div class="back-nav">
846      <a href="/api/v1/root/${nodeId}${queryString}" class="back-link">← Back to Tree</a>
847      <a href="/api/v1/node/${nodeId}/${parsedVersion}/values${queryString}" class="back-link">Back to Values</a>
848    </div>
849
850    <div class="header">
851      <h1>🪙 Solana Wallet</h1>
852      <span class="version-badge">Version ${parsedVersion}</span>
853    </div>
854
855    <!-- Wallet Address -->
856    <div class="card">
857      <h3>Wallet Address</h3>
858      <div class="address-box">
859        <div class="address-code">${walletInfo.publicKey}</div>
860        <a href="https://solscan.io/account/${
861          walletInfo.publicKey
862        }" target="_blank" rel="noopener noreferrer" class="external-link">
863          View on Solscan →
864        </a>
865      </div>
866
867      <div class="balance-display">
868        <div class="balance-label">SOL Balance</div>
869        <div class="balance-amount">${(walletInfo.solBalance / 1e9).toFixed(
870          4,
871        )} SOL</div>
872      </div>
873    </div>
874
875    <!-- Token Balances -->
876    <div class="card">
877      <h3>Token Balances</h3>
878      ${tokensHtml}
879    </div>
880
881    <!-- Send SOL -->
882    <div class="card">
883      <h3>💸 Send SOL</h3>
884      <form method="POST" action="/api/v1/node/${nodeId}/${parsedVersion}/values/solana/send?token=${token}&html">
885        <div class="form-group">
886          <input type="text" name="destination" placeholder="Destination address or node ID" required />
887          <input type="number" name="amount" step="any" min="0" placeholder="Amount in SOL" required />
888          <button type="submit">Send SOL</button>
889        </div>
890      </form>
891      <div class="info-box">
892        <strong>Transaction Fee:</strong> Each transaction requires a small network fee. Minimum balance of 0.001 SOL recommended. New wallets require 0.0009 SOL rent-exempt minimum.
893      </div>
894    </div>
895
896    <!-- Swap -->
897    <div class="card">
898      <h3>🔄 Swap Tokens</h3>
899      <form method="POST" action="/api/v1/node/${nodeId}/${parsedVersion}/values/solana/transaction?token=${token}&html">
900        <div class="swap-container">
901          <div class="swap-input-group">
902            <div class="swap-label">From</div>
903            <div class="form-row">
904              <select name="fromType" id="fromType" required style="flex: 1;">
905                <option value="sol">SOL</option>
906                <option value="token">Token</option>
907              </select>
908              <input type="number" name="amount" step="any" min="0" placeholder="Amount" required style="flex: 2;" />
909            </div>
910            <input type="text" name="inputMint" id="fromTokenMint" placeholder="Token Mint Address" style="display:none; margin-top: 8px;" />
911          </div>
912
913          <div class="swap-arrow">↓</div>
914
915          <div class="swap-input-group">
916            <div class="swap-label">To</div>
917            <div class="form-row">
918              <select name="toType" id="toType" required style="flex: 1;">
919                <option value="token">Token</option>
920                <option value="sol">SOL</option>
921              </select>
922              <input type="text" name="outputMint" id="toTokenMint" placeholder="Token Mint Address" style="flex: 2;" />
923            </div>
924          </div>
925        </div>
926
927        <button type="submit">Execute Swap</button>
928      </form>
929
930    </div>
931  </div>
932
933  <script>
934    const fromType = document.getElementById("fromType");
935    const toType = document.getElementById("toType");
936    const fromTokenMint = document.getElementById("fromTokenMint");
937    const toTokenMint = document.getElementById("toTokenMint");
938    const SOL_MINT = "So11111111111111111111111111111111111111112";
939
940    function updateSwapFields() {
941      // Prevent SOL -> SOL
942      if (fromType.value === "sol" && toType.value === "sol") {
943        toType.value = "token";
944      }
945
946      // FROM field
947      if (fromType.value === "token") {
948        fromTokenMint.style.display = "block";
949        fromTokenMint.required = true;
950        if (fromTokenMint.value === SOL_MINT) {
951          fromTokenMint.value = "";
952        }
953      } else {
954        fromTokenMint.style.display = "none";
955        fromTokenMint.required = false;
956        fromTokenMint.value = SOL_MINT;
957      }
958
959      // TO field
960      if (toType.value === "token") {
961        toTokenMint.style.display = "block";
962        toTokenMint.required = true;
963        if (toTokenMint.value === SOL_MINT) {
964          toTokenMint.value = "";
965        }
966      } else {
967        toTokenMint.style.display = "none";
968        toTokenMint.required = false;
969        toTokenMint.value = SOL_MINT;
970      }
971    }
972
973    fromType.addEventListener("change", updateSwapFields);
974    toType.addEventListener("change", updateSwapFields);
975    updateSwapFields();
976  </script>
977</body>
978</html>
979`;
980}
981
1import { getExtension } from "../loader.js";
2
3export async function init(core) {
4  const { setModels, setMetadata } = await import("./core.js");
5  setModels(core.models);
6  setMetadata(core.metadata);
7  const { default: router } = await import("./routes.js");
8
9  // Register wallet link on the values page
10  try {
11    const treeos = getExtension("treeos-base");
12    treeos?.exports?.registerSlot?.("values-nav-links", "solana", ({ nodeId, version, queryString }) =>
13      `<a href="/api/v1/node/${nodeId}/${version}/values/solana${queryString}" class="back-link">Solana Wallet</a>`,
14      { priority: 10 }
15    );
16  } catch {}
17
18  return { router };
19}
20
1export default {
2  name: "solana",
3  version: "1.0.1",
4  builtFor: "TreeOS",
5  scope: "confined",
6  description:
7    "Every node in the tree can own a Solana wallet. The extension generates keypairs " +
8    "on demand, encrypts private keys with AES-256-GCM using a server-side master key, " +
9    "and stores them in node metadata. No private key ever leaves the server unencrypted. " +
10    "Wallets are per-node: a Finance branch holds SOL, a Rewards node holds tokens, a " +
11    "project node accepts payments. Each wallet is a real Solana address on mainnet.\n\n" +
12    "Three operations: create a wallet on any node, send SOL or SPL tokens to a Solana " +
13    "address or another node (auto-creates the destination wallet if needed), and swap " +
14    "tokens through Jupiter's Ultra API with configurable slippage. Sends are fee-aware " +
15    "and auto-adjust amounts to cover rent exemption and transaction fees. Swaps handle " +
16    "the full Jupiter flow: order creation, server-side transaction signing, and execution.\n\n" +
17    "Balance sync reads on-chain SOL balance and Jupiter token holdings, then writes them " +
18    "to the values namespace using the _auto__ prefix convention so they appear alongside " +
19    "user-set values. USD prices fetch from Jupiter's price API. Stale token entries " +
20    "auto-cleanup when holdings change. CLI commands expose wallet info, send, and swap " +
21    "without touching the AI conversation. HTML views available when html-rendering is " +
22    "installed.",
23
24  npm: ["@solana/spl-token@^0.4.14", "@solana/web3.js@^1.98.4"],
25
26  needs: {
27    models: ["Node"],
28  },
29
30  optional: {
31    services: ["energy"],
32    extensions: ["html-rendering", "treeos-base"],
33  },
34
35  provides: {
36    env: [
37      { key: "NODE_WALLET_MASTER_KEY", required: true, secret: true, description: "32-byte hex key for server-side wallets. Back this up." },
38      { key: "SOLANA_RPC_URL", required: true, description: "Solana RPC endpoint", default: "https://api.mainnet-beta.solana.com" },
39      { key: "JUP_API_KEY", required: false, description: "Jupiter API key for swaps and price data" },
40    ],
41    models: {},
42    routes: "./routes.js",
43    tools: false,
44    jobs: false,
45    orchestrator: false,
46    energyActions: {},
47    sessionTypes: {},
48    schemaVersion: 1,
49    migrations: "./migrations.js",
50    cli: [
51      { command: "wallet [action] [args...]", description: "Solana wallet. Actions: create, send, swap. No action shows info.", method: "GET", endpoint: "/node/:nodeId/values/solana", subcommands: {
52        "create": { method: "POST", endpoint: "/node/:nodeId/values/solana", description: "Create a wallet on this node" },
53        "send": { method: "POST", endpoint: "/node/:nodeId/0/values/solana/send", args: ["amount", "destination"], description: "Send SOL. Usage: wallet send <amount> <address or nodeId>" },
54        "swap": { method: "POST", endpoint: "/node/:nodeId/0/values/solana/transaction", args: ["inputMint", "outputMint", "amountUi"], description: "Swap tokens via Jupiter. Usage: wallet swap <fromMint> <toMint> <amount>. Type 'sol' instead of SOL's full mint address." },
55      }},
56    ],
57  },
58};
59
1/**
2 * Solana extension migrations.
3 * Migration 1 already ran (moved wallet data from old versions[].wallet to metadata.solana.wallets).
4 * No further migrations needed after schema flatten.
5 */
6export default [];
7
1{
2  "name": "treeos-ext-solana",
3  "version": "1.0.0",
4  "lockfileVersion": 3,
5  "requires": true,
6  "packages": {
7    "": {
8      "name": "treeos-ext-solana",
9      "version": "1.0.0",
10      "dependencies": {
11        "@solana/spl-token": "^0.4.14",
12        "@solana/web3.js": "^1.98.4"
13      }
14    },
15    "node_modules/@babel/runtime": {
16      "version": "7.29.2",
17      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
18      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
19      "license": "MIT",
20      "engines": {
21        "node": ">=6.9.0"
22      }
23    },
24    "node_modules/@noble/curves": {
25      "version": "1.9.7",
26      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
27      "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
28      "license": "MIT",
29      "dependencies": {
30        "@noble/hashes": "1.8.0"
31      },
32      "engines": {
33        "node": "^14.21.3 || >=16"
34      },
35      "funding": {
36        "url": "https://paulmillr.com/funding/"
37      }
38    },
39    "node_modules/@noble/hashes": {
40      "version": "1.8.0",
41      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
42      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
43      "license": "MIT",
44      "engines": {
45        "node": "^14.21.3 || >=16"
46      },
47      "funding": {
48        "url": "https://paulmillr.com/funding/"
49      }
50    },
51    "node_modules/@solana/buffer-layout": {
52      "version": "4.0.1",
53      "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz",
54      "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==",
55      "license": "MIT",
56      "dependencies": {
57        "buffer": "~6.0.3"
58      },
59      "engines": {
60        "node": ">=5.10"
61      }
62    },
63    "node_modules/@solana/buffer-layout-utils": {
64      "version": "0.2.0",
65      "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz",
66      "integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==",
67      "license": "Apache-2.0",
68      "dependencies": {
69        "@solana/buffer-layout": "^4.0.0",
70        "@solana/web3.js": "^1.32.0",
71        "bigint-buffer": "^1.1.5",
72        "bignumber.js": "^9.0.1"
73      },
74      "engines": {
75        "node": ">= 10"
76      }
77    },
78    "node_modules/@solana/codecs": {
79      "version": "2.0.0-rc.1",
80      "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz",
81      "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==",
82      "license": "MIT",
83      "dependencies": {
84        "@solana/codecs-core": "2.0.0-rc.1",
85        "@solana/codecs-data-structures": "2.0.0-rc.1",
86        "@solana/codecs-numbers": "2.0.0-rc.1",
87        "@solana/codecs-strings": "2.0.0-rc.1",
88        "@solana/options": "2.0.0-rc.1"
89      },
90      "peerDependencies": {
91        "typescript": ">=5"
92      }
93    },
94    "node_modules/@solana/codecs-core": {
95      "version": "2.0.0-rc.1",
96      "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz",
97      "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==",
98      "license": "MIT",
99      "dependencies": {
100        "@solana/errors": "2.0.0-rc.1"
101      },
102      "peerDependencies": {
103        "typescript": ">=5"
104      }
105    },
106    "node_modules/@solana/codecs-data-structures": {
107      "version": "2.0.0-rc.1",
108      "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz",
109      "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==",
110      "license": "MIT",
111      "dependencies": {
112        "@solana/codecs-core": "2.0.0-rc.1",
113        "@solana/codecs-numbers": "2.0.0-rc.1",
114        "@solana/errors": "2.0.0-rc.1"
115      },
116      "peerDependencies": {
117        "typescript": ">=5"
118      }
119    },
120    "node_modules/@solana/codecs-numbers": {
121      "version": "2.0.0-rc.1",
122      "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz",
123      "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==",
124      "license": "MIT",
125      "dependencies": {
126        "@solana/codecs-core": "2.0.0-rc.1",
127        "@solana/errors": "2.0.0-rc.1"
128      },
129      "peerDependencies": {
130        "typescript": ">=5"
131      }
132    },
133    "node_modules/@solana/codecs-strings": {
134      "version": "2.0.0-rc.1",
135      "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz",
136      "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==",
137      "license": "MIT",
138      "dependencies": {
139        "@solana/codecs-core": "2.0.0-rc.1",
140        "@solana/codecs-numbers": "2.0.0-rc.1",
141        "@solana/errors": "2.0.0-rc.1"
142      },
143      "peerDependencies": {
144        "fastestsmallesttextencoderdecoder": "^1.0.22",
145        "typescript": ">=5"
146      }
147    },
148    "node_modules/@solana/errors": {
149      "version": "2.0.0-rc.1",
150      "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz",
151      "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==",
152      "license": "MIT",
153      "dependencies": {
154        "chalk": "^5.3.0",
155        "commander": "^12.1.0"
156      },
157      "bin": {
158        "errors": "bin/cli.mjs"
159      },
160      "peerDependencies": {
161        "typescript": ">=5"
162      }
163    },
164    "node_modules/@solana/options": {
165      "version": "2.0.0-rc.1",
166      "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz",
167      "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==",
168      "license": "MIT",
169      "dependencies": {
170        "@solana/codecs-core": "2.0.0-rc.1",
171        "@solana/codecs-data-structures": "2.0.0-rc.1",
172        "@solana/codecs-numbers": "2.0.0-rc.1",
173        "@solana/codecs-strings": "2.0.0-rc.1",
174        "@solana/errors": "2.0.0-rc.1"
175      },
176      "peerDependencies": {
177        "typescript": ">=5"
178      }
179    },
180    "node_modules/@solana/spl-token": {
181      "version": "0.4.14",
182      "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.14.tgz",
183      "integrity": "sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==",
184      "license": "Apache-2.0",
185      "dependencies": {
186        "@solana/buffer-layout": "^4.0.0",
187        "@solana/buffer-layout-utils": "^0.2.0",
188        "@solana/spl-token-group": "^0.0.7",
189        "@solana/spl-token-metadata": "^0.1.6",
190        "buffer": "^6.0.3"
191      },
192      "engines": {
193        "node": ">=16"
194      },
195      "peerDependencies": {
196        "@solana/web3.js": "^1.95.5"
197      }
198    },
199    "node_modules/@solana/spl-token-group": {
200      "version": "0.0.7",
201      "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz",
202      "integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==",
203      "license": "Apache-2.0",
204      "dependencies": {
205        "@solana/codecs": "2.0.0-rc.1"
206      },
207      "engines": {
208        "node": ">=16"
209      },
210      "peerDependencies": {
211        "@solana/web3.js": "^1.95.3"
212      }
213    },
214    "node_modules/@solana/spl-token-metadata": {
215      "version": "0.1.6",
216      "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz",
217      "integrity": "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==",
218      "license": "Apache-2.0",
219      "dependencies": {
220        "@solana/codecs": "2.0.0-rc.1"
221      },
222      "engines": {
223        "node": ">=16"
224      },
225      "peerDependencies": {
226        "@solana/web3.js": "^1.95.3"
227      }
228    },
229    "node_modules/@solana/web3.js": {
230      "version": "1.98.4",
231      "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz",
232      "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==",
233      "license": "MIT",
234      "dependencies": {
235        "@babel/runtime": "^7.25.0",
236        "@noble/curves": "^1.4.2",
237        "@noble/hashes": "^1.4.0",
238        "@solana/buffer-layout": "^4.0.1",
239        "@solana/codecs-numbers": "^2.1.0",
240        "agentkeepalive": "^4.5.0",
241        "bn.js": "^5.2.1",
242        "borsh": "^0.7.0",
243        "bs58": "^4.0.1",
244        "buffer": "6.0.3",
245        "fast-stable-stringify": "^1.0.0",
246        "jayson": "^4.1.1",
247        "node-fetch": "^2.7.0",
248        "rpc-websockets": "^9.0.2",
249        "superstruct": "^2.0.2"
250      }
251    },
252    "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": {
253      "version": "2.3.0",
254      "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz",
255      "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==",
256      "license": "MIT",
257      "dependencies": {
258        "@solana/errors": "2.3.0"
259      },
260      "engines": {
261        "node": ">=20.18.0"
262      },
263      "peerDependencies": {
264        "typescript": ">=5.3.3"
265      }
266    },
267    "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": {
268      "version": "2.3.0",
269      "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz",
270      "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==",
271      "license": "MIT",
272      "dependencies": {
273        "@solana/codecs-core": "2.3.0",
274        "@solana/errors": "2.3.0"
275      },
276      "engines": {
277        "node": ">=20.18.0"
278      },
279      "peerDependencies": {
280        "typescript": ">=5.3.3"
281      }
282    },
283    "node_modules/@solana/web3.js/node_modules/@solana/errors": {
284      "version": "2.3.0",
285      "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz",
286      "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==",
287      "license": "MIT",
288      "dependencies": {
289        "chalk": "^5.4.1",
290        "commander": "^14.0.0"
291      },
292      "bin": {
293        "errors": "bin/cli.mjs"
294      },
295      "engines": {
296        "node": ">=20.18.0"
297      },
298      "peerDependencies": {
299        "typescript": ">=5.3.3"
300      }
301    },
302    "node_modules/@solana/web3.js/node_modules/commander": {
303      "version": "14.0.3",
304      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
305      "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
306      "license": "MIT",
307      "engines": {
308        "node": ">=20"
309      }
310    },
311    "node_modules/@swc/helpers": {
312      "version": "0.5.20",
313      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz",
314      "integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==",
315      "license": "Apache-2.0",
316      "dependencies": {
317        "tslib": "^2.8.0"
318      }
319    },
320    "node_modules/@types/connect": {
321      "version": "3.4.38",
322      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
323      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
324      "license": "MIT",
325      "dependencies": {
326        "@types/node": "*"
327      }
328    },
329    "node_modules/@types/node": {
330      "version": "12.20.55",
331      "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
332      "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
333      "license": "MIT"
334    },
335    "node_modules/@types/uuid": {
336      "version": "10.0.0",
337      "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
338      "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
339      "license": "MIT"
340    },
341    "node_modules/@types/ws": {
342      "version": "7.4.7",
343      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
344      "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
345      "license": "MIT",
346      "dependencies": {
347        "@types/node": "*"
348      }
349    },
350    "node_modules/agentkeepalive": {
351      "version": "4.6.0",
352      "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
353      "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
354      "license": "MIT",
355      "dependencies": {
356        "humanize-ms": "^1.2.1"
357      },
358      "engines": {
359        "node": ">= 8.0.0"
360      }
361    },
362    "node_modules/base-x": {
363      "version": "3.0.11",
364      "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
365      "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
366      "license": "MIT",
367      "dependencies": {
368        "safe-buffer": "^5.0.1"
369      }
370    },
371    "node_modules/base64-js": {
372      "version": "1.5.1",
373      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
374      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
375      "funding": [
376        {
377          "type": "github",
378          "url": "https://github.com/sponsors/feross"
379        },
380        {
381          "type": "patreon",
382          "url": "https://www.patreon.com/feross"
383        },
384        {
385          "type": "consulting",
386          "url": "https://feross.org/support"
387        }
388      ],
389      "license": "MIT"
390    },
391    "node_modules/bigint-buffer": {
392      "version": "1.1.5",
393      "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz",
394      "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==",
395      "hasInstallScript": true,
396      "license": "Apache-2.0",
397      "dependencies": {
398        "bindings": "^1.3.0"
399      },
400      "engines": {
401        "node": ">= 10.0.0"
402      }
403    },
404    "node_modules/bignumber.js": {
405      "version": "9.3.1",
406      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
407      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
408      "license": "MIT",
409      "engines": {
410        "node": "*"
411      }
412    },
413    "node_modules/bindings": {
414      "version": "1.5.0",
415      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
416      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
417      "license": "MIT",
418      "dependencies": {
419        "file-uri-to-path": "1.0.0"
420      }
421    },
422    "node_modules/bn.js": {
423      "version": "5.2.3",
424      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
425      "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
426      "license": "MIT"
427    },
428    "node_modules/borsh": {
429      "version": "0.7.0",
430      "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
431      "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
432      "license": "Apache-2.0",
433      "dependencies": {
434        "bn.js": "^5.2.0",
435        "bs58": "^4.0.0",
436        "text-encoding-utf-8": "^1.0.2"
437      }
438    },
439    "node_modules/bs58": {
440      "version": "4.0.1",
441      "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
442      "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
443      "license": "MIT",
444      "dependencies": {
445        "base-x": "^3.0.2"
446      }
447    },
448    "node_modules/buffer": {
449      "version": "6.0.3",
450      "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
451      "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
452      "funding": [
453        {
454          "type": "github",
455          "url": "https://github.com/sponsors/feross"
456        },
457        {
458          "type": "patreon",
459          "url": "https://www.patreon.com/feross"
460        },
461        {
462          "type": "consulting",
463          "url": "https://feross.org/support"
464        }
465      ],
466      "license": "MIT",
467      "dependencies": {
468        "base64-js": "^1.3.1",
469        "ieee754": "^1.2.1"
470      }
471    },
472    "node_modules/bufferutil": {
473      "version": "4.1.0",
474      "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
475      "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
476      "hasInstallScript": true,
477      "license": "MIT",
478      "optional": true,
479      "dependencies": {
480        "node-gyp-build": "^4.3.0"
481      },
482      "engines": {
483        "node": ">=6.14.2"
484      }
485    },
486    "node_modules/chalk": {
487      "version": "5.6.2",
488      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
489      "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
490      "license": "MIT",
491      "engines": {
492        "node": "^12.17.0 || ^14.13 || >=16.0.0"
493      },
494      "funding": {
495        "url": "https://github.com/chalk/chalk?sponsor=1"
496      }
497    },
498    "node_modules/commander": {
499      "version": "12.1.0",
500      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
501      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
502      "license": "MIT",
503      "engines": {
504        "node": ">=18"
505      }
506    },
507    "node_modules/delay": {
508      "version": "5.0.0",
509      "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
510      "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
511      "license": "MIT",
512      "engines": {
513        "node": ">=10"
514      },
515      "funding": {
516        "url": "https://github.com/sponsors/sindresorhus"
517      }
518    },
519    "node_modules/es6-promise": {
520      "version": "4.2.8",
521      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
522      "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
523      "license": "MIT"
524    },
525    "node_modules/es6-promisify": {
526      "version": "5.0.0",
527      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
528      "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
529      "license": "MIT",
530      "dependencies": {
531        "es6-promise": "^4.0.3"
532      }
533    },
534    "node_modules/eventemitter3": {
535      "version": "5.0.4",
536      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
537      "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
538      "license": "MIT"
539    },
540    "node_modules/eyes": {
541      "version": "0.1.8",
542      "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
543      "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
544      "engines": {
545        "node": "> 0.1.90"
546      }
547    },
548    "node_modules/fast-stable-stringify": {
549      "version": "1.0.0",
550      "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz",
551      "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==",
552      "license": "MIT"
553    },
554    "node_modules/fastestsmallesttextencoderdecoder": {
555      "version": "1.0.22",
556      "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz",
557      "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==",
558      "license": "CC0-1.0",
559      "peer": true
560    },
561    "node_modules/file-uri-to-path": {
562      "version": "1.0.0",
563      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
564      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
565      "license": "MIT"
566    },
567    "node_modules/humanize-ms": {
568      "version": "1.2.1",
569      "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
570      "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
571      "license": "MIT",
572      "dependencies": {
573        "ms": "^2.0.0"
574      }
575    },
576    "node_modules/ieee754": {
577      "version": "1.2.1",
578      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
579      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
580      "funding": [
581        {
582          "type": "github",
583          "url": "https://github.com/sponsors/feross"
584        },
585        {
586          "type": "patreon",
587          "url": "https://www.patreon.com/feross"
588        },
589        {
590          "type": "consulting",
591          "url": "https://feross.org/support"
592        }
593      ],
594      "license": "BSD-3-Clause"
595    },
596    "node_modules/isomorphic-ws": {
597      "version": "4.0.1",
598      "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
599      "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
600      "license": "MIT",
601      "peerDependencies": {
602        "ws": "*"
603      }
604    },
605    "node_modules/jayson": {
606      "version": "4.3.0",
607      "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz",
608      "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==",
609      "license": "MIT",
610      "dependencies": {
611        "@types/connect": "^3.4.33",
612        "@types/node": "^12.12.54",
613        "@types/ws": "^7.4.4",
614        "commander": "^2.20.3",
615        "delay": "^5.0.0",
616        "es6-promisify": "^5.0.0",
617        "eyes": "^0.1.8",
618        "isomorphic-ws": "^4.0.1",
619        "json-stringify-safe": "^5.0.1",
620        "stream-json": "^1.9.1",
621        "uuid": "^8.3.2",
622        "ws": "^7.5.10"
623      },
624      "bin": {
625        "jayson": "bin/jayson.js"
626      },
627      "engines": {
628        "node": ">=8"
629      }
630    },
631    "node_modules/jayson/node_modules/commander": {
632      "version": "2.20.3",
633      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
634      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
635      "license": "MIT"
636    },
637    "node_modules/json-stringify-safe": {
638      "version": "5.0.1",
639      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
640      "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
641      "license": "ISC"
642    },
643    "node_modules/ms": {
644      "version": "2.1.3",
645      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
646      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
647      "license": "MIT"
648    },
649    "node_modules/node-fetch": {
650      "version": "2.7.0",
651      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
652      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
653      "license": "MIT",
654      "dependencies": {
655        "whatwg-url": "^5.0.0"
656      },
657      "engines": {
658        "node": "4.x || >=6.0.0"
659      },
660      "peerDependencies": {
661        "encoding": "^0.1.0"
662      },
663      "peerDependenciesMeta": {
664        "encoding": {
665          "optional": true
666        }
667      }
668    },
669    "node_modules/node-gyp-build": {
670      "version": "4.8.4",
671      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
672      "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
673      "license": "MIT",
674      "optional": true,
675      "bin": {
676        "node-gyp-build": "bin.js",
677        "node-gyp-build-optional": "optional.js",
678        "node-gyp-build-test": "build-test.js"
679      }
680    },
681    "node_modules/rpc-websockets": {
682      "version": "9.3.7",
683      "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.7.tgz",
684      "integrity": "sha512-dQal1U0yKH2umW0DgqSecP4G1jNxyPUGY60uUMB8bLoXabC2aWT3Cag9hOhZXsH/52QJEcggxNNWhF+Fp48ykw==",
685      "license": "LGPL-3.0-only",
686      "dependencies": {
687        "@swc/helpers": "^0.5.11",
688        "@types/uuid": "^10.0.0",
689        "@types/ws": "^8.2.2",
690        "buffer": "^6.0.3",
691        "eventemitter3": "^5.0.1",
692        "uuid": "^11.0.0",
693        "ws": "^8.5.0"
694      },
695      "funding": {
696        "type": "paypal",
697        "url": "https://paypal.me/kozjak"
698      },
699      "optionalDependencies": {
700        "bufferutil": "^4.0.1",
701        "utf-8-validate": "^6.0.0"
702      }
703    },
704    "node_modules/rpc-websockets/node_modules/@types/ws": {
705      "version": "8.18.1",
706      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
707      "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
708      "license": "MIT",
709      "dependencies": {
710        "@types/node": "*"
711      }
712    },
713    "node_modules/rpc-websockets/node_modules/utf-8-validate": {
714      "version": "6.0.6",
715      "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz",
716      "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
717      "hasInstallScript": true,
718      "license": "MIT",
719      "optional": true,
720      "dependencies": {
721        "node-gyp-build": "^4.3.0"
722      },
723      "engines": {
724        "node": ">=6.14.2"
725      }
726    },
727    "node_modules/rpc-websockets/node_modules/uuid": {
728      "version": "11.1.0",
729      "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
730      "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
731      "funding": [
732        "https://github.com/sponsors/broofa",
733        "https://github.com/sponsors/ctavan"
734      ],
735      "license": "MIT",
736      "bin": {
737        "uuid": "dist/esm/bin/uuid"
738      }
739    },
740    "node_modules/rpc-websockets/node_modules/ws": {
741      "version": "8.20.0",
742      "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
743      "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
744      "license": "MIT",
745      "engines": {
746        "node": ">=10.0.0"
747      },
748      "peerDependencies": {
749        "bufferutil": "^4.0.1",
750        "utf-8-validate": ">=5.0.2"
751      },
752      "peerDependenciesMeta": {
753        "bufferutil": {
754          "optional": true
755        },
756        "utf-8-validate": {
757          "optional": true
758        }
759      }
760    },
761    "node_modules/safe-buffer": {
762      "version": "5.2.1",
763      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
764      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
765      "funding": [
766        {
767          "type": "github",
768          "url": "https://github.com/sponsors/feross"
769        },
770        {
771          "type": "patreon",
772          "url": "https://www.patreon.com/feross"
773        },
774        {
775          "type": "consulting",
776          "url": "https://feross.org/support"
777        }
778      ],
779      "license": "MIT"
780    },
781    "node_modules/stream-chain": {
782      "version": "2.2.5",
783      "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
784      "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
785      "license": "BSD-3-Clause"
786    },
787    "node_modules/stream-json": {
788      "version": "1.9.1",
789      "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
790      "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
791      "license": "BSD-3-Clause",
792      "dependencies": {
793        "stream-chain": "^2.2.5"
794      }
795    },
796    "node_modules/superstruct": {
797      "version": "2.0.2",
798      "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
799      "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==",
800      "license": "MIT",
801      "engines": {
802        "node": ">=14.0.0"
803      }
804    },
805    "node_modules/text-encoding-utf-8": {
806      "version": "1.0.2",
807      "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
808      "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="
809    },
810    "node_modules/tr46": {
811      "version": "0.0.3",
812      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
813      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
814      "license": "MIT"
815    },
816    "node_modules/tslib": {
817      "version": "2.8.1",
818      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
819      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
820      "license": "0BSD"
821    },
822    "node_modules/typescript": {
823      "version": "6.0.2",
824      "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
825      "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
826      "license": "Apache-2.0",
827      "peer": true,
828      "bin": {
829        "tsc": "bin/tsc",
830        "tsserver": "bin/tsserver"
831      },
832      "engines": {
833        "node": ">=14.17"
834      }
835    },
836    "node_modules/utf-8-validate": {
837      "version": "5.0.10",
838      "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
839      "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
840      "hasInstallScript": true,
841      "license": "MIT",
842      "optional": true,
843      "peer": true,
844      "dependencies": {
845        "node-gyp-build": "^4.3.0"
846      },
847      "engines": {
848        "node": ">=6.14.2"
849      }
850    },
851    "node_modules/uuid": {
852      "version": "8.3.2",
853      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
854      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
855      "license": "MIT",
856      "bin": {
857        "uuid": "dist/bin/uuid"
858      }
859    },
860    "node_modules/webidl-conversions": {
861      "version": "3.0.1",
862      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
863      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
864      "license": "BSD-2-Clause"
865    },
866    "node_modules/whatwg-url": {
867      "version": "5.0.0",
868      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
869      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
870      "license": "MIT",
871      "dependencies": {
872        "tr46": "~0.0.3",
873        "webidl-conversions": "^3.0.0"
874      }
875    },
876    "node_modules/ws": {
877      "version": "7.5.10",
878      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
879      "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
880      "license": "MIT",
881      "engines": {
882        "node": ">=8.3.0"
883      },
884      "peerDependencies": {
885        "bufferutil": "^4.0.1",
886        "utf-8-validate": "^5.0.2"
887      },
888      "peerDependenciesMeta": {
889        "bufferutil": {
890          "optional": true
891        },
892        "utf-8-validate": {
893          "optional": true
894        }
895      }
896    }
897  }
898}
899
1{
2  "type": "module",
3  "name": "treeos-ext-solana",
4  "version": "1.0.0",
5  "private": true,
6  "dependencies": {
7    "@solana/spl-token": "^0.4.14",
8    "@solana/web3.js": "^1.98.4"
9  }
10}
1import log from "../../seed/log.js";
2import express from "express";
3import Node from "../../seed/models/node.js";
4import authenticate from "../../seed/middleware/authenticate.js";
5import { sendOk, sendError, ERR } from "../../seed/protocol.js";
6import {
7  ensureVersionWallet,
8  syncVersionSOLBalance,
9  syncVersionTokenHoldings,
10  getVersionWalletInfo,
11  sendSOLFromVersion,
12  swapFromVersion,
13} from "./core.js";
14import { renderSolanaNoWallet, renderSolanaWallet } from "./html.js";
15import { getExtension } from "../loader.js";
16
17const router = express.Router();
18
19function parseVersion(v) {
20  const n = Number(v);
21  if (!Number.isInteger(n) || n < 0) return null;
22  return n;
23}
24
25function isLikelySolanaAddress(value) {
26  return (
27    typeof value === "string" && (value.length === 43 || value.length === 44)
28  );
29}
30
31function isLikelyNodeId(value) {
32  return typeof value === "string" && value.length === 36;
33}
34
35const allowedParams = ["html", "token", "success", "sig", "error"];
36
37// GET wallet info + balances
38router.get(
39  "/node/:nodeId/:version/values/solana",
40  authenticate,
41  async (req, res) => {
42    try {
43      const { nodeId, version } = req.params;
44      const parsedVersion = parseVersion(version);
45      if (parsedVersion === null) {
46        return sendError(res, 400, ERR.INVALID_INPUT, "Invalid version");
47      }
48
49      const filtered = Object.entries(req.query)
50        .filter(([key]) => allowedParams.includes(key))
51        .map(([key, val]) => (val === "" ? key : `${key}=${val}`))
52        .join("&");
53      const queryString = filtered ? `?${filtered}` : "";
54
55      const node = await Node.findById(nodeId);
56      await syncVersionSOLBalance(node, parsedVersion);
57      await syncVersionTokenHoldings(node, parsedVersion);
58      const walletInfo = await getVersionWalletInfo(nodeId, parsedVersion);
59
60      if (
61        !("html" in req.query) ||
62        !getExtension("html-rendering")
63      ) {
64        return sendOk(res, { nodeId, version: parsedVersion, ...walletInfo });
65      }
66
67      const token = req.query.token ?? "";
68
69      if (!walletInfo.exists) {
70        return res.send(
71          renderSolanaNoWallet({ nodeId, parsedVersion, queryString, token }),
72        );
73      }
74
75      return res.send(
76        renderSolanaWallet({
77          nodeId,
78          parsedVersion,
79          queryString,
80          token,
81          walletInfo,
82          successMsg: req.query.success ? { sig: req.query.sig } : null,
83          errorMsg: req.query.error || null,
84        }),
85      );
86    } catch (err) {
87 log.error("Solana", "Error in /node/:nodeId/:version/values/solana:", err);
88      sendError(res, 500, ERR.INTERNAL, err.message);
89    }
90  },
91);
92
93// POST create wallet
94router.post(
95  "/node/:nodeId/:version/values/solana",
96  authenticate,
97  async (req, res) => {
98    try {
99      const { nodeId, version } = req.params;
100      const parsedVersion = parseVersion(version);
101      if (parsedVersion === null) {
102        return sendError(res, 400, ERR.INVALID_INPUT, "Invalid version");
103      }
104
105      await ensureVersionWallet(nodeId, parsedVersion);
106
107      if ("html" in req.query) {
108        return res.redirect(
109          `/api/v1/node/${nodeId}/${parsedVersion}/values/solana?token=${
110            req.query.token ?? ""
111          }&html`,
112        );
113      }
114
115      sendOk(res, {}, 201);
116    } catch (err) {
117      sendError(res, 500, ERR.INTERNAL, err.message);
118    }
119  },
120);
121
122// POST send SOL
123router.post(
124  "/node/:nodeId/:version/values/solana/send",
125  authenticate,
126  async (req, res) => {
127    try {
128      const { nodeId, version } = req.params;
129      const { destination, amount } = req.body;
130      const parsedVersion = parseVersion(version);
131      if (parsedVersion === null) {
132        return sendError(res, 400, ERR.INVALID_INPUT, "Invalid version");
133      }
134
135      if (typeof destination !== "string" || !destination.trim()) {
136        return sendError(res, 400, ERR.INVALID_INPUT, "Destination is required");
137      }
138
139      const dest = destination.trim();
140      let toAddress;
141      let toNodeId;
142
143      if (isLikelySolanaAddress(dest)) {
144        toAddress = dest;
145      } else if (isLikelyNodeId(dest)) {
146        toNodeId = dest;
147      } else {
148        return sendError(res, 400, ERR.INVALID_INPUT, "Destination must be a Solana address or a nodeId");
149      }
150
151      const solAmount = Number(amount);
152      if (!Number.isFinite(solAmount) || solAmount <= 0) {
153        return sendError(res, 400, ERR.INVALID_INPUT, "Invalid SOL amount");
154      }
155
156      const lamports = Math.round(solAmount * 1e9);
157      const result = await sendSOLFromVersion({
158        nodeId,
159        versionIndex: parsedVersion,
160        toAddress,
161        toNodeId,
162        lamports,
163      });
164
165      if ("html" in req.query) {
166        return res.redirect(
167          `/api/v1/node/${nodeId}/${parsedVersion}/values/solana?token=${
168            req.query.token ?? ""
169          }&html`,
170        );
171      }
172
173      sendOk(res, { signature: result.signature, to: result.to });
174    } catch (err) {
175 log.error("Solana", "Send SOL error:", err);
176      sendError(res, 500, ERR.INTERNAL, err.message);
177    }
178  },
179);
180
181// POST swap (Jupiter)
182router.post(
183  "/node/:nodeId/:version/values/solana/transaction",
184  authenticate,
185  async (req, res) => {
186    const { nodeId, version } = req.params;
187    const parsedVersion = parseVersion(version);
188
189    try {
190      if (parsedVersion === null) throw new Error("Invalid version");
191
192      const { fromType, toType, amount, slippageBps } = req.body;
193      const SOL_MINT = "So11111111111111111111111111111111111111112";
194
195      if (!["sol", "token"].includes(fromType)) throw new Error("Invalid fromType");
196      if (!["sol", "token"].includes(toType)) throw new Error("Invalid toType");
197      if (fromType === "sol" && toType === "sol") throw new Error("SOL to SOL swap is not allowed");
198
199      const inputMint = fromType === "sol" ? SOL_MINT : req.body.inputMint;
200      const outputMint = toType === "sol" ? SOL_MINT : req.body.outputMint;
201
202      const uiAmount = Number(amount);
203      if (!Number.isFinite(uiAmount) || uiAmount <= 0) throw new Error("Invalid amount");
204
205      const result = await swapFromVersion({
206        nodeId,
207        versionIndex: parsedVersion,
208        inputMint,
209        outputMint,
210        amountUi: uiAmount,
211        slippageBps,
212      });
213
214      if ("html" in req.query) {
215        return res.redirect(
216          `/api/v1/node/${nodeId}/${parsedVersion}/values/solana?` +
217            `success=1&sig=${result.signature}&token=${req.query.token ?? ""}&html`,
218        );
219      }
220
221      return sendOk(res, result);
222    } catch (err) {
223 log.error("Solana", "Swap transaction error:", err);
224
225      if ("html" in req.query) {
226        return res.redirect(
227          `/api/v1/node/${nodeId}/${parsedVersion}/values/solana?` +
228            `error=${encodeURIComponent(err.message)}&token=${req.query.token ?? ""}&html`,
229        );
230      }
231
232      sendError(res, 500, ERR.INTERNAL, err.message);
233    }
234  },
235);
236
237// Versionless aliases (protocol-compliant)
238router.get("/node/:nodeId/values/solana", authenticate, async (req, res) => {
239  try {
240    const info = await getVersionWalletInfo(req.params.nodeId, 0);
241    if ("html" in req.query && getExtension("html-rendering")) {
242      req.params.version = "0";
243      // Fall through to versioned HTML route
244      const node = await Node.findById(req.params.nodeId);
245      if (!node) return sendError(res, 404, ERR.NODE_NOT_FOUND, "Node not found");
246      return res.redirect(`/api/v1/node/${req.params.nodeId}/0/values/solana?${new URLSearchParams(req.query)}`);
247    }
248    sendOk(res, info);
249  } catch (err) {
250    sendError(res, 500, ERR.INTERNAL, err.message);
251  }
252});
253
254router.post("/node/:nodeId/values/solana", authenticate, async (req, res) => {
255  try {
256    const result = await ensureVersionWallet(req.params.nodeId, 0);
257    sendOk(res, { publicKey: result.publicKey, created: result.created }, 201);
258  } catch (err) {
259    sendError(res, 500, ERR.INTERNAL, err.message);
260  }
261});
262
263export default router;
264

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star solana

Comments

Loading comments...

Post comments from the CLI: treeos ext comment solana "your comment"
Max 3 comments per extension. One star and one flag per user.