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
Loading comments...