SHA256
214 lines
7.2 KiB
JavaScript
214 lines
7.2 KiB
JavaScript
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'),
|
|
};
|
|
}
|