diff --git a/.env.example b/.env.example index 3bfe915..e44fafb 100644 --- a/.env.example +++ b/.env.example @@ -49,3 +49,26 @@ ETHEREUM_RPC_URL= # that route respond with ERR_PAYMENT_NOT_CONFIGURED instead of a fabricated # result. EVM_SOURCE_PRIVATE_KEY= + +# ── Solana (devnet by default) — Issue #142 ─────────────────────────────── +# Optional RPC override. Public default (api.devnet.solana.com) is used +# when unset — fine for light development use. +SOLANA_RPC_URL= + +# Server-only base58 secret key for the account that signs and submits real +# `/api/solana/send` transactions, mirroring EVM_SOURCE_PRIVATE_KEY above. +# NEVER commit a real value. Leave unset to have that route respond with +# ERR_PAYMENT_NOT_CONFIGURED instead of a fabricated result. +SOLANA_SOURCE_PRIVATE_KEY= + +# ── Sui (testnet by default) — Issue #143 ───────────────────────────────── +# Optional RPC override. Public default (fullnode.testnet.sui.io) is used +# when unset — fine for light development use. +SUI_RPC_URL= + +# Server-only bech32 "suiprivkey1..." secret key for the account that signs +# and submits real `/api/sui/send` transactions, mirroring +# EVM_SOURCE_PRIVATE_KEY above. NEVER commit a real value. Leave unset to +# have that route respond with ERR_PAYMENT_NOT_CONFIGURED instead of a +# fabricated result. +SUI_SOURCE_PRIVATE_KEY= diff --git a/app/[locale]/ledger/page.tsx b/app/[locale]/ledger/page.tsx new file mode 100644 index 0000000..72ed29c --- /dev/null +++ b/app/[locale]/ledger/page.tsx @@ -0,0 +1,10 @@ +import { AppShell } from "@/components/app/app-shell" +import { LedgerSendCard } from "@/components/app/ledger-send-card" + +export default function LedgerPage() { + return ( + + + + ) +} diff --git a/app/[locale]/solana/page.tsx b/app/[locale]/solana/page.tsx new file mode 100644 index 0000000..e1d1257 --- /dev/null +++ b/app/[locale]/solana/page.tsx @@ -0,0 +1,10 @@ +import { AppShell } from "@/components/app/app-shell" +import { SolanaWalletCard } from "@/components/app/solana-wallet-card" + +export default function SolanaPage() { + return ( + + + + ) +} diff --git a/app/[locale]/sui/page.tsx b/app/[locale]/sui/page.tsx new file mode 100644 index 0000000..d82901b --- /dev/null +++ b/app/[locale]/sui/page.tsx @@ -0,0 +1,10 @@ +import { AppShell } from "@/components/app/app-shell" +import { SuiWalletCard } from "@/components/app/sui-wallet-card" + +export default function SuiPage() { + return ( + + + + ) +} diff --git a/app/api/solana/balance/route.ts b/app/api/solana/balance/route.ts new file mode 100644 index 0000000..cd96917 --- /dev/null +++ b/app/api/solana/balance/route.ts @@ -0,0 +1,43 @@ +/** + * GET /api/solana/balance?address=... + * + * Real, read-only balance query against Solana devnet RPC — no signing key + * required. See lib/solana/solana.service.ts. + */ + +import { NextRequest, NextResponse } from 'next/server' +import { solanaService } from '@/lib/solana/solana.service' +import { SolanaServiceError } from '@/lib/types' +import { ErrorCodes, apiError } from '@/lib/errors' + +export async function GET(request: NextRequest) { + const address = request.nextUrl.searchParams.get('address') + + if (!address || !solanaService.validateAddress(address)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_ADDRESS, 'address is required and must be a valid Solana address'), + { status: 422 }, + ) + } + + try { + const balances = await solanaService.getBalances(address) + const network = solanaService.getNetwork() + return NextResponse.json( + { + network: network.name, + address, + balances, + }, + { status: 200 }, + ) + } catch (err) { + if (err instanceof SolanaServiceError) { + return NextResponse.json({ error: err.message }, { status: 502 }) + } + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to fetch balances' }, + { status: 500 }, + ) + } +} diff --git a/app/api/solana/send/route.ts b/app/api/solana/send/route.ts new file mode 100644 index 0000000..482bd40 --- /dev/null +++ b/app/api/solana/send/route.ts @@ -0,0 +1,75 @@ +/** + * POST /api/solana/send + * + * Builds, signs, and broadcasts a real native SOL transfer on Solana devnet, + * mirroring app/api/evm/send/route.ts. MVP scope is native SOL transfers + * only — SPL-token (USDC) sends are follow-up work. + */ + +import { NextRequest, NextResponse } from 'next/server' +import { validateBearerToken } from '@/lib/auth' +import { ErrorCodes, apiError } from '@/lib/errors' +import { solanaService } from '@/lib/solana/solana.service' +import { getSolanaPaymentService, SolanaPaymentConfigError } from '@/lib/solana/solana-payment.service' +import type { SolanaTransactionResult } from '@/lib/types' + +interface SendBody { + destination?: string + amount?: number +} + +export async function POST(request: NextRequest) { + if (!validateBearerToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + let body: SendBody = {} + try { + body = (await request.json()) as SendBody + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) + } + + const { destination, amount } = body + + if (!destination || typeof destination !== 'string' || !solanaService.validateAddress(destination)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_ADDRESS, 'destination is required and must be a valid Solana address'), + { status: 422 }, + ) + } + + if (!amount || typeof amount !== 'number' || amount <= 0) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_AMOUNT, 'amount must be a positive number'), + { status: 422 }, + ) + } + + const paymentService = getSolanaPaymentService() + + try { + const submission = await paymentService.submitPayment({ destination, amount }) + + const result: SolanaTransactionResult = { + success: submission.status !== 'failed', + signature: submission.signature, + status: submission.status, + error: submission.error, + } + + return NextResponse.json(result, { status: 200 }) + } catch (err) { + if (err instanceof SolanaPaymentConfigError) { + return NextResponse.json( + apiError(ErrorCodes.ERR_PAYMENT_NOT_CONFIGURED, err.message), + { status: 503 }, + ) + } + + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Payment submission failed' }, + { status: 500 }, + ) + } +} diff --git a/app/api/sui/balance/route.ts b/app/api/sui/balance/route.ts new file mode 100644 index 0000000..487a37d --- /dev/null +++ b/app/api/sui/balance/route.ts @@ -0,0 +1,43 @@ +/** + * GET /api/sui/balance?address=0x... + * + * Real, read-only balance query against Sui testnet RPC — no signing key + * required. See lib/sui/sui.service.ts. + */ + +import { NextRequest, NextResponse } from 'next/server' +import { suiService } from '@/lib/sui/sui.service' +import { SuiServiceError } from '@/lib/types' +import { ErrorCodes, apiError } from '@/lib/errors' + +export async function GET(request: NextRequest) { + const address = request.nextUrl.searchParams.get('address') + + if (!address || !suiService.validateAddress(address)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_ADDRESS, 'address is required and must be a valid Sui address'), + { status: 422 }, + ) + } + + try { + const balances = await suiService.getBalances(address) + const network = suiService.getNetwork() + return NextResponse.json( + { + network: network.name, + address, + balances, + }, + { status: 200 }, + ) + } catch (err) { + if (err instanceof SuiServiceError) { + return NextResponse.json({ error: err.message }, { status: 502 }) + } + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to fetch balances' }, + { status: 500 }, + ) + } +} diff --git a/app/api/sui/send/route.ts b/app/api/sui/send/route.ts new file mode 100644 index 0000000..648d68c --- /dev/null +++ b/app/api/sui/send/route.ts @@ -0,0 +1,75 @@ +/** + * POST /api/sui/send + * + * Builds, signs, and broadcasts a real native SUI transfer on Sui testnet, + * mirroring app/api/evm/send/route.ts. MVP scope is native SUI transfers + * only — coin-type (USDC) sends are follow-up work. + */ + +import { NextRequest, NextResponse } from 'next/server' +import { validateBearerToken } from '@/lib/auth' +import { ErrorCodes, apiError } from '@/lib/errors' +import { suiService } from '@/lib/sui/sui.service' +import { getSuiPaymentService, SuiPaymentConfigError } from '@/lib/sui/sui-payment.service' +import type { SuiTransactionResult } from '@/lib/types' + +interface SendBody { + destination?: string + amount?: number +} + +export async function POST(request: NextRequest) { + if (!validateBearerToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + let body: SendBody = {} + try { + body = (await request.json()) as SendBody + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) + } + + const { destination, amount } = body + + if (!destination || typeof destination !== 'string' || !suiService.validateAddress(destination)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_ADDRESS, 'destination is required and must be a valid Sui address'), + { status: 422 }, + ) + } + + if (!amount || typeof amount !== 'number' || amount <= 0) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_AMOUNT, 'amount must be a positive number'), + { status: 422 }, + ) + } + + const paymentService = getSuiPaymentService() + + try { + const submission = await paymentService.submitPayment({ destination, amount }) + + const result: SuiTransactionResult = { + success: submission.status !== 'failed', + digest: submission.digest, + status: submission.status, + error: submission.error, + } + + return NextResponse.json(result, { status: 200 }) + } catch (err) { + if (err instanceof SuiPaymentConfigError) { + return NextResponse.json( + apiError(ErrorCodes.ERR_PAYMENT_NOT_CONFIGURED, err.message), + { status: 503 }, + ) + } + + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Payment submission failed' }, + { status: 500 }, + ) + } +} diff --git a/app/api/wallet/send/prepare/route.ts b/app/api/wallet/send/prepare/route.ts new file mode 100644 index 0000000..578295a --- /dev/null +++ b/app/api/wallet/send/prepare/route.ts @@ -0,0 +1,95 @@ +/** + * POST /api/wallet/send/prepare — Issue #92 + * + * Builds (but does not sign) a Stellar payment transaction for an arbitrary + * source account. Counterpart to /api/wallet/send/submit-signed. Together + * these two routes are the external-signer path (Ledger today): the private + * key never touches this server, so signing happens client-side against the + * XDR/signatureBase returned here. + */ + +import { NextRequest, NextResponse } from 'next/server' +import { StrKey } from '@stellar/stellar-sdk' +import { validateBearerToken } from '@/lib/auth' +import { ErrorCodes, apiError } from '@/lib/errors' +import { SUPPORTED_STELLAR_ASSETS } from '@/lib/fixtures' +import { getStellarPaymentService, StellarPaymentConfigError } from '@/lib/services/stellar-payment.service' + +interface PrepareBody { + sourcePublicKey?: string + destination?: string + amount?: number + asset?: string + memo?: string +} + +const MAX_MEMO_BYTES = 28 + +export async function POST(request: NextRequest) { + if (!validateBearerToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + let body: PrepareBody = {} + try { + body = (await request.json()) as PrepareBody + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) + } + + const { sourcePublicKey, destination, amount, asset, memo } = body + + if (!sourcePublicKey || !StrKey.isValidEd25519PublicKey(sourcePublicKey)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_ADDRESS, 'sourcePublicKey is required and must be a valid Stellar public key'), + { status: 422 }, + ) + } + + if (!destination || !StrKey.isValidEd25519PublicKey(destination)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_ADDRESS, 'destination is required and must be a valid Stellar public key'), + { status: 422 }, + ) + } + + if (!amount || typeof amount !== 'number' || amount <= 0) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_AMOUNT, 'amount must be a positive number'), + { status: 422 }, + ) + } + + if (!asset || !SUPPORTED_STELLAR_ASSETS.some((a) => a.code === asset)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_UNSUPPORTED_ASSET, `asset must be one of: ${SUPPORTED_STELLAR_ASSETS.map((a) => a.code).join(', ')}`), + { status: 422 }, + ) + } + + if (memo && Buffer.byteLength(memo, 'utf8') > MAX_MEMO_BYTES) { + return NextResponse.json( + apiError(ErrorCodes.ERR_MEMO_TOO_LONG, `memo must be at most ${MAX_MEMO_BYTES} bytes`), + { status: 422 }, + ) + } + + try { + const prepared = await getStellarPaymentService().buildUnsignedTransaction({ + sourcePublicKey, + destination, + amount, + asset, + memo, + }) + return NextResponse.json(prepared, { status: 200 }) + } catch (err) { + if (err instanceof StellarPaymentConfigError) { + return NextResponse.json(apiError(ErrorCodes.ERR_PAYMENT_NOT_CONFIGURED, err.message), { status: 503 }) + } + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to build transaction' }, + { status: 500 }, + ) + } +} diff --git a/app/api/wallet/send/submit-signed/route.ts b/app/api/wallet/send/submit-signed/route.ts new file mode 100644 index 0000000..3705998 --- /dev/null +++ b/app/api/wallet/send/submit-signed/route.ts @@ -0,0 +1,78 @@ +/** + * POST /api/wallet/send/submit-signed — Issue #92 + * + * Submits a transaction that was already signed externally (e.g. by a + * Ledger device via lib/ledger/ledger.service.ts). Counterpart to + * /api/wallet/send/prepare. This route never sees a private key — only the + * XDR and the signature the client already produced. + */ + +import { NextRequest, NextResponse } from 'next/server' +import { StrKey } from '@stellar/stellar-sdk' +import { type TransactionResult } from '@/lib/types' +import { validateBearerToken } from '@/lib/auth' +import { ErrorCodes, apiError } from '@/lib/errors' +import { getStellarPaymentService, StellarPaymentConfigError } from '@/lib/services/stellar-payment.service' + +interface SubmitSignedBody { + xdr?: string + publicKey?: string + signature?: string + networkPassphrase?: string +} + +export async function POST(request: NextRequest) { + if (!validateBearerToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + let body: SubmitSignedBody = {} + try { + body = (await request.json()) as SubmitSignedBody + } catch { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) + } + + const { xdr, publicKey, signature, networkPassphrase } = body + + if (!xdr || typeof xdr !== 'string') { + return NextResponse.json(apiError(ErrorCodes.ERR_MISSING_XDR, 'xdr is required'), { status: 422 }) + } + + if (!publicKey || !StrKey.isValidEd25519PublicKey(publicKey)) { + return NextResponse.json( + apiError(ErrorCodes.ERR_INVALID_ADDRESS, 'publicKey is required and must be a valid Stellar public key'), + { status: 422 }, + ) + } + + if (!signature || typeof signature !== 'string') { + return NextResponse.json(apiError(ErrorCodes.ERR_MISSING_SIGNATURE, 'signature is required (base64)'), { status: 422 }) + } + + try { + const submission = await getStellarPaymentService().submitSignedTransaction( + xdr, + publicKey, + signature, + networkPassphrase, + ) + + const result: TransactionResult = { + success: submission.status !== 'failed', + hash: submission.hash, + status: submission.status, + error: submission.error, + } + + return NextResponse.json(result, { status: 200 }) + } catch (err) { + if (err instanceof StellarPaymentConfigError) { + return NextResponse.json(apiError(ErrorCodes.ERR_PAYMENT_NOT_CONFIGURED, err.message), { status: 503 }) + } + return NextResponse.json( + { error: err instanceof Error ? err.message : 'Failed to submit transaction' }, + { status: 500 }, + ) + } +} diff --git a/components/app/crypto-holdings.tsx b/components/app/crypto-holdings.tsx index a8b46ef..c9be377 100644 --- a/components/app/crypto-holdings.tsx +++ b/components/app/crypto-holdings.tsx @@ -41,6 +41,15 @@ export function CryptoHoldings() { Base & Ethereum + + Solana + + + Sui + + + Ledger + diff --git a/components/app/ledger-send-card.tsx b/components/app/ledger-send-card.tsx new file mode 100644 index 0000000..7f6d5cb --- /dev/null +++ b/components/app/ledger-send-card.tsx @@ -0,0 +1,169 @@ +"use client" + +import { useState } from "react" +import { Loader2, CheckCircle2, Usb } from "lucide-react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { WalletErrorAlert } from "@/components/ui/wallet-error-alert" +import { ledgerService } from "@/lib/ledger/ledger.service" + +type Step = "disconnected" | "connecting" | "connected" | "sending" | "success" | "error" + +/** + * Issue #92 — alternative signing path alongside the existing software-key + * flow (send-form.tsx / STELLAR_SOURCE_SECRET_KEY). Instead of the server + * signing with a configured secret key, this card walks a Ledger device + * through: connect (WebHID) -> read public key -> build unsigned tx + * server-side (/api/wallet/send/prepare) -> sign on-device -> submit + * (/api/wallet/send/submit-signed). The private key never leaves the + * device. + * + * XLM-only for now (Ledger's Stellar app) — Solana/Sui Ledger apps are a + * TODO, noted in lib/ledger/ledger.service.ts. + */ +export function LedgerSendCard() { + const [step, setStep] = useState("disconnected") + const [publicKey, setPublicKey] = useState(null) + const [destination, setDestination] = useState("") + const [amount, setAmount] = useState("") + const [error, setError] = useState(null) + const [hash, setHash] = useState(null) + + const handleConnect = async () => { + setError(null) + setStep("connecting") + try { + await ledgerService.connect() + const pk = await ledgerService.getPublicKey(undefined, true) + setPublicKey(pk) + setStep("connected") + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to connect to Ledger") + setStep("error") + } + } + + const handleSend = async (e: React.FormEvent) => { + e.preventDefault() + if (!publicKey) return + const parsedAmount = Number(amount) + if (!destination.trim() || !Number.isFinite(parsedAmount) || parsedAmount <= 0) return + + setError(null) + setStep("sending") + + try { + const prepareResponse = await fetch("/api/wallet/send/prepare", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: "Bearer globe-wallet-client" }, + body: JSON.stringify({ + sourcePublicKey: publicKey, + destination: destination.trim(), + amount: parsedAmount, + asset: "XLM", + }), + }) + const prepared = await prepareResponse.json() + if (!prepareResponse.ok) throw new Error(prepared.error || "Failed to prepare transaction") + + const signature = await ledgerService.signTransaction(Buffer.from(prepared.signatureBase, "base64")) + + const submitResponse = await fetch("/api/wallet/send/submit-signed", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: "Bearer globe-wallet-client" }, + body: JSON.stringify({ + xdr: prepared.xdr, + publicKey, + signature: signature.toString("base64"), + networkPassphrase: prepared.networkPassphrase, + }), + }) + const result = await submitResponse.json() + if (!submitResponse.ok || !result.success) { + throw new Error(result.error || "Send failed") + } + + setHash(result.hash ?? null) + setStep("success") + } catch (err) { + setError(err instanceof Error ? err.message : "Ledger send failed") + setStep("error") + } + } + + return ( +
+

