import { getBalanceSol, getStoredSolanaWalletChoices, encodeSolanaSecretKeyBase58, formatSol } from './solana-wallet-service.js'; import { state } from '../state.js'; const FREE_TURBO_UPLOAD_BYTES = 100 * 1024; const WINC_PER_CREDIT = 1_000_000_000_000n; let turboLibPromise = null; function parseSolToLamports(amountSol) { const raw = String(amountSol ?? '').trim().replace(',', '.'); const match = raw.match(/^(\d+)(?:\.(\d+))?$/); if (!match) { throw new Error('Сумма пополнения должна быть числом'); } const intPart = BigInt(match[1] || '0'); const frac = String(match[2] || ''); if (frac.length > 9) { throw new Error('Слишком много знаков после запятой для SOL'); } const fracPadded = `${frac}${'0'.repeat(9 - frac.length)}`; const lamports = (intPart * 1_000_000_000n) + BigInt(fracPadded || '0'); if (lamports <= 0n) { throw new Error('Сумма пополнения должна быть больше 0'); } return lamports.toString(); } function normalizeKeySource(value) { return String(value || '').trim().toLowerCase() === 'root' ? 'root' : 'client'; } async function loadTurboLib() { if (!turboLibPromise) { turboLibPromise = import('../vendor/turbo-sdk.web.bundle.js'); } return turboLibPromise; } async function getTurboWalletChoice({ login, storagePwd, keySource = 'client' } = {}) { const normalizedKeySource = normalizeKeySource(keySource); const choices = await getStoredSolanaWalletChoices({ login, storagePwd, includeRoot: true }); const selected = choices.find((item) => item.keySource === normalizedKeySource) || choices[0] || null; if (!selected?.keypair) { throw new Error(normalizedKeySource === 'root' ? 'На устройстве не найден root key для Turbo.' : 'На устройстве не найден client key для Turbo.'); } return selected; } async function createTurboClientFromWalletChoice(choice) { const moduleRef = await loadTurboLib(); const TurboFactory = moduleRef?.TurboFactory; if (!TurboFactory?.authenticated) { throw new Error('Turbo SDK не удалось инициализировать.'); } const privateKey = await encodeSolanaSecretKeyBase58(choice.keypair.secretKey); return TurboFactory.authenticated({ privateKey, token: 'solana', }); } function creditsFromWinc(winc) { const value = BigInt(String(winc || '0')); return Number(value) / Number(WINC_PER_CREDIT); } function shortAddress(value) { const raw = String(value || '').trim(); if (raw.length <= 16) return raw; return `${raw.slice(0, 8)}...${raw.slice(-6)}`; } export function isFreeTurboUploadSize(byteLength) { const bytes = Number(byteLength || 0); return Number.isFinite(bytes) && bytes > 0 && bytes <= FREE_TURBO_UPLOAD_BYTES; } export function getFreeTurboUploadBytesLimit() { return FREE_TURBO_UPLOAD_BYTES; } export function formatTurboCredits(value, digits = 6) { const n = Number(value); if (!Number.isFinite(n)) return '0'; return n.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: digits, }); } export async function getTurboContextForStoredSolanaKey({ login, storagePwd, keySource = 'client' } = {}) { const choice = await getTurboWalletChoice({ login, storagePwd, keySource }); const turbo = await createTurboClientFromWalletChoice(choice); return { turbo, address: choice.address, keySource: choice.keySource, label: choice.label, keypair: choice.keypair, shortAddress: shortAddress(choice.address), }; } export async function getTurboBalanceForStoredSolanaKey({ login, storagePwd, keySource = 'client' } = {}) { const ctx = await getTurboContextForStoredSolanaKey({ login, storagePwd, keySource }); const balance = await ctx.turbo.getBalance(ctx.address); const solanaBalance = await getBalanceSol({ endpoint: state.registrationPayment?.endpoint || state.entrySettings?.solanaRpc || undefined, address: ctx.address, }); return { ...ctx, controlledWinc: String(balance.controlledWinc || '0'), spendableWinc: String(balance.winc || '0'), effectiveWinc: String(balance.effectiveBalance || '0'), controlledCredits: creditsFromWinc(balance.controlledWinc || '0'), spendableCredits: creditsFromWinc(balance.winc || '0'), effectiveCredits: creditsFromWinc(balance.effectiveBalance || '0'), solBalance: solanaBalance.sol, solLamports: solanaBalance.lamports, }; } export async function estimateTurboUploadPrice({ login, storagePwd, keySource = 'client', byteLength } = {}) { const bytes = Number(byteLength); if (!Number.isInteger(bytes) || bytes <= 0) { throw new Error('Некорректный размер файла для Turbo.'); } const ctx = await getTurboContextForStoredSolanaKey({ login, storagePwd, keySource }); const rows = await ctx.turbo.getUploadCosts({ bytes: [bytes] }); const price = Array.isArray(rows) ? rows[0] : null; if (!price) { throw new Error('Turbo не вернул цену загрузки.'); } const winc = String(price.winc || '0'); return { ...ctx, byteLength: bytes, isFree: isFreeTurboUploadSize(bytes), winc, credits: creditsFromWinc(winc), }; } export async function uploadFileWithTurbo({ login, storagePwd, keySource = 'client', file, tags = [], shineType = 'attachment', onProgress, } = {}) { if (!(file instanceof File)) { throw new Error('Выберите файл для загрузки через Turbo.'); } const ctx = await getTurboContextForStoredSolanaKey({ login, storagePwd, keySource }); const result = await ctx.turbo.uploadFile({ file, dataItemOpts: { tags: [ { name: 'Content-Type', value: String(file.type || 'application/octet-stream') }, { name: 'App-Name', value: 'SHiNE' }, { name: 'SHiNE-Type', value: String(shineType || 'attachment') }, ...((Array.isArray(tags) ? tags : []).filter((item) => item?.name && item?.value)), ], }, events: typeof onProgress === 'function' ? { onProgress: ({ totalBytes, processedBytes, step }) => { onProgress({ totalBytes, processedBytes, step, }); }, } : undefined, }); return { ...ctx, id: String(result?.id || '').trim(), owner: String(result?.owner || '').trim(), winc: String(result?.winc || '0'), credits: creditsFromWinc(result?.winc || '0'), }; } export async function topUpTurboWithSolana({ login, storagePwd, keySource = 'client', amountSol, turboCreditDestinationAddress = '', } = {}) { const ctx = await getTurboContextForStoredSolanaKey({ login, storagePwd, keySource }); const tokenAmount = parseSolToLamports(amountSol); const result = await ctx.turbo.topUpWithTokens({ tokenAmount, turboCreditDestinationAddress: String(turboCreditDestinationAddress || '').trim() || undefined, }); return { ...ctx, amountSol: formatSol(amountSol, 9), lamports: tokenAmount, txId: String(result?.id || '').trim(), target: String(result?.target || '').trim(), rewardWinc: String(result?.reward || '0'), rewardCredits: creditsFromWinc(result?.reward || '0'), }; }