Обновление архитектуры ключей и кошельков

This commit is contained in:
AidarKC
2026-10-01 17:11:38 +03:00
parent f452ce1f1e
commit 8d22602761
51 changed files with 565 additions and 898 deletions
+58 -3
View File
@@ -3,6 +3,7 @@ import { authService, state } from '../state.js';
import { bytesToBase64 } from '../services/crypto-utils.js';
import { readShineUserPda } from '../services/shine-user-pda-service.js';
import { KeyRotationClient, KEY_ROTATION_REASONS } from '../services/key-rotation-service.js';
import { formatSol, getBalanceSol, getSolanaWalletFromStoredSecret, transferAllSol } from '../services/solana-wallet-service.js';
export const pageMeta = { id:'key-rotation-view', title:'Смена ключей', hideToolbar:true };
@@ -56,12 +57,66 @@ export function render({navigate,chrome}){
async function renderChainReady(s){
const oldP=input('Текущий пароль');const newP=input('Новый пароль');const go=document.createElement('button');go.className='primary-btn';go.textContent='Изменить ключи в Solana';const abort=document.createElement('button');abort.className='secondary-btn';abort.textContent='Прервать смену ключей';const err=document.createElement('div');err.className='meta-muted';
go.addEventListener('click',async()=>{go.disabled=true;try{cachedOldBundle=await derive(oldP.el.value);cachedNewBundle=await derive(newP.el.value);await client.rotatePda({login,solanaEndpoint,oldBundle:cachedOldBundle,newBundle:cachedNewBundle,storagePwd});await refresh();}catch(e){err.textContent=e?.message||String(e);go.disabled=false;}});abort.addEventListener('click',async()=>{await client.abort();await refresh();});setRoot(statusCard(s),note('Новая цепочка полностью готова. Следующий шаг — точка невозврата: старый root разрешит атомарную смену root + client + blockchain fork, а новое PDA подпишет новый blockchain key.'),oldP.wrap,newP.wrap,go,abort,err);
go.addEventListener('click',async()=>{go.disabled=true;try{cachedOldBundle=await derive(oldP.el.value);cachedNewBundle=await derive(newP.el.value);await client.rotatePda({login,solanaEndpoint,oldBundle:cachedOldBundle,newBundle:cachedNewBundle,storagePwd});await refresh();}catch(e){err.textContent=e?.message||String(e);go.disabled=false;}});abort.addEventListener('click',async()=>{await client.abort();await refresh();});setRoot(statusCard(s),note('Новая цепочка полностью готова. Следующий шаг — точка невозврата: старый ключ восстановления временно используется для атомарной смены recovery + client + blockchain fork, а новое PDA подпишет новый blockchain key. Приватный ключ восстановления на устройстве не сохраняется.'),oldP.wrap,newP.wrap,go,abort,err);
}
function pollView(s){setRoot(statusCard(s),note(s.rotationStatus==='REBUILDING_SERVER'?'Сервер перестраивает текущую рабочую историю по новому fork.':'Ожидаем подтверждение нового состояния PDA в Solana.'));timer=setTimeout(()=>void refresh(),POLL_MS);}
function placeholderView(s,kind){const wallet=kind==='wallet';const text=wallet?'Перевод SOL со старого blockchain-wallet на новый пока не реализован. Этот этап сейчас будет отмечен как NOT_IMPLEMENTED и пропущен.':'Перешифрование старых личных сообщений новым client key пока не реализовано. Этот этап сейчас будет отмечен как NOT_IMPLEMENTED и пропущен.';const go=document.createElement('button');go.className='primary-btn';go.textContent='Продолжить';go.addEventListener('click',async()=>{go.disabled=true;try{const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();}catch(e){go.disabled=false;root.append(note(e?.message||String(e)));}});setRoot(statusCard(s),note(text),go);}
async function renderWalletMigration(s){
const oldP=cachedOldBundle?null:input('Старый пароль');
const newP=cachedNewBundle?null:input('Новый пароль');
const info=note('После смены blockchain key меняется и Solana-кошелёк. На этом шаге можно перевести весь доступный SOL со старого кошелька на новый за вычетом сетевой комиссии.');
const warning=note('Сейчас автоматически переносится только SOL. Миграцию AR/Turbo при смене blockchain key нужно доработать отдельно.');
const details=document.createElement('div');details.className='card stack';details.innerHTML='<span class="meta-muted">Проверьте старый и новый кошельки.</span>';
const prepare=document.createElement('button');prepare.className='secondary-btn';prepare.type='button';prepare.textContent='Проверить кошельки';
const transfer=document.createElement('button');transfer.className='primary-btn';transfer.type='button';transfer.textContent='Перевести SOL и продолжить';transfer.disabled=true;
const skip=document.createElement('button');skip.className='secondary-btn';skip.type='button';skip.textContent='Пропустить перенос';
const err=document.createElement('div');err.className='meta-muted';
let oldWallet=null,newWallet=null,oldBalance=null;
async function refresh(){if(disposed)return;if(timer){clearTimeout(timer);timer=null;}try{const s=await client.status();const st=String(s.rotationStatus||'NONE');if(st==='NONE'){await renderNone();return;}if(st==='COPYING_CHAIN'){if(cachedNewBundle)await runCopy(s);else await askNewPasswordAndCopy(s);return;}if(st==='CHAIN_READY'){await renderChainReady(s);return;}if(['ROTATING_PDA','PDA_ROTATED','REBUILDING_SERVER'].includes(st)){pollView(s);return;}if(st==='WALLET_MIGRATION'){placeholderView(s,'wallet');return;}if(st==='MESSAGE_MIGRATION'){placeholderView(s,'messages');return;}if(st==='FINALIZING'){const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();return;}if(st==='COMPLETE'){setRoot(note('Смена ключей завершена.'));return;}setRoot(note(`Неизвестное состояние ротации: ${st}`));}catch(e){setRoot(note(`Не удалось прочитать состояние смены ключей: ${e?.message||e}`));}}
const resolveBundles=async()=>{
if(!cachedOldBundle){if(!oldP?.el.value)throw new Error('Введите старый пароль');cachedOldBundle=await derive(oldP.el.value);}
if(!cachedNewBundle){if(!newP?.el.value)throw new Error('Введите новый пароль');cachedNewBundle=await derive(newP.el.value);}
if(String(cachedOldBundle?.blockchainPair?.publicKeyB64||'')!==String(s.oldBlockchainKey||''))throw new Error('Старый пароль выводит другой blockchain key');
if(String(cachedNewBundle?.blockchainPair?.publicKeyB64||'')!==String(s.newBlockchainKey||''))throw new Error('Новый пароль выводит другой blockchain key');
};
const prepareWallets=async()=>{
await resolveBundles();
oldWallet=await getSolanaWalletFromStoredSecret(cachedOldBundle.blockchainPair.privatePkcs8B64);
newWallet=await getSolanaWalletFromStoredSecret(cachedNewBundle.blockchainPair.privatePkcs8B64);
const [oldB,newB]=await Promise.all([
getBalanceSol({endpoint:solanaEndpoint,address:oldWallet.address}),
getBalanceSol({endpoint:solanaEndpoint,address:newWallet.address}),
]);
oldBalance=oldB;
details.innerHTML=`<strong>Старый кошелёк</strong><span class="meta-muted">${h(oldWallet.address)}</span><span>${h(formatSol(oldB.sol,9))} SOL</span><strong>Новый кошелёк</strong><span class="meta-muted">${h(newWallet.address)}</span><span>${h(formatSol(newB.sol,9))} SOL</span>`;
transfer.textContent=Number(oldB.lamports||0)>0?'Перевести SOL и продолжить':'Средств нет — продолжить';
transfer.disabled=false;
};
prepare.addEventListener('click',async()=>{prepare.disabled=true;transfer.disabled=true;err.textContent='';try{await prepareWallets();}catch(e){err.textContent=e?.message||String(e);}finally{prepare.disabled=false;}});
transfer.addEventListener('click',async()=>{transfer.disabled=true;prepare.disabled=true;skip.disabled=true;err.textContent='';try{
if(!oldWallet||!newWallet||!oldBalance)await prepareWallets();
if(Number(oldBalance?.lamports||0)>0){
const result=await transferAllSol({endpoint:solanaEndpoint,fromKeypair:oldWallet.keypair,toAddress:newWallet.address});
details.insertAdjacentHTML('beforeend',`<span class="meta-muted">${result.skipped?'Перевод не потребовался.':`Переведено ${h(formatSol(Number(result.lamports)/1_000_000_000,9))} SOL. Tx: ${h(result.signature)}`}</span>`);
}
const next=await client.continuePlaceholder();
if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}
await refresh();
}catch(e){err.textContent=e?.message||String(e);transfer.disabled=false;prepare.disabled=false;skip.disabled=false;}});
skip.addEventListener('click',async()=>{skip.disabled=true;transfer.disabled=true;prepare.disabled=true;err.textContent='';try{
const next=await client.continuePlaceholder();
if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}
await refresh();
}catch(e){err.textContent=e?.message||String(e);skip.disabled=false;transfer.disabled=!oldWallet;prepare.disabled=false;}});
const nodes=[statusCard(s),info,warning];if(oldP)nodes.push(oldP.wrap);if(newP)nodes.push(newP.wrap);nodes.push(details,prepare,transfer,skip,err);setRoot(...nodes);
if(cachedOldBundle&&cachedNewBundle)void prepareWallets().catch(e=>{err.textContent=e?.message||String(e);});
}
function placeholderView(s){const text='Перешифрование старых личных сообщений новым client key пока не реализовано. Этот этап сейчас будет отмечен как NOT_IMPLEMENTED и пропущен.';const go=document.createElement('button');go.className='primary-btn';go.textContent='Продолжить';go.addEventListener('click',async()=>{go.disabled=true;try{const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();}catch(e){go.disabled=false;root.append(note(e?.message||String(e)));}});setRoot(statusCard(s),note(text),go);}
async function refresh(){if(disposed)return;if(timer){clearTimeout(timer);timer=null;}try{const s=await client.status();const st=String(s.rotationStatus||'NONE');if(st==='NONE'){await renderNone();return;}if(st==='COPYING_CHAIN'){if(cachedNewBundle)await runCopy(s);else await askNewPasswordAndCopy(s);return;}if(st==='CHAIN_READY'){await renderChainReady(s);return;}if(['ROTATING_PDA','PDA_ROTATED','REBUILDING_SERVER'].includes(st)){pollView(s);return;}if(st==='WALLET_MIGRATION'){await renderWalletMigration(s);return;}if(st==='MESSAGE_MIGRATION'){placeholderView(s);return;}if(st==='FINALIZING'){const next=await client.continuePlaceholder();if(String(next?.rotationStatus||'')==='COMPLETE'){navigate('my-blockchain-view');return;}await refresh();return;}if(st==='COMPLETE'){setRoot(note('Смена ключей завершена.'));return;}setRoot(note(`Неизвестное состояние ротации: ${st}`));}catch(e){setRoot(note(`Не удалось прочитать состояние смены ключей: ${e?.message||e}`));}}
void refresh();screen.cleanup=()=>{disposed=true;if(timer)clearTimeout(timer);};return screen;
}