SHiNE-server/shine-UI/js/services/arweave-wallet-service.js

174 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { loadEncryptedUserSecrets } from './key-vault.js';
import { extractDeviceKey32FromStoredValue } from './device-key-utils.js';
import { deriveArweaveWalletFromDeviceKey32 } 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;
}
export async function getArweaveWalletFromStoredDeviceKey({ login, storagePwd }) {
const cleanLogin = String(login || '').trim();
const cleanPwd = String(storagePwd || '').trim();
if (!cleanLogin || !cleanPwd) {
throw new Error('Нет активной сессии для доступа к wallet.key');
}
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
const storedDeviceKey = String(secrets?.deviceKey || '').trim();
if (!storedDeviceKey) {
throw new Error('На устройстве не найден device.key (wallet.key)');
}
const deviceKey32 = extractDeviceKey32FromStoredValue(storedDeviceKey);
let wallet;
try {
wallet = await deriveArweaveWalletFromDeviceKey32(deviceKey32);
} finally {
deviceKey32.fill(0);
}
return {
derivation: wallet.derivation,
address: wallet.address,
owner: wallet.owner,
jwk: wallet.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 || ''),
};
}