SHA256
Обновление архитектуры ключей и кошельков
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { loadEncryptedUserSecrets, updateEncryptedUserSecrets } from './key-vault.js';
|
||||
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
|
||||
import { deriveArweaveWalletFromClientKey32 } from './sawd-v1.js';
|
||||
import { extractEd25519Seed32FromStoredValue } from './client-key-utils.js';
|
||||
import { deriveArweaveWalletFromBlockchainKey32 } 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';
|
||||
@@ -76,10 +76,11 @@ function pickCachedWallet(secrets) {
|
||||
const cached = secrets?.arweaveWallet;
|
||||
if (!cached || typeof cached !== 'object') return null;
|
||||
const derivation = String(cached.derivation || '').trim();
|
||||
const sourceKey = String(cached.sourceKey || '').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') {
|
||||
if (derivation !== 'SAWD-v1' || sourceKey !== 'blockchain' || !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()) {
|
||||
@@ -87,6 +88,7 @@ function pickCachedWallet(secrets) {
|
||||
}
|
||||
return {
|
||||
derivation,
|
||||
sourceKey,
|
||||
address,
|
||||
owner,
|
||||
jwk,
|
||||
@@ -150,13 +152,13 @@ function safeStatus(onStatus, text) {
|
||||
}
|
||||
|
||||
export async function getArweaveWalletChoices({ login, storagePwd, onStatus } = {}) {
|
||||
const derived = await getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus });
|
||||
const derived = await getArweaveWalletFromStoredBlockchainKey({ 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',
|
||||
id: 'derived-blockchain-key',
|
||||
label: 'Стандартный из ключа блокчейна',
|
||||
address: derived.address,
|
||||
owner: derived.owner,
|
||||
jwk: derived.jwk,
|
||||
@@ -206,11 +208,11 @@ export async function addArweaveWalletSecret({ login, storagePwd, secret, label
|
||||
return wallet;
|
||||
}
|
||||
|
||||
export async function getArweaveWalletFromStoredClientKey({ login, storagePwd, onStatus } = {}) {
|
||||
export async function getArweaveWalletFromStoredBlockchainKey({ login, storagePwd, onStatus } = {}) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
throw new Error('Нет активной сессии для доступа к client.key');
|
||||
throw new Error('Нет активной сессии для доступа к blockchain key');
|
||||
}
|
||||
|
||||
const secrets = await loadEncryptedUserSecrets(cleanLogin, cleanPwd);
|
||||
@@ -220,23 +222,24 @@ export async function getArweaveWalletFromStoredClientKey({ login, storagePwd, o
|
||||
return cached;
|
||||
}
|
||||
|
||||
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего client key. Это может занять немного времени.');
|
||||
safeStatus(onStatus, 'Сейчас мы впервые получаем Arweave-кошелёк из вашего ключа блокчейна. Это может занять немного времени.');
|
||||
|
||||
const storedClientKey = String(secrets?.clientKey || '').trim();
|
||||
if (!storedClientKey) {
|
||||
throw new Error('На устройстве не найден client.key');
|
||||
const storedBlockchainKey = String(secrets?.blockchainKey || '').trim();
|
||||
if (!storedBlockchainKey) {
|
||||
throw new Error('На устройстве не найден blockchain key');
|
||||
}
|
||||
|
||||
const clientKey32 = extractClientKey32FromStoredValue(storedClientKey);
|
||||
const blockchainKey32 = extractEd25519Seed32FromStoredValue(storedBlockchainKey);
|
||||
let wallet;
|
||||
try {
|
||||
wallet = await deriveArweaveWalletFromClientKey32(clientKey32);
|
||||
wallet = await deriveArweaveWalletFromBlockchainKey32(blockchainKey32);
|
||||
} finally {
|
||||
clientKey32.fill(0);
|
||||
blockchainKey32.fill(0);
|
||||
}
|
||||
|
||||
const cachedWallet = {
|
||||
derivation: wallet.derivation,
|
||||
sourceKey: 'blockchain',
|
||||
address: wallet.address,
|
||||
owner: wallet.owner,
|
||||
jwk: wallet.jwk,
|
||||
|
||||
@@ -674,7 +674,7 @@ function targetLoginBytes(value) {
|
||||
|
||||
function normalizeTargetForkNumber(value, actionName = 'target') {
|
||||
const n = Number(value);
|
||||
if (!Number.isInteger(n) || n < 0 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
||||
if (!Number.isInteger(n) || n < 1 || n > 999) throw new Error(`Invalid toForkNumber for ${actionName}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -1142,8 +1142,10 @@ export class AuthService {
|
||||
async createSessionFromImportedSecrets(login, secrets) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('В QR-коде нет логина');
|
||||
const clientKey = String(secrets?.clientKey || secrets?.clientKey || '').trim();
|
||||
if (!clientKey) throw new Error('В QR-коде нет client key для входа');
|
||||
const clientKey = String(secrets?.clientKey || '').trim();
|
||||
const blockchainKey = String(secrets?.blockchainKey || '').trim();
|
||||
if (!clientKey) throw new Error('В QR-коде нет ключа доступа для входа');
|
||||
if (!blockchainKey) throw new Error('В QR-коде нет ключа блокчейна');
|
||||
|
||||
const privateKey = await importPkcs8Ed25519(clientKey);
|
||||
const publicKeyB64 = await publicKeyB64FromPkcs8Ed25519(clientKey);
|
||||
@@ -1219,7 +1221,7 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
async persistSelectedKeys(login, storagePwd, keyBundle, saveOptions = { saveRoot: true, saveBlockchain: true }) {
|
||||
async persistSelectedKeys(login, storagePwd, keyBundle) {
|
||||
let currentSecrets = {};
|
||||
try {
|
||||
const loaded = await loadEncryptedUserSecrets(login, storagePwd);
|
||||
@@ -1230,12 +1232,23 @@ export class AuthService {
|
||||
// Если контейнера ещё нет или пароль новый для этого логина — создадим новый ниже.
|
||||
}
|
||||
|
||||
const clientKey = String(keyBundle?.clientPair?.privatePkcs8B64 || '').trim();
|
||||
const blockchainKey = String(keyBundle?.blockchainPair?.privatePkcs8B64 || '').trim();
|
||||
if (!clientKey || !blockchainKey) {
|
||||
throw new Error('Для входа нужно сохранить client key и blockchain key');
|
||||
}
|
||||
|
||||
const secrets = {
|
||||
...currentSecrets,
|
||||
clientKey: keyBundle.clientPair.privatePkcs8B64,
|
||||
clientKey,
|
||||
blockchainKey,
|
||||
};
|
||||
if (saveOptions.saveRoot) secrets.rootKey = keyBundle.rootPair.privatePkcs8B64;
|
||||
if (saveOptions.saveBlockchain) secrets.blockchainKey = keyBundle.blockchainPair.privatePkcs8B64;
|
||||
// SAWD-v1 wallet зависит от blockchain key. При смене ключа старый кэш
|
||||
// нельзя переносить в новую identity: он будет лениво выведен заново.
|
||||
if (String(currentSecrets?.blockchainKey || '').trim() !== blockchainKey) {
|
||||
delete secrets.arweaveWallet;
|
||||
}
|
||||
delete secrets.rootKey;
|
||||
await saveEncryptedUserSecrets(login, storagePwd, secrets);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@ function parseKeypairJson64(raw) {
|
||||
if (!isByteArrayLike(parsed)) return null;
|
||||
const asArray = Array.from(parsed);
|
||||
if (asArray.length < 32) {
|
||||
throw new Error('Некорректный JSON ключ client.key: ожидалось минимум 32 байта');
|
||||
throw new Error('Некорректный JSON Ed25519-ключ: ожидалось минимум 32 байта');
|
||||
}
|
||||
|
||||
const out = new Uint8Array(asArray.length);
|
||||
for (let i = 0; i < asArray.length; i += 1) {
|
||||
const n = Number(asArray[i]);
|
||||
if (!Number.isInteger(n) || n < 0 || n > 255) {
|
||||
throw new Error('Некорректный JSON ключ client.key: найдены не-байтовые значения');
|
||||
throw new Error('Некорректный JSON Ed25519-ключ: найдены не-байтовые значения');
|
||||
}
|
||||
out[i] = n;
|
||||
}
|
||||
@@ -34,13 +34,13 @@ function parseKeypairJson64(raw) {
|
||||
|
||||
export function extractSeed32FromPkcs8B64(pkcs8B64) {
|
||||
const bytes = base64ToBytes(String(pkcs8B64 || '').trim());
|
||||
if (bytes.length < 32) throw new Error('Некорректный PKCS8 ключ client.key');
|
||||
if (bytes.length < 32) throw new Error('Некорректный PKCS8 Ed25519-ключ');
|
||||
return bytes.slice(bytes.length - 32);
|
||||
}
|
||||
|
||||
export function extractClientKey32FromStoredValue(storedClientKey) {
|
||||
const raw = String(storedClientKey || '').trim();
|
||||
if (!raw) throw new Error('Пустой client.key');
|
||||
export function extractEd25519Seed32FromStoredValue(storedKey) {
|
||||
const raw = String(storedKey || '').trim();
|
||||
if (!raw) throw new Error('Пустой приватный Ed25519-ключ');
|
||||
|
||||
const jsonBytes = parseKeypairJson64(raw);
|
||||
if (jsonBytes) {
|
||||
@@ -49,3 +49,6 @@ export function extractClientKey32FromStoredValue(storedClientKey) {
|
||||
|
||||
return extractSeed32FromPkcs8B64(raw);
|
||||
}
|
||||
|
||||
// Legacy API name retained for callers outside the wallet subsystem.
|
||||
export const extractClientKey32FromStoredValue = extractEd25519Seed32FromStoredValue;
|
||||
|
||||
@@ -73,15 +73,15 @@ function normalizeKeys(keys = {}) {
|
||||
return {
|
||||
clientKey: String(keys?.clientKey || '').trim(),
|
||||
blockchainKey: String(keys?.blockchainKey || '').trim(),
|
||||
rootKey: String(keys?.rootKey || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function detectPairingPayloadType(keys = {}) {
|
||||
const normalized = normalizeKeys(keys);
|
||||
if (normalized.rootKey) return 3;
|
||||
if (normalized.blockchainKey) return 2;
|
||||
return 1;
|
||||
if (!normalized.clientKey || !normalized.blockchainKey) {
|
||||
throw new Error('Для подключения устройства нужны ключ доступа и ключ блокчейна');
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
export async function deriveEspPairingPasswordHash(login, password) {
|
||||
@@ -159,13 +159,14 @@ export async function decryptPairingPayloadFromEnvelope(encryptedPayload, reques
|
||||
}
|
||||
|
||||
export function buildSecretsPayload({ login, keys, mode }) {
|
||||
const normalizedKeys = normalizeKeys(keys);
|
||||
return {
|
||||
v: 1,
|
||||
type: 'shine-esp-pairing-transfer',
|
||||
login: String(login || '').trim(),
|
||||
mode: String(mode || 'device-only').trim() || 'device-only',
|
||||
keys: normalizeKeys(keys),
|
||||
payloadType: detectPairingPayloadType(keys),
|
||||
mode: String(mode || 'full-device').trim() || 'full-device',
|
||||
keys: normalizedKeys,
|
||||
payloadType: detectPairingPayloadType(normalizedKeys),
|
||||
createdAtMs: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,12 +160,17 @@ export class KeyRotationClient {
|
||||
}
|
||||
const state=await this.notifyPdaRotation(signature);
|
||||
if(storagePwd) {
|
||||
await updateEncryptedUserSecrets(login, storagePwd, (current)=>({
|
||||
...(current||{}),
|
||||
rootKey:newBundle.rootPair.privatePkcs8B64,
|
||||
blockchainKey:newBundle.blockchainPair.privatePkcs8B64,
|
||||
clientKey:newBundle.clientPair.privatePkcs8B64,
|
||||
}));
|
||||
await updateEncryptedUserSecrets(login, storagePwd, (current)=>{
|
||||
const next={
|
||||
...(current||{}),
|
||||
blockchainKey:newBundle.blockchainPair.privatePkcs8B64,
|
||||
clientKey:newBundle.clientPair.privatePkcs8B64,
|
||||
};
|
||||
// Arweave SAWD-v1 привязан к blockchain key: после ротации старый JWK-cache недействителен.
|
||||
delete next.arweaveWallet;
|
||||
delete next.rootKey;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,15 @@ function normalizeLoginStorageKey(login) {
|
||||
return String(login || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function sanitizePersistentSecrets(keys) {
|
||||
const source = keys && typeof keys === 'object' ? keys : {};
|
||||
const safe = { ...source };
|
||||
// Recovery/root private key is intentionally ephemeral: it may exist in RAM
|
||||
// during a protected operation, but must never be persisted on the device.
|
||||
delete safe.rootKey;
|
||||
return safe;
|
||||
}
|
||||
|
||||
function openDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
@@ -53,7 +62,8 @@ async function get(storeName, key) {
|
||||
}
|
||||
|
||||
export async function saveEncryptedUserSecrets(login, storagePwd, keys) {
|
||||
const encrypted = await encryptJsonWithStoragePwd(keys, storagePwd);
|
||||
const safeKeys = sanitizePersistentSecrets(keys);
|
||||
const encrypted = await encryptJsonWithStoragePwd(safeKeys, storagePwd);
|
||||
const normalizedLogin = normalizeLoginStorageKey(login);
|
||||
await put(STORE_SECRETS, {
|
||||
login: normalizedLogin,
|
||||
@@ -71,7 +81,22 @@ export async function loadEncryptedUserSecrets(login, storagePwd) {
|
||||
if (!row?.encrypted) {
|
||||
throw new Error('На устройстве нет сохранённых ключей для этого логина');
|
||||
}
|
||||
return decryptJsonWithStoragePwd(row.encrypted, storagePwd);
|
||||
|
||||
const decrypted = await decryptJsonWithStoragePwd(row.encrypted, storagePwd);
|
||||
const safe = sanitizePersistentSecrets(decrypted);
|
||||
|
||||
// Одноразовая очистка контейнеров, созданных старыми версиями UI, где rootKey
|
||||
// мог быть сохранён. Перезаписываем тем же storagePwd уже без Recovery private key.
|
||||
if (decrypted && typeof decrypted === 'object' && Object.prototype.hasOwnProperty.call(decrypted, 'rootKey')) {
|
||||
const encrypted = await encryptJsonWithStoragePwd(safe, storagePwd);
|
||||
await put(STORE_SECRETS, {
|
||||
login: normalizedLogin,
|
||||
encrypted,
|
||||
updatedAtMs: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return safe;
|
||||
}
|
||||
|
||||
export async function updateEncryptedUserSecrets(login, storagePwd, updater) {
|
||||
@@ -84,8 +109,9 @@ export async function updateEncryptedUserSecrets(login, storagePwd, updater) {
|
||||
if (!next || typeof next !== 'object') {
|
||||
throw new Error('updateEncryptedUserSecrets: updater должен вернуть объект secrets');
|
||||
}
|
||||
await saveEncryptedUserSecrets(login, storagePwd, next);
|
||||
return next;
|
||||
const safe = sanitizePersistentSecrets(next);
|
||||
await saveEncryptedUserSecrets(login, storagePwd, safe);
|
||||
return safe;
|
||||
}
|
||||
|
||||
export async function saveSessionMaterial(login, material) {
|
||||
|
||||
@@ -22,30 +22,29 @@ function base64UrlToBytes(value) {
|
||||
}
|
||||
|
||||
export function keyLabel(id) {
|
||||
if (id === 'root') return 'root';
|
||||
if (id === 'blockchain') return 'blockchain';
|
||||
if (id === 'device') return 'device';
|
||||
if (id === 'blockchain') return 'ключ блокчейна';
|
||||
if (id === 'device' || id === 'client') return 'ключ доступа';
|
||||
return id;
|
||||
}
|
||||
|
||||
export function describeTransferKeys(keys = {}) {
|
||||
const out = [];
|
||||
if (keys.clientKey) out.push('device');
|
||||
if (keys.blockchainKey) out.push('blockchain');
|
||||
if (keys.rootKey) out.push('root');
|
||||
if (keys.clientKey) out.push('ключ доступа');
|
||||
if (keys.blockchainKey) out.push('ключ блокчейна');
|
||||
return out;
|
||||
}
|
||||
|
||||
export function makeKeyTransferText({ login, keys }) {
|
||||
const clientKey = String(keys?.clientKey || '').trim();
|
||||
const blockchainKey = String(keys?.blockchainKey || '').trim();
|
||||
if (!clientKey || !blockchainKey) {
|
||||
throw new Error('Для подключения нужны ключ доступа и ключ блокчейна');
|
||||
}
|
||||
const payload = {
|
||||
v: 1,
|
||||
type: 'shine-key-transfer',
|
||||
login: String(login || '').trim(),
|
||||
keys: {
|
||||
clientKey: String(keys?.clientKey || ''),
|
||||
blockchainKey: String(keys?.blockchainKey || ''),
|
||||
rootKey: String(keys?.rootKey || ''),
|
||||
},
|
||||
keys: { clientKey, blockchainKey },
|
||||
createdAtMs: Date.now(),
|
||||
};
|
||||
const json = JSON.stringify(payload);
|
||||
@@ -65,17 +64,15 @@ export function parseKeyTransferText(text) {
|
||||
const login = String(payload.login || '').trim();
|
||||
if (!login) throw new Error('В QR-коде нет логина');
|
||||
const keys = payload.keys && typeof payload.keys === 'object' ? payload.keys : {};
|
||||
if (!keys.clientKey && !keys.blockchainKey && !keys.rootKey) {
|
||||
throw new Error('В QR-коде нет ключей');
|
||||
const clientKey = String(keys.clientKey || '').trim();
|
||||
const blockchainKey = String(keys.blockchainKey || '').trim();
|
||||
if (!clientKey || !blockchainKey) {
|
||||
throw new Error('В QR-коде нет полного набора рабочих ключей');
|
||||
}
|
||||
return {
|
||||
login,
|
||||
keys: {
|
||||
clientKey: String(keys.clientKey || ''),
|
||||
blockchainKey: String(keys.blockchainKey || ''),
|
||||
rootKey: String(keys.rootKey || ''),
|
||||
},
|
||||
keyTypes: describeTransferKeys(keys),
|
||||
keys: { clientKey, blockchainKey },
|
||||
keyTypes: describeTransferKeys({ clientKey, blockchainKey }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -323,20 +323,26 @@ function toJwkB64(value) {
|
||||
return base64UrlEncode(bigIntToUnsignedBytes(value));
|
||||
}
|
||||
|
||||
async function deriveArweaveWalletParts(clientKey32) {
|
||||
if (!(clientKey32 instanceof Uint8Array)) {
|
||||
throw new Error('SAWD-v1: clientKey32 должен быть Uint8Array');
|
||||
async function deriveArweaveWalletParts(blockchainKey32) {
|
||||
if (!(blockchainKey32 instanceof Uint8Array)) {
|
||||
throw new Error('SAWD-v1: blockchainKey32 должен быть Uint8Array');
|
||||
}
|
||||
if (clientKey32.length !== 32) {
|
||||
throw new Error('SAWD-v1: clientKey32 должен быть ровно 32 байта');
|
||||
if (blockchainKey32.length !== 32) {
|
||||
throw new Error('SAWD-v1: blockchainKey32 должен быть ровно 32 байта');
|
||||
}
|
||||
|
||||
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, clientKey32);
|
||||
const masterSeed32 = await hmacSha256(MASTER_LABEL_UTF8, blockchainKey32);
|
||||
const masterSeedKey = await importHmacKey(masterSeed32);
|
||||
|
||||
const pResult = await derivePrimeWithImportedKey(masterSeedKey, 'p');
|
||||
let qResult = await derivePrimeWithImportedKey(masterSeedKey, 'q');
|
||||
while (qResult.prime === pResult.prime) {
|
||||
// Две 2048-битные простые не гарантируют 4096-битный product: n может иметь
|
||||
// 4095 бит. До публичного запуска legacy-output не фиксирован, поэтому SAWD-v1
|
||||
// детерминированно продолжает q-sequence, пока modulus не станет ровно RSA_BITS.
|
||||
while (
|
||||
qResult.prime === pResult.prime
|
||||
|| bitLength(pResult.prime * qResult.prime) !== RSA_BITS
|
||||
) {
|
||||
qResult = await derivePrimeWithImportedKey(masterSeedKey, 'q', qResult.index + 1n);
|
||||
}
|
||||
|
||||
@@ -381,8 +387,8 @@ async function deriveArweaveWalletParts(clientKey32) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function deriveArweaveWalletFromClientKey32(clientKey32) {
|
||||
const result = await deriveArweaveWalletParts(clientKey32);
|
||||
export async function deriveArweaveWalletFromBlockchainKey32(blockchainKey32) {
|
||||
const result = await deriveArweaveWalletParts(blockchainKey32);
|
||||
return {
|
||||
derivation: result.derivation,
|
||||
jwk: result.jwk,
|
||||
@@ -395,7 +401,7 @@ export async function selfTestSawdV1() {
|
||||
const invalid = new Uint8Array(31);
|
||||
let invalidFailed = false;
|
||||
try {
|
||||
await deriveArweaveWalletFromClientKey32(invalid);
|
||||
await deriveArweaveWalletFromBlockchainKey32(invalid);
|
||||
} catch {
|
||||
invalidFailed = true;
|
||||
}
|
||||
@@ -403,13 +409,13 @@ export async function selfTestSawdV1() {
|
||||
throw new Error('SAWD-v1 self-test: длина != 32 должна приводить к ошибке');
|
||||
}
|
||||
|
||||
const clientKey = new Uint8Array(32);
|
||||
for (let i = 0; i < clientKey.length; i += 1) {
|
||||
clientKey[i] = i + 1;
|
||||
const blockchainKey = new Uint8Array(32);
|
||||
for (let i = 0; i < blockchainKey.length; i += 1) {
|
||||
blockchainKey[i] = i + 1;
|
||||
}
|
||||
|
||||
const first = await deriveArweaveWalletParts(clientKey);
|
||||
const second = await deriveArweaveWalletParts(clientKey);
|
||||
const first = await deriveArweaveWalletParts(blockchainKey);
|
||||
const second = await deriveArweaveWalletParts(blockchainKey);
|
||||
|
||||
if (!first.address || !second.address) {
|
||||
throw new Error('SAWD-v1 self-test: адрес пустой');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractClientKey32FromStoredValue } from './client-key-utils.js';
|
||||
import { extractEd25519Seed32FromStoredValue } from './client-key-utils.js';
|
||||
import { base64ToBytes } from './crypto-utils.js';
|
||||
import { loadEncryptedUserSecrets } from './key-vault.js';
|
||||
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
|
||||
@@ -104,7 +104,7 @@ function decodeBase58(input) {
|
||||
|
||||
async function keypairFromPkcs8(pkcs8B64) {
|
||||
const solana = await loadSolanaLib();
|
||||
const seed32 = extractClientKey32FromStoredValue(pkcs8B64);
|
||||
const seed32 = extractEd25519Seed32FromStoredValue(pkcs8B64);
|
||||
try {
|
||||
return solana.Keypair.fromSeed(seed32);
|
||||
} finally {
|
||||
@@ -114,7 +114,7 @@ async function keypairFromPkcs8(pkcs8B64) {
|
||||
|
||||
async function keypairFromStoredSecret(storedSecret) {
|
||||
const solana = await loadSolanaLib();
|
||||
const seed32 = extractClientKey32FromStoredValue(storedSecret);
|
||||
const seed32 = extractEd25519Seed32FromStoredValue(storedSecret);
|
||||
try {
|
||||
return solana.Keypair.fromSeed(seed32);
|
||||
} finally {
|
||||
@@ -161,7 +161,7 @@ export async function createSolanaWalletFromPrivateBase58(privateKey32Base58) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWalletFromStoredClientKey({ login, storagePwd }) {
|
||||
) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
@@ -200,7 +200,7 @@ export async function getWalletFromStoredBlockchainKey({ login, storagePwd }) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getWalletFromStoredRootKey({ login, storagePwd }) {
|
||||
) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
const cleanPwd = String(storagePwd || '').trim();
|
||||
if (!cleanLogin || !cleanPwd) {
|
||||
@@ -227,7 +227,7 @@ export async function getStoredSolanaWalletChoices({ login, storagePwd } = {}) {
|
||||
return [{
|
||||
id: 'blockchain-key',
|
||||
keySource: 'blockchain',
|
||||
label: 'blockchain key',
|
||||
label: 'Ключ блокчейна',
|
||||
address: wallet.address,
|
||||
keypair: wallet.keypair,
|
||||
}];
|
||||
@@ -289,6 +289,70 @@ export async function transferSol({ endpoint, fromKeypair, toAddress, amountSol
|
||||
return { endpoint: rpc, signature, lamports };
|
||||
}
|
||||
|
||||
export async function transferAllSol({ endpoint, fromKeypair, toAddress }) {
|
||||
const solana = await loadSolanaLib();
|
||||
const rpc = normalizeEndpoint(endpoint);
|
||||
const cleanTo = String(toAddress || '').trim();
|
||||
if (!fromKeypair?.publicKey) throw new Error('Не передан старый Solana-кошелёк');
|
||||
if (!cleanTo) throw new Error('Не указан новый адрес кошелька');
|
||||
if (fromKeypair.publicKey.toBase58() === cleanTo) {
|
||||
throw new Error('Старый и новый кошельки совпадают');
|
||||
}
|
||||
|
||||
const conn = new solana.Connection(rpc, 'confirmed');
|
||||
const balanceLamports = await conn.getBalance(fromKeypair.publicKey, 'confirmed');
|
||||
if (balanceLamports <= 0) {
|
||||
return { endpoint: rpc, signature: '', lamports: 0, feeLamports: 0, balanceLamports, skipped: true };
|
||||
}
|
||||
|
||||
const latest = await conn.getLatestBlockhash('confirmed');
|
||||
const probeTx = new solana.Transaction({
|
||||
feePayer: fromKeypair.publicKey,
|
||||
recentBlockhash: latest.blockhash,
|
||||
}).add(solana.SystemProgram.transfer({
|
||||
fromPubkey: fromKeypair.publicKey,
|
||||
toPubkey: new solana.PublicKey(cleanTo),
|
||||
lamports: 1,
|
||||
}));
|
||||
const feeResult = await conn.getFeeForMessage(probeTx.compileMessage(), 'confirmed');
|
||||
const feeLamports = Number(feeResult?.value ?? 5000);
|
||||
const transferLamports = balanceLamports - feeLamports;
|
||||
if (transferLamports <= 0) {
|
||||
return {
|
||||
endpoint: rpc,
|
||||
signature: '',
|
||||
lamports: 0,
|
||||
feeLamports,
|
||||
balanceLamports,
|
||||
skipped: true,
|
||||
};
|
||||
}
|
||||
|
||||
const tx = new solana.Transaction({
|
||||
feePayer: fromKeypair.publicKey,
|
||||
recentBlockhash: latest.blockhash,
|
||||
}).add(solana.SystemProgram.transfer({
|
||||
fromPubkey: fromKeypair.publicKey,
|
||||
toPubkey: new solana.PublicKey(cleanTo),
|
||||
lamports: transferLamports,
|
||||
}));
|
||||
tx.sign(fromKeypair);
|
||||
const signature = await conn.sendRawTransaction(tx.serialize(), { skipPreflight: false });
|
||||
await conn.confirmTransaction({
|
||||
signature,
|
||||
blockhash: latest.blockhash,
|
||||
lastValidBlockHeight: latest.lastValidBlockHeight,
|
||||
}, 'confirmed');
|
||||
return {
|
||||
endpoint: rpc,
|
||||
signature,
|
||||
lamports: transferLamports,
|
||||
feeLamports,
|
||||
balanceLamports,
|
||||
skipped: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatSol(value, digits = 6) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return '0';
|
||||
|
||||
@@ -146,7 +146,7 @@ export function toUserMessage(error, fallback = 'Действие не выпо
|
||||
}
|
||||
|
||||
if (code === 'UNSUPPORTED_KEY_ALGORITHM' || text.includes('unsupported key algorithm')) {
|
||||
return 'Ключ устройства не поддерживается сервером. Очистите локальные ключи и войдите заново.';
|
||||
return 'Ключ доступа не поддерживается сервером. Очистите локальные ключи и войдите заново.';
|
||||
}
|
||||
|
||||
if (!raw) return fallback;
|
||||
|
||||
Reference in New Issue
Block a user