SHA256
Checkpoint: первая рабочая версия звонков, сигналинг будет переделан
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { state } from '../state.js';
|
||||
import { buildArweaveDataUrl, validateArweaveTxId } from '../services/arweave-file-service.js';
|
||||
import { buildArweaveDataUrl, validateArweaveTxId, validateSha256Hex } from '../services/arweave-file-service.js';
|
||||
import { getCachedAvatarObjectUrl } from '../services/arweave-avatar-cache-service.js';
|
||||
|
||||
function normalizeLogin(value) {
|
||||
@@ -52,6 +52,8 @@ export function renderUserAvatar({
|
||||
if (!validateArweaveTxId(txId)) {
|
||||
return wrap;
|
||||
}
|
||||
const sha256Hex = String(avatar?.sha256Hex || avatar?.sha256 || '').trim().toLowerCase();
|
||||
const expectedSha256Hex = validateSha256Hex(sha256Hex) ? sha256Hex : '';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.alt = 'Аватар';
|
||||
@@ -65,7 +67,7 @@ export function renderUserAvatar({
|
||||
setLoadedState(false);
|
||||
|
||||
const gateway = state?.entrySettings?.arweaveServer;
|
||||
void getCachedAvatarObjectUrl({ gateway, txId })
|
||||
void getCachedAvatarObjectUrl({ gateway, txId, expectedSha256Hex })
|
||||
.then((objectUrl) => {
|
||||
const directUrl = buildArweaveDataUrl({ gateway, txId });
|
||||
let triedDirectUrl = false;
|
||||
@@ -83,6 +85,12 @@ export function renderUserAvatar({
|
||||
};
|
||||
img.onerror = () => {
|
||||
if (!triedDirectUrl) {
|
||||
if (expectedSha256Hex) {
|
||||
releaseObjectUrl();
|
||||
img.removeAttribute('src');
|
||||
setLoadedState(false);
|
||||
return;
|
||||
}
|
||||
triedDirectUrl = true;
|
||||
releaseObjectUrl();
|
||||
img.src = directUrl;
|
||||
@@ -95,6 +103,11 @@ export function renderUserAvatar({
|
||||
img.src = objectUrl;
|
||||
})
|
||||
.catch(() => {
|
||||
if (expectedSha256Hex) {
|
||||
img.removeAttribute('src');
|
||||
setLoadedState(false);
|
||||
return;
|
||||
}
|
||||
let directUrl = '';
|
||||
try {
|
||||
directUrl = buildArweaveDataUrl({ gateway, txId });
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
buildArweaveDataUrl,
|
||||
getArweaveUploadPrice,
|
||||
prepareAvatarImageFile,
|
||||
sha256HexFromArrayBuffer,
|
||||
uploadArweaveFile,
|
||||
validateArweaveTxId,
|
||||
validateSha256Hex,
|
||||
validateAvatarSourceFile,
|
||||
} from '../services/arweave-file-service.js';
|
||||
import { saveProfileAvatarArweave } from '../services/user-profile-params.js';
|
||||
@@ -69,6 +71,7 @@ export function openAvatarWizard({
|
||||
let optimized = null;
|
||||
let priceInfo = null;
|
||||
let uploadedTxId = '';
|
||||
let uploadedSha256Hex = '';
|
||||
|
||||
function revokePreviewUrl() {
|
||||
if (!lastPreviewUrl) return;
|
||||
@@ -158,6 +161,7 @@ export function openAvatarWizard({
|
||||
const showStepExistingPreview = async (txId) => {
|
||||
if (closed) return;
|
||||
const previewUrl = buildArweaveDataUrl({ gateway: cleanGateway, txId });
|
||||
let existingSha256Hex = '';
|
||||
root.innerHTML = `
|
||||
<div class="modal" data-avatar-wizard-modal="true">
|
||||
<div class="modal-card stack avatar-wizard-card">
|
||||
@@ -165,7 +169,7 @@ export function openAvatarWizard({
|
||||
<div class="avatar-preview-circle avatar-wizard-preview">
|
||||
<img alt="Предпросмотр аватара" data-preview-image="true" />
|
||||
</div>
|
||||
<p class="meta-muted">После сохранения в профиль будет записан только Transaction ID. Сам файл хранится в Arweave.</p>
|
||||
<p class="meta-muted">После сохранения в профиль будут записаны SHA-256 и Transaction ID. Сам файл хранится в Arweave.</p>
|
||||
<p class="avatar-wizard-error" data-error="true"></p>
|
||||
<div class="avatar-wizard-actions">
|
||||
<button class="secondary-btn" type="button" data-action="back">Назад</button>
|
||||
@@ -186,15 +190,19 @@ export function openAvatarWizard({
|
||||
|
||||
try {
|
||||
await ensurePreviewImage(previewUrl, imageEl);
|
||||
const response = await fetch(previewUrl, { method: 'GET', cache: 'no-store' });
|
||||
if (!response.ok) throw new Error('BAD_AVATAR_FETCH');
|
||||
existingSha256Hex = await sha256HexFromArrayBuffer(await response.arrayBuffer());
|
||||
if (!validateSha256Hex(existingSha256Hex)) throw new Error('BAD_AVATAR_HASH');
|
||||
if (saveBtn instanceof HTMLButtonElement) saveBtn.disabled = false;
|
||||
} catch (error) {
|
||||
setNodeText(errorEl, 'Не удалось загрузить изображение по этому Transaction ID');
|
||||
setNodeText(errorEl, 'Не удалось проверить файл по этому Transaction ID.');
|
||||
if (saveBtn instanceof HTMLButtonElement) saveBtn.disabled = true;
|
||||
}
|
||||
|
||||
saveBtn?.addEventListener('click', async () => {
|
||||
try {
|
||||
await saveProfileAvatarArweave(cleanLogin, txId);
|
||||
await saveProfileAvatarArweave(cleanLogin, txId, existingSha256Hex);
|
||||
if (typeof onAvatarSaved === 'function') await onAvatarSaved();
|
||||
close(true, resolve);
|
||||
} catch {
|
||||
@@ -238,7 +246,7 @@ export function openAvatarWizard({
|
||||
<img alt="Предпросмотр аватара" data-preview-image="true" />
|
||||
</div>
|
||||
<div class="avatar-wizard-meta" data-meta="true"></div>
|
||||
<p class="meta-muted">После сохранения в профиль будет записан только Transaction ID. Сам файл хранится в Arweave.</p>
|
||||
<p class="meta-muted">После сохранения в профиль будут записаны SHA-256 и Transaction ID. Сам файл хранится в Arweave.</p>
|
||||
<p class="avatar-wizard-error" data-error="true"></p>
|
||||
<div class="avatar-wizard-actions">
|
||||
<button class="secondary-btn" type="button" data-action="back">Назад</button>
|
||||
@@ -259,6 +267,7 @@ export function openAvatarWizard({
|
||||
let selectedFile = null;
|
||||
optimized = null;
|
||||
priceInfo = null;
|
||||
uploadedSha256Hex = '';
|
||||
|
||||
modal?.addEventListener('click', (event) => {
|
||||
if (event.target === modal) close(false, resolve);
|
||||
@@ -295,6 +304,7 @@ export function openAvatarWizard({
|
||||
<div>Итоговый размер: ${escapeHtml(formatBytes(optimized.optimizedSizeBytes))}</div>
|
||||
<div>Итоговое разрешение: ${escapeHtml(`${optimized.width} × ${optimized.height}`)}</div>
|
||||
<div>Тип файла: ${escapeHtml(optimized.contentType)}</div>
|
||||
<div>SHA-256: ${escapeHtml(String(optimized.sha256Hex || '').toLowerCase())}</div>
|
||||
<div>Примерная цена загрузки: ${escapeHtml(formatAr(priceInfo.ar))} AR</div>
|
||||
`;
|
||||
|
||||
@@ -338,9 +348,13 @@ export function openAvatarWizard({
|
||||
],
|
||||
});
|
||||
uploadedTxId = String(uploaded.id || '').trim();
|
||||
uploadedSha256Hex = String(optimized?.sha256Hex || '').trim().toLowerCase();
|
||||
if (!uploadedTxId) {
|
||||
throw new Error('Пустой Transaction ID');
|
||||
}
|
||||
if (!validateSha256Hex(uploadedSha256Hex)) {
|
||||
throw new Error('Некорректный SHA256');
|
||||
}
|
||||
showStepUploaded();
|
||||
} catch {
|
||||
setNodeText(errorEl, 'Не удалось загрузить файл в Arweave.');
|
||||
@@ -357,6 +371,8 @@ export function openAvatarWizard({
|
||||
<h3 class="modal-title">Файл загружен в Arweave</h3>
|
||||
<p class="meta-muted">Transaction ID:</p>
|
||||
<p class="avatar-wizard-meta">${escapeHtml(uploadedTxId)}</p>
|
||||
<p class="meta-muted">SHA-256:</p>
|
||||
<p class="avatar-wizard-meta">${escapeHtml(uploadedSha256Hex)}</p>
|
||||
<p class="avatar-wizard-error" data-error="true"></p>
|
||||
<div class="avatar-wizard-actions">
|
||||
<button class="ghost-btn" type="button" data-action="copy-id">Скопировать ID</button>
|
||||
@@ -378,7 +394,7 @@ export function openAvatarWizard({
|
||||
});
|
||||
root.querySelector('[data-action="set-avatar"]')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await saveProfileAvatarArweave(cleanLogin, uploadedTxId);
|
||||
await saveProfileAvatarArweave(cleanLogin, uploadedTxId, uploadedSha256Hex);
|
||||
if (typeof onAvatarSaved === 'function') await onAvatarSaved();
|
||||
close(true, resolve);
|
||||
} catch {
|
||||
|
||||
@@ -151,7 +151,7 @@ export function render({ navigate }) {
|
||||
let currentFields = [];
|
||||
let currentToggles = [];
|
||||
let currentGender = PROFILE_GENDER_UNKNOWN;
|
||||
let currentAvatar = { value: '', source: '', txId: '', timeMs: 0 };
|
||||
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
|
||||
const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]');
|
||||
|
||||
@@ -226,7 +226,9 @@ export function render({ navigate }) {
|
||||
login,
|
||||
firstName,
|
||||
lastName,
|
||||
avatar: currentAvatar?.txId ? { ar: currentAvatar.txId } : null,
|
||||
avatar: currentAvatar?.txId
|
||||
? { ar: currentAvatar.txId, sha256Hex: String(currentAvatar?.sha256Hex || '').trim().toLowerCase() }
|
||||
: null,
|
||||
size: 'large',
|
||||
className: 'profile-avatar',
|
||||
}));
|
||||
@@ -574,7 +576,7 @@ export function render({ navigate }) {
|
||||
currentFields = snapshot.fields;
|
||||
currentToggles = snapshot.toggles;
|
||||
currentGender = snapshot.gender || PROFILE_GENDER_UNKNOWN;
|
||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', timeMs: 0 };
|
||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
|
||||
syncIdentity();
|
||||
renderFields(currentFields);
|
||||
|
||||
@@ -88,7 +88,7 @@ export function render({ navigate }) {
|
||||
let currentFields = [];
|
||||
let currentToggles = [];
|
||||
let currentGender = 'unknown';
|
||||
let currentAvatar = { value: '', source: '', txId: '', timeMs: 0 };
|
||||
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
|
||||
function syncIdentity() {
|
||||
if (!identityEl) return;
|
||||
@@ -109,7 +109,9 @@ export function render({ navigate }) {
|
||||
login,
|
||||
firstName,
|
||||
lastName,
|
||||
avatar: currentAvatar?.txId ? { ar: currentAvatar.txId } : null,
|
||||
avatar: currentAvatar?.txId
|
||||
? { ar: currentAvatar.txId, sha256Hex: String(currentAvatar?.sha256Hex || '').trim().toLowerCase() }
|
||||
: null,
|
||||
size: 'large',
|
||||
className: 'profile-avatar',
|
||||
}));
|
||||
@@ -161,7 +163,7 @@ export function render({ navigate }) {
|
||||
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
|
||||
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
|
||||
currentGender = snapshot.gender || 'unknown';
|
||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', timeMs: 0 };
|
||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
|
||||
syncIdentity();
|
||||
updateAvatarUi();
|
||||
updateTogglesUi();
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { buildArweaveDataUrl, validateArweaveTxId } from './arweave-file-service.js';
|
||||
import {
|
||||
buildArweaveDataUrl,
|
||||
sha256HexFromArrayBuffer,
|
||||
validateArweaveTxId,
|
||||
validateSha256Hex,
|
||||
} from './arweave-file-service.js';
|
||||
|
||||
const DB_NAME = 'shine-ui-avatar-cache';
|
||||
const DB_VERSION = 1;
|
||||
@@ -55,6 +60,14 @@ async function putRecord(record) {
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyBlobSha256(blob, expectedSha256Hex) {
|
||||
const expected = String(expectedSha256Hex || '').trim().toLowerCase();
|
||||
if (!validateSha256Hex(expected)) return true;
|
||||
if (!(blob instanceof Blob)) return false;
|
||||
const actual = await sha256HexFromArrayBuffer(await blob.arrayBuffer());
|
||||
return actual === expected;
|
||||
}
|
||||
|
||||
async function getAllRecords() {
|
||||
return withStore('readonly', (store, _tx, resolve, reject) => {
|
||||
const req = store.getAll();
|
||||
@@ -146,22 +159,41 @@ function detectImageMime(bytes) {
|
||||
return '';
|
||||
}
|
||||
|
||||
async function getBlobFromCacheOrGateway({ gateway, txId }) {
|
||||
async function getBlobFromCacheOrGateway({ gateway, txId, expectedSha256Hex = '' }) {
|
||||
const expected = String(expectedSha256Hex || '').trim().toLowerCase();
|
||||
try {
|
||||
const cached = await getRecord(txId);
|
||||
if (cached?.blob instanceof Blob) {
|
||||
return cached.blob;
|
||||
if (!validateSha256Hex(expected)) return cached.blob;
|
||||
const cacheSha = String(cached?.sha256Hex || '').trim().toLowerCase();
|
||||
if (cacheSha && cacheSha === expected) {
|
||||
return cached.blob;
|
||||
}
|
||||
const ok = await verifyBlobSha256(cached.blob, expected);
|
||||
if (ok) {
|
||||
return cached.blob;
|
||||
}
|
||||
// кэш повреждён или не совпадает с ожидаемым хэшем: удаляем и перекачиваем
|
||||
await deleteRecords([txId]);
|
||||
}
|
||||
} catch {
|
||||
// ignore IndexedDB errors and fallback to fetch
|
||||
}
|
||||
|
||||
const blob = await fetchAvatarBlob({ gateway, txId });
|
||||
if (validateSha256Hex(expected)) {
|
||||
const ok = await verifyBlobSha256(blob, expected);
|
||||
if (!ok) {
|
||||
throw new Error('SHA256_MISMATCH');
|
||||
}
|
||||
}
|
||||
const computedSha256Hex = await sha256HexFromArrayBuffer(await blob.arrayBuffer());
|
||||
const record = {
|
||||
txId,
|
||||
blob,
|
||||
contentType: String(blob.type || 'application/octet-stream'),
|
||||
sizeBytes: Number(blob.size || 0),
|
||||
sha256Hex: computedSha256Hex,
|
||||
cachedAtMs: Date.now(),
|
||||
};
|
||||
try {
|
||||
@@ -173,12 +205,16 @@ async function getBlobFromCacheOrGateway({ gateway, txId }) {
|
||||
return blob;
|
||||
}
|
||||
|
||||
export async function getCachedAvatarObjectUrl({ gateway, txId }) {
|
||||
export async function getCachedAvatarObjectUrl({ gateway, txId, expectedSha256Hex = '' }) {
|
||||
const cleanTxId = String(txId || '').trim();
|
||||
if (!validateArweaveTxId(cleanTxId)) {
|
||||
throw new Error('Некорректный Transaction ID Arweave');
|
||||
}
|
||||
const blob = await getBlobFromCacheOrGateway({ gateway, txId: cleanTxId });
|
||||
const blob = await getBlobFromCacheOrGateway({
|
||||
gateway,
|
||||
txId: cleanTxId,
|
||||
expectedSha256Hex,
|
||||
});
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const MAX_AVATAR_SOURCE_BYTES = 10 * 1024 * 1024;
|
||||
const MAX_AVATAR_SIDE_PX = 768;
|
||||
const AVATAR_QUALITY = 0.86;
|
||||
const TX_ID_RE = /^[A-Za-z0-9_-]{43}$/;
|
||||
const SHA256_HEX_RE = /^[A-Fa-f0-9]{64}$/;
|
||||
|
||||
let arweaveLibPromise = null;
|
||||
|
||||
@@ -92,29 +93,64 @@ function canvasToBlob(canvas, type, quality) {
|
||||
});
|
||||
}
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes, (item) => item.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export function validateArweaveTxId(txId) {
|
||||
const value = String(txId || '').trim();
|
||||
return TX_ID_RE.test(value);
|
||||
}
|
||||
|
||||
export function buildArweaveAvatarValue(txId) {
|
||||
export function validateSha256Hex(sha256Hex) {
|
||||
const value = String(sha256Hex || '').trim();
|
||||
return SHA256_HEX_RE.test(value);
|
||||
}
|
||||
|
||||
export async function sha256HexFromArrayBuffer(buffer) {
|
||||
if (!(buffer instanceof ArrayBuffer)) throw new Error('Некорректные данные для SHA256');
|
||||
const digest = await crypto.subtle.digest('SHA-256', buffer);
|
||||
return bytesToHex(new Uint8Array(digest));
|
||||
}
|
||||
|
||||
export function buildArweaveAvatarValue(txId, sha256Hex = '') {
|
||||
const cleanTxId = String(txId || '').trim();
|
||||
if (!validateArweaveTxId(cleanTxId)) {
|
||||
throw new Error('Некорректный Transaction ID Arweave');
|
||||
}
|
||||
return `AR:${cleanTxId}`;
|
||||
const cleanSha = String(sha256Hex || '').trim().toLowerCase();
|
||||
if (!cleanSha) return `AR:${cleanTxId}`;
|
||||
if (!validateSha256Hex(cleanSha)) {
|
||||
throw new Error('Некорректный SHA256 хэш аватара');
|
||||
}
|
||||
return `SHA256:${cleanSha},AR:${cleanTxId}`;
|
||||
}
|
||||
|
||||
export function parseArweaveAvatarValue(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw.startsWith('AR:')) {
|
||||
return { ok: false, network: '', txId: '' };
|
||||
if (!raw) {
|
||||
return { ok: false, network: '', txId: '', sha256Hex: '' };
|
||||
}
|
||||
|
||||
const arMatch = raw.match(/(?:^|,)\s*AR:([A-Za-z0-9_-]{43})\s*(?:,|$)/);
|
||||
let txId = String(arMatch?.[1] || '').trim();
|
||||
if (!txId) {
|
||||
// fallback для старых/кривых значений без запятых: "...AR:<txid>..."
|
||||
const fallbackAr = raw.match(/AR:([A-Za-z0-9_-]{43})/);
|
||||
txId = String(fallbackAr?.[1] || '').trim();
|
||||
}
|
||||
const txId = raw.slice(3).trim();
|
||||
if (!validateArweaveTxId(txId)) {
|
||||
return { ok: false, network: '', txId: '' };
|
||||
return { ok: false, network: '', txId: '', sha256Hex: '' };
|
||||
}
|
||||
return { ok: true, network: 'AR', txId };
|
||||
|
||||
const shaMatch = raw.match(/(?:^|,)\s*SHA256:([A-Fa-f0-9]{64})\s*(?:,|$)/);
|
||||
let sha256Hex = String(shaMatch?.[1] || '').trim().toLowerCase();
|
||||
if (!sha256Hex) {
|
||||
const fallbackSha = raw.match(/SHA256:([A-Fa-f0-9]{64})/);
|
||||
sha256Hex = String(fallbackSha?.[1] || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
return { ok: true, network: 'AR', txId, sha256Hex };
|
||||
}
|
||||
|
||||
export function buildArweaveDataUrl({ gateway, txId }) {
|
||||
@@ -209,6 +245,8 @@ export async function prepareAvatarImageFile(file) {
|
||||
}
|
||||
|
||||
const optimizedFile = blobToFile(blob, fileName, contentType);
|
||||
const optimizedArrayBuffer = await optimizedFile.arrayBuffer();
|
||||
const sha256Hex = await sha256HexFromArrayBuffer(optimizedArrayBuffer);
|
||||
return {
|
||||
file: optimizedFile,
|
||||
originalSizeBytes: Number(file.size || 0),
|
||||
@@ -218,6 +256,7 @@ export async function prepareAvatarImageFile(file) {
|
||||
width,
|
||||
height,
|
||||
contentType,
|
||||
sha256Hex,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) throw error;
|
||||
|
||||
@@ -199,7 +199,12 @@ export async function loadUserProfileCard(login) {
|
||||
gender: String(snapshot?.gender || 'unknown').trim().toLowerCase() || 'unknown',
|
||||
official: Boolean(toggles.official),
|
||||
shine: Boolean(toggles.shine),
|
||||
avatar: snapshot?.avatar?.txId ? { ar: String(snapshot.avatar.txId).trim() } : null,
|
||||
avatar: snapshot?.avatar?.txId
|
||||
? {
|
||||
ar: String(snapshot.avatar.txId).trim(),
|
||||
sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { authService, state } from '../state.js';
|
||||
import { buildArweaveAvatarValue, parseArweaveAvatarValue, validateArweaveTxId } from './arweave-file-service.js';
|
||||
import {
|
||||
buildArweaveAvatarValue,
|
||||
parseArweaveAvatarValue,
|
||||
validateArweaveTxId,
|
||||
validateSha256Hex,
|
||||
} from './arweave-file-service.js';
|
||||
|
||||
export const profileFieldDefs = [
|
||||
{ key: 'first_name', readKeys: ['first_name'], label: 'Имя', placeholder: 'Введите имя' },
|
||||
@@ -123,15 +128,17 @@ export async function loadProfileSnapshot(login) {
|
||||
const parsedAvatar = parseArweaveAvatarValue(latestAvatar?.value || '');
|
||||
const avatar = parsedAvatar.ok
|
||||
? {
|
||||
value: buildArweaveAvatarValue(parsedAvatar.txId),
|
||||
value: buildArweaveAvatarValue(parsedAvatar.txId, parsedAvatar.sha256Hex),
|
||||
source: 'arweave',
|
||||
txId: parsedAvatar.txId,
|
||||
sha256Hex: parsedAvatar.sha256Hex || '',
|
||||
timeMs: latestAvatar?.timeMs || 0,
|
||||
}
|
||||
: {
|
||||
value: '',
|
||||
source: '',
|
||||
txId: '',
|
||||
sha256Hex: '',
|
||||
timeMs: latestAvatar?.timeMs || 0,
|
||||
};
|
||||
|
||||
@@ -175,10 +182,14 @@ export async function saveProfileGender(login, gender) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveProfileAvatarArweave(login, txId) {
|
||||
export async function saveProfileAvatarArweave(login, txId, sha256Hex) {
|
||||
const cleanTxId = String(txId || '').trim();
|
||||
const cleanSha = String(sha256Hex || '').trim().toLowerCase();
|
||||
if (!validateArweaveTxId(cleanTxId)) {
|
||||
throw new Error('Некорректный Transaction ID Arweave');
|
||||
}
|
||||
await saveProfileParamBlock(login, 'ava', buildArweaveAvatarValue(cleanTxId));
|
||||
if (!validateSha256Hex(cleanSha)) {
|
||||
throw new Error('Некорректный SHA256 хэш аватара');
|
||||
}
|
||||
await saveProfileParamBlock(login, 'ava', buildArweaveAvatarValue(cleanTxId, cleanSha));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user