kira-wallet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/THIRD_PARTY_NOTICES.md +13 -0
- package/bin/kira.cjs +8 -0
- package/curve_pricing.py +86 -0
- package/docs/AGENT_TOOLS.md +53 -0
- package/docs/CLI.md +144 -0
- package/docs/JOB_CONTRACT.md +111 -0
- package/docs/KIRA_JOBS_BUILD_REPORT.md +92 -0
- package/docs/KIRA_REDESIGN_REPORT.md +125 -0
- package/docs/ONBOARDING_CHAT_CONTRACT.md +36 -0
- package/docs/RELEASE.md +45 -0
- package/docs/WALLET_CAPABILITIES.md +98 -0
- package/docs/character/brand-generation.json +20 -0
- package/docs/character/workspace-generation.json +29 -0
- package/docs/screenshots/kira-jobs-desktop.jpg +0 -0
- package/kira_agent.py +280 -0
- package/kira_cli.py +263 -0
- package/kira_config.py +98 -0
- package/kira_jobs.py +504 -0
- package/kira_tools.py +104 -0
- package/networks.json +214 -0
- package/onchain.cjs +34 -0
- package/package.json +78 -0
- package/pipeline-onchain.cjs +284 -0
- package/research.py +39 -0
- package/sources/job-envelope-v1.schema.json +469 -0
- package/sources/job-request-v1.schema.json +352 -0
- package/sources/mintclub-bond-abi.json +1457 -0
- package/sources/rpc-candidates.json +294 -0
- package/sources/uniswap-deployments.json +18261 -0
- package/viewer/README.md +119 -0
- package/viewer/chain_images.py +21 -0
- package/viewer/details.py +119 -0
- package/viewer/model.py +216 -0
- package/viewer/refresh-curve-prices.cjs +35 -0
- package/viewer/refresh_prices.py +97 -0
- package/viewer/server.py +153 -0
- package/viewer/static/agent.css +100 -0
- package/viewer/static/agent.js +203 -0
- package/viewer/static/app.js +252 -0
- package/viewer/static/favicon.svg +1 -0
- package/viewer/static/index.html +139 -0
- package/viewer/static/jobs.js +113 -0
- package/viewer/static/kira-attention.png +0 -0
- package/viewer/static/kira-explain.png +0 -0
- package/viewer/static/kira-logo.png +0 -0
- package/viewer/static/kira-research.png +0 -0
- package/viewer/static/kira-review.png +0 -0
- package/viewer/static/kira.png +0 -0
- package/viewer/static/style.css +51 -0
- package/viewer/static/watching-model.js +129 -0
- package/viewer/static/watching.js +167 -0
- package/viewer/static/workspace-model.js +43 -0
- package/viewer/static/workspace.css +463 -0
- package/viewer/static/workspace.js +102 -0
- package/viewer/token_images.py +137 -0
- package/wallet.py +389 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
let localSession=null, jobList=[], jobsSignature='', localPolling=false;
|
|
3
|
+
const pendingActions=new Set();
|
|
4
|
+
const jobLabels={'wallet.add':'Wallet research','wallet.refresh':'Holdings refresh','prices.refresh':'Price refresh','wallet.setTags':'Wallet names','settings.rpc':'RPC settings','settings.discovery':'Token discovery'};
|
|
5
|
+
const stageLabels={queued:'Waiting to start',starting:'Opening saved evidence',registered:'Wallet registered',discovery:'Token discovery',chain:'Checking on-chain holdings',dex_discovery:'Checking markets',token_images:'Preparing token artwork',published:'Evidence saved',failed:'Research stopped',stopped:'Research stopped',interrupted:'Ready for recovery'};
|
|
6
|
+
async function localAPI(path,options={}){
|
|
7
|
+
const response=await fetch(path,{cache:'no-store',...options,headers:{'X-Kira-Session':localSession?.token||'',...options.headers},signal:AbortSignal.timeout(12000)});
|
|
8
|
+
if(response.status===403){localSession=null;throw new Error('The local session changed. Reopen this action after reconnecting.');}
|
|
9
|
+
const data=await response.json();if(!response.ok)throw new Error(data.error?.message||'The local action could not be completed.');return data;
|
|
10
|
+
}
|
|
11
|
+
async function submitOperation(operation,input){
|
|
12
|
+
const fingerprint=JSON.stringify({operation,input});if(pendingActions.has(fingerprint))throw new Error('This request is already being submitted.');
|
|
13
|
+
pendingActions.add(fingerprint);
|
|
14
|
+
const storageKey='kira-request:'+fingerprint;
|
|
15
|
+
const key=sessionStorage.getItem(storageKey)||crypto.randomUUID();sessionStorage.setItem(storageKey,key);
|
|
16
|
+
try{
|
|
17
|
+
const result=await localAPI('/api/operations',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({schema_version:1,operation,input,idempotency_key:key})});
|
|
18
|
+
sessionStorage.removeItem(storageKey);toast('Saved '+(jobLabels[operation]||'job').toLowerCase());await pollJobs();return result;
|
|
19
|
+
}finally{pendingActions.delete(fingerprint);}
|
|
20
|
+
}
|
|
21
|
+
function refreshControlState(){
|
|
22
|
+
const enabled=localSession?.controls===true;
|
|
23
|
+
$('research-controls').hidden=!enabled;$('empty-add-wallet').hidden=!enabled;$('wallet-actions').hidden=!enabled;
|
|
24
|
+
$('service-mode').textContent=enabled?'Local controls':'Watch only';
|
|
25
|
+
const sample=state?.demo===true;
|
|
26
|
+
for(const id of ['open-wallet','empty-add-wallet','refresh-wallet','refresh-prices']){
|
|
27
|
+
const active=id.startsWith('refresh')&&jobList.some(j=>['queued','running'].includes(j.state)&&j.input.wallet===selectedWallet);
|
|
28
|
+
$(id).disabled=(sample&&id.startsWith('refresh'))||active||(id==='refresh-prices'&&!currentWallet()?.analysed_at);
|
|
29
|
+
$(id).title=sample?'Sample research cannot contact providers.':active?'A research job is already active for this wallet.':'';
|
|
30
|
+
}
|
|
31
|
+
if(typeof renderWatching==='function')renderWatching();
|
|
32
|
+
}
|
|
33
|
+
let jobsPage=1, jobsConnected=true, lastJobsPoll=null;
|
|
34
|
+
function renderJobs(){
|
|
35
|
+
const active=jobList.filter(KiraView.active);
|
|
36
|
+
$('activity-count').textContent=active.length||'';
|
|
37
|
+
$('jobs-summary').textContent=active.length?active.length+' in progress':jobList.length+(jobList.length===1?' saved job':' saved jobs');
|
|
38
|
+
$('jobs-freshness').textContent=!localSession?.controls?'Activity needs a local control session.':!jobsConnected?'Reconnecting. Last recorded states shown.':lastJobsPoll?'Checked '+stamp(lastJobsPoll):'Loading saved jobs…';
|
|
39
|
+
const filter=$('job-filter').value;
|
|
40
|
+
const filtered=jobList.filter(j=>filter==='all'||(filter==='active'?KiraView.active(j):filter==='attention'?['partial','interrupted','failed'].includes(j.state):j.state===filter));
|
|
41
|
+
const pages=Math.max(1,Math.ceil(filtered.length/20));jobsPage=Math.min(jobsPage,pages);
|
|
42
|
+
$('jobs-page').textContent=jobsPage+' / '+pages;$('jobs-prev').disabled=jobsPage===1;$('jobs-next').disabled=jobsPage===pages;
|
|
43
|
+
const rows=filtered.slice((jobsPage-1)*20,jobsPage*20);
|
|
44
|
+
$('jobs-list').innerHTML=rows.map(j=>{
|
|
45
|
+
const presentation=KiraView.jobPresentation(j,Date.now(),jobsConnected);
|
|
46
|
+
const counts=j.events?.at(-1)?.counts||{},context=j.events?.at(-1)?.context;
|
|
47
|
+
const evidence=[...Object.entries(counts).filter(([name])=>name!=='block_number').map(([name,count])=>`${name.replaceAll('_',' ')} ${count}`),...(context?.block_number?['block '+context.block_number]:[])].join(' · ');
|
|
48
|
+
const controls=KiraView.active(j)?`<button class="quiet-button" data-job-action="cancel" data-job-id="${escapeHTML(j.job_id)}" ${j.cancel_requested?'disabled':''}>${j.cancel_requested?'Stopping…':'Stop'}</button>`:['interrupted','failed','cancelled'].includes(j.state)?`<button class="quiet-button" data-job-action="resume" data-job-id="${escapeHTML(j.job_id)}">Resume saved work</button>`:'';
|
|
49
|
+
const link=j.input.wallet||j.input.address;
|
|
50
|
+
const name=state?.wallets.find(w=>w.key===link?.toLowerCase())?.name||(j.operation==='wallet.add'?j.input.tag:null);
|
|
51
|
+
const recorded=(j.events||[]).map(event=>{const context=event.context;const detail=Object.entries(event.counts||{}).filter(([name])=>name!=='block_number').map(([name,value])=>name.replaceAll('_',' ')+': '+value).join(' · ');return `<li><strong>${escapeHTML(stageLabels[event.stage]||event.stage)}</strong><span>${stamp(event.observed_at)} · ${escapeHTML(detail)}${context?.block_number?' · block '+escapeHTML(context.block_number):''}</span></li>`;}).join('');
|
|
52
|
+
const stages=recorded?`<details class="job-stages"><summary>Recorded stages · ${j.events.length}</summary><ol>${recorded}</ol></details>`:'';
|
|
53
|
+
return `<article class="job-row"><div class="job-identity"><span class="job-state ${escapeHTML(j.state)}">${presentation.spinner?'<span class="spinner" aria-hidden="true"></span>':''}${escapeHTML(presentation.label)}</span><strong>${escapeHTML(jobLabels[j.operation]||j.operation)}</strong>${link?`<a href="#/wallet/${escapeHTML(link.toLowerCase())}">${escapeHTML(name||shortAddress(link))}</a>`:''}<p>${escapeHTML(stageLabels[j.stage]||j.stage)}${j.state==='partial'?' · coverage gaps remain':''}</p><p class="job-timer" data-job-timer="${escapeHTML(j.job_id)}">${presentation.timerLabel} ${KiraView.duration(presentation.elapsed)}</p>${evidence?`<p class="job-evidence">${escapeHTML(evidence)}</p>`:''}${presentation.freshness?`<p class="job-freshness">${escapeHTML(presentation.freshness)}</p>`:''}${j.errors?.length?`<p class="job-error">${escapeHTML(j.errors[0].message)}</p>`:''}${stages}<small>Last update ${stamp(j.updated_at)} · attempt ${j.attempt} · ${escapeHTML(j.job_id.slice(0,8))}</small></div><div class="job-actions">${controls}</div></article>`;
|
|
54
|
+
}).join('')||'<div class="empty"><h3>'+(!localSession?.controls?'Activity is not available in this session':'No jobs in this view')+'</h3><p>Research jobs appear here when a local action starts.</p></div>';
|
|
55
|
+
$('active-work').hidden=active.length===0||selectedView==='activity';
|
|
56
|
+
$('active-work').innerHTML=active.slice(0,2).map(j=>{
|
|
57
|
+
const p=KiraView.jobPresentation(j,Date.now(),jobsConnected);
|
|
58
|
+
return `<div><a href="#/activity"><strong>${p.spinner?'<span class="spinner" aria-hidden="true"></span>':''}${escapeHTML(p.label)} · ${escapeHTML(jobLabels[j.operation]||j.operation)}</strong><small data-job-timer="${escapeHTML(j.job_id)}">${p.timerLabel} ${KiraView.duration(p.elapsed)}</small></a><p>${escapeHTML(stageLabels[j.stage]||j.stage)}${p.freshness?' · '+escapeHTML(p.freshness):''}</p></div>`;
|
|
59
|
+
}).join('')+(active.length>2?'<small>'+ (active.length-2)+' more jobs in Activity</small>':'');
|
|
60
|
+
if(typeof renderBriefing==='function')renderBriefing();
|
|
61
|
+
}
|
|
62
|
+
function tickJobTimers(){
|
|
63
|
+
if(document.hidden)return;
|
|
64
|
+
document.querySelectorAll('[data-job-timer]').forEach(node=>{const job=jobList.find(j=>j.job_id===node.dataset.jobTimer);if(!job)return;const p=KiraView.jobPresentation(job,Date.now(),jobsConnected);node.textContent=p.timerLabel+' '+KiraView.duration(p.elapsed);});
|
|
65
|
+
}
|
|
66
|
+
$('job-filter').addEventListener('change',()=>{jobsPage=1;renderJobs();});
|
|
67
|
+
$('jobs-prev').addEventListener('click',()=>{jobsPage--;renderJobs();});
|
|
68
|
+
$('jobs-next').addEventListener('click',()=>{jobsPage++;renderJobs();});
|
|
69
|
+
setInterval(tickJobTimers,1000);
|
|
70
|
+
async function pollJobs(){
|
|
71
|
+
if(localPolling)return;localPolling=true;
|
|
72
|
+
try{
|
|
73
|
+
if(!localSession){const response=await fetch('/api/session',{cache:'no-store',signal:AbortSignal.timeout(5000)});if(!response.ok)return;localSession=await response.json();}
|
|
74
|
+
refreshControlState();if(!localSession.controls){renderJobs();return;}
|
|
75
|
+
const jobs=await localAPI('/api/jobs');jobList=jobs;const reconnected=!jobsConnected;jobsConnected=true;lastJobsPoll=new Date().toISOString();
|
|
76
|
+
const signature=JSON.stringify(jobs)+JSON.stringify(jobs.map(j=>KiraView.jobPresentation(j).quiet));if(signature!==jobsSignature||reconnected){jobsSignature=signature;renderJobs();}else{$('jobs-freshness').textContent='Checked '+stamp(lastJobsPoll);}
|
|
77
|
+
refreshControlState();
|
|
78
|
+
}catch{jobsConnected=false;renderJobs();}
|
|
79
|
+
finally{localPolling=false;}
|
|
80
|
+
}
|
|
81
|
+
for(const button of document.querySelectorAll('[data-close-dialog]'))button.addEventListener('click',()=>button.closest('dialog').close());
|
|
82
|
+
function openAdd(){if(!localSession?.controls)return;if(!watchingSubmission)$('wallet-form-error').textContent='';if(typeof renderWatching==='function')renderWatching();$('wallet-dialog').showModal();}
|
|
83
|
+
$('open-wallet').addEventListener('click',openAdd);$('empty-add-wallet').addEventListener('click',openAdd);
|
|
84
|
+
function formAction(form,error,action){
|
|
85
|
+
form.addEventListener('submit',async e=>{e.preventDefault();error.textContent='';const buttons=[...form.querySelectorAll('button[type="submit"]')];if(buttons.some(b=>b.disabled))return;buttons.forEach(b=>b.disabled=true);
|
|
86
|
+
try{await action();}catch(failure){error.textContent=failure.message;}finally{buttons.forEach(b=>b.disabled=false);}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
$('wallet-form').addEventListener('submit',async event=>{
|
|
90
|
+
event.preventDefault();if(watchingSubmitting||$('wallet-register').disabled)return;
|
|
91
|
+
$('wallet-form-error').textContent='';
|
|
92
|
+
try{
|
|
93
|
+
const input=Object.freeze({...watchingRegistrationInput()});watchingSubmission=input;watchingSubmitting=true;renderWatching();
|
|
94
|
+
await submitOperation('wallet.add',input);
|
|
95
|
+
watchingSubmission=null;
|
|
96
|
+
$('wallet-dialog').close();$('wallet-form').reset();watchingConnection.clearSelection();
|
|
97
|
+
navigate('#/activity');
|
|
98
|
+
}catch(error){$('wallet-form-error').textContent=error.message;}
|
|
99
|
+
finally{watchingSubmitting=false;renderWatching();}
|
|
100
|
+
});
|
|
101
|
+
$('refresh-wallet').addEventListener('click',async()=>{try{await submitOperation('wallet.refresh',{wallet:selectedWallet});}catch(error){toast(error.message);}});
|
|
102
|
+
$('refresh-prices').addEventListener('click',async()=>{try{await submitOperation('prices.refresh',{wallet:selectedWallet});}catch(error){toast(error.message);}});
|
|
103
|
+
$('edit-tags').addEventListener('click',()=>{if(!currentWallet())return;$('tags-dialog').dataset.wallet=selectedWallet;$('tag-values').value=currentWallet().tags.join('\n');$('tags-error').textContent='';$('tags-dialog').showModal();});
|
|
104
|
+
formAction($('tags-form'),$('tags-error'),async()=>{await submitOperation('wallet.setTags',{wallet:$('tags-dialog').dataset.wallet,tags:$('tag-values').value.split('\n').filter(t=>t.length)});$('tags-dialog').close();});
|
|
105
|
+
$('jobs-list').addEventListener('click',async event=>{const button=event.target.closest('[data-job-action]');if(!button)return;button.disabled=true;try{await submitOperation('job.'+button.dataset.jobAction,{job_id:button.dataset.jobId});}catch(error){button.disabled=false;toast(error.message);}});
|
|
106
|
+
$('open-settings').addEventListener('click',async()=>{try{const config=await localAPI('/api/settings');$('rpc-mode').value=config.rpc.mode;$('rpc-fallback').checked=config.rpc.allow_public_fallback;$('rpc-references').value=Object.entries(config.rpc.chains).map(([chain,row])=>chain+' '+row.url_env).join('\n');$('discovery-provider').value=config.discovery.provider;$('discovery-key').value=config.discovery.key_env;$('settings-error').textContent='';$('settings-dialog').showModal();}catch(error){toast(error.message);}});
|
|
107
|
+
formAction($('rpc-form'),$('settings-error'),async()=>{const lines=$('rpc-references').value.split('\n').filter(s=>s.trim());const chains=lines.map(line=>{const parts=line.trim().split(/\s+/);if(parts.length!==2)throw new Error('Enter a chain ID and environment variable name on each line.');return {chain_id:Number(parts[0]),url_env:parts[1]};});await submitOperation('settings.rpc',{mode:$('rpc-mode').value,allow_public_fallback:$('rpc-fallback').checked,chains});});
|
|
108
|
+
formAction($('discovery-form'),$('settings-error'),async()=>{await submitOperation('settings.discovery',{provider:$('discovery-provider').value,key_env:$('discovery-key').value});});
|
|
109
|
+
$('view-history').addEventListener('click',async()=>{try{const rows=await localAPI('/api/snapshots?wallet='+encodeURIComponent(selectedWallet));$('history-list').innerHTML=rows.map(row=>`<article class="history-row"><strong>${stamp(row.compiled_at)}</strong><span>${escapeHTML(row.status==='completed'?'Saved':'Saved with coverage gaps')}</span><small>${escapeHTML(row.snapshot_id)}</small></article>`).join('')||'<p>No analysis has been published yet.</p>';$('compare-form').hidden=rows.length<2;for(const id of ['compare-before','compare-after'])$(id).innerHTML=rows.map(row=>`<option value="${escapeHTML(row.snapshot_id)}">${stamp(row.compiled_at)} · ${escapeHTML(row.snapshot_id.split('/').at(-1))}</option>`).join('');$('compare-after').selectedIndex=Math.max(0,rows.length-1);$('comparison-result').textContent='';$('history-error').textContent='';$('history-dialog').showModal();}catch(error){toast(error.message);}});
|
|
110
|
+
formAction($('compare-form'),$('history-error'),async()=>{const comparison=await localAPI('/api/compare?before='+encodeURIComponent($('compare-before').value)+'&after='+encodeURIComponent($('compare-after').value));$('comparison-result').innerHTML=`<p>${escapeHTML(comparison.note)}</p><p>Coverage ${comparison.coverage_changed?'changed':'unchanged'} · native price references ${comparison.price_references_changed?'changed':'unchanged'}</p><div class="table-wrap"><table><thead><tr><th>Token / chain</th><th>Earlier balance</th><th>Later balance</th><th>Change</th></tr></thead><tbody>${comparison.positions.map(p=>`<tr><td>${escapeHTML(p.symbol||shortAddress(p.address))} · ${p.chain_id}<small>${escapeHTML(p.presence.replaceAll('_',' '))}${p.price_references_changed?' · price references changed':''}${p.valuation_method_changed?' · method changed':''}</small></td><td>${escapeHTML(p.before_balance??'Unknown')}</td><td>${escapeHTML(p.after_balance??'Unknown')}</td><td>${escapeHTML(p.balance_delta??'Unknown')}</td></tr>`).join('')}</tbody></table></div>`;});
|
|
111
|
+
window.addEventListener('hashchange',()=>{refreshControlState();renderJobs();});
|
|
112
|
+
document.addEventListener('visibilitychange',()=>{if(!document.hidden)pollJobs();});
|
|
113
|
+
pollJobs();setInterval(pollJobs,2500);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
:root{--bg:#fff;--sidebar:#f7f8fa;--ink:#202127;--secondary:#737780;--muted:#9ba0aa;--line:#eceef2;--accent:#4162df;--accent-soft:#edf1ff;--radius:12px;--space:8px;font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","SF Pro Text","Helvetica Neue",Arial,sans-serif;color:var(--ink);background:var(--bg);font-synthesis:none;-webkit-font-smoothing:antialiased}
|
|
2
|
+
*{box-sizing:border-box}body{margin:0;font-size:16px;letter-spacing:-.015em}button,select{font:inherit}button,a,select{-webkit-tap-highlight-color:transparent}button{cursor:pointer}a{color:inherit;text-decoration:none}button:focus-visible,a:focus-visible,select:focus-visible,summary:focus-visible{outline:3px solid #4162df70;outline-offset:4px}button{color:inherit}button:disabled{cursor:default}svg{stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.skip-link{position:fixed;z-index:9;left:20px;top:-100px;padding:12px 20px;background:var(--accent);color:white;border-radius:8px}.skip-link:focus{top:12px}.app{display:flex;min-height:100vh}
|
|
3
|
+
.sidebar{width:232px;flex-shrink:0;position:fixed;inset:0 auto 0 0;background:var(--sidebar);border-right:1px solid var(--line);padding:38px 20px 24px;display:flex;flex-direction:column;z-index:2}.brand{font-size:23px;font-weight:650;letter-spacing:-.055em;display:flex;align-items:center;gap:10px;padding:0 12px}.brand-dot{color:var(--accent);margin-left:-9px}.brand-symbol{width:27px;height:27px;display:flex;flex-direction:column;gap:4px;justify-content:center;background:var(--accent);border-radius:8px;padding:6px}.brand-symbol i{height:2px;border-radius:1px;background:white;width:100%}.brand-symbol i:nth-child(2){width:72%}.brand-symbol i:nth-child(3){width:45%}.sidebar-label{font-size:11px;letter-spacing:.09em;color:#81858e;font-weight:600;display:flex;justify-content:space-between;padding:0 12px;margin:54px 0 14px}.sidebar-label span{letter-spacing:0;background:#e9ebef;padding:1px 5px;border-radius:4px}.wallet-button{width:100%;text-align:left;border:1px solid transparent;border-radius:10px;padding:14px 13px;background:transparent;display:grid;grid-template-columns:28px 1fr 12px;align-items:center;gap:9px}.wallet-button:hover{background:#eef0f4}.wallet-button.active{background:var(--accent-soft);border-color:#e0e6fb;color:#3656d3}.wallet-button.active .wallet-arrow{opacity:1}.wallet-arrow{opacity:0;color:var(--accent);font-size:18px}.wallet-glyph{width:26px;height:26px;display:flex;align-items:center;justify-content:center}.wallet-glyph svg{width:23px;height:23px}.wallet-nav-name{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wallet-nav-address{color:#8b92a3;font-size:12px;letter-spacing:0;margin-top:5px}.sidebar-bottom{margin-top:auto;border-top:1px solid #e5e8ed;padding-top:18px;display:flex;align-items:center;gap:6px;font-size:11px;color:#848994;letter-spacing:0}.status-dot,.sync-dot{width:5px;height:5px;background:#6e9c83;border-radius:50%;flex-shrink:0}.read-only{margin-left:auto;color:#a0a5ae}
|
|
4
|
+
main{width:calc(100% - 232px);margin-left:232px;min-width:0}.topbar{height:56px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 56px;color:var(--secondary);font-size:13px}.breadcrumb{min-width:0;flex:1}.breadcrumb ol{display:flex;align-items:center;gap:10px;list-style:none;margin:0;padding:0;min-width:0}.breadcrumb li{display:flex;align-items:center;gap:10px;min-width:0}.breadcrumb li:first-child{flex-shrink:0}.breadcrumb li+li::before{content:"/";color:#c8ccd3;flex-shrink:0}.breadcrumb a,.breadcrumb [aria-current]{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:24ch}.breadcrumb a{color:var(--secondary)}.breadcrumb a:hover{color:var(--accent)}.breadcrumb [aria-current]{color:#50535e;font-weight:500}.sync-status{display:flex;gap:8px;align-items:center;font-size:12px}.sync-status .sync-dot{background:var(--accent);opacity:.6}.content{max-width:1432px;padding:48px 56px 24px;margin:auto}.eyebrow{font-size:11px;letter-spacing:.105em;font-weight:600;color:#8b8f98;margin-bottom:12px}.wallet-heading{display:flex;align-items:center;justify-content:space-between;gap:16px}h1{font-size:38px;line-height:1.15;font-weight:600;letter-spacing:-.045em;margin:0 0 12px}.wallet-address{font-size:12px;font-family:"SF Mono",Menlo,monospace;color:#858b96;display:flex;align-items:center;gap:8px;letter-spacing:-.015em}.icon-button{border:0;background:transparent;color:#9ba0aa;padding:3px;display:inline-flex;align-items:center;justify-content:center;transition:color .15s}.icon-button:hover{color:var(--accent)}.icon-button svg{width:15px;height:15px}.tag{font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:11px;letter-spacing:0;border:1px solid #e3e7f0;color:#838a9a;padding:3px 7px;border-radius:5px}.text-link{font-size:13px;color:#777e8b;white-space:nowrap}.text-link:hover{color:var(--accent)}.text-link span{margin-left:6px;color:#a0a6b1}.portfolio-summary{display:grid;grid-template-columns:minmax(230px,1fr) 144px 144px;gap:24px;margin-top:48px;align-items:center;padding-bottom:43px;border-bottom:1px solid var(--line)}.summary-label{font-size:13px;font-weight:400;color:#7b808b;letter-spacing:-.01em}.total-value{font-size:60px;font-weight:500;line-height:1.1;letter-spacing:-.055em;margin-top:10px;font-variant-numeric:tabular-nums}.valuation-note{font-size:12px;color:#9a9fab;margin-top:14px;line-height:1.5;letter-spacing:0}.summary-stat{padding-left:24px;border-left:1px solid var(--line);display:flex;flex-direction:column;gap:9px}.summary-stat strong{font-size:26px;line-height:1.2;font-weight:500;letter-spacing:-.04em;font-variant-numeric:tabular-nums}.stat-note{font-size:12px;color:#9ba1ae;letter-spacing:0;line-height:1.4}.holdings{padding-top:36px}.holdings-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:34px}h2{font-size:21px;font-weight:550;letter-spacing:-.035em;margin:0;display:flex;align-items:center;gap:9px}#visible-count{font-size:11px;font-weight:500;color:#9ba1ae;background:#f4f5f7;border:1px solid #eceef2;padding:3px 6px;border-radius:5px;letter-spacing:0}.filters{display:flex;align-items:center;gap:12px}.chain-select{position:relative}.chain-select select{appearance:none;font-size:12px;border:1px solid #e7e9ee;border-radius:7px;padding:9px 30px 9px 12px;background:white;color:#5f6572;cursor:pointer;min-width:116px;max-width:200px}.chain-select svg{position:absolute;right:10px;top:12px;width:12px;height:12px;pointer-events:none;fill:none;stroke:#979da9}.segment{display:flex;gap:2px;background:#f4f5f7;padding:3px;border-radius:8px;border:1px solid #eff0f2}.segment button{font-size:12px;padding:6px 11px;background:transparent;border:1px solid transparent;border-radius:5px;color:#858b97;font-weight:500;letter-spacing:-.015em;min-width:45px}.segment button[aria-pressed=true]{background:white;border-color:#e7e9ee;box-shadow:0 1px 3px #17192308;color:#3859d7}.chain-section{margin-bottom:34px}.chain-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.chain-name{display:flex;gap:9px;align-items:center;font-size:14px;font-weight:550;letter-spacing:-.015em;margin:0}.chain-icon{width:19px;height:19px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-size:10px;font-weight:600;background:#636b83}.chain-icon.base{background:#4162df}.chain-icon.ethereum{background:#69758a}.chain-icon.blast{background:#829545}.chain-icon.zora{background:#a587c1}.chain-assets{font-size:11px;color:#9ba1ae;font-weight:400;margin-left:2px}.chain-total{font-size:13px;color:#6c7280;font-variant-numeric:tabular-nums}.table-wrap{width:100%;border:1px solid var(--line);border-radius:11px;overflow:hidden}.token-table{border-collapse:collapse;width:100%;table-layout:fixed}.token-table col:nth-child(1){width:29%}.token-table col:nth-child(2){width:17%}.token-table col:nth-child(3){width:16%}.token-table col:nth-child(4){width:17%}.token-table col:nth-child(5){width:21%}.token-table th{font-size:11px;font-weight:500;color:#979daa;letter-spacing:.005em;padding:12px 17px;text-align:right;background:#fafbfc;border-bottom:1px solid var(--line)}.token-table th:first-child,.token-table th:last-child{text-align:left}.token-table td{padding:18px 17px;border-bottom:1px solid var(--line);font-size:14px;text-align:right;font-variant-numeric:tabular-nums;vertical-align:middle}.token-table tr:last-child td{border-bottom:0}.token-table td:first-child,.token-table td:last-child{text-align:left}.token-table tbody tr{transition:background .15s}.token-table tbody tr:hover{background:#fafbfe}.token-name{display:flex;align-items:center;gap:11px;min-width:0}.coin-icon{width:34px;height:34px;flex-shrink:0;border:1px solid #eaedf2;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#f5f6fa;color:#7c8496;font-size:12px;font-weight:600;letter-spacing:-.035em}.coin-icon.ether{background:#f1f3f8;color:#737f99;font-size:19px}.coin-icon.chicken{background:#fff9ed;font-size:19px}.coin-icon.bitcoin{background:#fff5e6;color:#dc9a35;font-size:19px}.coin-icon.member{background:#f3edff;color:#9b73c1}.coin-title{min-width:0}.symbol{font-size:14px;font-weight:550;color:#333740;letter-spacing:-.015em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}.token-description{font-size:11px;color:#a0a6b2;margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}.amount{color:#59606e;white-space:nowrap;font-size:13px}.token-price{color:#59606e;white-space:nowrap;font-size:13px}.price-basis{font-size:10px;color:#a1a7b3;margin-top:5px;letter-spacing:0}.unpriced{color:#b1b6c1}.token-value{font-weight:550;color:#313640;letter-spacing:-.02em;white-space:nowrap}.market-links{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center}.market-link{color:#727b8f;font-size:10px;font-weight:500;line-height:1.4;letter-spacing:0;white-space:nowrap}.market-link.mint{color:#5069bb}.market-link:hover{color:var(--accent);text-decoration:underline;text-underline-offset:3px}.market-link span{font-size:10px;opacity:.6;margin-left:2px}.native-label{font-size:11px;color:#a5abb7}.mobile-label{display:none}.empty{padding:62px 20px;text-align:center;border:1px solid var(--line);border-radius:12px}.empty-mark{color:#bcc5df;font-size:34px;margin-bottom:12px}.empty h3{font-size:17px;font-weight:500;margin:0}.empty p{font-size:13px;color:#979eab;line-height:1.5;margin:10px 0 0}.page-footer{display:flex;justify-content:space-between;gap:16px;color:#a4aab5;font-size:11px;letter-spacing:0;padding:9px 0 10px}.load-error{margin:64px;line-height:1.5}.load-error p{font-size:14px;color:var(--secondary)}.toast{position:fixed;left:50%;bottom:28px;transform:translate(-50%,12px);padding:10px 17px;border-radius:9px;background:#252b3b;color:white;font-size:13px;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;z-index:10;box-shadow:0 4px 24px #1a223a20}.toast.show{opacity:1;transform:translate(-50%,0)}[hidden]{display:none!important}
|
|
5
|
+
.wallet-nav-address{display:block}.topbar{height:56px;gap:24px}.content{padding-top:28px}.portfolio-summary{margin-top:32px;padding-bottom:32px}.holdings{padding-top:28px}.holdings-toolbar{margin-bottom:26px}.price-basis{font-size:11px}.market-link{font-size:11px}
|
|
6
|
+
@media(min-width:1700px){.content{padding-top:32px}.portfolio-summary{margin-top:52px}.total-value{font-size:68px}}
|
|
7
|
+
@media(max-width:1180px){.sidebar{width:200px;padding:34px 14px 24px}main{margin-left:200px;width:calc(100% - 200px)}.topbar{padding:0 34px;height:56px}.content{padding:28px 34px 24px}.portfolio-summary{grid-template-columns:1fr 110px 118px;gap:16px}.summary-stat{padding-left:16px}.total-value{font-size:54px}.token-table th,.token-table td{padding-left:13px;padding-right:13px}.symbol,.token-description{max-width:150px}.market-links{gap:6px}}
|
|
8
|
+
@media(max-width:960px){.sidebar{width:176px;padding:30px 10px 20px}.brand{font-size:21px;gap:8px;padding:0 8px}.sidebar-label{padding:0 8px;margin-top:44px}.wallet-button{padding:12px 8px;grid-template-columns:24px 1fr 9px;gap:5px}.wallet-nav-address{font-size:10px}main{margin-left:176px;width:calc(100% - 176px)}.content{padding:26px 24px 20px}.topbar{padding:0 24px}.sync-status{font-size:10px}.wallet-address{font-size:10px}h1{font-size:34px}.portfolio-summary{grid-template-columns:1fr 88px 98px;gap:10px}.total-value{font-size:46px}.summary-stat{padding-left:12px}.stat-note{font-size:10px}.token-table td,.token-table th{padding-left:10px;padding-right:10px}.coin-icon{width:29px;height:29px}.token-name{gap:8px}.symbol{font-size:12px}.amount,.token-price,.token-value{font-size:12px}.symbol,.token-description{max-width:116px}.token-table col:nth-child(1){width:29%}.token-table col:nth-child(2){width:17%}.token-table col:nth-child(3){width:15%}.token-table col:nth-child(4){width:17%}.token-table col:nth-child(5){width:22%}.text-link{font-size:11px}.chain-select select{min-width:95px}.segment button{padding:6px 8px}.filters{gap:8px}.market-link{font-size:9px}.sidebar-bottom{font-size:10px;gap:4px}}
|
|
9
|
+
@media(max-width:700px){.app{display:block}.sidebar{position:static;width:100%;padding:22px 22px 13px;border-right:0;border-bottom:1px solid var(--line);background:#fafbfc}.brand{padding:0;font-size:22px}.sidebar-label,.sidebar-bottom{display:none}#wallet-list{display:flex;gap:8px;overflow-x:auto;margin-top:20px}.wallet-button{width:auto;min-width:156px;padding:10px 13px;grid-template-columns:24px 1fr}.wallet-arrow{display:none}.wallet-nav-address{font-size:10px}.wallet-nav-name{font-size:12px}main{margin-left:0;width:100%}.topbar{height:48px;padding:0 24px;font-size:11px;gap:0}.topbar .sync-status{display:none}.content{padding:24px 24px 18px}.wallet-heading{align-items:flex-start}.eyebrow{font-size:10px;margin-bottom:10px}h1{font-size:32px}.wallet-address{gap:4px;font-size:10px;flex-wrap:wrap}#address{max-width:238px;overflow:hidden;text-overflow:ellipsis}.text-link{padding-top:23px;font-size:10px}.portfolio-summary{grid-template-columns:1fr 1fr;gap:22px;margin-top:30px;padding-bottom:29px}.valuation{grid-column:1/-1}.total-value{font-size:54px}.valuation-note{font-size:11px;margin-top:10px}.summary-stat{padding-left:0;border:0;gap:6px}.summary-stat strong{font-size:22px}.summary-label{font-size:12px}.stat-note{font-size:11px}.holdings{padding-top:27px}.holdings-toolbar{align-items:flex-start;gap:18px;flex-direction:column;margin-bottom:25px}.filters{width:100%;justify-content:space-between}.chain-select select{font-size:12px;min-width:118px}.segment button{font-size:12px;padding:6px 10px}.table-wrap{border-radius:10px}.token-table,.token-table tbody{display:block}.token-table colgroup,.token-table thead{display:none}.token-table tr{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:17px 16px;border-bottom:1px solid var(--line)}.token-table tr:last-child{border:0}.token-table td{display:block;padding:0;border:0!important}.token-table td:first-child{grid-column:1;grid-row:1}.token-table td:nth-child(4){grid-column:2;grid-row:1;align-self:center}.token-table td:nth-child(2){grid-column:1;grid-row:2;text-align:left;margin-left:40px}.token-table td:nth-child(3){grid-column:2;grid-row:2}.token-table td:nth-child(5){grid-column:1/-1;grid-row:3;margin-left:40px}.coin-icon{width:30px;height:30px}.symbol{font-size:13px;max-width:130px}.token-description{font-size:10px;max-width:130px}.token-value{font-size:15px}.amount,.token-price{font-size:11px}.mobile-label{display:block;font-size:9px;color:#a7aeba;margin-bottom:3px}.price-basis{font-size:9px}.market-links{gap:8px 13px}.market-link{font-size:10px}.chain-name{font-size:13px}.chain-assets{font-size:10px}.chain-total{font-size:12px}.chain-section{margin-bottom:27px}.page-footer{font-size:10px;flex-direction:column;gap:6px;line-height:1.5}.load-error{margin:36px 24px}.tag{font-size:10px}.empty{padding:48px 15px}}
|
|
10
|
+
@media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}}
|
|
11
|
+
.segment button{white-space:nowrap}.wallet-heading>div{min-width:0}
|
|
12
|
+
.primary-nav{margin-top:40px}.home-button{grid-template-columns:28px 1fr;min-height:52px}.sidebar-label{margin-top:28px}
|
|
13
|
+
.wallet-button:not(.home-button){grid-template-columns:24px minmax(0,1fr);gap:8px;padding-left:11px;padding-right:11px}.wallet-arrow{display:none}
|
|
14
|
+
.coin-icon{position:relative;overflow:hidden}.coin-icon img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#fff}.coin-icon.has-image .coin-fallback{visibility:hidden}
|
|
15
|
+
.home-heading p{font-size:14px;color:#8b919d;margin:0;line-height:1.5;letter-spacing:-.01em}.home-heading h1{margin-bottom:10px}
|
|
16
|
+
.home-summary{display:grid;grid-template-columns:minmax(300px,1fr) auto;gap:32px;align-items:center;margin-top:34px;padding-bottom:34px;border-bottom:1px solid var(--line)}.home-stats{display:grid;grid-template-columns:repeat(3,minmax(88px,1fr));gap:24px}.home-stats .summary-stat{padding-left:22px}.home-stats .stat-note{white-space:nowrap}.home-summary .valuation-note{max-width:390px}
|
|
17
|
+
.dashboard-grid{display:grid;grid-template-columns:1.12fr 1fr;gap:38px;margin-top:34px}.dashboard-panel{min-width:0}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:21px}.section-heading h2{font-size:19px;white-space:nowrap}.section-note{font-size:11px;color:#959ba7;letter-spacing:0;white-space:nowrap}.section-note span{margin-left:4px}
|
|
18
|
+
.wallet-cards{display:grid;gap:14px}.wallet-card{display:block;width:100%;text-align:left;padding:23px 24px;border:1px solid var(--line);border-radius:12px;background:#fff;transition:border-color .15s,background .15s}.wallet-card:hover{border-color:#d3dcfb;background:#fbfcff}.wallet-card-heading{display:flex;align-items:center;gap:11px}.card-wallet-icon{display:flex;width:34px;height:34px;border:1px solid #e8ecf6;border-radius:9px;align-items:center;justify-content:center;color:#7080af;background:#f8faff;flex-shrink:0}.card-wallet-icon svg{width:20px;height:20px}.wallet-card-identity{min-width:0;display:grid;gap:4px}.wallet-card-identity strong{font-size:14px;font-weight:550;letter-spacing:-.015em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.wallet-card-identity>span{font:11px "SF Mono",Menlo,monospace;color:#a0a6b1;letter-spacing:0}.card-arrow{margin-left:auto;color:#a2aabe;font-size:16px;align-self:flex-start}.wallet-card-value{display:block;margin:21px 0 9px;font-size:32px;font-weight:500;letter-spacing:-.045em;font-variant-numeric:tabular-nums}.wallet-card-meta{display:flex;gap:10px;justify-content:space-between;font-size:11px;color:#969daa;letter-spacing:0;line-height:1.5}.wallet-card-meta>span:last-child{text-align:right;white-space:nowrap}
|
|
19
|
+
.allocation-progress{display:block;appearance:none;-webkit-appearance:none;border:0;width:100%;height:4px;background:#f0f2f7;border-radius:4px;overflow:hidden;margin-top:14px;color:var(--accent)}.allocation-progress::-webkit-progress-bar{background:#f0f2f7;border-radius:4px}.allocation-progress::-webkit-progress-value{background:var(--accent);border-radius:4px}.allocation-progress::-moz-progress-bar{background:var(--accent);border-radius:4px}
|
|
20
|
+
.network-allocation{border:1px solid var(--line);border-radius:12px;padding:8px 23px}.network-row{padding:16px 0;border-bottom:1px solid var(--line)}.network-row:last-child{border:0}.network-row-heading,.network-row-meta{display:flex;justify-content:space-between;align-items:center;gap:12px}.network-label{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:500}.network-value{font-size:13px;font-weight:500;white-space:nowrap;font-variant-numeric:tabular-nums}.network-row-meta{font-size:10px;color:#a0a6b2;letter-spacing:0;margin-top:7px;padding-left:27px}.network-row .allocation-progress{margin-top:9px;margin-left:27px;width:calc(100% - 27px);height:3px}.network-row .chain-icon{width:18px;height:18px;font-size:9px}
|
|
21
|
+
.largest-holdings{margin-top:36px}.home-holdings{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));border:1px solid var(--line);border-radius:12px;overflow:hidden}.home-holding{display:flex;flex-direction:column;gap:20px;padding:22px 19px;border-right:1px solid var(--line);min-width:0}.home-holding:last-child{border:0}.home-holding .coin-icon{width:31px;height:31px}.home-holding .token-name{gap:8px}.home-holding .symbol{font-size:13px}.home-holding .token-description{font-size:10px;max-width:none}.holding-value{font-size:20px;font-weight:500;letter-spacing:-.035em;font-variant-numeric:tabular-nums}.home-footer{margin-top:22px;line-height:1.5}.panel-empty{color:#9ba1ae;font-size:13px;line-height:1.5;padding:20px}.home-holdings>.panel-empty{grid-column:1/-1}
|
|
22
|
+
@media(max-width:1280px){.home-summary{gap:22px}.home-stats{gap:14px;grid-template-columns:repeat(3,minmax(80px,1fr))}.home-stats .summary-stat{padding-left:14px}.dashboard-grid{gap:26px}.wallet-card{padding:22px 20px}.wallet-card-meta{flex-wrap:wrap;gap:4px 10px}.home-holding{padding:19px 14px}.home-holding .token-name{flex-wrap:wrap;gap:10px}.home-holding .token-description{font-size:9px}.holding-value{font-size:18px}}
|
|
23
|
+
@media(max-width:1050px){.home-summary{grid-template-columns:1fr;gap:26px}.home-stats{grid-template-columns:repeat(3,1fr);gap:16px}.home-stats .summary-stat{padding-left:16px}.home-stats .summary-stat:first-child{padding-left:0;border:0}.dashboard-grid{gap:22px}.section-heading{gap:8px}.section-heading h2{font-size:18px}.section-note{font-size:10px}.wallet-card{padding:19px 18px}.wallet-card-value{font-size:29px}.wallet-card-meta>span{font-size:10px}.network-allocation{padding:6px 17px}.home-holdings{grid-template-columns:repeat(3,1fr)}.home-holding{padding:20px;border-bottom:1px solid var(--line)}.home-holding:nth-child(3){border-right:0}.home-holding:nth-child(n+4){border-bottom:0}.home-holding .token-name{flex-wrap:nowrap}.home-holding .token-description{font-size:10px}}
|
|
24
|
+
@media(max-width:850px){.dashboard-grid{grid-template-columns:1fr;gap:32px}.wallet-cards{grid-template-columns:1fr 1fr}.wallet-card-meta{flex-direction:column}.wallet-card-meta>span:last-child{text-align:left}.network-allocation{display:grid;grid-template-columns:1fr 1fr;gap:0 24px;padding:8px 20px}.network-row:nth-last-child(2):nth-child(odd){border:0}.home-holdings{grid-template-columns:repeat(2,1fr)}.home-holding:nth-child(3){border-right:1px solid var(--line)}.home-holding:nth-child(even){border-right:0}.home-holding:nth-child(4){border-bottom:1px solid var(--line)}}
|
|
25
|
+
@media(max-width:700px){.sidebar{display:grid;grid-template-columns:1fr auto;align-items:center}.primary-nav{margin:0}.home-button{min-width:0;min-height:0;padding:9px 12px;grid-template-columns:20px 1fr;gap:7px;border-radius:8px}.home-button .wallet-glyph{width:20px;height:20px}.home-button svg{width:18px;height:18px}#wallet-list{grid-column:1/-1;width:100%;margin-top:18px}.home-summary{margin-top:28px;gap:28px;padding-bottom:28px}.home-heading p{font-size:13px}.home-summary .total-value{font-size:52px}.home-stats{gap:10px}.home-stats .summary-stat{padding-left:12px;border-left:1px solid var(--line)}.home-stats .summary-stat:first-child{padding-left:0;border:0}.home-stats .stat-note{font-size:10px}.home-stats .summary-label{font-size:11px}.home-stats strong{font-size:23px}.dashboard-grid{margin-top:28px;gap:29px}.wallet-cards{grid-template-columns:1fr;gap:12px}.wallet-card{padding:21px 22px}.wallet-card-meta{flex-direction:row;flex-wrap:wrap;gap:4px 12px}.wallet-card-meta>span{font-size:11px}.wallet-card-meta>span:last-child{text-align:right}.wallet-card-value{font-size:32px}.section-heading{margin-bottom:17px}.section-heading h2{font-size:19px}.section-note{font-size:10px}.network-allocation{grid-template-columns:1fr;padding:6px 22px}.network-row:nth-last-child(2):nth-child(odd){border-bottom:1px solid var(--line)}.network-row{padding:17px 0}.network-label,.network-value{font-size:13px}.largest-holdings{margin-top:30px}.home-holdings{display:block}.home-holding{flex-direction:row;align-items:center;justify-content:space-between;gap:15px;border:0!important;border-bottom:1px solid var(--line)!important;padding:18px 20px}.home-holding:last-child{border-bottom:0!important}.home-holding .coin-icon{width:33px;height:33px}.home-holding .token-name{gap:11px}.home-holding .symbol{font-size:13px}.holding-value{font-size:17px;white-space:nowrap}.home-footer{margin-top:20px}}
|
|
26
|
+
@media(max-width:700px){.chain-select select{width:140px;max-width:140px}.wallet-address{flex-wrap:nowrap;min-width:0}#address{max-width:none;min-width:0}.wallet-address .icon-button{flex-shrink:0}.wallet-address #wallet-tags:empty{display:none}}
|
|
27
|
+
@media(max-width:380px){.chain-select select{width:118px;min-width:118px;max-width:118px}.filters{gap:8px}.segment button{min-width:40px;padding:6px 7px}}
|
|
28
|
+
|
|
29
|
+
.chain-icon{position:relative;overflow:hidden;flex-shrink:0}.chain-icon img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#fff}.chain-icon.has-image .chain-fallback{visibility:hidden}
|
|
30
|
+
.coin-icon img,.chain-icon img{opacity:0}.coin-icon.has-image img,.chain-icon.has-image img{opacity:1}
|
|
31
|
+
.network-row{display:block;color:inherit}.network-row:hover .network-label,.network-link:hover,.token-link:hover .symbol{color:var(--accent)}.network-link,.token-link{color:inherit;text-decoration:none}.detail-arrow{font-size:11px;color:#a0a8bc;margin-left:2px}.home-holding:hover{background:#fafbff}.token-link:focus-visible,.network-link:focus-visible,.network-row:focus-visible,.home-holding:focus-visible,.account-card-heading:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:5px}
|
|
32
|
+
|
|
33
|
+
.detail-heading{display:flex;align-items:center;justify-content:space-between;gap:24px}.detail-identity{display:flex;align-items:center;gap:18px;min-width:0}.detail-identity>div{min-width:0}.detail-identity>.coin-icon,.detail-identity>.chain-icon{width:52px;height:52px;font-size:22px}.detail-heading h1{overflow-wrap:anywhere;margin-bottom:8px}.detail-heading .eyebrow{margin-bottom:8px}.detail-heading p{margin:0;color:#9299a6;font-size:13px;line-height:1.5;overflow-wrap:anywhere}.detail-summary{grid-template-columns:minmax(260px,1fr) minmax(120px,210px) 136px}.detail-summary .summary-stat strong{overflow-wrap:anywhere;font-size:24px}.detail-summary .valuation-note{max-width:400px}
|
|
34
|
+
.detail-facts{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:30px}.fact-panel{border:1px solid var(--line);border-radius:12px;padding:24px;min-width:0}.fact-panel h2{font-size:16px;margin-bottom:19px}.contract-address{font:12px/1.6 "SF Mono",Menlo,monospace;overflow-wrap:anywhere;color:#7d8595}.fact-panel dl{margin:18px 0 0;display:grid;gap:12px}.fact-panel dl>div{display:grid;gap:5px}.fact-panel dt{font-size:11px;color:#9299a6}.fact-panel dd{margin:0;font-size:13px;line-height:1.6;color:#565e6c;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.fact-panel p{font-size:11px;line-height:1.6;color:#9299a6;margin:18px 0 0}.detail-markets{gap:12px 18px}.detail-markets .market-link{font-size:12px}
|
|
35
|
+
.detail-accounts{margin-top:34px}.account-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.account-card{border:1px solid var(--line);border-radius:12px;padding:24px;min-width:0}.account-card-heading{display:flex;align-items:center;gap:11px;color:inherit}.account-card-heading:hover strong{color:var(--accent)}.account-value{font-size:31px;font-weight:500;letter-spacing:-.045em;margin:21px 0 8px;font-variant-numeric:tabular-nums}.account-meta{font-size:12px;line-height:1.5;color:#9098a6;overflow-wrap:anywhere}.account-facts{display:grid;gap:11px;margin:22px 0 0}.account-facts>div{display:flex;align-items:baseline;justify-content:space-between;gap:20px}.account-facts dt{font-size:11px;line-height:1.5;color:#9098a6}.account-facts dd{font-size:12px;line-height:1.5;margin:0;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}.account-facts dd span{display:block;font-size:10px;color:#9098a6;margin-top:3px}.account-footnote{border-top:1px solid var(--line);margin-top:20px;padding-top:14px;display:grid;gap:5px;color:#979eaa;font-size:10px;line-height:1.5}.account-quote{display:grid;gap:7px;margin-top:18px;background:#f7f9fd;padding:14px;border-radius:8px}.account-quote>span,.account-quote small{font-size:10px;line-height:1.5;color:#9299a6}.account-quote strong{font-size:12px;font-weight:500;color:#65728b;font-variant-numeric:tabular-nums}.detail-note{font-size:11px;line-height:1.7;color:#9299a6;margin:15px 0 0}.filter-note{margin:-10px 0 20px}.detail-footer{margin-top:24px;line-height:1.6}.count-badge{font-size:11px;font-weight:500;color:#9ba1ae;background:#f4f5f7;padding:3px 6px;border:1px solid #eceef2;border-radius:5px;letter-spacing:0}.token-search input{border:1px solid #e7e9ee;border-radius:7px;padding:9px 12px;background:#fff;font:12px inherit;width:150px;min-width:0}.token-search input::placeholder{color:#9ba1ae}.token-search input:focus{outline:2px solid #d3dcfb;outline-offset:1px}
|
|
36
|
+
@media(max-width:1180px){.detail-summary{grid-template-columns:minmax(220px,1fr) 150px 110px;gap:16px}.detail-summary .summary-stat strong{font-size:21px}.fact-panel,.account-card{padding:21px}.detail-facts{gap:18px}.account-facts>div{gap:12px}}
|
|
37
|
+
@media(max-width:960px){.detail-summary{grid-template-columns:1fr 1fr;gap:24px}.detail-summary .valuation{grid-column:1/-1}.detail-summary .summary-stat{padding-left:0;border:0}.detail-summary .summary-stat strong{font-size:24px}.account-grid,.detail-facts{gap:14px}.account-card,.fact-panel{padding:19px}.account-facts>div{display:grid;gap:4px}.account-facts dd{text-align:left;white-space:normal}.account-facts dd span{display:inline;margin-left:5px}.detail-heading{align-items:flex-start}.detail-heading .chain-select select{width:125px;min-width:125px;font-size:11px}}
|
|
38
|
+
@media(max-width:700px){.detail-heading{flex-wrap:wrap;gap:20px}.detail-identity{gap:14px}.detail-identity>.coin-icon,.detail-identity>.chain-icon{width:46px;height:46px}.detail-heading h1{font-size:31px}.detail-heading p{font-size:12px}.detail-heading .chain-select select{width:180px;max-width:180px;font-size:12px}.detail-summary .summary-stat strong{font-size:23px}.detail-facts,.account-grid{grid-template-columns:1fr;gap:14px}.fact-panel,.account-card{padding:23px}.account-facts>div{display:flex;gap:15px}.account-facts dd{text-align:right}.account-facts dd span{display:block;margin-left:0}.account-value{font-size:32px}.token-search{flex:1;min-width:0}.token-search input{width:100%;max-width:170px}.detail-footer{gap:9px}.detail-summary{margin-top:28px}.filter-note{font-size:10px}.detail-note{font-size:10px}.chain-name{flex-wrap:wrap;row-gap:5px}.detail-heading .eyebrow{font-size:10px}}
|
|
39
|
+
@media(max-width:380px){.token-search input{font-size:11px;padding:9px;width:100%}.fact-panel,.account-card{padding:20px}.account-facts>div{gap:10px}.account-facts dt{max-width:120px}.detail-summary .total-value{font-size:47px}}
|
|
40
|
+
|
|
41
|
+
/* Kira's adult webtoon direction: paper, ink and a restrained violet accent. */
|
|
42
|
+
:root{--bg:#fcfbf8;--sidebar:#f8f6f2;--line:#e8e4de;--accent:#6755ce;--accent-soft:#f0edf8;--text:#292733;--secondary:#716b78;color-scheme:light}
|
|
43
|
+
body{background:#fcfbf8;color:#292733}main{background:#fcfbf8}.sidebar{background:#f8f6f2}.brand{letter-spacing:-.045em;gap:11px}.kira-monogram{width:36px;height:36px;display:grid;place-items:center;border-radius:11px;background:#eae5f3;color:#6050ae;font-family:Georgia,serif;font-size:30px;font-weight:400;font-style:italic;line-height:1}.brand-caption{display:block;font-size:8px;line-height:1.3;font-weight:500;letter-spacing:.13em;margin-top:4px;color:#77707c}.wallet-button.active{background:#ede8f5;border-color:#e2dbef;color:#6050ae}.wallet-button:hover{background:#f0ece6}.wallet-nav-address{color:#797280}.sidebar-label{color:#756e7c}.sidebar-label span{background:#eae5de}.sidebar-bottom{border-color:#e3ded6;color:#79717e}.read-only{color:#817a87}.sync-status{color:#79717e}.breadcrumb [aria-current]{color:#4c4654}.topbar{background:#fcfbf8}.eyebrow{color:#776885;letter-spacing:.12em;font-size:10px}h1{color:#292733;letter-spacing:-.045em}.kira-welcome{display:flex;align-items:center;justify-content:space-between;gap:24px;min-height:190px;position:relative;margin-top:-12px}.kira-welcome>div{max-width:calc(100% - 180px)}.kira-welcome h1{font-size:37px;line-height:1.15;max-width:650px}.kira-welcome p{color:#716b78;font-size:13px;line-height:1.6}.kira-welcome-art{object-fit:contain;object-position:center bottom;align-self:flex-end;flex-shrink:0;width:165px;height:215px;mix-blend-mode:multiply}.home-summary{border:1px solid #e5dfeb;border-radius:22px;background:linear-gradient(115deg,#f0edf7,#faf7ef);padding:30px 32px;margin-top:14px}.home-summary .total-value{font-weight:500;font-size:52px;letter-spacing:-.045em}.summary-label{color:#6d6475}.valuation-note,.stat-note{color:#797180}.home-stats .summary-stat{border-color:#ded7e5}.portfolio-summary{border-color:#e4ddd5}.home-summary .home-stats{background:transparent}.wallet-card,.network-allocation,.home-holdings,.account-card,.fact-panel{background:#fff;border-color:#e7e2da;border-radius:16px}.wallet-card:hover,.home-holding:hover{background:#fbf9fd;border-color:#d6cce7}.card-wallet-icon{background:#f0edf7;color:#7361aa}.card-arrow{color:#95859f}.wallet-card-value{color:#302b38}.wallet-card-meta,.network-row-meta{color:#817786}.network-label,.network-value,.token-name,.symbol{color:#403847}.allocation-progress{background:#eee8f4}.allocation-progress::-webkit-progress-bar{background:#eee8f4}.allocation-progress::-webkit-progress-value{background:#a493cd}.allocation-progress::-moz-progress-bar{background:#a493cd}.network-row{border-color:#eee9e2}.section-note{color:#817687}.home-holding .token-description{color:#837988}.holding-value{color:#413747}.table-wrap{border-color:#e5dfd7;border-radius:14px;background:white}.token-table thead{background:#f8f6f2}.token-table th{color:#827888}.token-table td{border-color:#eee8e1}.chain-icon{background:#eee8f5;color:#766292}.chain-heading{border-color:#e9e2db}.chain-assets{background:#f0ece7;color:#827786}.token-description,.price-basis,.native-label{color:#827888}.unpriced{color:#8e8495}.market-link{color:#766091}.market-link.mint{color:#6c6383}.segment{background:#f0ece7;border-color:#e6ded7;border-radius:10px}.segment button{color:#7b7185}.segment button[aria-pressed=true]{background:white;color:#6452aa;box-shadow:0 1px 4px #51405610}.chain-select select,.token-search input{border-color:#e2dacf;background:#fff;color:#62576c}.account-quote{background:#f4f0f8}.fact-panel dt,.account-facts dt,.account-meta,.account-footnote,.detail-note,.page-footer{color:#837a8b}.fact-panel dd,.account-facts dd{color:#5c5169}.detail-heading p{color:#817487}.count-badge{color:#827589;background:#f1ede6;border-color:#e6ddd1}.kira-note{display:flex;gap:15px;align-items:flex-start;border:1px solid #e3dbe9;background:#f6f2f8;border-radius:17px;padding:21px 24px;margin:30px 0 6px;max-width:100%;color:#6b5e76}.kira-note-mark{font-family:Georgia,serif;font-size:27px;font-style:italic;color:#867294;line-height:1;flex:none}.kira-note-label{font-size:9px;letter-spacing:.14em;color:#85718e;font-weight:600;margin-bottom:7px}.kira-note p{font-size:12px;line-height:1.75;margin:0}.kira-note summary{font-size:11px;color:#715e86;margin-top:9px;cursor:pointer;list-style-position:inside}.kira-note details p{margin-top:10px;padding-top:10px;border-top:1px solid #e5dce9}.toast{background:#3b3048;color:white}.account-card-heading:hover strong{color:#6652aa}
|
|
44
|
+
@media(max-width:1180px){.kira-welcome h1{font-size:33px}.kira-welcome-art{width:140px;height:182px}.kira-welcome>div{max-width:calc(100% - 152px)}.home-summary{padding:26px}.home-summary .total-value{font-size:46px}}
|
|
45
|
+
@media(max-width:700px){.sidebar{background:#f8f6f2}.brand-caption{font-size:7px}.kira-monogram{width:31px;height:31px;font-size:25px}.kira-welcome{gap:10px;min-height:130px;margin-top:-4px;align-items:center}.kira-welcome>div{max-width:calc(100% - 87px)}.kira-welcome h1{font-size:27px;line-height:1.15}.kira-welcome .eyebrow{font-size:8px;letter-spacing:.08em;margin-bottom:10px}.kira-welcome p{font-size:11px;line-height:1.6;margin-top:10px}.kira-welcome-art{width:87px;height:126px;align-self:center}.home-summary{display:block;border-radius:18px;padding:24px 22px;margin-top:22px}.home-summary .total-value{font-size:47px}.home-stats{margin-top:24px;padding-top:20px;border-top:1px solid #ded5e5;gap:10px}.home-stats .summary-stat{border:0}.home-stats .summary-label{font-size:10px}.home-stats strong{font-size:23px}.home-stats .stat-note{font-size:9px}.wallet-card,.network-allocation,.fact-panel,.account-card{border-radius:14px}.home-holdings{border-radius:14px}.kira-note{padding:18px;margin-top:24px;gap:12px}.kira-note p{font-size:11px}.kira-note summary{font-size:10px}.home-summary .valuation-note{font-size:10px;line-height:1.7}.detail-summary .total-value{font-size:44px}.page-footer{line-height:1.7}}
|
|
46
|
+
@media(max-width:380px){.kira-welcome h1{font-size:24px}.kira-welcome-art{width:77px;height:112px}.kira-welcome>div{max-width:calc(100% - 85px)}.home-summary{padding:22px 18px}.home-summary .total-value{font-size:41px}.home-stats strong{font-size:21px}}
|
|
47
|
+
.demo-badge{font-size:9px;color:#776388;letter-spacing:.1em;padding:4px 7px;background:#f0eaf5;border-radius:6px;margin:0 12px}.demo-badge[hidden]{display:none}@media(max-width:700px){.demo-badge{font-size:8px;margin-right:0}}
|
|
48
|
+
/* Kira brand and direct research controls. */
|
|
49
|
+
.brand-logo,.nav-logo,.note-logo{object-fit:contain;flex-shrink:0}.brand-logo{width:46px;height:46px}.nav-logo{width:28px;height:28px}.note-logo{width:42px;height:42px}.research-controls{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 0 26px}.research-controls p{font-size:12px;color:#686174;margin:6px 0 0}.control-buttons,.wallet-actions{display:flex;flex-wrap:wrap;gap:9px}.wallet-actions{justify-content:flex-end;margin:0 0 22px}.primary-button,.quiet-button{font:inherit;font-size:12px;font-weight:600;min-height:40px;padding:10px 15px;border-radius:10px;cursor:pointer}.primary-button{background:#6755CE;color:white;border:1px solid #6755CE}.quiet-button{background:#FCFBF8;color:#51465F;border:1px solid #DED7E7}.primary-button:hover{background:#5543B8}.quiet-button:hover{background:#F0EDF9}button:disabled{opacity:.55;cursor:default}.research-controls[hidden],.research-jobs[hidden],.wallet-actions[hidden]{display:none}.research-jobs{background:#fff;border:1px solid #E6DFEB;border-radius:18px;padding:20px 23px;margin-bottom:26px}.research-jobs .section-heading{margin-bottom:7px}.job-row{display:flex;gap:18px;justify-content:space-between;align-items:center;padding:15px 0;border-top:1px solid #EFEAF2}.job-identity{min-width:0;font-size:12px}.job-identity strong{font-weight:600}.job-identity a{margin-left:8px;color:#6755CE;font-size:11px}.job-identity p{margin:7px 0;color:#686174;font-size:12px}.job-identity small{color:#81778D;font-size:10px}.job-state{display:inline-block;font-size:10px;padding:4px 7px;margin-right:9px;border-radius:6px;background:#F0EDF9;color:#6755CE;text-transform:capitalize}.job-state.partial,.job-state.failed,.job-state.interrupted{background:#FAEFE0;color:#775529}.job-state.succeeded{background:#EDF4EF;color:#366047}.job-identity .job-error{color:#8B513D;line-height:1.6}.job-actions{flex-shrink:0}.research-dialog{width:min(490px,calc(100% - 32px));max-height:85dvh;padding:28px;border:1px solid #E2DCE8;border-radius:22px;background:#FCFBF8;color:#292536;box-shadow:0 25px 90px #29253625;overflow:auto}.research-dialog::backdrop{background:#29253666}.dialog-heading{display:flex;align-items:center;gap:12px;margin-bottom:16px}.dialog-heading h2{font-size:19px;line-height:1.35;margin:0;flex:1}.dialog-heading button{border:0;background:none;color:#686174;font-size:23px;min-width:36px;min-height:36px;cursor:pointer}.research-dialog p{font-size:13px;line-height:1.7;color:#686174}.research-dialog label{display:block;font-size:12px;font-weight:600;margin:18px 0 10px}.research-dialog input:not([type=checkbox]),.research-dialog textarea,.research-dialog select{display:block;margin-top:9px;padding:12px;border:1px solid #DAD3E3;border-radius:9px;background:white;color:#292536;width:100%;font:inherit;line-height:1.5}.research-dialog textarea{resize:vertical}.research-dialog .form-note{font-size:11px}.research-dialog .form-error{color:#9B4437;min-height:0}.research-dialog .form-error:empty{display:none}.research-dialog form+form,.assistant-gate{border-top:1px solid #E2DCE8;margin-top:23px;padding-top:17px}.research-dialog .checkbox-label{display:flex;gap:10px;align-items:center;font-weight:400}.checkbox-label input{width:17px;height:17px;margin:0}.assistant-gate strong{font-size:12px}.assistant-gate p{font-size:11px}.history-dialog{width:min(720px,calc(100% - 32px))}.history-row{display:grid;gap:7px;padding:15px 0;border-top:1px solid #E2DCE8;font-size:12px}.history-row small{word-break:break-all;color:#81778D}.comparison-result{margin-top:24px}.comparison-result table{width:100%;font-size:12px;text-align:left;border-collapse:collapse}.comparison-result td,.comparison-result th{padding:10px 12px;border-bottom:1px solid #E2DCE8}.comparison-result small{display:block;color:#81778D;font-size:10px;margin-top:5px}.comparison-result td:not(:first-child){font-variant-numeric:tabular-nums}.empty .primary-button{margin-top:15px}.research-dialog :focus-visible,.research-controls :focus-visible,.wallet-actions :focus-visible,.job-actions :focus-visible{outline:3px solid #AFA1E7;outline-offset:3px}
|
|
50
|
+
@media(max-width:700px){.brand-logo{width:36px;height:36px}.nav-logo{width:23px;height:23px}.research-controls{align-items:flex-start;flex-direction:column;gap:13px}.research-controls .eyebrow{font-size:9px}.control-buttons{width:100%}.control-buttons button{flex:1}.wallet-actions{justify-content:flex-start}.wallet-actions button{flex:1;white-space:nowrap}.research-jobs{padding:17px}.job-row{align-items:flex-start;gap:8px}.job-identity a{display:block;margin:7px 0}.job-actions .quiet-button{font-size:10px;padding:9px}.research-dialog{padding:21px}.dialog-heading h2{font-size:17px}.note-logo{width:34px;height:34px}.research-controls p{line-height:1.6}.comparison-result .table-wrap{overflow-x:auto}.comparison-result table{min-width:540px}}
|
|
51
|
+
.job-stages{margin:11px 0;font-size:11px;color:#686174}.job-stages summary{cursor:pointer;color:#6755CE}.job-stages ol{padding-left:18px}.job-stages li{padding:5px 0}.job-stages li span{display:block;font-size:10px;line-height:1.7;word-break:break-word}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
(function(root, factory) {
|
|
3
|
+
const api = factory();
|
|
4
|
+
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
5
|
+
else root.KiraWatching = api;
|
|
6
|
+
})(typeof window === 'undefined' ? globalThis : window, function() {
|
|
7
|
+
const addressPattern = /^0x[0-9a-fA-F]{40}$/;
|
|
8
|
+
const invalidAccountsMessage = 'The wallet did not return valid EVM accounts. Try again or enter an address.';
|
|
9
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10
|
+
function safeIcon(value) {
|
|
11
|
+
// No remote images or inline SVG. The browser renders data icons in img.
|
|
12
|
+
return typeof value === 'string' && value.length <= 32768 &&
|
|
13
|
+
/^data:image\/(?:png|jpeg|webp|svg\+xml)(?:;base64)?,/i.test(value) ? value : null;
|
|
14
|
+
}
|
|
15
|
+
function displayText(value, fallback, limit = 80) {
|
|
16
|
+
return typeof value === 'string' && value.trim() ?
|
|
17
|
+
value.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '').slice(0, limit) || fallback : fallback;
|
|
18
|
+
}
|
|
19
|
+
function accounts(value) {
|
|
20
|
+
if (!Array.isArray(value) || value.length > 100 || value.some(a => typeof a !== 'string' || !addressPattern.test(a)))
|
|
21
|
+
throw new Error(invalidAccountsMessage);
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
return value.filter(a => { const key = a.toLowerCase(); if (seen.has(key)) return false; seen.add(key); return true; });
|
|
24
|
+
}
|
|
25
|
+
function errorMessage(error) {
|
|
26
|
+
let code;
|
|
27
|
+
try { code = error?.code; if (!['number','string'].includes(typeof code)) code = undefined; } catch { /* Ignore hostile provider error objects. */ }
|
|
28
|
+
const messages = {4001:'Account access was declined. Choose a wallet to try again.',
|
|
29
|
+
4100:'Account access is not authorized. Open your wallet and try again.',
|
|
30
|
+
4200:'This wallet does not support account access. Enter a public address instead.',
|
|
31
|
+
4900:'The wallet is disconnected. Open it and try again.',
|
|
32
|
+
4901:'The wallet network is unavailable. Open it or enter a public address.',
|
|
33
|
+
'-32002':'An account request is already open in your wallet. Finish it or cancel here.'};
|
|
34
|
+
return Object.hasOwn(messages, code) ? messages[code] :
|
|
35
|
+
'Account access could not be completed. Open your wallet and try again, or enter an address.';
|
|
36
|
+
}
|
|
37
|
+
function create(onChange = () => {}) {
|
|
38
|
+
const providers = new Map();
|
|
39
|
+
let generation = 0, session = null;
|
|
40
|
+
let state = {status:'idle', providerId:null, accounts:[], selected:null, chainId:null, message:''};
|
|
41
|
+
const snapshot = () => ({...state, accounts:[...state.accounts], providers:[...providers.values()].map(p => ({id:p.id, name:p.name, icon:p.icon, rdns:p.rdns}))});
|
|
42
|
+
const publish = () => onChange(snapshot());
|
|
43
|
+
function detach() {
|
|
44
|
+
const old = session; session = null;
|
|
45
|
+
if (old) for (const [event, listener] of old.listeners) {
|
|
46
|
+
try { old.provider.removeListener(event, listener); } catch { /* The generation guard also invalidates callbacks. */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function disconnect(message = 'Kira disconnected. Saved Watching wallets stay in your notebook.') {
|
|
50
|
+
generation++; detach();
|
|
51
|
+
state = {status:'disconnected', providerId:null, accounts:[], selected:null, chainId:null, message};
|
|
52
|
+
publish();
|
|
53
|
+
}
|
|
54
|
+
function announce(detail) {
|
|
55
|
+
try {
|
|
56
|
+
const {info, provider} = detail || {};
|
|
57
|
+
if (!info || !uuidPattern.test(info.uuid) || !provider || typeof provider.request !== 'function' ||
|
|
58
|
+
typeof provider.on !== 'function' || typeof provider.removeListener !== 'function' || providers.size >= 48 ||
|
|
59
|
+
providers.has(info.uuid.toLowerCase()) || [...providers.values()].some(p => p.provider === provider)) return false;
|
|
60
|
+
const entry = {id:info.uuid.toLowerCase(), provider, name:displayText(info.name,'Browser wallet'),
|
|
61
|
+
rdns:displayText(info.rdns,'Unspecified provider',120), icon:safeIcon(info.icon)};
|
|
62
|
+
providers.set(entry.id, entry); publish(); return true;
|
|
63
|
+
} catch { return false; }
|
|
64
|
+
}
|
|
65
|
+
async function connect(id) {
|
|
66
|
+
const entry = providers.get(id); if (!entry) return;
|
|
67
|
+
generation++; detach(); const attempt = generation;
|
|
68
|
+
state = {status:'requesting', providerId:id, accounts:[], selected:null, chainId:null,
|
|
69
|
+
message:'Choose which accounts to share in your wallet. You can cancel here while it is open.'};
|
|
70
|
+
const active = {provider:entry.provider, listeners:[]}; session = active;
|
|
71
|
+
function listen(event, handler) {
|
|
72
|
+
const listener = value => { if (session === active) handler(value); };
|
|
73
|
+
// Record before calling untrusted on(), so partial installation is cleaned up.
|
|
74
|
+
active.listeners.push([event,listener]); entry.provider.on(event,listener);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
listen('accountsChanged', value => {
|
|
78
|
+
generation++; state.selected = null;
|
|
79
|
+
try {
|
|
80
|
+
state.accounts = accounts(value);
|
|
81
|
+
if (!state.accounts.length) { disconnect('No accounts are shared. Open your wallet or enter a public address.'); return; }
|
|
82
|
+
state.status = 'accounts'; state.message = 'Shared accounts changed. Choose an account again before adding it.';
|
|
83
|
+
} catch { state.accounts = []; state.status = 'error'; state.message = invalidAccountsMessage; }
|
|
84
|
+
publish();
|
|
85
|
+
});
|
|
86
|
+
listen('chainChanged', value => {
|
|
87
|
+
state.chainId = typeof value === 'string' && /^0x[0-9a-f]{1,16}$/i.test(value) ? value : null;
|
|
88
|
+
publish();
|
|
89
|
+
});
|
|
90
|
+
listen('disconnect', () => disconnect('The wallet disconnected. Saved Watching wallets remain available.'));
|
|
91
|
+
publish();
|
|
92
|
+
if (generation !== attempt || session !== active) return;
|
|
93
|
+
// Account access is the only provider request. Callers invoke this after a user choice.
|
|
94
|
+
const result = await entry.provider.request({method:'eth_requestAccounts'});
|
|
95
|
+
if (generation !== attempt || session !== active) return;
|
|
96
|
+
const shared = accounts(result);
|
|
97
|
+
if (!shared.length) { disconnect('No accounts are shared. Open your wallet or enter a public address.'); return; }
|
|
98
|
+
state.accounts = shared; state.status = 'accounts'; state.message = 'Choose the public account to watch.'; publish();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (generation !== attempt || session !== active) return;
|
|
101
|
+
detach(); state.status = 'error'; state.providerId = null; state.accounts = []; state.selected = null;
|
|
102
|
+
state.message = errorMessage(error); publish();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function select(address) {
|
|
106
|
+
const match = state.accounts.find(a => a.toLowerCase() === String(address).toLowerCase());
|
|
107
|
+
if (!session || !match || !['accounts','selected'].includes(state.status)) return false;
|
|
108
|
+
state.selected = match; state.status = 'selected'; state.message = 'Check the full address and wallet name below.'; publish(); return true;
|
|
109
|
+
}
|
|
110
|
+
function clearSelection() {
|
|
111
|
+
if (state.status === 'requesting') { disconnect('Account request cancelled in Kira. The wallet may still show its request.'); return; }
|
|
112
|
+
if (state.selected) { state.selected = null; state.status = 'accounts'; state.message = 'Choose the public account to watch.'; publish(); }
|
|
113
|
+
}
|
|
114
|
+
function registration(tag) {
|
|
115
|
+
if (!session || state.status !== 'selected' || !state.accounts.includes(state.selected))
|
|
116
|
+
throw new Error('Choose a current shared account before adding this wallet.');
|
|
117
|
+
if (typeof tag !== 'string' || !tag.trim() || tag.length > 200) throw new Error('Enter a wallet name.');
|
|
118
|
+
return {address:state.selected, tag};
|
|
119
|
+
}
|
|
120
|
+
return {announce, connect, select, disconnect, clearSelection, registration, snapshot};
|
|
121
|
+
}
|
|
122
|
+
function discover(target, connection) {
|
|
123
|
+
const listener = event => connection.announce(event.detail);
|
|
124
|
+
target.addEventListener('eip6963:announceProvider', listener);
|
|
125
|
+
target.dispatchEvent(new Event('eip6963:requestProvider'));
|
|
126
|
+
return () => target.removeEventListener('eip6963:announceProvider', listener);
|
|
127
|
+
}
|
|
128
|
+
return {create, discover, safeIcon, addressPattern};
|
|
129
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Extension state is memory only. Registration still uses the protected local job API in jobs.js.
|
|
3
|
+
let watchingMode = 'manual', watchingSubmitting = false, watchingLastStatus = 'idle';
|
|
4
|
+
let watchingSubmission = null;
|
|
5
|
+
const watchingConnection = KiraWatching.create(renderWatching);
|
|
6
|
+
KiraWatching.discover(window, watchingConnection);
|
|
7
|
+
|
|
8
|
+
function watchingCanAct() {
|
|
9
|
+
return localSession?.controls === true && !!state && state.demo !== true;
|
|
10
|
+
}
|
|
11
|
+
function setWatchingMode(mode) {
|
|
12
|
+
if (watchingSubmission) return;
|
|
13
|
+
watchingMode = mode;
|
|
14
|
+
watchingConnection.clearSelection();
|
|
15
|
+
$('new-wallet-address').value = '';
|
|
16
|
+
$('wallet-form-error').textContent = '';
|
|
17
|
+
renderWatching();
|
|
18
|
+
}
|
|
19
|
+
function walletChoice(label, description, icon, action, pressed) {
|
|
20
|
+
const button = document.createElement('button');
|
|
21
|
+
button.type = 'button'; button.className = 'wallet-choice';
|
|
22
|
+
button.setAttribute('aria-pressed', String(pressed));
|
|
23
|
+
const image = document.createElement('img'); image.alt = ''; image.width = 32; image.height = 32;
|
|
24
|
+
image.src = icon || '/kira-logo.png';
|
|
25
|
+
image.addEventListener('error', () => { if (!image.src.endsWith('/kira-logo.png')) image.src = '/kira-logo.png'; });
|
|
26
|
+
const text = document.createElement('span'), title = document.createElement('strong'), subtitle = document.createElement('small');
|
|
27
|
+
title.textContent = label; subtitle.textContent = description; text.append(title, subtitle);
|
|
28
|
+
const arrow = document.createElement('span'); arrow.className = 'wallet-choice-arrow'; arrow.setAttribute('aria-hidden','true');
|
|
29
|
+
arrow.innerHTML = '<svg class="ui-icon" viewBox="0 0 24 24" fill="none" focusable="false"><path d="'+(pressed ? 'm5 12 4 4L19 6' : 'm9 6 6 6-6 6')+'"/></svg>';
|
|
30
|
+
button.append(image, text, arrow);
|
|
31
|
+
button.addEventListener('click', action); return button;
|
|
32
|
+
}
|
|
33
|
+
function replaceWatchingChoices(list, choices) {
|
|
34
|
+
const activeLabel = list.contains(document.activeElement) ? document.activeElement.getAttribute('aria-label') : null;
|
|
35
|
+
list.replaceChildren(...choices);
|
|
36
|
+
if (activeLabel && $('wallet-dialog').open) {
|
|
37
|
+
const replacement = choices.find(choice => choice.getAttribute('aria-label') === activeLabel);
|
|
38
|
+
if (replacement && !replacement.disabled) replacement.focus({preventScroll:true});
|
|
39
|
+
else $('wallet-connection-status').focus({preventScroll:true});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function renderWatching() {
|
|
43
|
+
const current = watchingConnection.snapshot(), browser = watchingMode === 'browser';
|
|
44
|
+
const permitted = watchingCanAct();
|
|
45
|
+
const submission = watchingSubmission;
|
|
46
|
+
$('wallet-entry-modes').hidden = !!submission;
|
|
47
|
+
$('wallet-submission').hidden = !submission;
|
|
48
|
+
if (submission) {
|
|
49
|
+
$('wallet-submission-title').textContent = watchingSubmitting ? 'Saving this Watching wallet' : 'We could not confirm this request';
|
|
50
|
+
$('wallet-submission-address').textContent = submission.address;
|
|
51
|
+
$('wallet-submission-name').textContent = submission.tag;
|
|
52
|
+
$('wallet-submission-note').textContent = watchingSubmitting ? 'You can close this window. The submitted address and name stay here while the request finishes.' : 'Check Activity before choosing a wallet again. A connection error can occur after a request is accepted.';
|
|
53
|
+
}
|
|
54
|
+
$('wallet-choose-again').disabled = watchingSubmitting;
|
|
55
|
+
$('wallet-manual-mode').setAttribute('aria-pressed', String(!browser));
|
|
56
|
+
$('wallet-browser-mode').setAttribute('aria-pressed', String(browser));
|
|
57
|
+
$('wallet-browser-panel').hidden = !!submission || !browser;
|
|
58
|
+
$('wallet-dialog-title').textContent = submission ? 'Watching wallet request' : browser && current.selected ? 'Review your Watching wallet' : 'Add a Watching wallet';
|
|
59
|
+
$('wallet-dialog').classList.toggle('is-review', !!submission || browser && !!current.selected);
|
|
60
|
+
$('wallet-intro').hidden = !!submission || browser && !!current.selected;
|
|
61
|
+
$('wallet-address-field').hidden = browser;
|
|
62
|
+
$('wallet-session-note').hidden = !!current.selected;
|
|
63
|
+
$('wallet-connection-status').hidden = !!current.selected;
|
|
64
|
+
$('wallet-demo-note').hidden = state?.demo !== true;
|
|
65
|
+
$('wallet-registration-fields').hidden = !!submission || browser && !current.selected;
|
|
66
|
+
$('new-wallet-address').readOnly = browser;
|
|
67
|
+
for (const id of ['wallet-manual-mode','wallet-browser-mode']) $(id).disabled = watchingSubmitting;
|
|
68
|
+
for (const id of ['new-wallet-address','new-wallet-tag']) $(id).disabled = watchingSubmitting || browser && !current.selected;
|
|
69
|
+
if (browser) $('new-wallet-address').value = current.selected || '';
|
|
70
|
+
const selectedProvider = current.providers.find(p => p.id === current.providerId);
|
|
71
|
+
$('wallet-providers-title').textContent = current.accounts.length ? 'Connected with '+selectedProvider.name : 'Choose a browser wallet';
|
|
72
|
+
$('wallet-providers').hidden = current.accounts.length > 0;
|
|
73
|
+
$('wallet-provider-note').hidden = current.accounts.length > 0;
|
|
74
|
+
$('wallet-change-provider').hidden = !current.accounts.length;
|
|
75
|
+
$('wallet-change-provider').disabled = watchingSubmitting;
|
|
76
|
+
$('wallet-change-account').hidden = !current.selected;
|
|
77
|
+
$('wallet-change-account').disabled = watchingSubmitting;
|
|
78
|
+
const sessionButton = $('wallet-session');
|
|
79
|
+
$('wallet-session-panel').hidden = !submission && !selectedProvider;
|
|
80
|
+
sessionButton.textContent = submission ? watchingSubmitting ? 'Wallet registration pending' : 'Wallet request details' : current.status === 'requesting' ? 'Wallet request pending' : 'Browser wallet session';
|
|
81
|
+
$('wallet-session-name').textContent = selectedProvider?.name || '';
|
|
82
|
+
// Do not rebuild focusable lists on every poll or name input.
|
|
83
|
+
const providerSignature = JSON.stringify([current.providers, current.providerId, current.status === 'requesting', permitted, watchingSubmitting]);
|
|
84
|
+
if ($('wallet-providers').dataset.signature !== providerSignature) {
|
|
85
|
+
$('wallet-providers').dataset.signature = providerSignature;
|
|
86
|
+
replaceWatchingChoices($('wallet-providers'), current.providers.map(provider => {
|
|
87
|
+
const choice = walletChoice(provider.name, provider.rdns, provider.icon, () => {
|
|
88
|
+
if (watchingCanAct() && !watchingSubmitting) watchingConnection.connect(provider.id);
|
|
89
|
+
}, provider.id === current.providerId);
|
|
90
|
+
choice.setAttribute('aria-label', 'Connect to '+provider.name);
|
|
91
|
+
choice.disabled = !permitted || watchingSubmitting || (provider.id === current.providerId && current.status === 'requesting');
|
|
92
|
+
return choice;
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
$('wallet-no-providers').hidden = current.providers.length > 0;
|
|
96
|
+
$('wallet-connection-status').textContent = current.message;
|
|
97
|
+
$('wallet-chain-context').hidden = !current.chainId;
|
|
98
|
+
$('wallet-chain-context').textContent = 'Wallet network: '+(current.chainId || '')+'. Research uses the saved network coverage.';
|
|
99
|
+
$('wallet-disconnect').hidden = !current.providerId;
|
|
100
|
+
$('wallet-disconnect').textContent = current.status === 'requesting' ? 'Cancel request' : 'Disconnect from Kira';
|
|
101
|
+
$('wallet-disconnect').disabled = watchingSubmitting;
|
|
102
|
+
const accountSignature = JSON.stringify([current.accounts, current.selected, watchingSubmitting]);
|
|
103
|
+
if ($('wallet-accounts').dataset.signature !== accountSignature) {
|
|
104
|
+
$('wallet-accounts').dataset.signature = accountSignature;
|
|
105
|
+
replaceWatchingChoices($('wallet-accounts'), current.accounts.map((address, index) => {
|
|
106
|
+
const choice = walletChoice('Account '+(index+1), address, null, () => {
|
|
107
|
+
if (watchingConnection.select(address)) $('new-wallet-tag').focus();
|
|
108
|
+
}, address === current.selected);
|
|
109
|
+
choice.classList.add('wallet-account'); choice.setAttribute('aria-label', 'Select account '+address);
|
|
110
|
+
choice.disabled = watchingSubmitting; return choice;
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
$('wallet-accounts-section').hidden = !current.accounts.length || !!current.selected;
|
|
114
|
+
const address = $('new-wallet-address').value, tag = $('new-wallet-tag').value;
|
|
115
|
+
const valid = KiraWatching.addressPattern.test(address) && tag.trim().length > 0 && tag.length <= 200 &&
|
|
116
|
+
(!browser || current.status === 'selected' && address === current.selected);
|
|
117
|
+
$('wallet-preview').hidden = !valid;
|
|
118
|
+
$('wallet-preview-address').textContent = address;
|
|
119
|
+
$('wallet-preview-name').textContent = tag;
|
|
120
|
+
const existing = state?.wallets.find(wallet => wallet.key === address.toLowerCase());
|
|
121
|
+
$('wallet-existing').hidden = !valid || !existing;
|
|
122
|
+
$('wallet-existing-link').href = '#/wallet/'+address.toLowerCase();
|
|
123
|
+
$('wallet-register').disabled = !!submission || !permitted || !valid || !!existing;
|
|
124
|
+
$('wallet-register').textContent = watchingSubmitting ? 'Saving wallet…' : 'Add and research';
|
|
125
|
+
if (browser && $('wallet-dialog').open && watchingLastStatus === 'requesting' && current.status === 'accounts')
|
|
126
|
+
$('wallet-accounts').querySelector('button')?.focus();
|
|
127
|
+
watchingLastStatus = current.status;
|
|
128
|
+
}
|
|
129
|
+
function watchingRegistrationInput() {
|
|
130
|
+
if (!watchingCanAct()) throw new Error('A personal local control session is required to add a wallet.');
|
|
131
|
+
const address = $('new-wallet-address').value, tag = $('new-wallet-tag').value;
|
|
132
|
+
const input = watchingMode === 'browser' ? watchingConnection.registration(tag) : {address, tag};
|
|
133
|
+
if (input.address !== address || !KiraWatching.addressPattern.test(address) || !tag.trim() || tag.length > 200)
|
|
134
|
+
throw new Error('Check the public address and wallet name before adding this wallet.');
|
|
135
|
+
if (state?.wallets.some(wallet => wallet.key === address.toLowerCase()))
|
|
136
|
+
throw new Error('This Watching wallet is already saved. Open it below to keep its current names.');
|
|
137
|
+
return input;
|
|
138
|
+
}
|
|
139
|
+
$('wallet-manual-mode').addEventListener('click', () => setWatchingMode('manual'));
|
|
140
|
+
$('wallet-browser-mode').addEventListener('click', () => setWatchingMode('browser'));
|
|
141
|
+
$('wallet-disconnect').addEventListener('click', () => {
|
|
142
|
+
if (watchingConnection.snapshot().status === 'requesting') watchingConnection.clearSelection();
|
|
143
|
+
else watchingConnection.disconnect();
|
|
144
|
+
});
|
|
145
|
+
$('wallet-change-provider').addEventListener('click', () => watchingConnection.disconnect('Choose a browser wallet.'));
|
|
146
|
+
$('wallet-change-account').addEventListener('click', () => {
|
|
147
|
+
watchingConnection.clearSelection(); $('wallet-accounts').querySelector('button')?.focus();
|
|
148
|
+
});
|
|
149
|
+
$('wallet-session').addEventListener('click', () => {
|
|
150
|
+
if (!localSession?.controls) return;
|
|
151
|
+
if (!watchingSubmission) setWatchingMode('browser');
|
|
152
|
+
renderWatching(); $('wallet-dialog').showModal();
|
|
153
|
+
});
|
|
154
|
+
$('wallet-submission-activity').addEventListener('click', () => {
|
|
155
|
+
$('wallet-dialog').close(); navigate('#/activity');
|
|
156
|
+
});
|
|
157
|
+
$('wallet-choose-again').addEventListener('click', () => {
|
|
158
|
+
if (watchingSubmitting) return;
|
|
159
|
+
watchingSubmission = null; $('wallet-form-error').textContent = '';
|
|
160
|
+
watchingConnection.clearSelection(); renderWatching();
|
|
161
|
+
if (watchingMode === 'browser') $('wallet-accounts').querySelector('button')?.focus();
|
|
162
|
+
else $('new-wallet-address').focus();
|
|
163
|
+
});
|
|
164
|
+
for (const id of ['new-wallet-address','new-wallet-tag']) $(id).addEventListener('input', renderWatching);
|
|
165
|
+
$('wallet-dialog').addEventListener('close', () => watchingConnection.clearSelection());
|
|
166
|
+
$('wallet-existing-link').addEventListener('click', () => $('wallet-dialog').close());
|
|
167
|
+
renderWatching();
|