SHA256
feat(ui): кошелек на device.key и проверки серверов (тоже пока не проверено)
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { base64ToBytes, deriveEd25519FromPassword } from './crypto-utils.js';
|
||||
import { loadEncryptedUserSecrets } from './key-vault.js';
|
||||
|
||||
const DEFAULT_SOLANA_ENDPOINT = 'https://api.devnet.solana.com';
|
||||
const TOPUP_SITE_URL = 'https://www.moonpay.com/buy/sol';
|
||||
|
||||
let solanaLibPromise = null;
|
||||
|
||||
function normalizeEndpoint(url) {
|
||||
const raw = String(url || '').trim();
|
||||
if (!raw) return DEFAULT_SOLANA_ENDPOINT;
|
||||
return raw;
|
||||
}
|
||||
|
||||
async function loadSolanaLib() {
|
||||
if (!solanaLibPromise) {
|
||||
solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
|
||||
}
|
||||
return solanaLibPromise;
|
||||
}
|
||||
|
||||
function extractSeed32FromPkcs8B64(pkcs8B64) {
|
||||
const bytes = base64ToBytes(String(pkcs8B64 || '').trim());
|
||||
if (bytes.length < 32) throw new Error('Некорректный PKCS8 ключ device.key');
|
||||
return bytes.slice(bytes.length - 32);
|
||||
}
|
||||
|
||||
async function keypairFromPkcs8(pkcs8B64) {
|
||||
const solana = await loadSolanaLib();
|
||||
const seed32 = extractSeed32FromPkcs8B64(pkcs8B64);
|
||||
return solana.Keypair.fromSeed(seed32);
|
||||
}
|
||||
|
||||
export async function deriveWalletFromPassword(password) {
|
||||
const keyBundle = await deriveEd25519FromPassword(String(password ?? ''), 'dev.key');
|
||||
const keypair = await keypairFromPkcs8(keyBundle.privatePkcs8B64);
|
||||
return {
|
||||
address: keypair.publicKey.toBase58(),
|
||||
keypair,
|
||||
devicePublicKeyB64: keyBundle.publicKeyB64,
|
||||
devicePrivatePkcs8B64: keyBundle.privatePkcs8B64,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWalletFromStoredDeviceKey({ 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 devicePrivate = String(secrets?.deviceKey || '').trim();
|
||||
if (!devicePrivate) {
|
||||
throw new Error('На устройстве не найден device.key (wallet.key)');
|
||||
}
|
||||
const keypair = await keypairFromPkcs8(devicePrivate);
|
||||
return {
|
||||
address: keypair.publicKey.toBase58(),
|
||||
keypair,
|
||||
devicePrivatePkcs8B64: devicePrivate,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getBalanceSol({ endpoint, address }) {
|
||||
const solana = await loadSolanaLib();
|
||||
const rpc = normalizeEndpoint(endpoint);
|
||||
const conn = new solana.Connection(rpc, 'confirmed');
|
||||
const pubkey = new solana.PublicKey(String(address || '').trim());
|
||||
const lamports = await conn.getBalance(pubkey, 'confirmed');
|
||||
return {
|
||||
endpoint: rpc,
|
||||
lamports,
|
||||
sol: lamports / solana.LAMPORTS_PER_SOL,
|
||||
};
|
||||
}
|
||||
|
||||
export async function requestAirdropSol({ endpoint, address, amountSol = 1 }) {
|
||||
const solana = await loadSolanaLib();
|
||||
const rpc = normalizeEndpoint(endpoint);
|
||||
const conn = new solana.Connection(rpc, 'confirmed');
|
||||
const pubkey = new solana.PublicKey(String(address || '').trim());
|
||||
const lamports = Math.max(1, Math.floor(Number(amountSol) * solana.LAMPORTS_PER_SOL));
|
||||
const signature = await conn.requestAirdrop(pubkey, lamports);
|
||||
await conn.confirmTransaction(signature, 'confirmed');
|
||||
return { endpoint: rpc, signature, lamports };
|
||||
}
|
||||
|
||||
export async function transferSol({ endpoint, fromKeypair, toAddress, amountSol }) {
|
||||
const solana = await loadSolanaLib();
|
||||
const rpc = normalizeEndpoint(endpoint);
|
||||
const cleanTo = String(toAddress || '').trim();
|
||||
const amount = Number(amountSol);
|
||||
if (!cleanTo) throw new Error('Не указан адрес получателя');
|
||||
if (!Number.isFinite(amount) || amount <= 0) throw new Error('Сумма перевода должна быть больше 0');
|
||||
|
||||
const conn = new solana.Connection(rpc, 'confirmed');
|
||||
const lamports = Math.floor(amount * solana.LAMPORTS_PER_SOL);
|
||||
if (lamports <= 0) throw new Error('Сумма слишком мала');
|
||||
|
||||
const tx = new solana.Transaction().add(
|
||||
solana.SystemProgram.transfer({
|
||||
fromPubkey: fromKeypair.publicKey,
|
||||
toPubkey: new solana.PublicKey(cleanTo),
|
||||
lamports,
|
||||
}),
|
||||
);
|
||||
|
||||
const signature = await solana.sendAndConfirmTransaction(conn, tx, [fromKeypair], {
|
||||
commitment: 'confirmed',
|
||||
});
|
||||
return { endpoint: rpc, signature, lamports };
|
||||
}
|
||||
|
||||
export function formatSol(value, digits = 6) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return '0';
|
||||
return n.toLocaleString('ru-RU', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: digits,
|
||||
});
|
||||
}
|
||||
|
||||
export function getTopupSiteUrl() {
|
||||
return TOPUP_SITE_URL;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user