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,252 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const $ = id => document.getElementById(id);
|
|
3
|
+
const escapeHTML = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
4
|
+
let state = null, selectedWallet = localStorage.getItem('wallet-selection'), selectedView = localStorage.getItem('wallet-view') === 'wallet' ? 'wallet' : 'home', selectedChain = 'all', minimum = Number(localStorage.getItem('wallet-minimum') ?? 10), etag = null, currentSignature = null, busy = false;
|
|
5
|
+
let selectedToken = null, selectedNetwork = null, tokenSearch = '';
|
|
6
|
+
if (![0,5,10].includes(minimum)) minimum=10;
|
|
7
|
+
const money = value => value == null ? '—' : value > 0 && value < .01 ? '< $0.01' : new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',maximumFractionDigits:2}).format(value);
|
|
8
|
+
const unitPrice = value => value == null ? '—' : value === 0 ? '$0.00' : value < .000001 ? '$'+value.toExponential(3) : '$'+new Intl.NumberFormat('en-US',{maximumFractionDigits:value < .01 ? 8 : value < 1 ? 6 : 2}).format(value);
|
|
9
|
+
const quantity = raw => raw==null ? '—' : Number(raw)>0&&Number(raw)<.000001 ? Number(raw).toExponential(3) : new Intl.NumberFormat('en-US',{maximumFractionDigits:Number(raw)<1 ? 6 : 3}).format(Number(raw));
|
|
10
|
+
const compactQuantity = raw => raw!=null&&Number(raw)>=100000 ? new Intl.NumberFormat('en-US',{notation:'compact',maximumFractionDigits:3}).format(Number(raw)) : quantity(raw);
|
|
11
|
+
const shortAddress = address => address.slice(0,6)+'…'+address.slice(-4);
|
|
12
|
+
const stamp = raw => raw ? new Date(raw).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit',hour12:false}) : 'Not analysed yet';
|
|
13
|
+
const safeURL = raw => {try{const url=new URL(raw,location.origin);return (url.origin===location.origin||url.protocol==='https:') ? url.href : '#';}catch{return '#';}};
|
|
14
|
+
const imageHosts = new Set(['mint.club','tokens.1inch.io','coin-images.coingecko.com','fc.hunt.town','mint-club-v2.s3.us-west-2.amazonaws.com']);
|
|
15
|
+
const safeImage = raw => {try{const url=new URL(raw);return url.protocol==='https:'&&imageHosts.has(url.hostname)&&!url.username&&!url.password&&(!url.port||url.port==='443') ? url.href : null;}catch{return null;}};
|
|
16
|
+
const walletGlyph = '<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 6.5h13a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 1.4-1.9L15 1.8v4.7"/><path d="M19 10h-4a2 2 0 0 0 0 4h4"/><path d="M15.5 12h.01"/></svg>';
|
|
17
|
+
|
|
18
|
+
const arrowGlyph = '<svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg>';
|
|
19
|
+
function toast(message){$('toast').textContent=message;$('toast').classList.add('show');setTimeout(()=>$('toast').classList.remove('show'),2200);}
|
|
20
|
+
function currentWallet(){return state?.wallets.find(w=>w.key===selectedWallet);}
|
|
21
|
+
const tokenHref = id => '#/token/'+id.split(':').map(encodeURIComponent).join('/');
|
|
22
|
+
const networkHref = id => '#/network/'+id;
|
|
23
|
+
function navigate(hash){if(typeof setWorkspacePane==='function')setWorkspacePane('portfolio');if(location.hash===hash)readRoute();else location.hash=hash;}
|
|
24
|
+
function readRoute(){
|
|
25
|
+
let parts=[];try{parts=location.hash.replace(/^#\/?/,'').split('/').map(decodeURIComponent);}catch{}
|
|
26
|
+
if(parts[0]==='wallet'&&/^0x[0-9a-f]{40}$/i.test(parts[1]||'')){selectedWallet=parts[1].toLowerCase();selectedView='wallet';selectedChain='all';}
|
|
27
|
+
else if(parts[0]==='network'&&/^\d+$/.test(parts[1]||'')){selectedNetwork=Number(parts[1]);selectedView='network';tokenSearch='';}
|
|
28
|
+
else if(parts[0]==='token'&&/^\d+$/.test(parts[1]||'')&&/^(native|0x[0-9a-f]{40})$/i.test(parts[2]||'')){selectedToken=Number(parts[1])+':'+parts[2].toLowerCase();selectedView='token';}
|
|
29
|
+
else if(['tokens','activity'].includes(parts[0])){selectedView=parts[0];}
|
|
30
|
+
else if(location.hash){selectedView='home';}
|
|
31
|
+
localStorage.setItem('wallet-view',selectedView);if(selectedWallet)localStorage.setItem('wallet-selection',selectedWallet);
|
|
32
|
+
if(state)renderView();window.scrollTo({top:0,behavior:'instant'});
|
|
33
|
+
}
|
|
34
|
+
function selectWallet(key){
|
|
35
|
+
if(!state?.wallets.some(w=>w.key===key))return;
|
|
36
|
+
navigate('#/wallet/'+key);
|
|
37
|
+
}
|
|
38
|
+
function goHome(){
|
|
39
|
+
navigate('#/home');
|
|
40
|
+
}
|
|
41
|
+
function renderBreadcrumb(items){
|
|
42
|
+
$('breadcrumbs').innerHTML='<ol>'+items.map(item=>`<li>${item.href?`<a href="${escapeHTML(item.href)}" title="${escapeHTML(item.label)}">${escapeHTML(item.label)}</a>`:`<span aria-current="page" title="${escapeHTML(item.label)}">${escapeHTML(item.label)}</span>`}</li>`).join('')+'</ol>';
|
|
43
|
+
}
|
|
44
|
+
function renderWallets(){
|
|
45
|
+
$('wallet-count').textContent=state.wallets.length;
|
|
46
|
+
$('wallet-list').innerHTML=state.wallets.map(w=>{
|
|
47
|
+
const active=selectedView==='wallet'&&w.key===selectedWallet;
|
|
48
|
+
return `<button class="wallet-button ${active?'active':''}" data-wallet="${escapeHTML(w.key)}" aria-label="Select ${escapeHTML(w.name)}" ${active?'aria-current="page"':''}><span class="wallet-glyph">${walletGlyph}</span><span class="wallet-nav-name">${escapeHTML(w.name)}<span class="wallet-nav-address">${escapeHTML(shortAddress(w.address))}</span></span><span class="wallet-arrow" aria-hidden="true">›</span></button>`;
|
|
49
|
+
}).join('');
|
|
50
|
+
}
|
|
51
|
+
function renderView(){
|
|
52
|
+
renderWallets();
|
|
53
|
+
$('home-view').hidden=selectedView!=='home';$('wallet-view').hidden=selectedView!=='wallet';
|
|
54
|
+
$('detail-view').hidden=!['token','network'].includes(selectedView);
|
|
55
|
+
if(selectedView==='home'){renderHome();document.title='Home · Kira Wallet';}
|
|
56
|
+
else if(selectedView==='wallet'){renderWallet();document.title=(currentWallet()?.name||'Wallet')+' · Kira Wallet';}
|
|
57
|
+
else if(['token','network'].includes(selectedView))renderDetail();
|
|
58
|
+
renderKiraNote();
|
|
59
|
+
if(typeof renderWorkspace==='function')renderWorkspace();
|
|
60
|
+
}
|
|
61
|
+
function renderKiraNote(){
|
|
62
|
+
let text='I keep unknown prices and incomplete coverage visible. These are recorded estimates.';
|
|
63
|
+
if(selectedView==='home'){
|
|
64
|
+
const s=state.dashboard;
|
|
65
|
+
text=s.wallet_count?`I have gathered ${s.wallet_count} registered ${s.wallet_count===1?'wallet':'wallets'}. ${s.priced_count} mainnet ${s.priced_count===1?'position has':'positions have'} a price reference; ${s.unpriced_count} remain unpriced. Open a wallet or a network to inspect the recorded evidence.`:'Your notebook is ready. Add a public wallet address to begin recorded research.';
|
|
66
|
+
}else if(selectedView==='wallet'){
|
|
67
|
+
const w=currentWallet();
|
|
68
|
+
if(w){const gaps=w.chains.filter(c=>c.environment==='mainnet'&&(!c.complete||!c.rpc_available)).length;
|
|
69
|
+
text=w.analysed_at?`This wallet has ${w.assets.filter(a=>a.environment==='mainnet').length} recorded mainnet ${w.assets.filter(a=>a.environment==='mainnet').length===1?'position':'positions'}. ${gaps?gaps+(gaps===1?' network still has a coverage gap.':' networks still have coverage gaps.'):'Recorded mainnet coverage is available.'} Unpriced positions stay outside the estimated total.`:'This wallet is registered. Its analysis has not been published yet.';}
|
|
70
|
+
}else if(selectedView==='token'){
|
|
71
|
+
const t=state.details.tokens.find(t=>t.id===selectedToken);
|
|
72
|
+
if(t)text=`This is ${t.symbol} on ${state.details.networks.find(c=>c.id===t.chain_id)?.name||'chain '+t.chain_id}, identified by its chain and contract. I found recorded holdings in ${t.wallet_count} ${t.wallet_count===1?'wallet':'wallets'}. Any curve backing is shared by all holders and excluded from their wallet totals.`;
|
|
73
|
+
}else if(selectedView==='network'){
|
|
74
|
+
const n=state.details.networks.find(n=>n.id===selectedNetwork);
|
|
75
|
+
if(n)text=`${n.name} has ${n.position_count} recorded positions across ${n.wallet_count} holding wallets. The account cards show each wallet's coverage, including missing records. An absent holding is not proof of a zero balance.`;
|
|
76
|
+
}
|
|
77
|
+
$('kira-note-text').textContent=text;
|
|
78
|
+
}
|
|
79
|
+
function progress(value,total,label){
|
|
80
|
+
return value!=null&&total>0 ? `<progress class="allocation-progress" value="${value}" max="${total}" aria-label="${escapeHTML(label)}">${(value/total*100).toFixed(1)}%</progress>` : '';
|
|
81
|
+
}
|
|
82
|
+
function share(value,total){return value!=null&&total>0 ? (value/total*100).toFixed(1)+'%' : '—';}
|
|
83
|
+
function chainIcon(chain){
|
|
84
|
+
const name=typeof chain==='string'?chain:chain.name;
|
|
85
|
+
const url=safeImage(typeof chain==='string'?state?.details?.networks.find(c=>c.name===name)?.image_url:chain.image_url);
|
|
86
|
+
const cls=name.toLowerCase().split(' ')[0],glyph=name==='Base'?'—':name==='Ethereum'?'◆':name.charAt(0);
|
|
87
|
+
return `<span class="chain-icon ${escapeHTML(cls)}" aria-hidden="true"><span class="chain-fallback">${escapeHTML(glyph)}</span>${url?`<img src="${escapeHTML(url)}" alt="" width="24" height="24" loading="lazy" decoding="async" referrerpolicy="no-referrer" data-chain-image>`:''}</span>`;
|
|
88
|
+
}
|
|
89
|
+
function renderHome(){
|
|
90
|
+
const s=state.dashboard;
|
|
91
|
+
renderBreadcrumb([{label:'Home'}]);
|
|
92
|
+
$('sync-label').textContent=s.analysed_wallet_count+' of '+s.wallet_count+' '+(s.wallet_count===1?'wallet':'wallets')+' analysed';
|
|
93
|
+
$('home-total').textContent=money(s.known_value_usd);
|
|
94
|
+
$('home-wallet-count').textContent=s.wallet_count;
|
|
95
|
+
$('home-network-count').textContent=s.chain_count;
|
|
96
|
+
$('home-position-count').textContent=s.position_count;
|
|
97
|
+
$('home-wallet-note').textContent=s.wallet_count>s.analysed_wallet_count?(s.wallet_count-s.analysed_wallet_count)+' awaiting analysis':'All registered';
|
|
98
|
+
$('home-position-note').textContent=s.unique_asset_count+' unique '+(s.unique_asset_count===1?'asset':'assets');
|
|
99
|
+
$('home-valuation-note').textContent=s.priced_count ? 'Market and curve spot estimates · '+s.unpriced_count+' unpriced positions excluded' : 'Value appears as priced holdings become available.';
|
|
100
|
+
$('home-empty').hidden=s.wallet_count>0;$('home-details').hidden=s.wallet_count===0;
|
|
101
|
+
const incomplete=state.wallets.some(w=>w.chains.some(c=>c.environment==='mainnet'&&(!c.complete||!c.rpc_available)));
|
|
102
|
+
$('home-coverage-note').textContent='Mainnet holdings only · '+(incomplete?'Research coverage is incomplete.':'Testnets excluded from totals.');
|
|
103
|
+
$('home-price-time').textContent=s.prices_from ? 'Prices '+stamp(s.prices_from)+(s.prices_from!==s.prices_to?' – '+stamp(s.prices_to):'') : '';
|
|
104
|
+
$('wallet-cards').innerHTML=state.wallets.map(w=>{
|
|
105
|
+
const assets=w.assets.filter(a=>a.environment==='mainnet'),unpriced=assets.filter(a=>a.value_usd==null).length;
|
|
106
|
+
const value=w.analysed_at&&assets.some(a=>a.value_usd!=null)?w.known_value_usd:null;
|
|
107
|
+
return `<button class="wallet-card" data-wallet="${escapeHTML(w.key)}" aria-label="Open ${escapeHTML(w.name)} holdings"><span class="wallet-card-heading"><span class="card-wallet-icon">${walletGlyph}</span><span class="wallet-card-identity"><strong>${escapeHTML(w.name)}</strong><span>${escapeHTML(shortAddress(w.address))}</span></span><span class="card-arrow" aria-hidden="true">${arrowGlyph}</span></span><span class="wallet-card-value">${money(value)}</span><span class="wallet-card-meta"><span>${w.analysed_at?assets.length+(assets.length===1?' position · ':' positions · ')+unpriced+' unpriced':'Awaiting analysis'}</span><span>${value!=null?share(value,s.known_value_usd)+' of priced value':''}</span></span>${progress(value,s.known_value_usd,w.name+' share of priced value')}</button>`;
|
|
108
|
+
}).join('');
|
|
109
|
+
$('network-allocation').innerHTML=s.networks.map(c=>`<a class="network-row" href="${networkHref(c.id)}" aria-label="Open ${escapeHTML(c.name)} network overview"><div class="network-row-heading"><span class="network-label">${chainIcon(c.name)}${escapeHTML(c.name)}<span class="detail-arrow" aria-hidden="true">${arrowGlyph}</span></span><span class="network-value">${money(c.value_usd)}</span></div><div class="network-row-meta"><span>${c.position_count} ${c.position_count===1?'position':'positions'}${c.value_usd==null?' · Unpriced':''}</span><span>${share(c.value_usd,s.known_value_usd)}</span></div>${progress(c.value_usd,s.known_value_usd,c.name+' share of priced value')}</a>`).join('')||'<p class="panel-empty">Networks appear after analysis.</p>';
|
|
110
|
+
$('home-holdings').innerHTML=s.largest_holdings.map(a=>`<a class="home-holding" href="${tokenHref(a.id)}" aria-label="Open ${escapeHTML(a.symbol)} on ${escapeHTML(a.chain_name)}"><div class="token-name">${coinIcon(a)}<div class="coin-title"><div class="symbol">${escapeHTML(a.symbol)}</div><div class="token-description">${escapeHTML(a.chain_name)} · ${a.wallet_count} ${a.wallet_count===1?'wallet':'wallets'}</div></div></div><span class="holding-value" title="${a.unpriced_count?'Unpriced positions excluded':'Sum across holding wallets'}">${money(a.value_usd)}</span></a>`).join('')||'<p class="panel-empty">Priced holdings will appear here.</p>';
|
|
111
|
+
bindImages();
|
|
112
|
+
}
|
|
113
|
+
function renderWallet(){
|
|
114
|
+
const w=currentWallet();if(!w)return;
|
|
115
|
+
$('wallet-name').textContent=w.name;renderBreadcrumb([{label:'Home',href:'#/home'},{label:w.name}]);$('address').textContent=w.address;$('address').title=w.address;
|
|
116
|
+
$('wallet-tags').innerHTML=w.tags.slice(1).map(t=>`<span class="tag">${escapeHTML(t)}</span>`).join(' ');
|
|
117
|
+
$('report-link').hidden=!w.report_url;if(w.report_url)$('report-link').href=safeURL(w.report_url);
|
|
118
|
+
$('total-value').textContent=w.analysed_at?money(w.known_value_usd):'—';
|
|
119
|
+
$('valuation-note').textContent=w.analysed_at?'Market and curve spot estimates · excludes unpriced assets':'Analysis will appear here when it is ready.';
|
|
120
|
+
$('asset-count').textContent=w.assets.length;$('unpriced-count').textContent=w.unpriced_count+' unpriced';
|
|
121
|
+
const mainnets=w.chains.filter(c=>c.environment==='mainnet');
|
|
122
|
+
$('chain-count').textContent=mainnets.filter(c=>c.assets>0).length;
|
|
123
|
+
const coverage=mainnets.filter(c=>c.complete).length;
|
|
124
|
+
$('coverage-note').textContent=coverage+' of '+mainnets.length+' researched';
|
|
125
|
+
$('coverage-note').title=w.chains.filter(c=>!c.complete).map(c=>c.name+(c.rpc_available?' · incomplete token discovery':' · unavailable')).join('\n');
|
|
126
|
+
$('sync-label').textContent=w.analysed_at?'Analysis '+stamp(w.analysed_at):'Awaiting analysis';
|
|
127
|
+
$('price-time').textContent=w.prices_at?'Prices '+stamp(w.prices_at):'';
|
|
128
|
+
const chainOptions=w.chains.filter(c=>c.assets>0||!c.complete);
|
|
129
|
+
if(selectedChain!=='all'&&!chainOptions.some(c=>String(c.id)===selectedChain))selectedChain='all';
|
|
130
|
+
$('chain-filter').innerHTML='<option value="all">All chains</option>'+chainOptions.map(c=>`<option value="${c.id}">${escapeHTML(c.name)}${c.environment==='testnet'?' · Testnet':''}${!c.complete?' · Incomplete':''}</option>`).join('');
|
|
131
|
+
$('chain-filter').value=selectedChain;renderHoldings();
|
|
132
|
+
}
|
|
133
|
+
function icon(asset){
|
|
134
|
+
if(asset.symbol==='ETH')return ['ether','◆'];
|
|
135
|
+
if(asset.symbol==='CHICKEN')return ['chicken','🐔'];
|
|
136
|
+
if(asset.symbol==='hcbBTC')return ['bitcoin','₿'];
|
|
137
|
+
if(asset.symbol.toLowerCase()==='member')return ['member','m'];
|
|
138
|
+
if(/[^\x00-\x7F]/.test(asset.symbol)&&asset.symbol.length<10)return ['',asset.symbol];
|
|
139
|
+
return ['',asset.symbol.slice(0,2).toUpperCase()];
|
|
140
|
+
}
|
|
141
|
+
function coinIcon(asset){
|
|
142
|
+
const [cls,glyph]=icon(asset),url=safeImage(asset.image_url);
|
|
143
|
+
return `<span class="coin-icon ${cls}" aria-hidden="true"><span class="coin-fallback">${escapeHTML(glyph)}</span>${url?`<img src="${escapeHTML(url)}" alt="" width="34" height="34" loading="lazy" decoding="async" referrerpolicy="no-referrer" data-token-image>`:''}</span>`;
|
|
144
|
+
}
|
|
145
|
+
function bindImages(){
|
|
146
|
+
document.querySelectorAll('img[data-token-image],img[data-chain-image]').forEach(img=>{
|
|
147
|
+
if(img.dataset.bound)return;img.dataset.bound='true';
|
|
148
|
+
const show=()=>{const ok=img.naturalWidth>0;img.hidden=!ok;img.parentElement.classList.toggle('has-image',ok);};
|
|
149
|
+
img.addEventListener('load',show,{once:true});img.addEventListener('error',show,{once:true});
|
|
150
|
+
if(img.complete)show();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function assetRow(a){
|
|
154
|
+
const p=a.price;
|
|
155
|
+
const source=p?.usd!=null ? p.basis : p?.quality==='unreliable'?'Thin quote side':p?.quality==='unfunded'?'No funded reserve':'No price';
|
|
156
|
+
const links=a.links.filter(l=>l.label!=='Explorer');
|
|
157
|
+
if(!links.length){const e=a.links.find(l=>l.label==='Explorer');if(e)links.push(e);}
|
|
158
|
+
const reserve=a.curve_reserve?`Curve reserve: ${a.curve_reserve.amount} ${a.curve_reserve.symbol}`:'';
|
|
159
|
+
return `<tr data-asset-id="${escapeHTML(a.id)}" data-value="${a.value_usd ?? ''}"><td><a class="token-name token-link" href="${tokenHref(a.id)}" aria-label="Open ${escapeHTML(a.symbol)} token overview">${coinIcon(a)}<div class="coin-title"><div class="symbol" title="${escapeHTML(a.symbol)}">${escapeHTML(a.symbol)}</div><div class="token-description" title="${escapeHTML((a.address||'Native asset')+' '+reserve)}">${escapeHTML(a.is_native?'Native asset':a.name)}</div></div></a></td><td><span class="mobile-label">Balance</span><span class="amount" title="${escapeHTML(a.balance)}">${quantity(a.balance)}</span></td><td><span class="mobile-label">Price</span><span class="token-price ${p?.usd==null?'unpriced':''}" title="${escapeHTML(p?.observed_at||'')}">${unitPrice(p?.usd)}</span><div class="price-basis">${escapeHTML(source)}</div></td><td><span class="token-value ${a.value_usd==null?'unpriced':''}">${money(a.value_usd)}</span></td><td><div class="market-links">${links.map(l=>`<a class="market-link ${l.label==='Mint Club'?'mint':''}" href="${escapeHTML(safeURL(l.url))}" title="${escapeHTML(l.label+' · '+l.url)}" target="_blank" rel="noreferrer">${escapeHTML(l.label)}<span aria-hidden="true">↗</span></a>`).join('')||'<span class="native-label">No market found</span>'}</div></td></tr>`;
|
|
160
|
+
}
|
|
161
|
+
function renderHoldings(){
|
|
162
|
+
const w=currentWallet();if(!w)return;
|
|
163
|
+
document.querySelectorAll('[data-min]').forEach(b=>b.setAttribute('aria-pressed',String(Number(b.dataset.min)===minimum)));
|
|
164
|
+
const visible=w.assets.filter(a=>(selectedChain==='all'?a.environment!=='testnet':String(a.chain_id)===selectedChain)&&(minimum===0||(a.value_usd!=null&&a.value_usd>=minimum)));
|
|
165
|
+
$('visible-count').textContent=visible.length;
|
|
166
|
+
const chains=w.chains.filter(c=>visible.some(a=>a.chain_id===c.id)).sort((a,b)=>(b.value_usd??0)-(a.value_usd??0));
|
|
167
|
+
$('asset-groups').innerHTML=chains.map(c=>{
|
|
168
|
+
const assets=visible.filter(a=>a.chain_id===c.id).sort((a,b)=>(b.value_usd??-1)-(a.value_usd??-1));
|
|
169
|
+
const values=assets.filter(a=>a.value_usd!=null),sum=values.length?values.reduce((acc,a)=>acc+a.value_usd,0):null;
|
|
170
|
+
return `<section class="chain-section" aria-label="${escapeHTML(c.name)} holdings"><div class="chain-heading"><h3 class="chain-name"><a class="network-label network-link" href="${networkHref(c.id)}" aria-label="Open ${escapeHTML(c.name)} network overview">${chainIcon(c)}${escapeHTML(c.name)}<span class="detail-arrow" aria-hidden="true">${arrowGlyph}</span></a><span class="chain-assets">${assets.length} assets${c.environment==='testnet'?' · Testnet':''}</span></h3><span class="chain-total">${c.environment==='testnet'?'Testnet':money(sum)}</span></div><div class="table-wrap"><table class="token-table"><colgroup><col><col><col><col><col></colgroup><thead><tr><th scope="col">Token</th><th scope="col">Balance</th><th scope="col">Price</th><th scope="col">Value</th><th scope="col">Markets</th></tr></thead><tbody>${assets.map(assetRow).join('')}</tbody></table></div></section>`;
|
|
171
|
+
}).join('');
|
|
172
|
+
$('empty').hidden=visible.length>0;
|
|
173
|
+
if(!visible.length&&selectedChain!=='all'){
|
|
174
|
+
const c=w.chains.find(c=>String(c.id)===selectedChain);
|
|
175
|
+
$('empty').querySelector('h3').textContent=c&&!c.complete?'Research is incomplete on '+c.name:'No assets in this view';
|
|
176
|
+
$('empty').querySelector('p').textContent=c&&!c.complete?'This chain has not been fully checked. Missing data does not mean a zero balance.':'Choose another chain or lower the minimum value.';
|
|
177
|
+
}else{$('empty').querySelector('h3').textContent='No assets in this view';$('empty').querySelector('p').textContent=w.analysed_at?'Choose another chain or lower the minimum value.':'Analysis will appear here when it is ready.';}
|
|
178
|
+
bindImages();
|
|
179
|
+
}
|
|
180
|
+
function timeRange(range){return range?.from?stamp(range.from)+(range.to!==range.from?' – '+stamp(range.to):''):'Not recorded';}
|
|
181
|
+
function marketLinks(links){return links.map(l=>`<a class="market-link ${l.label==='Mint Club'?'mint':''}" href="${escapeHTML(safeURL(l.url))}" target="_blank" rel="noreferrer">${escapeHTML(l.label)} <span aria-hidden="true">↗</span></a>`).join('')||'<span class="native-label">No market recorded</span>';}
|
|
182
|
+
function accountCard(row,token){
|
|
183
|
+
const a=row.asset,value=token?a?.value_usd:row.value_usd;
|
|
184
|
+
const meta=token?(a?quantity(a.balance)+' '+escapeHTML(token.symbol):escapeHTML(row.status)):row.position_count+' positions · '+row.unpriced_count+' unpriced';
|
|
185
|
+
const shareValue=token?share(value,token.value_usd):null;
|
|
186
|
+
const quote=a?.burn_quote?.net_refund!=null?`<div class="account-quote"><span>Recorded burn refund</span><strong>${quantity(a.burn_quote.net_refund)} ${escapeHTML(a.curve_reserve?.symbol||'')}</strong><small>At holdings analysis · net of creator royalty · excludes gas</small></div>`:'';
|
|
187
|
+
const price=token?.environment==='testnet'?null:a?.price?.usd;
|
|
188
|
+
return `<article class="account-card"><a class="account-card-heading" href="#/wallet/${escapeHTML(row.key)}"><span class="card-wallet-icon">${walletGlyph}</span><span class="wallet-card-identity"><strong>${escapeHTML(row.name)}</strong><span>${escapeHTML(shortAddress(row.address))}</span></span><span class="card-arrow" aria-hidden="true">${arrowGlyph}</span></a><div class="account-value">${money(value)}</div><div class="account-meta">${meta}</div>${token&&a?`<dl class="account-facts"><div><dt>Recorded price</dt><dd>${unitPrice(price)} <span>${escapeHTML(token.environment==='testnet'?'Testnet':a.price?.basis||'No price')}</span></dd></div><div><dt>Share of priced holdings</dt><dd>${shareValue}</dd></div></dl>${quote}`:''}<div class="account-footnote"><span>${escapeHTML(row.coverage)}</span><span>Analysis ${stamp(row.analysed_at)}</span>${token&&a?`<span>Price ${a.price?.observed_at?stamp(a.price.observed_at):'not recorded'}</span>`:''}</div></article>`;
|
|
189
|
+
}
|
|
190
|
+
function aggregateRow(a){
|
|
191
|
+
return `<tr data-asset-id="${escapeHTML(a.id)}"><td><a class="token-name token-link" href="${tokenHref(a.id)}" aria-label="Open ${escapeHTML(a.symbol)} token overview">${coinIcon(a)}<div class="coin-title"><div class="symbol">${escapeHTML(a.symbol)}</div><div class="token-description">${escapeHTML(a.is_native?'Native asset':a.name)}</div></div></a></td><td><span class="mobile-label">Total balance</span><span class="amount" title="${escapeHTML(a.balance)}">${quantity(a.balance)}</span></td><td><span class="mobile-label">Wallets</span><span class="amount">${a.wallet_count}</span></td><td><span class="token-value">${money(a.value_usd)}</span>${a.unpriced_count?`<div class="price-basis">${a.unpriced_count} unpriced</div>`:''}</td><td><div class="market-links">${marketLinks(a.links.filter(l=>l.label!=='Explorer'))}</div></td></tr>`;
|
|
192
|
+
}
|
|
193
|
+
function renderNetworkTokens(network){
|
|
194
|
+
const search=tokenSearch.trim().toLowerCase();
|
|
195
|
+
const assets=state.details.tokens.filter(a=>a.chain_id===network.id&&(minimum===0||(a.value_usd!=null&&a.value_usd>=minimum))&&(!search||[a.symbol,a.name,a.address||'native'].some(s=>s.toLowerCase().includes(search))));
|
|
196
|
+
$('network-visible-count').textContent=assets.length;
|
|
197
|
+
$('network-token-body').innerHTML=assets.map(aggregateRow).join('');
|
|
198
|
+
$('network-token-table').hidden=assets.length===0;$('network-token-empty').hidden=assets.length>0;
|
|
199
|
+
bindImages();
|
|
200
|
+
}
|
|
201
|
+
function renderDetail(){
|
|
202
|
+
const token=selectedView==='token'?state.details.tokens.find(a=>a.id===selectedToken):null;
|
|
203
|
+
const network=token?.network||state.details.networks.find(c=>c.id===selectedNetwork);
|
|
204
|
+
const record=selectedView==='token'?token:network;
|
|
205
|
+
if(!record){
|
|
206
|
+
renderBreadcrumb([{label:'Home',href:'#/home'},{label:'Research detail'}]);$('sync-label').textContent='Latest recorded research';
|
|
207
|
+
$('detail-view').innerHTML='<div class="empty"><h3>No recorded detail for this selection</h3><p>It may be absent from the latest analysis. Return Home to explore recorded holdings.</p></div>';
|
|
208
|
+
document.title='Research detail · Wallets';return;
|
|
209
|
+
}
|
|
210
|
+
const label=token?token.symbol:network.name,isTestnet=record.environment==='testnet';
|
|
211
|
+
renderBreadcrumb([{label:'Home',href:'#/home'},...(token?[{label:network.name,href:networkHref(network.id)}]:[]),{label}]);
|
|
212
|
+
$('sync-label').textContent='Across '+state.wallets.length+' registered wallets';document.title=label+' · Kira Wallet';
|
|
213
|
+
const note=isTestnet?'Testnet holdings · excluded from USD totals.':'Recorded spot estimates · '+record.unpriced_count+' unpriced positions excluded';
|
|
214
|
+
const header=`<section class="detail-heading"><div class="detail-identity">${token?coinIcon(token):chainIcon(network)}<div><div class="eyebrow">${token?'TOKEN':'NETWORK'} OVERVIEW${isTestnet?' · TESTNET':''}</div><h1>${escapeHTML(label)}</h1><p>${token?escapeHTML(token.name)+' · '+escapeHTML(network.name):'Holdings across all your registered wallets.'}</p></div></div>${!token?`<label class="chain-select"><span class="sr-only">Open another network</span><select id="network-switch">${state.details.networks.map(c=>`<option value="${c.id}" ${c.id===network.id?'selected':''}>${escapeHTML(c.name)}${c.environment==='testnet'?' · Testnet':''}</option>`).join('')}</select></label>`:''}</section>`;
|
|
215
|
+
const stats=`<section class="portfolio-summary detail-summary" aria-label="${token?'Token':'Network'} summary"><div class="valuation"><div class="summary-label">Estimated value across wallets</div><div class="total-value">${money(record.value_usd)}</div><div class="valuation-note">${note}</div></div><div class="summary-stat"><span class="summary-label">${token?'Total balance':'Unique assets'}</span><strong title="${escapeHTML(token?.balance||'')}">${token?compactQuantity(token.balance):network.unique_asset_count}</strong><span class="stat-note">${token?escapeHTML(token.symbol):network.position_count+' positions'}</span></div><div class="summary-stat"><span class="summary-label">Holding wallets</span><strong>${record.wallet_count}</strong><span class="stat-note">Of ${state.wallets.length} registered</span></div></section>`;
|
|
216
|
+
let facts='';
|
|
217
|
+
if(token){
|
|
218
|
+
const held=token.wallets.filter(r=>r.asset),prices=isTestnet?[]:held.map(r=>r.asset.price?.usd).filter(p=>p!=null);
|
|
219
|
+
const low=prices.length?Math.min(...prices):null,high=prices.length?Math.max(...prices):null;
|
|
220
|
+
const latest=[...held].sort((a,b)=>(b.asset.price?.observed_at||b.analysed_at||'').localeCompare(a.asset.price?.observed_at||a.analysed_at||''))[0]?.asset;
|
|
221
|
+
const reserve=latest?.curve_reserve;
|
|
222
|
+
facts=`<div class="detail-facts"><section class="fact-panel"><h2>Token identity</h2><div class="contract-address">${escapeHTML(token.address||'Native asset · chain '+token.chain_id)}</div><dl><div><dt>Recorded price${low!==high?' range':''}</dt><dd>${unitPrice(low)}${low!==high?' – '+unitPrice(high):''}</dd></div><div><dt>Price observations</dt><dd>${timeRange(token.price_times)}</dd></div></dl></section><section class="fact-panel"><h2>Markets & backing</h2><div class="market-links detail-markets">${marketLinks(token.links)}</div>${reserve?`<dl><div><dt>Recorded curve reserve</dt><dd>${quantity(reserve.amount)} ${escapeHTML(reserve.symbol)}</dd></div></dl><p>Shared curve backing. Excluded from wallet value.</p>`:'<p>Links reflect markets found in the recorded research.</p>'}</section></div>`;
|
|
223
|
+
}
|
|
224
|
+
const accounts=`<section class="detail-accounts"><div class="section-heading"><h2>By wallet</h2><span class="section-note">${token?'Recorded holdings':'Network allocation'}</span></div><div class="account-grid">${record.wallets.map(r=>accountCard(r,token)).join('')}</div><p class="detail-note">${token?'An absent record is not a verified zero balance. Prices and burn quotes can differ between analysis times.':'Totals include known mainnet values. Incomplete discovery can omit additional holdings.'}</p></section>`;
|
|
225
|
+
const holdings=token?'':`<section class="holdings"><div class="holdings-toolbar"><h2>Tokens <span id="network-visible-count" class="count-badge"></span></h2><div class="filters"><label class="token-search"><span class="sr-only">Search network tokens</span><input id="network-token-search" type="search" placeholder="Search tokens" value="${escapeHTML(tokenSearch)}"></label><div class="segment" role="group" aria-label="Minimum combined token value">${[0,5,10].map(n=>`<button data-min="${n}" aria-pressed="${n===minimum}">${n?'≥ $'+n:'All'}</button>`).join('')}</div></div></div><p class="detail-note filter-note">Filter by combined token value across wallets. Summary totals include all recorded holdings.</p><div id="network-token-table" class="table-wrap"><table class="token-table"><colgroup><col><col><col><col><col></colgroup><thead><tr><th scope="col">Token</th><th scope="col">Total balance</th><th scope="col">Wallets</th><th scope="col">Value</th><th scope="col">Markets</th></tr></thead><tbody id="network-token-body"></tbody></table></div><div id="network-token-empty" class="empty" hidden><h3>No tokens in this view</h3><p>Lower the minimum value or clear your search. Missing coverage does not mean zero holdings.</p></div></section>`;
|
|
226
|
+
$('detail-view').innerHTML=header+stats+facts+accounts+holdings+`<footer class="page-footer detail-footer"><span>Analysis ${timeRange(record.analysis_times)}</span><span>Prices ${timeRange(record.price_times)}</span></footer>`;
|
|
227
|
+
if(!token){$('network-switch').addEventListener('change',e=>navigate(networkHref(e.target.value)));$('network-token-search').addEventListener('input',e=>{tokenSearch=e.target.value;renderNetworkTokens(network);});renderNetworkTokens(network);}
|
|
228
|
+
bindImages();
|
|
229
|
+
}
|
|
230
|
+
async function poll(){
|
|
231
|
+
if(busy)return;busy=true;
|
|
232
|
+
try{
|
|
233
|
+
const response=await fetch('/api/state',{headers:etag?{'If-None-Match':etag}:{},cache:'no-cache',signal:AbortSignal.timeout(9000)});
|
|
234
|
+
if(response.status===304){$('connection').textContent='Connected';return;}
|
|
235
|
+
if(!response.ok)throw new Error('Research unavailable');
|
|
236
|
+
const data=await response.json();const signature=response.headers.get('ETag');
|
|
237
|
+
const wasLoaded=state!==null;
|
|
238
|
+
state=data;etag=signature;$('load-error').hidden=true;$('content').hidden=false;$('connection').textContent='Connected';$('demo-badge').hidden=data.demo!==true;
|
|
239
|
+
if(selectedView==='wallet'&&!state.wallets.some(w=>w.key===selectedWallet)){selectedWallet=null;selectedChain='all';selectedView='home';}
|
|
240
|
+
if(signature!==currentSignature){renderView();currentSignature=signature;if(wasLoaded)toast('Research updated');}
|
|
241
|
+
}catch{ $('connection').textContent='Reconnecting';if(!state){$('load-error').hidden=false;$('content').hidden=true;} }
|
|
242
|
+
finally{busy=false;}
|
|
243
|
+
}
|
|
244
|
+
document.querySelector('.skip-link').addEventListener('click',e=>{e.preventDefault();$('main').focus();$('main').scrollIntoView({block:'start'});});
|
|
245
|
+
$('brand-home').addEventListener('click',e=>{e.preventDefault();goHome();});
|
|
246
|
+
for(const id of ['wallet-list','wallet-cards'])$(id).addEventListener('click',e=>{const b=e.target.closest('[data-wallet]');if(b)selectWallet(b.dataset.wallet);});
|
|
247
|
+
$('chain-filter').addEventListener('change',e=>{selectedChain=e.target.value;renderHoldings();});
|
|
248
|
+
document.addEventListener('click',e=>{const b=e.target.closest('[data-min]');if(b){minimum=Number(b.dataset.min);localStorage.setItem('wallet-minimum',String(minimum));if(selectedView==='network')renderDetail();else renderHoldings();}});
|
|
249
|
+
$('copy-address').addEventListener('click',async()=>{try{await navigator.clipboard.writeText(currentWallet().address);toast('Address copied');}catch{toast('Select the address to copy it');}});
|
|
250
|
+
document.addEventListener('visibilitychange',()=>{if(!document.hidden)poll();});
|
|
251
|
+
window.addEventListener('hashchange',readRoute);
|
|
252
|
+
readRoute();poll();setInterval(poll,5000);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="9" fill="#6755CE"/><rect x="9" y="5" width="16" height="23" rx="3" fill="#FCFBF8"/><path d="M9 8v17" stroke="#C9BDEA" stroke-width="4"/><path d="M15 11v10m0-5 6-5m-6 5 6 5" fill="none" stroke="#292536" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5
|
+
<meta name="color-scheme" content="light"><meta name="description" content="A personal research workspace for your wallets, with Kira.">
|
|
6
|
+
<title>Kira Wallet</title><link rel="icon" href="/favicon.svg" type="image/svg+xml"><link rel="icon" href="/kira-logo.png" type="image/png" sizes="1254x1254"><link rel="apple-touch-icon" href="/kira-logo.png">
|
|
7
|
+
<link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/workspace.css"><script src="/workspace-model.js" defer></script><script src="/app.js" defer></script><script src="/jobs.js" defer></script><script src="/watching-model.js" defer></script><script src="/watching.js" defer></script><script src="/workspace.js" defer></script><link rel="stylesheet" href="/agent.css"><script src="/agent.js" defer></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<a class="skip-link" href="#main">Skip to content</a>
|
|
11
|
+
<div class="app">
|
|
12
|
+
<aside class="sidebar" aria-label="Wallet navigation">
|
|
13
|
+
<a id="brand-home" class="brand" href="#/home" aria-label="Kira Wallet home"><img class="brand-logo" src="/kira-logo.png" width="42" height="42" alt=""><span>Kira<span class="brand-caption">WALLET RESEARCH</span></span></a>
|
|
14
|
+
<div class="workspace-label">PERSONAL WORKSPACE</div>
|
|
15
|
+
<div class="sidebar-intro"><span class="sidebar-line"></span><p>A little clarity.<br>A better perspective.</p></div>
|
|
16
|
+
<div class="sidebar-label">WATCHING WALLETS <span id="wallet-count"></span></div>
|
|
17
|
+
<nav id="wallet-list" aria-label="Registered wallets"></nav>
|
|
18
|
+
<div id="research-controls" class="user-controls" hidden><button id="open-wallet" class="primary-button" aria-label="Add watching wallet"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg><span>Add <span class="watching-label">watching </span>wallet</span></button><button id="open-settings" class="settings-button" aria-label="Workspace settings" title="Workspace settings"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M9.5 3.5h5l.7 2.5 2.3 1.3 2.5-.6 2.5 4.3-1.8 1.9v2.6l1.8 1.9-2.5 4.3-2.5-.6-2.3 1.3-.7 2.5h-5l-.7-2.5-2.3-1.3-2.5.6L1.5 17.4l1.8-1.9v-2.6L1.5 11l2.5-4.3 2.5.6L8.8 6z" transform="translate(2 0) scale(.83)"/><circle cx="12" cy="12" r="3"/></svg><span class="settings-label">Workspace settings</span></button><p>Public addresses. Your keys stay yours.</p></div>
|
|
19
|
+
<div id="wallet-session-panel" class="wallet-session-panel" hidden><button id="wallet-session" class="text-button">Browser wallet session</button><small id="wallet-session-name"></small></div>
|
|
20
|
+
<div class="sidebar-bottom"><span class="status-dot" aria-hidden="true"></span><span id="connection">Connected</span><span id="service-mode" class="read-only">Watch only</span></div>
|
|
21
|
+
</aside>
|
|
22
|
+
<main id="main" tabindex="-1">
|
|
23
|
+
<header class="topbar"><nav id="breadcrumbs" class="breadcrumb" aria-label="Breadcrumb"><ol><li><span aria-current="page">Home</span></li></ol></nav><span id="demo-badge" class="demo-badge" hidden>SYNTHETIC DEMO</span><div class="sync-status"><span id="sync-label">Loading research…</span><span class="sync-dot" aria-hidden="true"></span></div></header>
|
|
24
|
+
<nav class="workspace-tabs" aria-label="Workspace sections"><a href="#/home" data-view="home">Overview</a><a href="#/tokens" data-view="tokens">All tokens <span id="all-token-count"></span></a><a href="#/activity" data-view="activity">Activity <span id="activity-count"></span></a><div class="mobile-pane-control" role="group" aria-label="Main panel"><button data-pane="portfolio" aria-pressed="true">Portfolio</button><button data-pane="kira" aria-pressed="false">Kira</button></div></nav>
|
|
25
|
+
<div class="content" id="content" hidden>
|
|
26
|
+
<div class="workspace">
|
|
27
|
+
<div class="evidence-pane">
|
|
28
|
+
<section id="active-work" class="active-work" hidden aria-label="Current research"></section>
|
|
29
|
+
<div id="home-view">
|
|
30
|
+
<section class="home-heading"><div><div class="eyebrow">THE BIG PICTURE</div><h1>Your wallets.<br>A sharper perspective.</h1><p>One workspace for the assets you follow.</p></div><span class="edition-mark" aria-hidden="true">K / 01</span></section>
|
|
31
|
+
<section class="home-summary" aria-label="All wallets summary">
|
|
32
|
+
<div class="valuation"><div class="summary-label">Estimated total value</div><div id="home-total" class="total-value">—</div><div id="home-valuation-note" class="valuation-note"></div></div>
|
|
33
|
+
<div class="home-stats">
|
|
34
|
+
<div class="summary-stat"><span class="summary-label">Wallets</span><strong id="home-wallet-count">—</strong><span id="home-wallet-note" class="stat-note"></span></div>
|
|
35
|
+
<div class="summary-stat"><span class="summary-label">Networks</span><strong id="home-network-count">—</strong><span class="stat-note">With holdings</span></div>
|
|
36
|
+
<div class="summary-stat"><span class="summary-label">Positions</span><strong id="home-position-count">—</strong><span id="home-position-note" class="stat-note"></span></div>
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
<div id="home-empty" class="empty" hidden><img src="/kira-logo.png" width="64" height="64" alt=""><h3>Your notebook starts with a wallet.</h3><p>Use a public EVM address and a name you recognise. You keep your keys.</p><button id="empty-add-wallet" class="primary-button" hidden>Add your first wallet</button></div>
|
|
40
|
+
<div id="home-details">
|
|
41
|
+
<div class="dashboard-grid">
|
|
42
|
+
<section class="dashboard-panel" aria-labelledby="home-wallets-title"><div class="section-heading"><h2 id="home-wallets-title">Your wallets</h2><span class="section-note">Explore holdings <span aria-hidden="true">↗</span></span></div><div id="wallet-cards" class="wallet-cards"></div></section>
|
|
43
|
+
<section class="dashboard-panel" aria-labelledby="home-networks-title"><div class="section-heading"><h2 id="home-networks-title">By network</h2><span class="section-note">Priced value</span></div><div id="network-allocation" class="network-allocation"></div></section>
|
|
44
|
+
</div>
|
|
45
|
+
<section class="largest-holdings" aria-labelledby="home-holdings-title"><div class="section-heading"><h2 id="home-holdings-title">Largest holdings</h2><span class="section-note">Across all wallets</span></div><div id="home-holdings" class="home-holdings"></div></section>
|
|
46
|
+
</div>
|
|
47
|
+
<footer class="page-footer home-footer"><span id="home-coverage-note"></span><span id="home-price-time"></span></footer>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="wallet-view" hidden>
|
|
50
|
+
<div id="wallet-actions" class="wallet-actions" hidden><button id="refresh-wallet" class="quiet-button">Refresh holdings</button><button id="refresh-prices" class="quiet-button">Refresh prices</button><button id="edit-tags" class="quiet-button">Edit names</button><button id="view-history" class="quiet-button">Saved analyses</button></div>
|
|
51
|
+
<section class="wallet-heading"><div><div class="eyebrow">WALLET OVERVIEW</div><h1 id="wallet-name">Your wallets</h1><div class="wallet-address"><span id="address"></span><button id="copy-address" class="icon-button" aria-label="Copy wallet address" title="Copy wallet address"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="8" y="8" width="11" height="12" rx="2"/><path d="M15 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h3"/></svg></button><span id="wallet-tags"></span></div></div><a id="report-link" class="text-link" target="_blank" rel="noreferrer">Research notes <span aria-hidden="true">↗</span></a></section>
|
|
52
|
+
<section class="portfolio-summary" aria-label="Portfolio summary"><div class="valuation"><div class="summary-label">Estimated portfolio value</div><div id="total-value" class="total-value">—</div><div id="valuation-note" class="valuation-note">Loading the latest analysis</div></div><div class="summary-stat"><span class="summary-label">Assets</span><strong id="asset-count">—</strong><span id="unpriced-count" class="stat-note"></span></div><div class="summary-stat"><span class="summary-label">Networks</span><strong id="chain-count">—</strong><span id="coverage-note" class="stat-note"></span></div></section>
|
|
53
|
+
<section class="holdings" aria-labelledby="holdings-title"><div class="holdings-toolbar"><div><h2 id="holdings-title">Holdings <span id="visible-count"></span></h2></div><div class="filters"><label class="chain-select"><span class="sr-only">Filter by chain</span><select id="chain-filter"><option value="all">All chains</option></select><svg viewBox="0 0 16 16" aria-hidden="true"><path d="m4 6 4 4 4-4"/></svg></label><div class="segment" role="group" aria-label="Minimum token value"><button data-min="0" aria-pressed="true">All</button><button data-min="5" aria-pressed="false">≥ $5</button><button data-min="10" aria-pressed="false">≥ $10</button></div></div></div>
|
|
54
|
+
<div id="asset-groups"></div>
|
|
55
|
+
<div id="empty" class="empty" hidden><div class="empty-mark" aria-hidden="true">∅</div><h3>No assets in this view</h3><p>Choose another chain or lower the minimum value.</p></div>
|
|
56
|
+
</section>
|
|
57
|
+
<footer class="page-footer"><span>Prices are estimates at the latest research update.</span><span id="price-time"></span></footer>
|
|
58
|
+
</div>
|
|
59
|
+
<div id="detail-view" hidden></div>
|
|
60
|
+
<div id="tokens-view" hidden>
|
|
61
|
+
<section class="view-heading"><div class="eyebrow">ACROSS YOUR WALLETS</div><h1>All tokens.</h1><p>One token per network and contract. Every recorded position, including unpriced assets.</p></section>
|
|
62
|
+
<div class="token-catalog-controls"><label class="catalog-search"><span class="sr-only">Search all tokens</span><input id="all-token-search" type="search" placeholder="Search name, symbol or contract" autocomplete="off"></label><label><span class="sr-only">Token network</span><select id="all-token-network"><option value="all">All networks</option></select></label><label><span class="sr-only">Token pricing</span><select id="all-token-pricing"><option value="all">Any pricing</option><option value="priced">Priced</option><option value="unpriced">Unpriced or partial</option></select></label><label><span class="sr-only">Token order</span><select id="all-token-sort"><option value="value">Value: high to low</option><option value="name">Name: A to Z</option><option value="wallets">Most wallets</option></select></label></div>
|
|
63
|
+
<div class="catalog-meta"><span id="all-token-result" role="status"></span><label class="checkbox-label"><input id="all-token-testnets" type="checkbox">Include testnets</label></div>
|
|
64
|
+
<div class="table-wrap"><table class="token-table catalog-table"><thead><tr><th scope="col">Token / network</th><th scope="col">Total balance</th><th scope="col">Wallets</th><th scope="col">Known value</th></tr></thead><tbody id="all-token-body"></tbody></table></div>
|
|
65
|
+
<div id="all-token-empty" class="empty" hidden><h3>No recorded tokens match</h3><p>Try another network or clear the search.</p></div>
|
|
66
|
+
<div class="pagination"><button id="tokens-prev" class="quiet-button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m10 6-6 6 6 6M4 12h16"/></svg><span>Previous</span></button><span id="tokens-page"></span><button id="tokens-next" class="quiet-button"><span>Next</span><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m14 6 6 6-6 6M4 12h16"/></svg></button></div>
|
|
67
|
+
<p class="detail-note">50 rows per page. Totals exclude testnets and unpriced amounts. An unknown value is not zero.</p>
|
|
68
|
+
</div>
|
|
69
|
+
<div id="activity-view" hidden>
|
|
70
|
+
<section class="view-heading"><div class="eyebrow">KIRA'S RESEARCH NOTEBOOK</div><h1>Work, recorded.</h1><p>Local research jobs and their saved evidence. Model conversations stay in the Kira panel, separate from research jobs.</p></section>
|
|
71
|
+
<section id="research-jobs" class="research-jobs" aria-label="Research activity"><div class="section-heading"><h2>Activity</h2><span id="jobs-summary" class="section-note"></span></div><div class="activity-toolbar"><label><span class="sr-only">Filter activity status</span><select id="job-filter"><option value="all">All statuses</option><option value="active">In progress</option><option value="succeeded">Completed</option><option value="attention">Needs attention</option><option value="cancelled">Stopped</option></select></label><span id="jobs-freshness" role="status"></span></div><div id="jobs-list"></div><div class="pagination"><button id="jobs-prev" class="quiet-button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m10 6-6 6 6 6M4 12h16"/></svg><span>Previous</span></button><span id="jobs-page"></span><button id="jobs-next" class="quiet-button"><span>Next</span><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m14 6 6 6-6 6M4 12h16"/></svg></button></div></section>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<aside id="kira-panel" class="kira-panel" aria-label="Kira conversation workspace">
|
|
75
|
+
<div class="kira-panel-heading"><div><span class="eyebrow">YOUR RESEARCH PARTNER</span><h2>Kira<span class="kira-spark" aria-hidden="true">✳</span></h2></div><button id="chat-new" class="chat-new" aria-label="Start a new conversation" title="New conversation"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button></div>
|
|
76
|
+
<div class="kira-scene"><div class="scene-caption"><span id="kira-mood">A little perspective.</span><span class="scene-rule"></span><small id="kira-scene-note">Here to make sense of it.</small></div><img id="kira-scene-art" src="/kira.png" width="190" height="250" alt="Kira, your research partner"></div>
|
|
77
|
+
<div class="kira-conversation" id="conversation-scroll"><div class="briefing-meta"><span class="briefing-dot"></span><span id="kira-context">Portfolio overview</span><span>RECORDED FACTS</span></div>
|
|
78
|
+
<section id="kira-note" class="kira-note" aria-label="Kira research note"><div><div class="kira-note-label">FROM MY NOTEBOOK</div><p id="kira-note-text"></p><details><summary>How to read these estimates</summary><p>Totals include priced direct mainnet holdings. Unpriced positions and testnets are excluded. Pool liquidity and shared curve reserves are separate from wallet value. Recorded prices and balances can have different observation times.</p></details></div></section>
|
|
79
|
+
<div class="kira-quick-actions" role="group" aria-label="Explore recorded research"><button data-brief="coverage"><span>Where are the gaps?</span><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></button><button data-brief="unpriced"><span>Show unpriced tokens</span><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></button><button data-brief="activity"><span>What is in progress?</span><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M7 17 17 7M7 7h10v10"/></svg></button></div>
|
|
80
|
+
<div id="chat-messages" class="chat-messages" role="log" aria-label="Conversation with Kira" aria-live="polite"></div>
|
|
81
|
+
</div>
|
|
82
|
+
<form id="chat-form" class="composer">
|
|
83
|
+
<div class="composer-top"><label for="kira-draft">Message Kira</label><span id="chat-status" role="status">Connect your account</span></div>
|
|
84
|
+
<div class="composer-box"><textarea id="kira-draft" rows="3" placeholder="What would you like to understand?" maxlength="8000" aria-describedby="chat-context chat-hint"></textarea>
|
|
85
|
+
<div class="composer-footer"><button id="chat-setup" class="model-button" type="button" aria-label="Model connection and permissions"><span class="model-dot" aria-hidden="true"></span><span id="chat-model-label">Connect a model</span><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m8 10 4 4 4-4"/></svg></button><button id="chat-send" class="send-button" type="submit" disabled aria-label="Send message"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m6 10 6-6 6 6M12 4v16"/></svg></button><button id="chat-stop" class="send-button stop-button" type="button" hidden aria-label="Stop response"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg></button></div>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="composer-context"><span id="chat-context">No wallet context shared</span><span id="chat-hint">Enter to send · Shift+Enter for a new line</span></div><p id="chat-error" class="form-error" role="alert"></p><span id="draft-status" class="sr-only" role="status"></span>
|
|
88
|
+
</form>
|
|
89
|
+
</aside>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<div id="load-error" class="load-error" hidden><h2>Research is temporarily unavailable</h2><p>The viewer will reconnect automatically.</p></div>
|
|
93
|
+
</main>
|
|
94
|
+
</div>
|
|
95
|
+
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
|
96
|
+
<dialog id="wallet-dialog" class="research-dialog watching-dialog" aria-labelledby="wallet-dialog-title">
|
|
97
|
+
<form id="wallet-form">
|
|
98
|
+
<div class="dialog-heading"><img src="/kira-logo.png" width="42" height="42" alt=""><h2 id="wallet-dialog-title">Add a Watching wallet</h2><button type="button" data-close-dialog aria-label="Close dialog"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div>
|
|
99
|
+
<p id="wallet-intro">Follow a public address. Your keys stay in your wallet.</p>
|
|
100
|
+
<div id="wallet-entry-modes" class="wallet-entry-modes" role="group" aria-label="Choose address source"><button type="button" id="wallet-manual-mode" aria-pressed="true">Enter address</button><button type="button" id="wallet-browser-mode" aria-pressed="false">Browser wallet</button></div>
|
|
101
|
+
<p id="wallet-demo-note" class="wallet-demo-note" hidden>Sample mode: explore this form with a public address. Connecting and research are available in a personal workspace.</p>
|
|
102
|
+
<section id="wallet-browser-panel" class="wallet-browser-panel" aria-label="Browser wallet connection" hidden>
|
|
103
|
+
<h3 id="wallet-providers-title">Choose a browser wallet</h3>
|
|
104
|
+
<p id="wallet-provider-note" class="form-note">Only the wallet you choose will be asked to share accounts. No signature is requested. Provider names and icons are supplied by the wallet.</p>
|
|
105
|
+
<div id="wallet-providers" class="wallet-choices"></div>
|
|
106
|
+
<p id="wallet-no-providers" class="wallet-unavailable">No compatible wallet is available in this browser. Open Kira in a browser with a wallet extension, or enter a public address.</p>
|
|
107
|
+
<p id="wallet-connection-status" class="wallet-connection-status" role="status" tabindex="-1"></p>
|
|
108
|
+
<p id="wallet-chain-context" class="form-note" hidden></p>
|
|
109
|
+
<button type="button" id="wallet-disconnect" class="text-button" hidden>Disconnect from Kira</button>
|
|
110
|
+
<button type="button" id="wallet-change-provider" class="text-button" aria-label="Choose another wallet" hidden>Change wallet</button>
|
|
111
|
+
<button type="button" id="wallet-change-account" class="text-button" aria-label="Choose another account" hidden>Change account</button>
|
|
112
|
+
<section id="wallet-accounts-section" class="wallet-accounts-section" aria-label="Shared accounts" hidden><h3>Choose an account</h3><div id="wallet-accounts" class="wallet-choices"></div></section>
|
|
113
|
+
<p id="wallet-session-note" class="form-note">Disconnecting from Kira keeps saved wallets. Manage site permissions in your extension.</p>
|
|
114
|
+
</section>
|
|
115
|
+
<div id="wallet-registration-fields">
|
|
116
|
+
<label id="wallet-address-field">Wallet address<input id="new-wallet-address" required pattern="0x[0-9a-fA-F]{40}" placeholder="0x…" autocomplete="off" spellcheck="false"></label>
|
|
117
|
+
<label>Wallet name<input id="new-wallet-tag" required maxlength="200" placeholder="My research wallet" autocomplete="off"></label>
|
|
118
|
+
<section id="wallet-preview" class="wallet-preview" aria-label="Wallet registration preview" hidden><h3>Ready to watch</h3><dl><dt>Public address</dt><dd id="wallet-preview-address"></dd><dt>Exact wallet name</dt><dd id="wallet-preview-name"></dd></dl></section>
|
|
119
|
+
<p id="wallet-existing" class="wallet-existing" hidden>This Watching wallet is already saved. <a id="wallet-existing-link">Open the saved wallet</a> to keep its current names.</p>
|
|
120
|
+
<p class="form-note">Public RPC checks known assets and Mint Club. Broader token discovery may be incomplete. Research can consume provider allowance.</p>
|
|
121
|
+
<button id="wallet-register" class="primary-button" type="submit" disabled>Add and research</button>
|
|
122
|
+
</div>
|
|
123
|
+
<section id="wallet-submission" class="wallet-preview" aria-label="Submitted wallet request" hidden><h3 id="wallet-submission-title" role="status"></h3><dl><dt>Submitted public address</dt><dd id="wallet-submission-address"></dd><dt>Exact submitted name</dt><dd id="wallet-submission-name"></dd></dl><p id="wallet-submission-note" class="form-note"></p><div class="wallet-submission-actions"><button type="button" id="wallet-submission-activity" class="quiet-button">View Activity</button><button type="button" id="wallet-choose-again" class="quiet-button" disabled>Choose wallet again</button></div></section>
|
|
124
|
+
<p id="wallet-form-error" class="form-error" role="alert"></p>
|
|
125
|
+
</form>
|
|
126
|
+
</dialog>
|
|
127
|
+
<dialog id="tags-dialog" class="research-dialog" aria-labelledby="tags-title"><form id="tags-form"><div class="dialog-heading"><h2 id="tags-title">Wallet names</h2><button type="button" data-close-dialog aria-label="Close dialog"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div><label>One exact name per line<textarea id="tag-values" required rows="4"></textarea></label><p class="form-note">The first name is shown in navigation. Saved analyses retain their original names.</p><p id="tags-error" class="form-error" role="alert"></p><button class="primary-button" type="submit">Save names</button></form></dialog>
|
|
128
|
+
<dialog id="settings-dialog" class="research-dialog" aria-labelledby="settings-title"><div class="dialog-heading"><h2 id="settings-title">Research connections</h2><button type="button" data-close-dialog aria-label="Close dialog"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div><form id="rpc-form"><label>RPC reads<select id="rpc-mode"><option value="public">Public RPC · no API key</option><option value="custom">Custom RPC references</option></select></label><p class="form-note">RPC reads and token discovery are separate. A connection is not proof of complete holdings.</p><label>Custom references, one per line<textarea id="rpc-references" rows="3" placeholder="8453 KIRA_BASE_RPC"></textarea></label><p class="form-note">Enter chain ID and environment variable name. Keep endpoint URLs and keys outside the browser. Restart the app after changing its environment.</p><label class="checkbox-label"><input id="rpc-fallback" type="checkbox">Allow public fallback for custom RPC</label><button class="quiet-button" type="submit">Save RPC settings</button></form><form id="discovery-form"><label>Broader token discovery<select id="discovery-provider"><option value="none">Limited · no indexer</option><option value="alchemy">Alchemy · your credential</option></select></label><label>Credential environment variable<input id="discovery-key" value="ALCHEMY_API_KEY" pattern="[A-Z_][A-Z0-9_]*"></label><button class="quiet-button" type="submit">Save discovery settings</button></form><p id="settings-error" class="form-error" role="alert"></p><div class="assistant-gate"><strong>Your model connection</strong><p>Use your installed CLI account and choose what Kira sends.</p><button id="settings-agent" class="quiet-button">Manage model and permissions</button></div></dialog>
|
|
129
|
+
<dialog id="history-dialog" class="research-dialog history-dialog" aria-labelledby="history-title"><div class="dialog-heading"><h2 id="history-title">Saved analyses</h2><button type="button" data-close-dialog aria-label="Close dialog"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div><div id="history-list"></div><form id="compare-form" hidden><label>Earlier analysis<select id="compare-before"></select></label><label>Later analysis<select id="compare-after"></select></label><button class="quiet-button" type="submit">Compare evidence</button></form><div id="comparison-result" class="comparison-result"></div><p id="history-error" class="form-error" role="alert"></p></dialog>
|
|
130
|
+
<dialog id="agent-dialog" class="research-dialog agent-dialog" aria-labelledby="agent-title">
|
|
131
|
+
<div class="dialog-heading"><div><span class="eyebrow">MAKE KIRA YOURS</span><h2 id="agent-title">Connect your account.</h2></div><button id="agent-close" type="button" data-close-dialog aria-label="Close setup"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m6 6 12 12M6 18 18 6"/></svg></button></div>
|
|
132
|
+
<ol class="setup-progress" aria-label="Setup progress"><li data-step="1" aria-current="step"><span>1</span> Account</li><li data-step="2"><span>2</span> Permissions</li><li data-step="3"><span>3</span> Ready</li></ol>
|
|
133
|
+
<section id="agent-step-1"><p class="setup-intro">Bring the model account you already use. Kira connects your installed CLI, with its existing login and subscription limits.</p><div id="agent-providers" class="provider-grid" role="group" aria-label="Choose your CLI account"></div><div id="agent-cli-help" class="cli-help"></div><button id="agent-recheck" class="text-button" type="button">Recheck installed CLIs</button><label class="model-field">Model ID <span>optional</span><input id="agent-model" placeholder="CLI default" maxlength="100" autocomplete="off"></label><p class="form-note">Leave blank to use the CLI default, or enter a model your account supports. A response check verifies access.</p></section>
|
|
134
|
+
<section id="agent-step-2" hidden><h3>What can Kira send?</h3><p class="setup-intro">Your portfolio stays on this computer. When you send a message, its text and the context below go through your CLI to its model service.</p><fieldset class="scope-choices"><legend class="sr-only">Automatic context</legend><label><input type="radio" name="agent-scope" value="none" checked><span><strong>No wallet context</strong><small>Start with general questions. Only your conversation text is sent.</small></span></label><label><input type="radio" name="agent-scope" value="wallet"><span><strong>One wallet</strong><small>Recorded holdings, addresses, names, timestamps and coverage for one wallet.</small></span></label><label><input type="radio" name="agent-scope" value="portfolio"><span><strong>Whole portfolio</strong><small>The same recorded facts for every registered wallet.</small></span></label></fieldset><label id="agent-wallet-field" hidden>Approved wallet<select id="agent-wallet"></select></label><label class="permission-choice"><input id="agent-retain" type="checkbox"><span><strong>Save conversations on this computer</strong><small>Off means Kira keeps this conversation in memory. Provider and native CLI data policies remain separate.</small></span></label><label class="permission-choice native-trust"><input id="agent-trust" type="checkbox"><span><strong>I trust my installed CLI and its account policy</strong><small>Kira limits the context it supplies and disables model execution tools. Native CLI and administrator policy remain trusted components. No passwords or API keys are entered here.</small></span></label><p class="form-note">Changing the account, model or context starts a fresh conversation. Chat cannot sign, trade or automatically run research.</p></section>
|
|
135
|
+
<section id="agent-step-3" hidden><div class="ready-art"><img src="/kira-logo.png" width="64" height="64" alt=""></div><h3>One last hello.</h3><p class="setup-intro">Check a real response before you start. This check sends a generic greeting with no wallet data.</p><dl id="agent-review" class="agent-review"></dl><button id="agent-test" class="quiet-button" type="button">Check connection</button><p id="agent-test-status" role="status"></p><p class="form-note">Messages use your chosen CLI account. Its subscription limits and data policies apply.</p></section>
|
|
136
|
+
<p id="agent-error" class="form-error" role="alert"></p><div class="setup-footer"><button id="agent-back" class="quiet-button" type="button" hidden>Back</button><button id="agent-next" class="primary-button" type="button">Continue</button><button id="agent-save" class="primary-button" type="button" hidden disabled>Start talking with Kira</button></div>
|
|
137
|
+
</dialog>
|
|
138
|
+
</body>
|
|
139
|
+
</html>
|