Sign with Ledger (XLM)

+ +

+ Alternative to the app's software-key signing — the private key never leaves your device. +

+ + {!publicKey ? ( + + ) : ( + <> +

+ {publicKey} +

+
+ setDestination(e.target.value)} + placeholder="Recipient G… address" + aria-label="Recipient address" + data-testid="ledger-send-destination" + /> +
+ setAmount(e.target.value)} + placeholder="Amount in XLM" + inputMode="decimal" + aria-label="Amount" + data-testid="ledger-send-amount" + /> + +
+
+ + )} + + {step === "error" && error && ( +
+ +
+ )} + + {step === "success" && ( +
+ +
+

Send complete

+ {hash &&

{hash}

} +
+
+ )} +
+
+ ) +} diff --git a/components/app/solana-wallet-card.tsx b/components/app/solana-wallet-card.tsx new file mode 100644 index 0000000..94b9fa5 --- /dev/null +++ b/components/app/solana-wallet-card.tsx @@ -0,0 +1,148 @@ +"use client" + +import { useState } from "react" +import { Loader2, CheckCircle2, Search } from "lucide-react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Skeleton } from "@/components/ui/skeleton" +import { WalletErrorAlert } from "@/components/ui/wallet-error-alert" +import { useSolanaBalances, useSolanaSend } from "@/hooks/useSolanaWallet" + +/** + * Real, read-only balance lookups against Solana devnet RPC + * (GET /api/solana/balance), plus a native SOL send form gated on the + * server's SOLANA_SOURCE_PRIVATE_KEY being configured (POST /api/solana/send) + * — mirrors components/app/evm-wallet-card.tsx's "not configured" handling + * rather than fabricating a result. + */ +export function SolanaWalletCard() { + const [address, setAddress] = useState("") + const [destination, setDestination] = useState("") + const [amount, setAmount] = useState("") + + const { balances, network, loading, error, fetchBalances } = useSolanaBalances() + const send = useSolanaSend() + + const handleCheck = (e: React.FormEvent) => { + e.preventDefault() + if (!address.trim()) return + fetchBalances(address.trim()) + } + + const handleSend = (e: React.FormEvent) => { + e.preventDefault() + const parsed = Number(amount) + if (!destination.trim() || !Number.isFinite(parsed) || parsed <= 0) return + send.send(destination.trim(), parsed) + } + + return ( +
+

