import { loadEncryptedUserSecrets, updateEncryptedUserSecrets } from './key-vault.js'; import { extractClientKey32FromStoredValue } from './client-key-utils.js'; import { deriveArweaveWalletFromClientKey32 } from './sawd-v1.js'; const DEFAULT_ARWEAVE_GATEWAY = 'https://arweave.net'; const AR_TOPUP_URL = 'https://changenow.io/exchange?from=usd&to=ar&amount=10&fiatMode=true'; const WINSTON_PER_AR = 1_000_000_000_000n; let arweaveLibPromise = null; function normalizeGateway(rawGateway) { const raw = String(rawGateway || '').trim(); if (!raw) return DEFAULT_ARWEAVE_GATEWAY; let parsed; try { parsed = new URL(raw); } catch { throw new Error('Некорректный Arweave gateway URL'); } if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { throw new Error('Arweave gateway должен использовать http или https'); } const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`.replace(/\/+$/g, ''); return normalized || DEFAULT_ARWEAVE_GATEWAY; } function parseGatewayForArweaveInit(gateway) { let parsed; try { parsed = new URL(gateway); } catch { throw new Error('Некорректный Arweave gateway URL'); } const protocol = parsed.protocol.replace(':', ''); if (protocol !== 'http' && protocol !== 'https') { throw new Error('Arweave gateway должен использовать http или https'); } const port = parsed.port ? Number(parsed.port) : (protocol === 'https' ? 443 : 80); return { protocol, host: parsed.hostname, port, }; } function parseArToWinston(amountAr) { const raw = String(amountAr ?? '').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 > 12) { throw new Error('Слишком много знаков после запятой (максимум 12)'); } const fracPadded = `${frac}${'0'.repeat(12 - frac.length)}`; const winston = (intPart * WINSTON_PER_AR) + BigInt(fracPadded || '0'); if (winston <= 0n) { throw new Error('Сумма перевода должна быть больше 0'); } return winston.toString(); } async function loadArweaveLib() { if (!arweaveLibPromise) { arweaveLibPromise = import('https://esm.sh/arweave@1.15.7?bundle'); } return arweaveLibPromise; } function pickCachedWallet(secrets) { const cached = secrets?.arweaveWallet; if (!cached || typeof cached !== 'object') return null; const derivation = String(cached.derivation || '').trim(); const address = String(cached.address || '').trim(); const owner = String(cached.owner || '').trim(); const jwk = cached.jwk; if (derivation !== 'SAWD-v1' || !address || !owner || !jwk || typeof jwk !== 'object') { return null; } if (!String(jwk.kty || '').trim() || !String(jwk.e || '').trim() || !String(jwk.n || '').trim() || !String(jwk.d || '').trim()) { return null; } return { derivation, address, owner, jwk, }; } function normalizeExtraWallets(secrets) { const rows = Array.isArray(secrets?.arweaveWallets) ? secrets.arweaveWallets : []; return rows .filter((item) => item && typeof item === 'object') .map((item) => ({ id: String(item.id || item.address || '').trim(), label: String(item.label || '').trim(), address: String(item.address || '').trim(), owner: String(item.owner || '').trim(), jwk: item.jwk, createdAtMs: Number(item.createdAtMs || 0), })) .filter((item) => item.id && item.address && item.jwk && typeof item.jwk === 'object'); } function parseJwkSecretString(secret) { const raw = String(secret || '').trim(); if (!raw) throw new Error('Введите секрет Arweave-кошелька.'); const tryParse = (value) => { const parsed = JSON.parse(value); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('Некорректный JWK Arweave.'); } return parsed; }; if (raw.startsWith('{')) return tryParse(raw); const normalized = raw .replace(/-/g, '+') .replace(/_/g, '/') .padEnd(Math.ceil(raw.length / 4) * 4, '='); try { return tryParse(atob(normalized)); } catch { throw new Error('Секрет должен быть JSON JWK или base64url(JSON JWK).'); } } function validateArweaveJwk(jwk) { if (!jwk || typeof jwk !== 'object') throw new Error('Некорректный JWK Arweave.'); ['kty', 'e', 'n', 'd', 'p', 'q', 'dp', 'dq', 'qi'].forEach((key) => { if (!String(jwk[key] || '').trim()) throw new Error(`В JWK Arweave нет поля ${key}.`); }); } function safeStatus(onStatus, text) { if (typeof onStatus !== 'function') return; try { onStatus(String(text || '')); } catch { // ignore callback errors } } export async function getArweaveWalletChoices({ login, storagePwd, onStatus } = {}) { const derived = await getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus }); const secrets = await loadEncryptedUserSecrets(String(login || '').trim(), String(storagePwd || '').trim()); const extras = normalizeExtraWallets(secrets); return [ { id: 'derived-client-key', label: 'Стандартный из client key', address: derived.address, owner: derived.owner, jwk: derived.jwk, isDefault: true, }, ...extras.map((item) => ({ ...item, label: item.label || `Arweave ${item.address.slice(0, 6)}...${item.address.slice(-6)}`, isDefault: false, })), ]; } export async function addArweaveWalletSecret({ login, storagePwd, secret, label = '' } = {}) { const cleanLogin = String(login || '').trim(); const cleanPwd = String(storagePwd || '').trim(); if (!cleanLogin || !cleanPwd) throw new Error('Нет активной сессии.'); const jwk = parseJwkSecretString(secret); validateArweaveJwk(jwk); const moduleRef = await loadArweaveLib(); const Arweave = moduleRef?.default || moduleRef; const arweave = Arweave.init({ host: 'arweave.net', port: 443, protocol: 'https' }); const address = await arweave.wallets.jwkToAddress(jwk); const owner = String(jwk.n || '').trim(); const id = `manual-${address}`; const createdAtMs = Date.now(); const wallet = { id, label: String(label || '').trim() || `Arweave ${address.slice(0, 6)}...${address.slice(-6)}`, address, owner, jwk, createdAtMs, }; await updateEncryptedUserSecrets(cleanLogin, cleanPwd, (current) => { const wallets = normalizeExtraWallets(current).filter((item) => item.address !== address); wallets.push(wallet); return { ...current, arweaveWallets: wallets, }; }); return wallet; } export async function getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus } = {}) { const cleanLogin = String(login || '').trim(); const cleanPwd = String(storagePwd || '').trim(); if (!cleanLogin || !cleanPwd) { throw new Error('Нет активной сессии для доступа к client.key'); } const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd); const cached = pickCachedWallet(secrets); if (cached) { safeStatus(onStatus, 'Arweave-кошелёк загружен.'); return cached; } safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего client key. Это может занять немного времени.'); const storedClientKey = String(secrets?.clientKey || '').trim(); if (!storedClientKey) { throw new Error('На устройстве не найден client.key'); } const clientKey32 = extractClientKey32FromStoredValue(storedClientKey); let wallet; try { wallet = await deriveArweaveWalletFromClientKey32(clientKey32); } finally { clientKey32.fill(0); } const cachedWallet = { derivation: wallet.derivation, address: wallet.address, owner: wallet.owner, jwk: wallet.jwk, createdAtMs: Date.now(), }; await updateEncryptedUserSecrets(cleanLogin, cleanPwd, (nextSecrets) => ({ ...nextSecrets, arweaveWallet: cachedWallet, })); return { derivation: cachedWallet.derivation, address: cachedWallet.address, owner: cachedWallet.owner, jwk: cachedWallet.jwk, }; } export async function getArweaveBalance({ gateway, address }) { const normalizedGateway = normalizeGateway(gateway); const cleanAddress = String(address || '').trim(); if (!cleanAddress) { throw new Error('Не указан адрес Arweave'); } const url = `${normalizedGateway}/wallet/${encodeURIComponent(cleanAddress)}/balance`; const response = await fetch(url, { method: 'GET' }); const rawText = (await response.text()).trim(); if (!response.ok) { throw new Error(`Не удалось получить баланс Arweave (${response.status} ${response.statusText})`); } if (!/^\d+$/.test(rawText)) { throw new Error('Arweave gateway вернул некорректный баланс'); } const winstonBig = BigInt(rawText); const ar = Number(winstonBig) / Number(WINSTON_PER_AR); return { gateway: normalizedGateway, winston: rawText, ar, }; } export function formatAr(value, digits = 6) { const n = Number(value); if (!Number.isFinite(n)) return '0'; return n.toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: digits, }); } export function getArweaveTopupSiteUrl() { return AR_TOPUP_URL; } export async function transferAr({ gateway, jwk, toAddress, amountAr }) { const cleanTo = String(toAddress || '').trim(); if (!cleanTo) throw new Error('Не указан адрес получателя Arweave'); if (!jwk || typeof jwk !== 'object') throw new Error('Кошелёк Arweave не инициализирован'); const normalizedGateway = normalizeGateway(gateway); const { host, port, protocol } = parseGatewayForArweaveInit(normalizedGateway); const winston = parseArToWinston(amountAr); const moduleRef = await loadArweaveLib(); const Arweave = moduleRef?.default || moduleRef; const arweave = Arweave.init({ host, port, protocol }); const tx = await arweave.createTransaction( { target: cleanTo, quantity: winston, }, jwk, ); await arweave.transactions.sign(tx, jwk); const postResult = await arweave.transactions.post(tx); return { gateway: normalizedGateway, id: tx.id, status: Number(postResult?.status || 0), statusText: String(postResult?.statusText || ''), }; }