Аввив 2.0 - работает, заливка!!

This commit is contained in:
AidarKC
2026-09-21 12:49:33 +03:00
parent 23969121e5
commit be696d116b
90 changed files with 2411 additions and 14938 deletions
-2
View File
@@ -73,7 +73,6 @@ import * as profileEditView from './pages/profile-edit-view.js';
import * as profilesView from './pages/profiles-view.js';
import * as walletView from './pages/wallet-view.js?v=202606281930';
import * as settingsView from './pages/settings-view.js';
import * as blockchainArchiveView from './pages/blockchain-archive-view.js';
import * as accessServersView from './pages/access-servers-view.js';
import * as developerSettingsView from './pages/developer-settings-view.js';
import * as advancedSettingsView from './pages/advanced-settings-view.js';
@@ -142,7 +141,6 @@ const routes = {
'profiles-view': profilesView,
'wallet-view': walletView,
'settings-view': settingsView,
'blockchain-archive-view': blockchainArchiveView,
'access-servers-view': accessServersView,
'developer-settings-view': developerSettingsView,
'advanced-settings-view': advancedSettingsView,
@@ -1,144 +0,0 @@
import { createTopBar } from '../components/topbar.js';
import { authService, state } from '../state.js';
export const pageMeta = { id: 'blockchain-archive-view', title: 'Архив блокчейна' };
function text(value) {
return String(value == null ? '' : value).trim();
}
function buildViewerUrl(location, blockchainName, channelName, messageNumber) {
const url = new URL('/Blockchain-Viewer.html', window.location.origin);
url.searchParams.set('tx', text(location.arweaveTxId));
url.searchParams.set('offset', String(location.chunkOffset));
url.searchParams.set('size', String(location.chunkSize));
url.searchParams.set('blockchain', blockchainName);
const channel = text(channelName);
if (channel) url.searchParams.set('channel', channel);
const message = text(messageNumber);
if (message) url.searchParams.set('message', message);
return url.toString();
}
async function copyText(value) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
return;
}
const area = document.createElement('textarea');
area.value = value;
document.body.appendChild(area);
area.select();
document.execCommand('copy');
area.remove();
}
export function render({ navigate, chrome }) {
const screen = document.createElement('section');
screen.className = 'stack';
chrome?.setTopbar(createTopBar({
title: 'Архив блокчейна',
back: { label: '←', onClick: () => navigate('settings-view') },
}));
const card = document.createElement('div');
card.className = 'card stack';
card.innerHTML = `
<div class="stack" style="gap:.35rem;">
<strong>Последний архивный блок</strong>
<div id="archive-status" class="meta-muted">Загружаю...</div>
</div>
<label class="field">
<span>Блокчейн</span>
<input id="archive-blockchain" type="text" readonly>
</label>
<label class="field">
<span>Канал (необязательно)</span>
<input id="archive-channel" type="text" placeholder="Название канала">
</label>
<label class="field">
<span>Номер сообщения / блока (необязательно)</span>
<input id="archive-message" type="number" min="0" step="1" placeholder="Например, 125">
</label>
<label class="field">
<span>Ссылка</span>
<textarea id="archive-link" rows="5" readonly style="resize:vertical;"></textarea>
</label>
<div style="display:flex; gap:.6rem; flex-wrap:wrap;">
<button id="archive-copy" class="shine-btn" type="button" disabled>Копировать ссылку</button>
<button id="archive-open" class="shine-btn" type="button" disabled>Открыть Viewer</button>
</div>
<details>
<summary>Технические данные</summary>
<pre id="archive-tech" style="white-space:pre-wrap; word-break:break-all;"></pre>
</details>
`;
screen.appendChild(card);
const status = card.querySelector('#archive-status');
const blockchainInput = card.querySelector('#archive-blockchain');
const channelInput = card.querySelector('#archive-channel');
const messageInput = card.querySelector('#archive-message');
const linkInput = card.querySelector('#archive-link');
const copyButton = card.querySelector('#archive-copy');
const openButton = card.querySelector('#archive-open');
const tech = card.querySelector('#archive-tech');
let location = null;
let blockchainName = '';
function refreshLink() {
if (!location || !blockchainName) {
linkInput.value = '';
copyButton.disabled = true;
openButton.disabled = true;
return;
}
linkInput.value = buildViewerUrl(location, blockchainName, channelInput.value, messageInput.value);
copyButton.disabled = false;
openButton.disabled = false;
}
channelInput.addEventListener('input', refreshLink);
messageInput.addEventListener('input', refreshLink);
copyButton.addEventListener('click', async () => {
if (!linkInput.value) return;
await copyText(linkInput.value);
status.textContent = 'Ссылка скопирована.';
});
openButton.addEventListener('click', () => {
if (linkInput.value) window.open(linkInput.value, '_blank', 'noopener,noreferrer');
});
void (async () => {
try {
const login = text(state.session.login);
if (!login) throw new Error('Нет активного пользователя');
const user = await authService.getUser(login);
blockchainName = text(user.blockchainName);
if (!blockchainName) throw new Error('У пользователя не найден blockchainName');
blockchainInput.value = blockchainName;
location = await authService.getArchiveBlockchainLocation(blockchainName);
if (!text(location.arweaveTxId) || !Number.isFinite(Number(location.chunkOffset)) || !Number.isFinite(Number(location.chunkSize))) {
throw new Error('Сервер вернул неполную архивную ссылку');
}
status.textContent = `Известна архивная история до блока ${location.sourceLastBlockNumber ?? '—'}.`;
tech.textContent = [
`publisher: ${text(location.publisherLogin) || '—'}`,
`archive big block: ${location.bigBlockNumber ?? '—'}`,
`Arweave TX: ${text(location.arweaveTxId)}`,
`archive hash: ${text(location.archiveHash) || '—'}`,
`chunk offset: ${location.chunkOffset}`,
`chunk size: ${location.chunkSize}`,
`source last block: ${location.sourceLastBlockNumber ?? '—'}`,
].join('\n');
refreshLink();
} catch (error) {
status.textContent = `Архивная ссылка пока недоступна: ${error?.message || error}`;
tech.textContent = '';
refreshLink();
}
})();
return screen;
}
-7
View File
@@ -71,12 +71,6 @@ export function render({navigate, chrome}) {
<span class="meta-muted" style="display:block; margin-top:0.2rem;">Solana, SHiNE и Arweave для публичных данных</span>
</span>
</button>
<button class="shine-btn shine-btn--settings" type="button" id="settings-blockchain-archive">
<span style="display:block; text-align:left;">
<strong>Архив блокчейна</strong>
<span class="meta-muted" style="display:block; margin-top:0.2rem;">Ссылка на архивную историю вашего публичного блокчейна</span>
</span>
</button>
<button class="shine-btn shine-btn--settings" type="button" id="settings-arweave-uploads">
<span style="display:block; text-align:left;">
<strong>Загрузить файлы в блокчейн</strong>
@@ -91,7 +85,6 @@ export function render({navigate, chrome}) {
card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view'));
card.querySelector('#settings-access-servers').addEventListener('click', () => navigate('access-servers-view'));
card.querySelector('#settings-blockchain-servers').addEventListener('click', () => navigate('server-settings-view'));
card.querySelector('#settings-blockchain-archive').addEventListener('click', () => navigate('blockchain-archive-view'));
card.querySelector('#settings-arweave-uploads').addEventListener('click', () => navigate('arweave-uploads-view'));
card.querySelector('#settings-language').addEventListener('click', () => {
sessionStorage.setItem('shine-language-return-page', 'settings-view');
+125
View File
@@ -0,0 +1,125 @@
const TE = new TextEncoder();
export const ANS104_ED25519_SIGNATURE_TYPE = 2;
function concatBytes(...chunks) {
const len = chunks.reduce((n, c) => n + c.length, 0);
const out = new Uint8Array(len);
let p = 0;
for (const c of chunks) { out.set(c, p); p += c.length; }
return out;
}
function uint16LE(value) {
const out = new Uint8Array(2);
new DataView(out.buffer).setUint16(0, Number(value), true);
return out;
}
function uint64LE(value) {
let v = BigInt(value);
if (v < 0n) throw new Error('uint64LE: negative value');
const out = new Uint8Array(8);
for (let i = 0; i < 8; i++) { out[i] = Number(v & 0xffn); v >>= 8n; }
return out;
}
function encodeAvroLong(value) {
let n = BigInt(value);
// Avro zig-zag signed long.
let u = n >= 0n ? (n << 1n) : ((-n << 1n) - 1n);
const bytes = [];
do {
let b = Number(u & 0x7fn);
u >>= 7n;
if (u !== 0n) b |= 0x80;
bytes.push(b);
} while (u !== 0n);
return Uint8Array.from(bytes);
}
function encodeAvroString(value) {
const bytes = TE.encode(String(value));
return concatBytes(encodeAvroLong(bytes.length), bytes);
}
/** Serialize ANS-104 tags as the Avro array used by arbundles/Turbo. */
export function serializeAns104Tags(tags = []) {
const normalized = tags.map(({ name, value }) => ({
name: String(name ?? ''),
value: String(value ?? ''),
}));
if (normalized.length > 128) throw new Error('ANS-104 supports at most 128 tags');
for (const tag of normalized) {
if (!tag.name || !tag.value) throw new Error('ANS-104 tag name/value must be non-empty');
if (TE.encode(tag.name).length > 1024) throw new Error('ANS-104 tag name exceeds 1024 bytes');
if (TE.encode(tag.value).length > 3072) throw new Error('ANS-104 tag value exceeds 3072 bytes');
}
if (normalized.length === 0) return Uint8Array.of(0);
return concatBytes(
encodeAvroLong(normalized.length),
...normalized.flatMap((tag) => [encodeAvroString(tag.name), encodeAvroString(tag.value)]),
Uint8Array.of(0),
);
}
async function digest(name, bytes) {
return new Uint8Array(await crypto.subtle.digest(name, bytes));
}
async function deepHashBlob(bytes) {
const tag = TE.encode(`blob${bytes.length}`);
return digest('SHA-384', concatBytes(await digest('SHA-384', tag), await digest('SHA-384', bytes)));
}
async function deepHash(item) {
if (item instanceof Uint8Array) return deepHashBlob(item);
if (Array.isArray(item)) {
let acc = await digest('SHA-384', TE.encode(`list${item.length}`));
for (const child of item) {
acc = await digest('SHA-384', concatBytes(acc, await deepHash(child)));
}
return acc;
}
throw new Error('Unsupported ANS-104 deepHash item');
}
/**
* Build the exact signing message used by current arbundles/Turbo DataItems.
* SHiNE intentionally uses no target and no anchor.
*/
export async function ans104SigningMessage({ owner32, tags = [], data }) {
if (!(owner32 instanceof Uint8Array) || owner32.length !== 32) throw new Error('ANS-104 owner must be 32 bytes');
if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array');
const rawTags = serializeAns104Tags(tags);
return deepHash([
TE.encode('dataitem'),
TE.encode('1'),
TE.encode(String(ANS104_ED25519_SIGNATURE_TYPE)),
owner32,
new Uint8Array(0),
new Uint8Array(0),
rawTags,
data,
]);
}
/** Create one complete serialized ANS-104 DataItem signed with Ed25519. */
export async function createAns104DataItem({ owner32, privateKey, data, tags = [] }) {
if (!(data instanceof Uint8Array)) throw new Error('ANS-104 data must be Uint8Array');
const rawTags = serializeAns104Tags(tags);
const signingMessage = await ans104SigningMessage({ owner32, tags, data });
const signature = new Uint8Array(await crypto.subtle.sign('Ed25519', privateKey, signingMessage));
if (signature.length !== 64) throw new Error(`Unexpected Ed25519 signature length: ${signature.length}`);
return concatBytes(
uint16LE(ANS104_ED25519_SIGNATURE_TYPE),
signature,
owner32,
Uint8Array.of(0), // target absent
Uint8Array.of(0), // anchor absent
uint64LE(tags.length),
uint64LE(rawTags.length),
rawTags,
data,
);
}
+41 -22
View File
@@ -37,6 +37,7 @@ import {
saveEncryptedUserSecrets,
saveSessionMaterial,
} from './key-vault.js';
import { createAns104DataItem } from './ans104-data-item.js';
import { defaultServerWs } from '../deploy-config.js';
const BCH_SUFFIX = '001';
@@ -973,7 +974,7 @@ function buildBlockPreimage({ prevBlockHashHex, blockNumber, msgType, msgSubType
const blockSize = 2 + 32 + 4 + 4 + 8 + 2 + 2 + 2 + body.length;
return concatBytes(
int16Bytes(0),
int16Bytes(1),
prevHashBytes,
int32Bytes(blockSize),
int32Bytes(blockNumber),
@@ -1077,14 +1078,6 @@ export class AuthService {
return response.payload || {};
}
async getArchiveBlockchainLocation(blockchainName) {
const cleanBlockchainName = String(blockchainName || '').trim();
if (!cleanBlockchainName) throw new Error('Не указано имя блокчейна');
const response = await this.ws.request('GetArchiveBlockchainLocation', { blockchainName: cleanBlockchainName });
if (response.status !== 200) throw opError('GetArchiveBlockchainLocation', response);
return response.payload || response || {};
}
async resolveLoginForAuth(login) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) throw new Error('Введите логин');
@@ -1645,7 +1638,7 @@ export class AuthService {
};
}
async submitPreparedAddBlock({ login, storagePwd, blockchainName, blockNumber, prevBlockHash, preimage }) {
async submitPreparedAddBlock({ login, storagePwd, blockchainName, blockNumber, prevBlockHash, preimage, ansTags }) {
const cleanLogin = String(login || '').trim();
const cleanBlockchainName = String(blockchainName || '').trim();
const cleanPrevBlockHash = normalizeHex32(prevBlockHash, ZERO_HASH_HEX);
@@ -1658,17 +1651,22 @@ export class AuthService {
const blockchainPrivatePkcs8 = String(savedKeys?.blockchainKey || '').trim();
if (blockchainPrivatePkcs8) {
const privateKey = await importPkcs8Ed25519(blockchainPrivatePkcs8);
const hash32 = await sha256Bytes(preimage);
const signatureBytes = await signBytes(privateKey, hash32);
const fullBlock = concatBytes(preimage, int16Bytes(0x0100), signatureBytes);
const owner32 = base64ToBytes(await publicKeyB64FromPkcs8Ed25519(blockchainPrivatePkcs8));
const dataItemBytes = await createAns104DataItem({
owner32,
privateKey,
data: preimage,
tags: Array.isArray(ansTags) && ansTags.length ? ansTags : [{ name: 'App', value: 'test5590' }],
});
return this.ws.request('AddBlock', {
blockchainName: cleanBlockchainName,
blockNumber: Number(blockNumber),
prevBlockHash: cleanPrevBlockHash,
blockBytesB64: bytesToBase64(fullBlock),
blockBytesB64: bytesToBase64(dataItemBytes),
});
}
const remoteSessionId = String(this.remoteAddBlockSessionId || '').trim();
if (!remoteSessionId) {
throw new Error('На устройстве нет blockchain key и не выбрана homeserver-сессия для remote AddBlock');
@@ -1782,7 +1780,7 @@ export class AuthService {
};
}
async runAddBlockWithRetry({ login, storagePwd, resolveFreshState, buildPreimage }) {
async runAddBlockWithRetry({ login, storagePwd, resolveFreshState, buildPreimage, ansTags }) {
let freshState = await resolveFreshState();
let blockchainName = String(freshState?.blockchainName || '').trim();
if (!blockchainName) throw new Error('runAddBlockWithRetry: blockchainName is empty');
@@ -1798,6 +1796,7 @@ export class AuthService {
blockNumber,
prevBlockHash,
preimage,
ansTags,
});
};
@@ -1824,7 +1823,7 @@ export class AuthService {
};
}
async addBlockSigned({ login, storagePwd, msgType, msgSubType, msgVersion = 1, bodyBytes }) {
async addBlockSigned({ login, storagePwd, msgType, msgSubType, msgVersion = 1, bodyBytes, channelSlug = '' }) {
const cleanLogin = (login || '').trim();
if (!cleanLogin) throw new Error('Missing login for AddBlock');
if (!storagePwd) throw new Error('Missing storagePwd for AddBlock signing');
@@ -1844,10 +1843,15 @@ export class AuthService {
return response.payload || {};
}
const ansTags = [{ name: 'App', value: 'test5590' }];
const cleanChannelSlug = String(channelSlug || '').trim();
if (cleanChannelSlug) ansTags.push({ name: 'c', value: cleanChannelSlug });
const { response, blockchainName } = await this.runAddBlockWithRetry({
login: cleanLogin,
storagePwd,
resolveFreshState: () => this.resolveFreshBlockchainCursor(cleanLogin),
ansTags,
buildPreimage: async ({ blockNumber, prevBlockHash }) => buildBlockPreimage({
prevBlockHashHex: prevBlockHash,
blockNumber,
@@ -2039,12 +2043,14 @@ export class AuthService {
if (!owner || !Number.isFinite(root) || root < 0) throw new Error('Invalid channel selector');
if (owner !== blockchainName) throw new Error('Repost is allowed only to your own channels');
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
if (rootHashHex === ZERO64) {
if (rootHashHex === ZERO64 || !channelSlug) {
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === root);
if (!rootChannel) throw new Error('Channel root not found');
rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
}
let prevLineNumber = root;
@@ -2089,6 +2095,7 @@ export class AuthService {
msgSubType: MSG_SUBTYPE_TEXT_REPOST,
msgVersion: 1,
bodyBytes,
channelSlug,
});
});
}
@@ -2115,11 +2122,15 @@ export class AuthService {
throw new Error('Invalid channel selector for edit');
}
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
if (rootHashHex === ZERO64) {
if (rootHashHex === ZERO64 || !channelSlug) {
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, ownerBlockchainName);
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
if (rootChannel) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
if (rootChannel) {
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
}
}
let prevLineNumber = lineCode;
@@ -2165,6 +2176,7 @@ export class AuthService {
msgSubType: MSG_SUBTYPE_TEXT_EDIT_POST,
msgVersion: 1,
bodyBytes,
channelSlug,
});
}
@@ -2375,6 +2387,7 @@ export class AuthService {
msgType: MSG_TYPE_TECH,
msgSubType: MSG_SUBTYPE_TECH_CREATE_CHANNEL,
msgVersion: CREATE_CHANNEL_BODY_VERSION,
channelSlug,
bodyBytes: makeCreateChannelBodyBytes({
lineCode: 0,
prevLineNumber,
@@ -2413,13 +2426,16 @@ export class AuthService {
throw new Error('Posting is allowed only to your own channels');
}
let channelSlug = toCanonicalChannelSlug(String(selector?.channelName || selector?.slug || ''));
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
if (rootHashHex === ZERO64) {
if (rootHashHex === ZERO64 || !channelSlug) {
const ownChannels = await this.listOwnChannelsForBlockchain(login, blockchainName);
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
if (!rootChannel) throw new Error('Channel root not found');
rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
if (rootHashHex === ZERO64) rootHashHex = normalizeHex32(rootChannel.rootBlockHash, ZERO64);
if (!channelSlug) channelSlug = toCanonicalChannelSlug(rootChannel.channelName);
}
if (!channelSlug) throw new Error('Cannot resolve canonical channel slug');
let prevLineNumber = lineCode;
let prevLineHashHex = rootHashHex;
@@ -2462,6 +2478,7 @@ export class AuthService {
prevLineNumber,
prevLineHashHex,
thisLineNumber,
channelSlug,
};
}
@@ -2506,6 +2523,7 @@ export class AuthService {
msgSubType: cleanSubType,
msgVersion: 1,
bodyBytes,
channelSlug: tail.channelSlug,
});
return {
@@ -2547,6 +2565,7 @@ export class AuthService {
msgSubType: MSG_SUBTYPE_TEXT_CHANNEL_META,
msgVersion: 1,
bodyBytes,
channelSlug: tail.channelSlug,
});
return {