Solana (devnet)

+ + +

+ Solana Devnet · live testnet balance lookup, no wallet connection required +

+ +
+ setAddress(e.target.value)} + placeholder="Base58 address to check" + aria-label="Solana address" + data-testid="solana-address-input" + /> + +
+ + {error && ( +
+ +
+ )} + + {loading && ( +
+ + +
+ )} + + {!loading && balances.length > 0 && ( +
+

{network}

+ {balances.map((balance) => ( +
+ {balance.symbol} + {balance.amount.toLocaleString(undefined, { maximumFractionDigits: 6 })} +
+ ))} +
+ )} + +
+

Send SOL

+
+ setDestination(e.target.value)} + placeholder="Recipient base58 address" + aria-label="Recipient address" + data-testid="solana-send-destination" + /> +
+ setAmount(e.target.value)} + placeholder="Amount in SOL" + inputMode="decimal" + aria-label="Amount" + data-testid="solana-send-amount" + /> + +
+
+ + {send.status === "error" && send.error && ( +
+ +
+ )} + + {send.status === "success" && send.result && ( +
+ +
+

+ {send.result.status === "pending" ? "Submitted — awaiting confirmation" : "Send complete"} +

+ {send.result.signature && ( +

{send.result.signature}

+ )} +
+
+ )} +
+
+
+ ) +} diff --git a/components/app/sui-wallet-card.tsx b/components/app/sui-wallet-card.tsx new file mode 100644 index 0000000..73d7699 --- /dev/null +++ b/components/app/sui-wallet-card.tsx @@ -0,0 +1,148 @@ +"use client" + +import { useState } from "react" +import { Loader2, CheckCircle2, Search } from "lucide-react" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Skeleton } from "@/components/ui/skeleton" +import { WalletErrorAlert } from "@/components/ui/wallet-error-alert" +import { useSuiBalances, useSuiSend } from "@/hooks/useSuiWallet" + +/** + * Real, read-only balance lookups against Sui testnet RPC + * (GET /api/sui/balance), plus a native SUI send form gated on the server's + * SUI_SOURCE_PRIVATE_KEY being configured (POST /api/sui/send) — mirrors + * components/app/evm-wallet-card.tsx's "not configured" handling rather + * than fabricating a result. + */ +export function SuiWalletCard() { + const [address, setAddress] = useState("") + const [destination, setDestination] = useState("") + const [amount, setAmount] = useState("") + + const { balances, network, loading, error, fetchBalances } = useSuiBalances() + const send = useSuiSend() + + const handleCheck = (e: React.FormEvent) => { + e.preventDefault() + if (!address.trim()) return + fetchBalances(address.trim()) + } + + const handleSend = (e: React.FormEvent) => { + e.preventDefault() + const parsed = Number(amount) + if (!destination.trim() || !Number.isFinite(parsed) || parsed <= 0) return + send.send(destination.trim(), parsed) + } + + return ( +
+

Sui (testnet)

+ + +

+ Sui Testnet · live testnet balance lookup, no wallet connection required +

+ +
+ setAddress(e.target.value)} + placeholder="0x… address to check" + aria-label="Sui address" + data-testid="sui-address-input" + /> + +
+ + {error && ( +
+ +
+ )} + + {loading && ( +
+ + +
+ )} + + {!loading && balances.length > 0 && ( +
+

{network}

+ {balances.map((balance) => ( +
+ {balance.symbol} + {balance.amount.toLocaleString(undefined, { maximumFractionDigits: 6 })} +
+ ))} +
+ )} + +
+

Send SUI

+
+ setDestination(e.target.value)} + placeholder="Recipient 0x… address" + aria-label="Recipient address" + data-testid="sui-send-destination" + /> +
+ setAmount(e.target.value)} + placeholder="Amount in SUI" + inputMode="decimal" + aria-label="Amount" + data-testid="sui-send-amount" + /> + +
+
+ + {send.status === "error" && send.error && ( +
+ +
+ )} + + {send.status === "success" && send.result && ( +
+ +
+

+ {send.result.status === "pending" ? "Submitted — awaiting confirmation" : "Send complete"} +

+ {send.result.digest && ( +

{send.result.digest}

+ )} +
+
+ )} +
+
+
+ ) +} diff --git a/hooks/useSolanaWallet.ts b/hooks/useSolanaWallet.ts new file mode 100644 index 0000000..ded3a6c --- /dev/null +++ b/hooks/useSolanaWallet.ts @@ -0,0 +1,91 @@ +'use client' + +import { useState, useCallback } from 'react' +import type { SolanaBalance, SolanaTransactionResult } from '../lib/types' + +interface SolanaBalancesState { + balances: SolanaBalance[] + network: string | null + loading: boolean + error: string | null +} + +/** + * Fetches real, read-only balances for an arbitrary Solana address on + * devnet via GET /api/solana/balance. No wallet connection or signing key + * required. Mirrors hooks/useEvmWallet.ts. + */ +export function useSolanaBalances() { + const [state, setState] = useState({ + balances: [], + network: null, + loading: false, + error: null, + }) + + const fetchBalances = useCallback(async (address: string) => { + setState((prev) => ({ ...prev, loading: true, error: null })) + try { + const response = await fetch(`/api/solana/balance?address=${address}`) + const body = await response.json().catch(() => ({})) + + if (!response.ok) { + setState({ balances: [], network: null, loading: false, error: body.error || 'Failed to fetch balances' }) + return + } + + setState({ balances: body.balances ?? [], network: body.network ?? null, loading: false, error: null }) + } catch (err) { + setState({ + balances: [], + network: null, + loading: false, + error: err instanceof Error ? err.message : 'Failed to fetch balances', + }) + } + }, []) + + return { ...state, fetchBalances } +} + +type SendStatus = 'idle' | 'processing' | 'success' | 'error' + +interface SolanaSendState { + status: SendStatus + error: string | null + result: SolanaTransactionResult | null +} + +/** + * Native SOL send via POST /api/solana/send. Mirrors useEvmSend's + * status-machine shape (idle -> processing -> success | error). + */ +export function useSolanaSend() { + const [state, setState] = useState({ status: 'idle', error: null, result: null }) + + const reset = useCallback(() => setState({ status: 'idle', error: null, result: null }), []) + + const send = useCallback(async (destination: string, amount: number) => { + setState({ status: 'processing', error: null, result: null }) + try { + const response = await fetch('/api/solana/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer globe-wallet-client' }, + body: JSON.stringify({ destination, amount }), + }) + + const body = await response.json().catch(() => ({})) + + if (!response.ok) { + setState({ status: 'error', error: body.error || 'Send failed', result: null }) + return + } + + setState({ status: 'success', error: null, result: body as SolanaTransactionResult }) + } catch (err) { + setState({ status: 'error', error: err instanceof Error ? err.message : 'Send failed', result: null }) + } + }, []) + + return { ...state, send, reset } +} diff --git a/hooks/useSuiWallet.ts b/hooks/useSuiWallet.ts new file mode 100644 index 0000000..f868d2b --- /dev/null +++ b/hooks/useSuiWallet.ts @@ -0,0 +1,91 @@ +'use client' + +import { useState, useCallback } from 'react' +import type { SuiBalance, SuiTransactionResult } from '../lib/types' + +interface SuiBalancesState { + balances: SuiBalance[] + network: string | null + loading: boolean + error: string | null +} + +/** + * Fetches real, read-only balances for an arbitrary Sui address on testnet + * via GET /api/sui/balance. No wallet connection or signing key required. + * Mirrors hooks/useEvmWallet.ts. + */ +export function useSuiBalances() { + const [state, setState] = useState({ + balances: [], + network: null, + loading: false, + error: null, + }) + + const fetchBalances = useCallback(async (address: string) => { + setState((prev) => ({ ...prev, loading: true, error: null })) + try { + const response = await fetch(`/api/sui/balance?address=${address}`) + const body = await response.json().catch(() => ({})) + + if (!response.ok) { + setState({ balances: [], network: null, loading: false, error: body.error || 'Failed to fetch balances' }) + return + } + + setState({ balances: body.balances ?? [], network: body.network ?? null, loading: false, error: null }) + } catch (err) { + setState({ + balances: [], + network: null, + loading: false, + error: err instanceof Error ? err.message : 'Failed to fetch balances', + }) + } + }, []) + + return { ...state, fetchBalances } +} + +type SendStatus = 'idle' | 'processing' | 'success' | 'error' + +interface SuiSendState { + status: SendStatus + error: string | null + result: SuiTransactionResult | null +} + +/** + * Native SUI send via POST /api/sui/send. Mirrors useEvmSend's + * status-machine shape (idle -> processing -> success | error). + */ +export function useSuiSend() { + const [state, setState] = useState({ status: 'idle', error: null, result: null }) + + const reset = useCallback(() => setState({ status: 'idle', error: null, result: null }), []) + + const send = useCallback(async (destination: string, amount: number) => { + setState({ status: 'processing', error: null, result: null }) + try { + const response = await fetch('/api/sui/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer globe-wallet-client' }, + body: JSON.stringify({ destination, amount }), + }) + + const body = await response.json().catch(() => ({})) + + if (!response.ok) { + setState({ status: 'error', error: body.error || 'Send failed', result: null }) + return + } + + setState({ status: 'success', error: null, result: body as SuiTransactionResult }) + } catch (err) { + setState({ status: 'error', error: err instanceof Error ? err.message : 'Send failed', result: null }) + } + }, []) + + return { ...state, send, reset } +} diff --git a/lib/env.mjs b/lib/env.mjs index 46b4147..b761a90 100644 --- a/lib/env.mjs +++ b/lib/env.mjs @@ -19,6 +19,10 @@ export const environmentVariableNames = [ 'BASE_RPC_URL', 'ETHEREUM_RPC_URL', 'EVM_SOURCE_PRIVATE_KEY', + 'SOLANA_RPC_URL', + 'SOLANA_SOURCE_PRIVATE_KEY', + 'SUI_RPC_URL', + 'SUI_SOURCE_PRIVATE_KEY', ] const optionalNonEmptyString = (schema) => z.preprocess( @@ -41,6 +45,10 @@ export const environmentSchema = z.object({ BASE_RPC_URL: optionalNonEmptyString(z.string().url()), ETHEREUM_RPC_URL: optionalNonEmptyString(z.string().url()), EVM_SOURCE_PRIVATE_KEY: optionalNonEmptyString(z.string().regex(/^0x[0-9a-fA-F]{64}$/, 'must be a 32-byte hex private key (starts with 0x, 64 hex chars)')), + SOLANA_RPC_URL: optionalNonEmptyString(z.string().url()), + SOLANA_SOURCE_PRIVATE_KEY: optionalNonEmptyString(z.string().min(1)), + SUI_RPC_URL: optionalNonEmptyString(z.string().url()), + SUI_SOURCE_PRIVATE_KEY: optionalNonEmptyString(z.string().min(1)), }) export function validateEnvironment(environment = process.env) { diff --git a/lib/errors.ts b/lib/errors.ts index a83a8aa..5f14290 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -19,6 +19,10 @@ export const ErrorCodes = { ERR_UNSUPPORTED_CHAIN: 'ERR_UNSUPPORTED_CHAIN', /** Issue #85: POST /api/wallet/send requires a client-generated idempotency key. */ ERR_MISSING_IDEMPOTENCY_KEY: 'ERR_MISSING_IDEMPOTENCY_KEY', + /** Issue #92: POST /api/wallet/send/submit-signed requires the signed transaction XDR. */ + ERR_MISSING_XDR: 'ERR_MISSING_XDR', + /** Issue #92: POST /api/wallet/send/submit-signed requires the external signer's signature. */ + ERR_MISSING_SIGNATURE: 'ERR_MISSING_SIGNATURE', } as const export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes] diff --git a/lib/ledger/ledger.service.ts b/lib/ledger/ledger.service.ts new file mode 100644 index 0000000..8ce4994 --- /dev/null +++ b/lib/ledger/ledger.service.ts @@ -0,0 +1,130 @@ +import type { LedgerDeviceInfo } from '../types' + +/** + * lib/ledger/ledger.service.ts — Issue #92 + * + * Client-only wrapper around @ledgerhq/hw-transport-webhid + + * @ledgerhq/hw-app-str (Ledger's Stellar app). This is the hardware-wallet + * counterpart to lib/services/stellar-payment.service.ts's software-key + * signing: instead of a server holding STELLAR_SOURCE_SECRET_KEY, the user's + * physical Ledger device holds the key and signs locally in the browser via + * WebHID. Nothing here ever sees a private key — only a signature comes + * back from the device. + * + * Must only run in the browser (WebHID is a browser API). The heavy + * `@ledgerhq/hw-transport-webhid` / `@ledgerhq/hw-app-str` packages are + * dynamically imported inside connect() so this module is safe to import + * from server code without pulling in browser-only globals. + * + * Solana/Sui signing via their respective Ledger apps + * (@ledgerhq/hw-app-solana, @ledgerhq/hw-app-sui or equivalent) is a TODO — + * out of scope for this pass, which focuses on Stellar (the app's primary + * chain) per Issue #92's own scoping note. + */ + +/** Standard Stellar (SLIP-0044 coin type 148) BIP-32 derivation path. */ +export const DEFAULT_STELLAR_LEDGER_PATH = "44'/148'/0'" + +export class LedgerNotSupportedError extends Error { + constructor() { + super('WebHID is not available in this browser. Ledger signing requires Chrome, Edge, or another WebHID-capable browser.') + this.name = 'LedgerNotSupportedError' + } +} + +export class LedgerNotConnectedError extends Error { + constructor() { + super('No Ledger device connected. Call connect() first.') + this.name = 'LedgerNotConnectedError' + } +} + +// Structural type for the pieces of the @ledgerhq/hw-app-str `Str` class we +// use, so this module doesn't need a static import of the (large, browser- +// dependent) library at the type level. +interface StellarLedgerApp { + getPublicKey(path: string, display?: boolean): Promise<{ rawPublicKey: Buffer }> + signTransaction(path: string, signatureBase: Buffer): Promise<{ signature: Buffer }> +} + +interface LedgerTransport { + close(): Promise +} + +export class LedgerService { + private transport: LedgerTransport | null = null + private stellarApp: StellarLedgerApp | null = null + private cachedPublicKey: string | null = null + + isSupported(): boolean { + return typeof navigator !== 'undefined' && 'hid' in navigator + } + + get connected(): boolean { + return this.transport !== null + } + + /** Opens a WebHID connection to a Ledger device and loads the Stellar app wrapper. */ + async connect(): Promise { + if (!this.isSupported()) { + throw new LedgerNotSupportedError() + } + + const [{ default: TransportWebHID }, { default: Str }] = await Promise.all([ + import('@ledgerhq/hw-transport-webhid'), + import('@ledgerhq/hw-app-str'), + ]) + + this.transport = (await TransportWebHID.create()) as unknown as LedgerTransport + this.stellarApp = new Str(this.transport as any) as unknown as StellarLedgerApp + } + + async disconnect(): Promise { + await this.transport?.close() + this.transport = null + this.stellarApp = null + this.cachedPublicKey = null + } + + /** + * Reads the Stellar public key from the device (does not sign anything). + * `display: true` makes the device prompt the user to confirm the address + * on-screen — used the first time a session connects to guard against a + * compromised host silently reading the wrong account. + */ + async getPublicKey(path = DEFAULT_STELLAR_LEDGER_PATH, display = false): Promise { + if (!this.stellarApp) throw new LedgerNotConnectedError() + + const { StrKey } = await import('@stellar/stellar-sdk') + const { rawPublicKey } = await this.stellarApp.getPublicKey(path, display) + const publicKey = StrKey.encodeEd25519PublicKey(rawPublicKey) + this.cachedPublicKey = publicKey + return publicKey + } + + /** + * Signs a Stellar transaction's signature base (StellarSdk.Transaction#signatureBase()) + * on-device. The user must physically approve the transaction on the + * Ledger's screen — this call blocks until they do (or reject/timeout). + * Returns the raw 64-byte ed25519 signature, suitable for + * Transaction#addSignature(publicKey, signature.toString('base64')). + */ + async signTransaction(signatureBase: Buffer, path = DEFAULT_STELLAR_LEDGER_PATH): Promise { + if (!this.stellarApp) throw new LedgerNotConnectedError() + const { signature } = await this.stellarApp.signTransaction(path, signatureBase) + return signature + } + + getDeviceInfo(path = DEFAULT_STELLAR_LEDGER_PATH): LedgerDeviceInfo { + return { + connected: this.connected, + publicKey: this.cachedPublicKey ?? undefined, + path, + } + } +} + +// Single shared instance — a Ledger device is a physical, singular resource +// per browser tab, same reasoning as the RPC client singletons in the other +// chain services. +export const ledgerService = new LedgerService() diff --git a/lib/services/stellar-payment.service.ts b/lib/services/stellar-payment.service.ts index e7e71d0..0f69166 100644 --- a/lib/services/stellar-payment.service.ts +++ b/lib/services/stellar-payment.service.ts @@ -198,6 +198,109 @@ export class StellarPaymentService { } } + /** + * Builds (but does not sign) a real Stellar payment transaction for an + * arbitrary source account — used by the Ledger signing path (Issue #92), + * where the private key lives on the user's device, not on this server. + * Unlike submitPayment(), this does not require STELLAR_SOURCE_SECRET_KEY + * to be configured at all: it only needs STELLAR_HORIZON_URL, which is + * always required. + * + * Returns the transaction XDR (for the client to hold onto and later + * resubmit alongside the signature) and the signature base — the exact + * bytes `@ledgerhq/hw-app-str`'s signTransaction() expects. + */ + async buildUnsignedTransaction( + params: SubmitPaymentParams & { sourcePublicKey: string }, + ): Promise<{ xdr: string; signatureBase: string; networkPassphrase: string }> { + const horizonUrl = this.config?.horizonUrl ?? process.env.STELLAR_HORIZON_URL + if (!horizonUrl) { + throw new StellarPaymentConfigError( + 'STELLAR_HORIZON_URL must be set as a server-only environment variable — see .env.example.', + ) + } + const networkPassphrase = this.config?.networkPassphrase ?? resolveNetworkPassphrase() + const server = this.server ?? new StellarSdk.Horizon.Server(horizonUrl) + + const asset = resolveAsset(params.asset) + if (!asset) { + throw new Error(`Unsupported or unconfigured asset: ${params.asset}`) + } + + const account = await server.loadAccount(params.sourcePublicKey) + + const txBuilder = new StellarSdk.TransactionBuilder(account, { + fee: StellarSdk.BASE_FEE, + networkPassphrase, + }) + .addOperation( + StellarSdk.Operation.payment({ + destination: params.destination, + asset, + amount: params.amount.toFixed(7), + }), + ) + .setTimeout(30) + + if (params.memo) { + txBuilder.addMemo(StellarSdk.Memo.text(params.memo)) + } + + const transaction = txBuilder.build() + + return { + xdr: transaction.toXDR(), + signatureBase: transaction.signatureBase().toString('base64'), + networkPassphrase, + } + } + + /** + * Submits a transaction that was already signed externally (e.g. by a + * Ledger device) — the counterpart to buildUnsignedTransaction(). Attaches + * the caller-supplied signature via Transaction#addSignature rather than + * signing itself, then submits exactly like submitPayment() does. + */ + async submitSignedTransaction( + xdr: string, + publicKey: string, + signatureBase64: string, + networkPassphrase: string = this.config?.networkPassphrase ?? resolveNetworkPassphrase(), + ): Promise { + const horizonUrl = this.config?.horizonUrl ?? process.env.STELLAR_HORIZON_URL + if (!horizonUrl) { + throw new StellarPaymentConfigError( + 'STELLAR_HORIZON_URL must be set as a server-only environment variable — see .env.example.', + ) + } + const server = this.server ?? new StellarSdk.Horizon.Server(horizonUrl) + + let transaction: StellarSdk.Transaction + try { + transaction = new StellarSdk.Transaction(xdr, networkPassphrase) + transaction.addSignature(publicKey, signatureBase64) + } catch (err) { + return { + hash: '', + status: 'failed', + error: err instanceof Error ? `Failed to attach signature: ${err.message}` : 'Failed to attach signature', + } + } + + const hash = transaction.hash().toString('hex') + + try { + const response = await server.submitTransaction(transaction) + return { + hash: response.hash || hash, + status: response.successful ? 'completed' : 'failed', + ledger: response.ledger, + } + } catch (err) { + return this.describeSubmissionOutcome(err, hash) + } + } + private describeLoadAccountError(err: unknown): string { const horizonResponse = getHorizonResponse(err) if (horizonResponse?.status === 404) { diff --git a/lib/signing/signer.ts b/lib/signing/signer.ts new file mode 100644 index 0000000..5368321 --- /dev/null +++ b/lib/signing/signer.ts @@ -0,0 +1,65 @@ +import type { SigningMethod } from '../types' + +/** + * lib/signing/signer.ts — Issue #92 + * + * Signing abstraction so transaction-*building* code doesn't need to know + * whether the signature comes from the server's configured software key + * (STELLAR_SOURCE_SECRET_KEY, via StellarPaymentService — the existing + * "local-key" path, unchanged by this file) or an external hardware signer. + * + * The "local-key" method needs no ISigner implementation here: it's the + * existing server-only flow in lib/services/stellar-payment.service.ts + * (StellarPaymentService#submitPayment), which builds, signs, and submits + * in one call because the key lives on the server. + * + * The "ledger" method is different in kind, not just in key source: signing + * must happen in the user's browser, with the device physically present. + * That's why StellarPaymentService also exposes buildUnsignedTransaction() + * and submitSignedTransaction() — the split lets an ISigner (like + * LedgerSigner below) sign in between those two server calls without the + * transaction-building logic itself changing at all. + * + * Flow for an external signer: + * 1. POST /api/wallet/send/prepare -> { xdr, signatureBase, networkPassphrase } + * 2. signer.sign(Buffer.from(signatureBase, 'base64')) -> raw signature + * 3. POST /api/wallet/send/submit-signed -> TransactionResult + */ + +export interface ISigner { + readonly method: SigningMethod + /** Stellar public key (G...) this signer will sign with. */ + getPublicKey(): Promise + /** Signs a transaction's signature base, returning the raw 64-byte ed25519 signature. */ + sign(signatureBase: Buffer): Promise +} + +/** + * Ledger hardware-wallet signer. Thin adapter over LedgerService so callers + * that only care about the ISigner contract (e.g. a generic "sign with + * whatever's selected" send flow) don't need to know about WebHID/hw-app-str + * directly. + */ +export class LedgerSigner implements ISigner { + readonly method: SigningMethod = 'ledger' + + constructor( + private readonly path?: string, + ) {} + + async getPublicKey(): Promise { + const { ledgerService, DEFAULT_STELLAR_LEDGER_PATH } = await import('../ledger/ledger.service') + if (!ledgerService.connected) { + await ledgerService.connect() + } + return ledgerService.getPublicKey(this.path ?? DEFAULT_STELLAR_LEDGER_PATH) + } + + async sign(signatureBase: Buffer): Promise { + const { ledgerService, DEFAULT_STELLAR_LEDGER_PATH } = await import('../ledger/ledger.service') + if (!ledgerService.connected) { + await ledgerService.connect() + } + return ledgerService.signTransaction(signatureBase, this.path ?? DEFAULT_STELLAR_LEDGER_PATH) + } +} diff --git a/lib/solana/networks.ts b/lib/solana/networks.ts new file mode 100644 index 0000000..8a22b0f --- /dev/null +++ b/lib/solana/networks.ts @@ -0,0 +1,20 @@ +import type { SolanaNetworkConfig } from '../types' + +/** + * Solana devnet by default, matching the DoD in Issue #142. USDC mint is + * Circle's official devnet deployment: + * https://developers.circle.com/stablecoins/usdc-contract-addresses + */ +export const SOLANA_NETWORK: SolanaNetworkConfig = { + name: 'Solana Devnet', + testnet: true, + rpcUrl: process.env.SOLANA_RPC_URL || 'https://api.devnet.solana.com', + blockExplorerUrl: 'https://explorer.solana.com/?cluster=devnet', + tokens: { + USDC: { mint: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU', decimals: 6 }, + }, +} + +export function getSolanaNetwork(): SolanaNetworkConfig { + return SOLANA_NETWORK +} diff --git a/lib/solana/solana-payment.service.ts b/lib/solana/solana-payment.service.ts new file mode 100644 index 0000000..3ae83b8 --- /dev/null +++ b/lib/solana/solana-payment.service.ts @@ -0,0 +1,130 @@ +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, +} from '@solana/web3.js' +import bs58 from 'bs58' +import type { SolanaTransactionStatus } from '../types' +import { solanaService } from './solana.service' +import { getSolanaNetwork } from './networks' + +/** + * lib/solana/solana-payment.service.ts + * Server-only counterpart to solana.service.ts (which is read-only). Mirrors + * lib/evm/evm-payment.service.ts / lib/services/stellar-payment.service.ts: + * build, sign, and broadcast a real native SOL transfer with a + * server-configured account, gated behind SOLANA_SOURCE_PRIVATE_KEY. + * + * MVP scope is native SOL transfers only — SPL-token (USDC) sends are + * tracked as follow-up work, same as ERC-20 sends in the EVM module. + * + * Never import this module from client code. It reads a secret key from + * server-only environment variables and must only ever run inside a Next.js + * Route Handler. + */ + +export interface SubmitSolanaPaymentParams { + /** Caller must have already validated this with solanaService.validateAddress. */ + destination: string + /** Positive SOL amount, already validated by the caller. */ + amount: number +} + +export interface SubmitSolanaPaymentResult { + signature?: string + status: SolanaTransactionStatus + error?: string +} + +export class SolanaPaymentConfigError extends Error { + constructor(message: string) { + super(message) + this.name = 'SolanaPaymentConfigError' + } +} + +function loadKeypair(): Keypair | null { + const key = process.env.SOLANA_SOURCE_PRIVATE_KEY + if (!key) return null + try { + // Accept either a base58-encoded secret key (Phantom/CLI export format) + // or a JSON array of bytes (solana-keygen format). + const bytes = key.trim().startsWith('[') ? Uint8Array.from(JSON.parse(key)) : bs58.decode(key) + return Keypair.fromSecretKey(bytes) + } catch { + return null + } +} + +export class SolanaPaymentService { + private readonly keypair: Keypair | null + private readonly connection: Connection + + constructor(keypair: Keypair | null = loadKeypair()) { + this.keypair = keypair + this.connection = new Connection(getSolanaNetwork().rpcUrl, 'confirmed') + } + + /** Address of the configured signer, if any — used by the route to catch account/key mismatches. */ + getSigningAddress(): string | null { + return this.keypair?.publicKey.toBase58() ?? null + } + + private requireKeypair(): Keypair { + if (!this.keypair) { + throw new SolanaPaymentConfigError( + 'Live Solana payment submission is not configured. Set SOLANA_SOURCE_PRIVATE_KEY (a funded ' + + 'devnet account\'s base58 secret key) as a server-only environment variable — see .env.example.', + ) + } + return this.keypair + } + + /** + * Builds, signs, and broadcasts a real native SOL transfer. + * Assumes `params` has already passed input validation (address shape, + * positive amount) — only throws for the config-missing case; every + * ledger-level outcome is returned, never thrown. + */ + async submitPayment(params: SubmitSolanaPaymentParams): Promise { + const keypair = this.requireKeypair() + + if (!solanaService.validateAddress(params.destination)) { + return { status: 'failed', error: 'Not a valid Solana address' } + } + + try { + const lamports = Math.round(params.amount * 1_000_000_000) + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: keypair.publicKey, + toPubkey: new PublicKey(params.destination), + lamports, + }), + ) + + const signature = await sendAndConfirmTransaction(this.connection, transaction, [keypair], { + commitment: 'confirmed', + }) + + return { signature, status: 'completed' } + } catch (err) { + return { + status: 'failed', + error: err instanceof Error ? err.message : 'Failed to broadcast transaction', + } + } + } +} + +let sharedInstance: SolanaPaymentService | null = null + +export function getSolanaPaymentService(): SolanaPaymentService { + if (!sharedInstance) { + sharedInstance = new SolanaPaymentService() + } + return sharedInstance +} diff --git a/lib/solana/solana.service.ts b/lib/solana/solana.service.ts new file mode 100644 index 0000000..10b83e4 --- /dev/null +++ b/lib/solana/solana.service.ts @@ -0,0 +1,101 @@ +import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js' +import { + ISolanaService, + SolanaNetworkConfig, + SolanaBalance, + SolanaServiceError, +} from '../types' +import { formatAddress } from '../helpers/format' +import { getSolanaNetwork } from './networks' + +let sharedConnection: Connection | null = null + +function getConnection(): Connection { + if (!sharedConnection) { + sharedConnection = new Connection(getSolanaNetwork().rpcUrl, 'confirmed') + } + return sharedConnection +} + +/** + * Read-only balance queries against real Solana devnet RPC — no secret key + * required. Signing/submission lives in solana-payment.service.ts, which is + * server-only. Mirrors lib/evm/evm.service.ts's shape. + */ +export class SolanaService implements ISolanaService { + getNetwork(): SolanaNetworkConfig { + return getSolanaNetwork() + } + + validateAddress(address: string): boolean { + try { + // PublicKey validates base58 + that it decodes to exactly 32 bytes. + new PublicKey(address) + return true + } catch { + return false + } + } + + async getBalances(address: string): Promise { + if (!this.validateAddress(address)) { + throw new SolanaServiceError(`Not a valid Solana address: ${address}`, 'ERR_INVALID_ADDRESS') + } + + const network = getSolanaNetwork() + const connection = getConnection() + const pubkey = new PublicKey(address) + + try { + const lamports = await connection.getBalance(pubkey) + + const balances: SolanaBalance[] = [ + { + symbol: 'SOL', + amount: lamports / LAMPORTS_PER_SOL, + raw: lamports.toString(), + decimals: 9, + }, + ] + + for (const [symbol, token] of Object.entries(network.tokens)) { + if (!token) continue + try { + // No @solana/spl-token dependency needed: getParsedTokenAccountsByOwner + // filtered by mint returns the owner's associated token account(s) + // directly via the SPL Token program's parsed account layout. + const { value } = await connection.getParsedTokenAccountsByOwner(pubkey, { + mint: new PublicKey(token.mint), + }) + const raw = value[0]?.account.data.parsed?.info?.tokenAmount?.amount ?? '0' + balances.push({ + symbol: symbol as SolanaBalance['symbol'], + amount: Number(raw) / 10 ** token.decimals, + raw, + decimals: token.decimals, + }) + } catch { + // No associated token account yet — treat as a zero balance rather + // than failing the whole lookup. + balances.push({ symbol: symbol as SolanaBalance['symbol'], amount: 0, raw: '0', decimals: token.decimals }) + } + } + + return balances + } catch (err) { + throw new SolanaServiceError( + err instanceof Error ? `Failed to fetch ${network.name} balances: ${err.message}` : 'Failed to fetch balances', + 'ERR_NETWORK_FAILURE', + ) + } + } + + shortenKey(key: string, lead = 4, tail = 4): string { + return formatAddress(key, lead, tail) + } +} + +export const solanaService = new SolanaService() + +// Re-exported for tests / advanced callers that need the raw connection. +export { getConnection as getSolanaConnection } diff --git a/lib/sui/networks.ts b/lib/sui/networks.ts new file mode 100644 index 0000000..87e2348 --- /dev/null +++ b/lib/sui/networks.ts @@ -0,0 +1,23 @@ +import type { SuiNetworkConfig } from '../types' + +/** + * Sui testnet by default, matching the DoD in Issue #143. USDC coin type is + * Circle's official testnet deployment: + * https://developers.circle.com/stablecoins/usdc-contract-addresses + */ +export const SUI_NETWORK: SuiNetworkConfig = { + name: 'Sui Testnet', + testnet: true, + rpcUrl: process.env.SUI_RPC_URL || 'https://fullnode.testnet.sui.io:443', + blockExplorerUrl: 'https://suiscan.xyz/testnet', + tokens: { + USDC: { + coinType: '0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e8::usdc::USDC', + decimals: 6, + }, + }, +} + +export function getSuiNetwork(): SuiNetworkConfig { + return SUI_NETWORK +} diff --git a/lib/sui/sui-payment.service.ts b/lib/sui/sui-payment.service.ts new file mode 100644 index 0000000..45dcb4e --- /dev/null +++ b/lib/sui/sui-payment.service.ts @@ -0,0 +1,126 @@ +import { SuiClient } from '@mysten/sui/client' +import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519' +import { Transaction as SuiTransaction } from '@mysten/sui/transactions' +import type { SuiTransactionStatus } from '../types' +import { suiService } from './sui.service' +import { getSuiNetwork } from './networks' + +/** + * lib/sui/sui-payment.service.ts + * Server-only counterpart to sui.service.ts (which is read-only). Mirrors + * lib/evm/evm-payment.service.ts / lib/solana/solana-payment.service.ts: + * build, sign, and broadcast a real native SUI transfer with a + * server-configured account, gated behind SUI_SOURCE_PRIVATE_KEY. + * + * MVP scope is native SUI transfers only — coin-type (USDC) sends are + * tracked as follow-up work, same as ERC-20/SPL-token sends in the other + * chain modules. + * + * Never import this module from client code. It reads a secret key from + * server-only environment variables and must only ever run inside a Next.js + * Route Handler. + */ + +export interface SubmitSuiPaymentParams { + /** Caller must have already validated this with suiService.validateAddress. */ + destination: string + /** Positive SUI amount, already validated by the caller. */ + amount: number +} + +export interface SubmitSuiPaymentResult { + digest?: string + status: SuiTransactionStatus + error?: string +} + +export class SuiPaymentConfigError extends Error { + constructor(message: string) { + super(message) + this.name = 'SuiPaymentConfigError' + } +} + +function loadKeypair(): Ed25519Keypair | null { + const key = process.env.SUI_SOURCE_PRIVATE_KEY + if (!key) return null + try { + // Accepts Sui's standard bech32 "suiprivkey1..." exported-key format. + return Ed25519Keypair.fromSecretKey(key.trim()) + } catch { + return null + } +} + +export class SuiPaymentService { + private readonly keypair: Ed25519Keypair | null + private readonly client: SuiClient + + constructor(keypair: Ed25519Keypair | null = loadKeypair()) { + this.keypair = keypair + this.client = new SuiClient({ url: getSuiNetwork().rpcUrl }) + } + + /** Address of the configured signer, if any — used by the route to catch account/key mismatches. */ + getSigningAddress(): string | null { + return this.keypair?.toSuiAddress() ?? null + } + + private requireKeypair(): Ed25519Keypair { + if (!this.keypair) { + throw new SuiPaymentConfigError( + 'Live Sui payment submission is not configured. Set SUI_SOURCE_PRIVATE_KEY (a funded ' + + 'testnet account\'s bech32 "suiprivkey1..." secret key) as a server-only environment ' + + 'variable — see .env.example.', + ) + } + return this.keypair + } + + /** + * Builds, signs, and broadcasts a real native SUI transfer. + * Assumes `params` has already passed input validation (address shape, + * positive amount) — only throws for the config-missing case; every + * ledger-level outcome is returned, never thrown. + */ + async submitPayment(params: SubmitSuiPaymentParams): Promise { + const keypair = this.requireKeypair() + + if (!suiService.validateAddress(params.destination)) { + return { status: 'failed', error: 'Not a valid Sui address' } + } + + try { + const mist = BigInt(Math.round(params.amount * 1_000_000_000)) + const tx = new SuiTransaction() + const [coin] = tx.splitCoins(tx.gas, [mist]) + tx.transferObjects([coin], params.destination) + tx.setSender(keypair.toSuiAddress()) + + const result = await this.client.signAndExecuteTransaction({ + signer: keypair, + transaction: tx, + options: { showEffects: true }, + }) + + await this.client.waitForTransaction({ digest: result.digest }).catch(() => null) + + const success = result.effects?.status?.status === 'success' + return { digest: result.digest, status: success ? 'completed' : 'failed', error: result.effects?.status?.error } + } catch (err) { + return { + status: 'failed', + error: err instanceof Error ? err.message : 'Failed to broadcast transaction', + } + } + } +} + +let sharedInstance: SuiPaymentService | null = null + +export function getSuiPaymentService(): SuiPaymentService { + if (!sharedInstance) { + sharedInstance = new SuiPaymentService() + } + return sharedInstance +} diff --git a/lib/sui/sui.service.ts b/lib/sui/sui.service.ts new file mode 100644 index 0000000..68fbba1 --- /dev/null +++ b/lib/sui/sui.service.ts @@ -0,0 +1,83 @@ +import { SuiClient } from '@mysten/sui/client' +import { isValidSuiAddress } from '@mysten/sui/utils' +import { + ISuiService, + SuiNetworkConfig, + SuiBalance, + SuiServiceError, +} from '../types' +import { formatAddress } from '../helpers/format' +import { getSuiNetwork } from './networks' + +let sharedClient: SuiClient | null = null + +function getClient(): SuiClient { + if (!sharedClient) { + sharedClient = new SuiClient({ url: getSuiNetwork().rpcUrl }) + } + return sharedClient +} + +/** + * Read-only balance queries against real Sui testnet RPC — no secret key + * required. Signing/submission lives in sui-payment.service.ts, which is + * server-only. Mirrors lib/evm/evm.service.ts's shape, adapted for Sui's + * object-centric coin model (getBalance sums owned Coin objects rather + * than reading a single account balance slot). + */ +export class SuiService implements ISuiService { + getNetwork(): SuiNetworkConfig { + return getSuiNetwork() + } + + validateAddress(address: string): boolean { + return isValidSuiAddress(address) + } + + async getBalances(address: string): Promise { + if (!this.validateAddress(address)) { + throw new SuiServiceError(`Not a valid Sui address: ${address}`, 'ERR_INVALID_ADDRESS') + } + + const network = getSuiNetwork() + const client = getClient() + + try { + const native = await client.getBalance({ owner: address }) + const balances: SuiBalance[] = [ + { + symbol: 'SUI', + amount: Number(native.totalBalance) / 1e9, + raw: native.totalBalance, + decimals: 9, + }, + ] + + for (const [symbol, token] of Object.entries(network.tokens)) { + if (!token) continue + const coin = await client.getBalance({ owner: address, coinType: token.coinType }) + balances.push({ + symbol: symbol as SuiBalance['symbol'], + amount: Number(coin.totalBalance) / 10 ** token.decimals, + raw: coin.totalBalance, + decimals: token.decimals, + }) + } + + return balances + } catch (err) { + throw new SuiServiceError( + err instanceof Error ? `Failed to fetch ${network.name} balances: ${err.message}` : 'Failed to fetch balances', + 'ERR_NETWORK_FAILURE', + ) + } + } + + shortenKey(key: string, lead = 6, tail = 4): string { + return formatAddress(key, lead, tail) + } +} + +export const suiService = new SuiService() + +export { getClient as getSuiClient } diff --git a/lib/types.ts b/lib/types.ts index 8f6bea0..a017fd6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1162,3 +1162,116 @@ export interface IEvmService { getBalances(chainId: EvmChainId, address: string): Promise; shortenKey(key: string, lead?: number, tail?: number): string; } + +// ── Solana chain (Issue #142) ─────────────────────────────────────────────── +// Same "self-contained module alongside IWalletService" pattern as the EVM +// slice above. Solana uses ed25519 keys + base58 addresses — nothing like +// EVM's secp256k1/checksum model — hence its own module rather than an +// extension of EvmService. + +export type SolanaAssetSymbol = "SOL" | "USDC"; + +export interface SolanaNetworkConfig { + name: string; + testnet: boolean; + rpcUrl: string; + blockExplorerUrl: string; + /** SPL token mint addresses available on this network, keyed by symbol. */ + tokens: Partial, { mint: string; decimals: number }>>; +} + +export interface SolanaBalance { + symbol: SolanaAssetSymbol; + amount: number; + raw: string; + decimals: number; +} + +export type SolanaTransactionStatus = "completed" | "pending" | "failed"; + +export interface SolanaTransactionResult { + success: boolean; + signature?: string; + status: SolanaTransactionStatus; + error?: string; +} + +export class SolanaServiceError extends ServiceError { + constructor(message: string, code?: string) { + super(message, code); + this.name = "SolanaServiceError"; + } +} + +export interface ISolanaService { + getNetwork(): SolanaNetworkConfig; + /** Structural check: valid base58, decodes to a 32-byte public key. */ + validateAddress(address: string): boolean; + getBalances(address: string): Promise; + shortenKey(key: string, lead?: number, tail?: number): string; +} + +// ── Sui chain (Issue #143) ────────────────────────────────────────────────── +// Sui is object-centric (coins are owned objects, not account balances) and +// uses its own address derivation, so — like Solana — it gets a dedicated +// module rather than a retrofit of IWalletService. + +export type SuiAssetSymbol = "SUI" | "USDC"; + +export interface SuiNetworkConfig { + name: string; + testnet: boolean; + rpcUrl: string; + blockExplorerUrl: string; + /** Coin type strings available on this network, keyed by symbol. */ + tokens: Partial, { coinType: string; decimals: number }>>; +} + +export interface SuiBalance { + symbol: SuiAssetSymbol; + amount: number; + raw: string; + decimals: number; +} + +export type SuiTransactionStatus = "completed" | "pending" | "failed"; + +export interface SuiTransactionResult { + success: boolean; + digest?: string; + status: SuiTransactionStatus; + error?: string; +} + +export class SuiServiceError extends ServiceError { + constructor(message: string, code?: string) { + super(message, code); + this.name = "SuiServiceError"; + } +} + +export interface ISuiService { + getNetwork(): SuiNetworkConfig; + /** Structural check: 0x-prefixed, 32-byte (64 hex char) address. */ + validateAddress(address: string): boolean; + getBalances(address: string): Promise; + shortenKey(key: string, lead?: number, tail?: number): string; +} + +// ── Hardware-wallet (Ledger) signing (Issue #92) ──────────────────────────── +// A signer abstraction so transaction-building code can target either the +// server-held software key (StellarPaymentService) or an external Ledger +// device without changing the caller. MVP scope is the Ledger Stellar app +// (XLM) via WebHID, run entirely client-side in the browser — Ledger's +// signing flow needs a physical device present in the user's browser +// session, so it cannot live in a server-only service the way +// StellarPaymentService does. Solana/Sui Ledger apps are TODO (see +// lib/ledger/ledger.service.ts). + +export type SigningMethod = "local-key" | "ledger"; + +export interface LedgerDeviceInfo { + connected: boolean; + publicKey?: string; + path: string; +} diff --git a/package-lock.json b/package-lock.json index a6d6fdf..b5637ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.10.0", + "@ledgerhq/hw-app-str": "^7.7.7", + "@ledgerhq/hw-transport-webhid": "^6.36.0", + "@mysten/sui": "^1.45.2", "@opentelemetry/api": "^1.9.1", "@opentelemetry/context-async-hooks": "^2.10.0", "@opentelemetry/core": "^2.10.0", @@ -27,9 +30,12 @@ "@radix-ui/react-switch": "1.1.2", "@radix-ui/react-tabs": "1.1.2", "@simplewebauthn/server": "^13.3.2", + "@solana/web3.js": "^1.98.4", "@stellar/stellar-sdk": "^16.0.1", "@vercel/analytics": "1.3.1", "autoprefixer": "^10.4.20", + "bcryptjs": "^2.4.3", + "bs58": "^4.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "4.1.0", @@ -62,6 +68,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/bs58": "^4.0.4", "@types/decimal.js": "^0.0.32", "@types/jest": "^30.0.0", "@types/node": "^22", @@ -86,6 +93,34 @@ "typescript": "^5" } }, + "node_modules/@0no-co/graphql.web": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.4.tgz", + "integrity": "sha512-imSwulOeDQodRy/olQmVEo2PiY6ntjkZ9eiGdw6lMYylh/tay9b7MusyJBmEnkL8GiKRKr6ltr+D42mY5bd8Bg==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@0no-co/graphqlsp": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.17.5.tgz", + "integrity": "sha512-wsygfTO3pdkz4idDw8BxdMdgB9AB8nuJDJTUwYy87BiZEZRXS4bEfn4oNl9VtxTo6j2lzX/gED+IvOgxsSy2Jg==", + "license": "MIT", + "dependencies": { + "@gql.tada/internal": "^1.2.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", @@ -873,6 +908,54 @@ "@formatjs/fast-memoize": "3.1.7" } }, + "node_modules/@gql.tada/cli-utils": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.9.3.tgz", + "integrity": "sha512-P1TiXErpJwIi73sei5fzwGA/SOeCaIHFWFR4RdZPLwqxZzQN0T6MAUivzXBgKCORL67rvYnLaaPoWeqWq/61ug==", + "license": "MIT", + "dependencies": { + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/internal": "1.2.2", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.16.0", + "@gql.tada/svelte-support": "1.0.3", + "@gql.tada/vue-support": "1.0.3", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "node_modules/@gql.tada/internal": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.2.2.tgz", + "integrity": "sha512-4lZcElPP6MC8Ct8KN70LR2WQsHjbAsyAmTGG09VsOPhx738UFIYaL0S1XiI2pqq2tj/sDs/y3b9jvKoWGL7iuQ==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.1" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@hexagon/base64": { "version": "1.1.28", "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", @@ -1973,12 +2056,199 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@ledgerhq/devices": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.17.0.tgz", + "integrity": "sha512-l+rrVQEjR1hSWOLD00LFX4zbS8yB1M/Mb6UYPmSHGO7TmE1CFbZ4CmDJC/kZSl3hdrXCJ+YUNpvaLCcSWDwA+Q==", + "license": "Apache-2.0", + "dependencies": { + "semver": "7.7.3" + } + }, + "node_modules/@ledgerhq/devices/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ledgerhq/errors": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-7.0.0.tgz", + "integrity": "sha512-+Q/vykUlNeIxiM+I3cu1B660WLkzlmIsHLTV9QNV5D2/Ocplx3QMg52NYq2X7OAfGQnfH1rQvhn/NrjT+t9wBA==", + "license": "Apache-2.0" + }, + "node_modules/@ledgerhq/hw-app-str": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-app-str/-/hw-app-str-7.7.7.tgz", + "integrity": "sha512-RNOacpfB9YI82o8Nw7oleec1I4a5jAmBtxyNeoyTPayXcnhbnLy6v14fzz7M4cNsNHOqFtWZ17LmrclOaU1P/g==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/hw-transport": "6.35.7", + "bip32-path": "^0.4.2" + } + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.35.7", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.35.7.tgz", + "integrity": "sha512-vVhAVQ56+7A5FY5Mr09HY+bmf3H6TXpwsj+/xbadNkjl//e/YzTWfpXk4eBnj3hwk1PQ9Mn6pKFH0BHjS2BKlg==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.17.0", + "@ledgerhq/errors": "^7.0.0", + "@ledgerhq/logs": "^6.17.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/hw-transport-webhid": { + "version": "6.36.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.36.0.tgz", + "integrity": "sha512-1mKWm3LyGOgmaYlAiqbmaGoupOZHbj2Kow5sXLxKZzQa4kFvQuUdinYOWhs7T8hqJyAkz9WlHYyKM7aIs8kjNg==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.16.0", + "@ledgerhq/errors": "^6.37.0", + "@ledgerhq/hw-transport": "6.35.5", + "@ledgerhq/logs": "^6.17.0" + } + }, + "node_modules/@ledgerhq/hw-transport-webhid/node_modules/@ledgerhq/devices": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.16.0.tgz", + "integrity": "sha512-brXLPzkvGM3D5YNsWQ25P5G4SmWdSNBed9W8wKoOIRLGdRfvE+bg9mzFty0iZ+aRLBkLoXwX7xKIL9zUi6LBKQ==", + "license": "Apache-2.0", + "dependencies": { + "semver": "7.7.3" + } + }, + "node_modules/@ledgerhq/hw-transport-webhid/node_modules/@ledgerhq/errors": { + "version": "6.37.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.37.0.tgz", + "integrity": "sha512-T5yiKI5UX7ugeocdTF3TUsCIN2BH41Bio4ZeN410YFjFOf3es08n/5JyMzzKwzRgP0blG3HfBf7s7vJKqCSAeg==", + "license": "Apache-2.0" + }, + "node_modules/@ledgerhq/hw-transport-webhid/node_modules/@ledgerhq/hw-transport": { + "version": "6.35.5", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.35.5.tgz", + "integrity": "sha512-P4+wtLewLWgxPtIb90h5kjpzXVlC6f4IBQBmvowVFkInvZt34ffXkX7wa5KfMzu4l3cqCcpNSqtPSCMp+0vuqg==", + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/devices": "8.16.0", + "@ledgerhq/errors": "^6.37.0", + "@ledgerhq/logs": "^6.17.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/hw-transport-webhid/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.17.0.tgz", + "integrity": "sha512-yra33g5q/AU7+PwAws+GaVpQGUuxnDREjVBnviJjcaJLVKuLzI4pnj8Bd3nY3fypM5k1yZEYKEXfUuGFUjP2+w==", + "license": "Apache-2.0" + }, "node_modules/@levischuck/tiny-cbor": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", "license": "MIT" }, + "node_modules/@mysten/bcs": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-1.9.2.tgz", + "integrity": "sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ==", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "0.2.0", + "@scure/base": "^1.2.6" + } + }, + "node_modules/@mysten/sui": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-1.45.2.tgz", + "integrity": "sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==", + "license": "Apache-2.0", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "@mysten/bcs": "1.9.2", + "@mysten/utils": "0.2.0", + "@noble/curves": "=1.9.4", + "@noble/hashes": "^1.8.0", + "@protobuf-ts/grpcweb-transport": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "@scure/base": "^1.2.6", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "gql.tada": "^1.8.13", + "graphql": "^16.11.0", + "poseidon-lite": "0.2.1", + "valibot": "^1.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mysten/sui/node_modules/@noble/curves": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", + "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui/node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/@mysten/utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.2.0.tgz", + "integrity": "sha512-CM6kJcJHX365cK6aXfFRLBiuyXc5WSBHQ43t94jqlCAIRw8umgNcTb5EnEA9n31wPAQgLDGgbG/rCUISCTJ66w==", + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^1.2.6" + } + }, "node_modules/@next/env": { "version": "16.2.11", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.11.tgz", @@ -2868,6 +3138,31 @@ "node": ">=18" } }, + "node_modules/@protobuf-ts/grpcweb-transport": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/grpcweb-transport/-/grpcweb-transport-2.11.1.tgz", + "integrity": "sha512-1W4utDdvOB+RHMFQ0soL4JdnxjXV+ddeGIUg08DvZrA8Ms6k5NN6GBFU2oHZdTOcJVpPrDJ02RJlqtaoCMNBtw==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -3733,6 +4028,115 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@stellar/js-xdr": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz", @@ -4492,6 +4896,26 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/bs58": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.4.tgz", + "integrity": "sha512-0IEpMFXXQi2zXaXl9GJ3sRwQo0uEkD+yFOv+FnAU5lkPtcu6h61xb7jc2CFPEZ5BUOaiP13ThuGc9HD4R8lR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "base-x": "^3.0.6" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -4665,7 +5089,6 @@ "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4705,6 +5128,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -5159,6 +5597,18 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5444,6 +5894,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/base32.js": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", @@ -5485,12 +5944,41 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, "node_modules/bignumber.js": { "version": "11.1.5", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.5.tgz", "integrity": "sha512-6WmzCNtUnfKpbozq+hOgWaZMMzORmYBwF1xZScyoIX3QRYWeKTtxxwDOW5tIz7C9BdjkIYHGTcelCLkXg0mndw==", "license": "MIT" }, + "node_modules/bip32-path": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", + "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, "node_modules/brace-expansion": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz", @@ -5534,6 +6022,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -5575,6 +6072,20 @@ "dev": true, "license": "MIT" }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6103,6 +6614,18 @@ "node": ">=0.10.0" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6331,6 +6854,21 @@ "node": ">= 0.4" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6370,6 +6908,15 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/eventsource": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-4.1.0.tgz", @@ -6450,6 +6997,14 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -6489,6 +7044,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -6742,6 +7303,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gql.tada": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.11.3.tgz", + "integrity": "sha512-5JCI4j2f0nug8ILaCQys/yjOP78QqqjVUf47OQsME63rZfemsHT3e5vcfbHsnZiG6vxyqpKUJArtXEVwSefUhw==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.2", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/cli-utils": "1.9.3", + "@gql.tada/internal": "1.2.2" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6749,6 +7329,15 @@ "dev": true, "license": "ISC" }, + "node_modules/graphql": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz", + "integrity": "sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==", + "license": "MIT", + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } + }, "node_modules/harmony-reflect": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", @@ -6872,6 +7461,15 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7127,6 +7725,15 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/isows": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", @@ -7242,6 +7849,65 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jest": { "version": "30.4.2", "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", @@ -8546,6 +9212,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9238,6 +9910,60 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9608,6 +10334,12 @@ "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", "license": "MIT" }, + "node_modules/poseidon-lite": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.2.1.tgz", + "integrity": "sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", @@ -10033,6 +10765,57 @@ "node": ">=8" } }, + "node_modules/rpc-websockets": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz", + "integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^14.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", + "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -10040,6 +10823,26 @@ "dev": true, "license": "MIT" }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -10281,6 +11084,21 @@ "node": ">=10" } }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -10474,6 +11292,15 @@ } } }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10599,6 +11426,11 @@ "node": "*" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -10719,7 +11551,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10745,7 +11576,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -10880,6 +11710,30 @@ } } }, + "node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -10895,6 +11749,20 @@ "node": ">=10.12.0" } }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", diff --git a/package.json b/package.json index 51c8878..8987170 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ }, "dependencies": { "@hookform/resolvers": "^3.10.0", + "@ledgerhq/hw-app-str": "^7.7.7", + "@ledgerhq/hw-transport-webhid": "^6.36.0", + "@mysten/sui": "^1.45.2", "@opentelemetry/api": "^1.9.1", "@opentelemetry/context-async-hooks": "^2.10.0", "@opentelemetry/core": "^2.10.0", @@ -40,10 +43,12 @@ "@radix-ui/react-switch": "1.1.2", "@radix-ui/react-tabs": "1.1.2", "@simplewebauthn/server": "^13.3.2", + "@solana/web3.js": "^1.98.4", "@stellar/stellar-sdk": "^16.0.1", "@vercel/analytics": "1.3.1", - "bcryptjs": "^2.4.3", "autoprefixer": "^10.4.20", + "bcryptjs": "^2.4.3", + "bs58": "^4.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "4.1.0", @@ -76,6 +81,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/bs58": "^4.0.4", "@types/decimal.js": "^0.0.32", "@types/jest": "^30.0.0", "@types/node": "^22",