kira-wallet 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -43,11 +43,12 @@ For a viewer without wallet or research actions, use `kira start --read-only`.
43
43
  Prefer an archive? [GitHub releases](https://github.com/realproject7/kira-wallet/releases/tag/v0.1.2)
44
44
  include the same package and its checksum.
45
45
 
46
- 1. **Add a watching wallet.** Paste a public address and give it a name. Add more
47
- wallets whenever you need them.
48
- 2. **Set up discovery.** In **Workspace settings**, connect an indexer for broad
46
+ 1. **Set up discovery.** In **Workspace settings**, connect an indexer for broad
49
47
  ERC20 discovery. Public RPC alone has limited discovery; incomplete coverage
50
- stays visible.
48
+ stays visible. An AI or browser wallet connection does not enable an indexer.
49
+ 2. **Add a watching wallet.** Paste a public address and give it a name. Add more
50
+ wallets whenever you need them. Without discovery, the form explicitly
51
+ offers limited research of native and Mint Club assets.
51
52
  3. **Connect your AI.** Install and sign in to the
52
53
  [Codex CLI](https://developers.openai.com/codex/cli) or
53
54
  [Claude Code](https://code.claude.com/docs/en/quickstart), then choose your
@@ -117,6 +118,12 @@ package. Use `--data-dir /absolute/private/directory` before a command to choose
117
118
  another workspace. For connection, model or provider help, see the
118
119
  [CLI guide](docs/CLI.md) and [viewer guide](viewer/README.md).
119
120
 
121
+ From 0.1.4, a partial wallet analysis explains disabled discovery, missing
122
+ credentials and network failures directly beside the holdings. After connecting
123
+ discovery, use **Refresh holdings** to create a new analysis. **Refresh prices**
124
+ does not discover missing tokens. Existing workspace provider settings and
125
+ published snapshots are preserved.
126
+
120
127
  ## Build and contribute
121
128
 
122
129
  ```sh
package/docs/CLI.md CHANGED
@@ -10,8 +10,9 @@ npm install -g kira-wallet
10
10
  kira setup
11
11
  ```
12
12
 
13
- The npm registry currently offers version 0.1.2. The same archive and checksum
14
- are available from the [GitHub release](https://github.com/realproject7/kira-wallet/releases/tag/v0.1.2).
13
+ The operator published npm version 0.1.3 on 2026-10-06. GitHub v0.1.3 remains
14
+ a draft; the previous archive and checksum are available from the
15
+ [0.1.2 GitHub release](https://github.com/realproject7/kira-wallet/releases/tag/v0.1.2).
15
16
  Registry publication remains an operator action. The code and original character assets use the MIT license.
16
17
 
17
18
  ## Lifecycle
@@ -33,7 +34,7 @@ read-only and control modes requires an explicit stop and restart. Starting
33
34
  an already-running app in the same mode reuses its URL. The old `--controls`
34
35
  option remains accepted for existing scripts but is no longer needed.
35
36
 
36
- These startup defaults apply from 0.1.3. In the published 0.1.2 package, use
37
+ These startup defaults apply from published 0.1.3. In the older 0.1.2 package, use
37
38
  `kira setup` or `kira start --controls` for the full app.
38
39
 
39
40
  The 0.1.2 GitHub release starts with a wallet checklist. Add a public EVM
@@ -41,6 +42,11 @@ address or use the browser wallet picker. Registration starts one explicit
41
42
  research job; follow it in Activity and read the saved portfolio report.
42
43
  No signature, seed phrase or private key is needed.
43
44
 
45
+ Candidate 0.1.4 explains limited discovery before wallet registration and beside
46
+ holdings. Missing coverage keeps the portfolio estimate partial. It is not yet
47
+ published to npm. Existing snapshots require a holdings refresh after connecting
48
+ an indexer; a price refresh does not discover missing tokens.
49
+
44
50
  Public RPC checks known assets and Mint Club. For broader ERC20 discovery,
45
51
  open Research connections, select Alchemy and name the private environment
46
52
  variable holding its key. Never paste the key into the browser. `kira doctor`
@@ -50,8 +56,8 @@ prove complete coverage; inspect the recorded results and gaps.
50
56
  Connecting Codex or Claude is optional. Use Connect your AI when ready, verify
51
57
  a generic response and explicitly choose context permissions. Assistant answers
52
58
  render a safe Markdown subset; raw HTML, images and links remain inert text.
53
- Install the 0.1.1 GitHub archive for this wallet-first setup. The npm registry's
54
- 0.1.0 package continues to use the earlier account-first setup.
59
+ The wallet-first setup is included in published npm 0.1.3. The historical
60
+ 0.1.0 package uses the earlier account-first setup.
55
61
 
56
62
  ## Your model account
57
63
 
@@ -0,0 +1,95 @@
1
+ # Token discovery patch acceptance
2
+
3
+ Version: 0.1.4 candidate. Published npm 0.1.3 remains immutable.
4
+
5
+ ## Problem and correction
6
+
7
+ A new personal data directory defaults to public RPC and no general ERC20
8
+ indexer. The installed app does not inherit discovery settings from a separate
9
+ research workspace. Public RPC reads native balances and enumerates Mint Club
10
+ assets, but cannot identify arbitrary ERC20 contracts for a wallet. Previously,
11
+ a finished analysis could show only native holdings without explaining this
12
+ configuration at the point of use.
13
+
14
+ The wallet registration dialog now explains limited discovery and offers a
15
+ connection action before research. The wallet overview explains disabled
16
+ discovery, missing credentials, stale analysis or incomplete provider checks.
17
+ It labels incomplete portfolio estimates as partial. Network details retain
18
+ separate discovery, RPC and registry coverage. Read-only and sample views do
19
+ not acquire research permissions.
20
+
21
+ RPC and candidate-balance failures now receive an explicit warning with
22
+ expanded affected-network details. Retry holdings starts the existing gated
23
+ research action; Review RPC settings opens the existing connection dialog.
24
+ The warning says to wait briefly before retrying and review the connection
25
+ or choose another provider if failures persist. Missing balances remain
26
+ unknown. Successful discovery alone cannot mark failed balance reads complete,
27
+ and old snapshots can project their fixed balance-failure note safely.
28
+
29
+ Connecting an indexer does not change an immutable saved analysis. Refresh
30
+ holdings runs discovery again. Refresh prices only updates existing holdings.
31
+ Discovery settings preserve existing explorer preferences, and the UI rereads
32
+ readiness after the settings job succeeds.
33
+
34
+ Registry diagnostics now retain up to five redacted error examples and numeric
35
+ HTTP or RPC codes in local evidence. The public model exposes only status and
36
+ counts. Oversized multicalls split at the same observed block, including errors
37
+ that viem converts into failed result rows. Transport outages and throttling
38
+ remain bounded instead of retrying every contract separately.
39
+
40
+ ## Verification
41
+
42
+ - Required `npm test` suite passed: 152 Python tests, required Node behavior
43
+ checks and browser script syntax checks.
44
+ - `scripts/check-install.py` passed with a fresh 73-member package installation
45
+ and startup/shutdown lifecycle in a temporary synthetic workspace.
46
+ - The tracked-source privacy scan passed with the active private data directory
47
+ included. The full-history scan retains the previously recorded non-noreply
48
+ merge identity at `2ed52a2`; no new finding was introduced. That historical
49
+ identity is not reproduced here and public history was not rewritten.
50
+ - Actual viem with an in-memory aggregate3 transport: eight calls rejected
51
+ above two calls per batch succeed in seven requests, all at block 103.
52
+ Independent review also checked that HTTP 429 and 503 stay at one request
53
+ and isolated contract failures remain isolated. No live RPC is used by this
54
+ regression.
55
+ - Missing, unreadable and disabled discovery credentials produce explicit
56
+ coverage status without exposing credentials or attempting discovery.
57
+ - Viewer projection keeps raw provider diagnostics private and preserves
58
+ unknown prices. UI behavior checks cover disabled discovery, missing
59
+ credentials, stale analysis after connection, read-only actions and
60
+ partially successful explorer coverage.
61
+ - RPC recovery regressions cover unavailable chains, registry failures,
62
+ non-Mint candidate-balance failures, mixed missing connections, unknown
63
+ valuations, read-only controls and removal after a successful analysis.
64
+ - Chrome fixture checks at 1440 x 900 and 390 x 844: partial estimate and
65
+ discovery notice render without horizontal overflow. Both connection actions
66
+ open Research connections. The add-wallet dialog says Add with limited
67
+ discovery when the provider is absent. Fixture actions submitted no research,
68
+ account authorization or provider requests.
69
+ - RPC-failure fixtures at the same desktop/mobile sizes show expanded Base
70
+ and BNB Chain details, both recovery actions and a partial estimate without
71
+ horizontal overflow. Review RPC settings opens the connection dialog.
72
+
73
+ The design references were Mixpanel's dashboard connection guidance and
74
+ Databricks' inline data-check failure state, retrieved
75
+ with [Lazyweb](https://www.lazyweb.com/agentic-search/51280761-491a-4a3f-a78e-ed2dcd427ee8).
76
+ No operator wallet information or screenshots were submitted to that service.
77
+
78
+ Independent review found a viem failed-result handling defect during
79
+ implementation. The correction and actual-client regression were reviewed
80
+ again and the finding was closed. Screenshots and operator test evidence
81
+ remain in the ignored local ticket ledger.
82
+
83
+ ## Acceptance limits and release boundary
84
+
85
+ Operator-approved reuse of an existing discovery connection recovered
86
+ non-native holdings in a new saved analysis. Some provider and network gaps
87
+ remain; this does not establish complete holdings or verify every expected
88
+ token. Those limits remain visible in coverage. The existing operator server
89
+ was not restarted or globally upgraded during patch verification.
90
+
91
+ The source/package privacy scan is required before preparation.
92
+ `scripts/prepare-release.py`
93
+ pins the clean source commit and archive checksum. Registry publication
94
+ remains operator-only. No npm publication, GitHub release publication or PR
95
+ merge is part of preparing this candidate.
package/docs/RELEASE.md CHANGED
@@ -5,7 +5,9 @@
5
5
  GitHub version releases and npm registry publication are separate. The GitHub
6
6
  0.1.2 release provides the reviewed `kira-wallet-0.1.2.tgz`, its SHA-256 file and
7
7
  release manifest. Its tag pins the source used to create those assets. Install
8
- that archive from GitHub or use `npm install -g kira-wallet`; npm also serves 0.1.2.
8
+ that archive from GitHub. The operator published npm 0.1.3 on 2026-10-06;
9
+ its archive and installed bytes were verified separately. GitHub v0.1.3
10
+ remains a draft. GitHub and registry status must not be inferred from each other.
9
11
  Version 0.1.2 was published to GitHub on 2026-10-05 from
10
12
  `f994e99acb4e2d778fb4c95479f04355c88c00b3`. Its archive SHA-256 is
11
13
  `866b7f1ad543bff1ff59d3bf840eca6313eb6e206e78b8611186e5a971153f28`.
@@ -21,7 +23,7 @@ their invented wallet addresses, balances and seeded chat are documented in
21
23
 
22
24
  ## npm publication preparation
23
25
 
24
- The next candidate is **0.1.3**, which makes `kira start` open the full local
26
+ Published **0.1.3** makes `kira start` open the full local
25
27
  app by default. `--read-only` preserves the viewer-only mode, `--no-open`
26
28
  supports terminal-only use, and `--controls` remains compatible with existing
27
29
  scripts. A running instance never changes modes silently. Local session,
@@ -30,9 +32,22 @@ creation now identifies Open Wallet Standard with its official locally served
30
32
  logo and explains encrypted local storage and public-account research. The
31
33
  OWS integration and wallet-operation permissions are unchanged.
32
34
 
33
- This candidate requires PR review and operator publication. It is not yet
34
- available from the registry. The showcase HTML animation is deployed separately
35
- and requires no npm release. Keep the published 0.1.2 archive and tag immutable.
35
+ The next candidate is **0.1.4**. It makes incomplete token discovery visible
36
+ before registration and beside wallet holdings, separates disabled discovery
37
+ from missing credentials and provider failures, and offers a connection or
38
+ holdings-refresh action. Registry failures retain bounded redacted examples
39
+ and RPC error codes. Oversized multicalls split into smaller batches while
40
+ transport outages remain bounded. Existing explorer preferences are preserved
41
+ when discovery settings are saved. Connecting an indexer and refreshing
42
+ holdings are still required to recover tokens absent from an older snapshot.
43
+ RPC failures show affected networks, retry guidance and a settings action.
44
+ Failed candidate balance reads keep the saved result and valuation partial
45
+ even when general token discovery succeeded.
46
+
47
+ Version 0.1.4 requires independent review and operator registry publication.
48
+ It is not yet available from the registry. The showcase HTML animation is
49
+ deployed separately and requires no npm release. Keep published 0.1.2 and
50
+ 0.1.3 package bytes immutable. See [discovery acceptance](KIRA_DISCOVERY_ACCEPTANCE.md).
36
51
 
37
52
  The operator published npm **0.1.2** on 2026-10-05. Its registry archive was
38
53
  downloaded and matched the reviewed GitHub archive above, including SHA-256,
@@ -54,8 +69,8 @@ The earlier unpublished 0.1.2 candidate is preserved in the ignored
54
69
  `dist/preserved-0.1.2-bad8f02/` directory. Use the current `dist/release.json`
55
70
  and its matching archive for publication; do not mix manifests from candidates.
56
71
 
57
- Keep GitHub v0.1.1, its tag and its released archive immutable. Version 0.1.2
58
- is also released and immutable. Do not rebuild it from later documentation
72
+ Keep GitHub v0.1.1, its tag and its released archive immutable. Versions 0.1.2
73
+ and 0.1.3 are also published and immutable. Do not rebuild them from later documentation
59
74
  commits. Future package changes need a new version. Release preparation does not
60
75
  publish a GitHub release, merge PR 19, promote the website or publish to npm.
61
76
  Registry publication remains operator-only. The coding agent never publishes.
@@ -70,12 +85,12 @@ python3 scripts/prepare-release.py
70
85
  ```
71
86
 
72
87
  `prepare-release.py` scans the package boundary and creates
73
- `dist/kira-wallet-0.1.3.tgz`. The ignored `dist/release.json` records its source
88
+ `dist/kira-wallet-0.1.4.tgz`. The ignored `dist/release.json` records its source
74
89
  commit, SHA-256 and member count. It never publishes. After reviewing that
75
90
  manifest, the operator's final command from the project root is:
76
91
 
77
92
  ```sh
78
- npm publish ./dist/kira-wallet-0.1.3.tgz --access public
93
+ npm publish ./dist/kira-wallet-0.1.4.tgz --access public
79
94
  ```
80
95
 
81
96
  Use the operator's npm account and complete authentication or OTP personally.
package/kira_jobs.py CHANGED
@@ -403,7 +403,7 @@ class JobStore:
403
403
  previous=config['rpc']['chains'].get(key,{})
404
404
  if row['url_env']==previous.get('url_env') and previous.get('alchemy_network'):row['alchemy_network']=previous['alchemy_network']
405
405
  config['rpc']=updated
406
- else:config['discovery']={**job['input'],'explorers':False}
406
+ else:config['discovery']={**job['input'],'explorers':config['discovery'].get('explorers',False)}
407
407
  atomic(config_path(),config)
408
408
  result={'status':'completed','setting':job['operation']};atomic(self.jobs/'results'/(job['job_id']+'.json'),result);return result
409
409
  env = {**os.environ, 'KIRA_DATA_DIR': str(self.root), 'KIRA_JOB_ID': job['job_id'], 'KIRA_JOB_SNAPSHOT': job['checkpoint'], 'KIRA_ANALYSIS_FD': str(writer.fileno()), 'PYTHONUNBUFFERED': '1'}
package/onchain.cjs CHANGED
@@ -52,7 +52,12 @@ function safeError(error){
52
52
  message=message.replace(/https?:\/\/[^\s"<>]+/g,'[RPC URL]');
53
53
  message=message.replace(/(bearer\s+)[^\s,;"<>]+/gi,'$1[redacted]');
54
54
  message=message.replace(/(authorization|api[-_]?key|bearer)([\s:=]+)[^\s,;"<>]+/gi,'$1$2[redacted]');
55
- return {error_type:error.name,message:message.slice(0,500)};
55
+ const diagnostic={error_type:error.name,message:message.slice(0,500)};
56
+ for(let cause=error,depth=0;cause&&depth<6;cause=cause.cause,depth++){
57
+ if(Number.isInteger(cause.status))diagnostic.http_status=cause.status;
58
+ if(Number.isInteger(cause.code))diagnostic.rpc_code=cause.code;
59
+ }
60
+ return diagnostic;
56
61
  }
57
62
  function rpcEndpointsUsed(c){return [...(endpointReads.get(c)||[])].filter(i=>i>=0).sort((a,b)=>a-b);}
58
63
  module.exports={viem,root,assets,networks,bondAbi,endpoints,client,connect,resilientTransport,rpcEndpointsUsed,serialize,safeError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kira-wallet",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Manage your wallets with your own AI. Track tokens, research markets and refresh holdings in a local workspace.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -55,7 +55,8 @@
55
55
  "kira_research_tools.py",
56
56
  "kira_ows.py",
57
57
  "ows_bridge.cjs",
58
- "docs/KIRA_LIVE_QA.md"
58
+ "docs/KIRA_LIVE_QA.md",
59
+ "docs/KIRA_DISCOVERY_ACCEPTANCE.md"
59
60
  ],
60
61
  "scripts": {
61
62
  "test": "python3 -m unittest discover -s . -p 'test_*.py' && python3 -m unittest discover -s viewer -p 'test_*.py' && node test-onchain.cjs && node --check viewer/static/app.js && node --check viewer/static/jobs.js && node test-workspace.cjs && node --check viewer/static/workspace.js && node --check viewer/static/workspace-model.js && node test-watching.cjs && node test-watching-ui.cjs && node --check viewer/static/watching.js && node --check viewer/static/watching-model.js && node --check viewer/static/agent.js && node test-agent-ui.cjs && node test-markdown.cjs && node --check viewer/static/report.js && node test-report-ui.cjs && node test-holdings-ui.cjs && node test-rpc-fallback.cjs && node test-ows.cjs && node test-ows-ui.cjs && node --check viewer/static/ows.js && node --check viewer/static/chat-workspace.js && node --check viewer/static/selects.js && node --check site/demo.js"
@@ -48,12 +48,28 @@ async function connect(n) {
48
48
  }
49
49
  throw error || new Error('No RPC endpoint configured; public fallback is disabled.');
50
50
  }
51
+ function rpcFailure(error) {
52
+ let sizeLimit=false,transportFailure=false;
53
+ for(let cause=error,depth=0;cause&&depth<10;cause=cause.cause,depth++) {
54
+ if(['HttpRequestError','TimeoutError'].includes(cause.name))transportFailure=true;
55
+ if(cause.status===413||/\b(batch|payload|response|request body)\b.*\b(large|size|limit|exceed)/i.test(cause.shortMessage||cause.message||'')||/gas.*(cap|limit).*exceed|exceed.*gas.*(cap|limit)/i.test(cause.shortMessage||cause.message||''))sizeLimit=true;
56
+ if(cause.status===429)return {sizeLimit:false,transportFailure:true};
57
+ }
58
+ return {sizeLimit,transportFailure};
59
+ }
51
60
  async function calls(c, contracts, block) {
52
61
  if (!contracts.length) return [];
53
62
  try {
54
- return await c.multicall({contracts,blockNumber:block,batchSize:0,multicallAddress:MULTICALL});
63
+ const results=await c.multicall({contracts,blockNumber:block,batchSize:0,multicallAddress:MULTICALL});
64
+ // viem allowFailure also wraps an aggregate RPC failure as per-contract rows.
65
+ if(results.length&&results.every(r=>r.status==='failure')) {
66
+ const limited=results.find(r=>rpcFailure(r.error).sizeLimit);
67
+ if(limited)throw limited.error;
68
+ }
69
+ return results;
55
70
  } catch (e) {
56
- if (['HttpRequestError','TimeoutError'].includes(e.name)) return contracts.map(()=>({status:'failure',error:e}));
71
+ const {sizeLimit,transportFailure}=rpcFailure(e);
72
+ if (!sizeLimit&&transportFailure) return contracts.map(()=>({status:'failure',error:e}));
57
73
  if (contracts.length > 1) {
58
74
  const mid = Math.ceil(contracts.length/2);
59
75
  return [...await calls(c,contracts.slice(0,mid),block), ...await calls(c,contracts.slice(mid),block)];
@@ -135,6 +151,7 @@ async function scanChain(input, n, output) {
135
151
  cache_reused:reg.cache_reused,balance_errors:errors.filter(t=>t.registered_mintclub).length,
136
152
  positive_balances:held.filter(t=>t.registered_mintclub).length,block_number:block,complete:reg.errors===0 && checked===reg.count,
137
153
  source:'On-chain Mint Club bond tokenCount()/tokens(index), then direct wallet balance calls',bond_address:n.mintclub_bond_address};
154
+ row.registry_scan.error_examples=reg.entries.filter(t=>t.error||!t.address).slice(0,5).map(t=>({index:t.index,error:t.error||{message:'Registry address unavailable'}}));
138
155
  row.balance_errors = errors;
139
156
  for (const t of held) {
140
157
  const item = {chain_id:n.chain_id,network:n.network,token_address:t.address,token_type:t.token_type,token_id:t.token_type==='ERC1155'?'0':null,
package/viewer/model.py CHANGED
@@ -196,6 +196,11 @@ def project_wallet(entry, snapshot, root=ROOT, images=None):
196
196
  'links':[{'label':'Explorer','url':EXPLORERS[c['chain_id']]+'/address/'+entry['address']}] if c['chain_id'] in EXPLORERS else []})
197
197
  chains=[]
198
198
  for c in coverage:
199
+ # Older snapshots retained this count only in a fixed pipeline note.
200
+ failed_balances=c.get('candidate_balance_errors')
201
+ if type(failed_balances) is not int or failed_balances<0:
202
+ failed_balances=max((int(m.group(1)) for note in c.get('notes',[]) or [] if isinstance(note,str)
203
+ and (m:=re.fullmatch(r'([0-9]{1,9}) candidate token balances could not be read\.',note))),default=0)
199
204
  rows=[a for a in assets if a['chain_id']==c['chain_id']]
200
205
  for a in rows:
201
206
  a['environment']=c['environment']
@@ -204,6 +209,11 @@ def project_wallet(entry, snapshot, root=ROOT, images=None):
204
209
  'image_url':chain_image(c['chain_id']),
205
210
  'assets':len(rows),'value_usd':sum(a['value_usd'] for a in rows if a['value_usd'] is not None) if c['environment']=='mainnet' and any(a['value_usd'] is not None for a in rows) else None,
206
211
  'complete':c['general_erc20_discovery']=='indexer_checked','rpc_available':c['rpc_status']=='available',
212
+ 'discovery_status':c.get('discovery_status') if c.get('discovery_status') in ('checked','disabled','missing_credential','unsupported','provider_error') else 'incomplete',
213
+ 'registry_complete':(c.get('mintclub_registry_scan') or {}).get('complete') is True,
214
+ 'registry_errors':(c.get('mintclub_registry_scan') or {}).get('registry_errors',0),
215
+ 'balance_errors':(c.get('mintclub_registry_scan') or {}).get('balance_errors',0),
216
+ 'candidate_balance_errors':failed_balances,
207
217
  'native_symbol':c.get('native_symbol'),
208
218
  'native_balance':c.get('native_balance') if c['rpc_status']=='available' and number(c.get('native_balance')) is not None else None,
209
219
  'native_observed_at':c.get('native_observed_at'),
@@ -129,19 +129,43 @@ function renderWallet(){
129
129
  $('wallet-tags').innerHTML=w.tags.slice(1).map(t=>`<span class="tag">${escapeHTML(t)}</span>`).join(' ');
130
130
  $('report-link').hidden=!w.report_url;if(w.report_url)$('report-link').href=safeURL(w.report_url);
131
131
  $('total-value').textContent=w.analysed_at?money(w.known_value_usd):'—';
132
- $('valuation-note').textContent=w.analysed_at?(w.known_value_usd==null?(w.assets.length?'Value is unknown because prices are unavailable.':'No positive holdings recorded · unchecked networks remain unknown.'):'Market and curve spot estimates · excludes unpriced assets'):'Analysis will appear here when it is ready.';
132
+ const partial=w.chains.some(c=>c.environment==='mainnet'&&KiraView.coverageGap(c));
133
+ $('valuation-note').textContent=w.analysed_at?(partial?'Partial portfolio · ':'')+(w.known_value_usd==null?(w.assets.length?'Value is unknown because prices are unavailable.':'No positive holdings recorded · unchecked networks remain unknown.'):'Market and curve spot estimates · excludes unpriced assets'):'Analysis will appear here when it is ready.';
133
134
  $('asset-count').textContent=w.assets.length;$('unpriced-count').textContent=w.unpriced_count+' unpriced';
134
135
  const mainnets=w.chains.filter(c=>c.environment==='mainnet');
135
136
  $('chain-count').textContent=mainnets.filter(c=>c.assets>0).length;
136
137
  const coverage=mainnets.filter(c=>c.complete).length;
137
- $('coverage-note').textContent=coverage+' of '+mainnets.length+' checked for ERC20 tokens';
138
- $('coverage-note').title=w.chains.filter(c=>!c.complete).map(c=>c.name+(c.rpc_available?' · incomplete token discovery':' · unavailable')).join('\n');
138
+ $('coverage-note').textContent='Token discovery: '+coverage+' of '+mainnets.length+' mainnets checked';
139
+ $('coverage-note').title='Token discovery and on-chain balance checks are separate. See network coverage details for failures.';
139
140
  $('sync-label').textContent=w.analysed_at?'Analysis '+stamp(w.analysed_at):'Awaiting analysis';
140
141
  $('price-time').textContent=w.prices_at?'Prices '+stamp(w.prices_at):'';
141
- const chainOptions=w.chains.filter(c=>c.assets>0||!c.complete);
142
+ const chainOptions=w.chains.filter(c=>c.assets>0||KiraView.coverageGap(c));
142
143
  if(selectedChain!=='all'&&!chainOptions.some(c=>String(c.id)===selectedChain))selectedChain='all';
143
- $('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('');
144
- $('chain-filter').value=selectedChain;renderHoldings();
144
+ $('chain-filter').innerHTML='<option value="all">All chains</option>'+chainOptions.map(c=>`<option value="${c.id}">${escapeHTML(c.name)}${c.environment==='testnet'?' · Testnet':''}${KiraView.coverageGap(c)?' · Incomplete':''}</option>`).join('');
145
+ $('chain-filter').value=selectedChain;renderHoldings();renderDiscoveryCoverage(w);
146
+ }
147
+ function renderDiscoveryCoverage(w) {
148
+ const readiness=typeof setupReadiness!=='undefined'?setupReadiness:null;
149
+ const notice=KiraView.discoveryNotice(w,readiness);
150
+ $('discovery-notice').hidden=!notice;
151
+ if(!notice)return;
152
+ $('discovery-notice-title').textContent=notice.title;
153
+ $('discovery-notice-text').textContent=notice.message;
154
+ const controls=typeof localSession!=='undefined'&&localSession?.controls===true&&state?.demo!==true;
155
+ $('discovery-settings').hidden=!controls||!(notice.action==='settings'||notice.settings);
156
+ $('discovery-settings').textContent=notice.settingsLabel||'Connect token discovery';
157
+ $('discovery-refresh').hidden=!controls||notice.action!=='refresh';
158
+ $('discovery-refresh').textContent=notice.refreshLabel||'Refresh holdings';
159
+ $('discovery-refresh').disabled=$('refresh-wallet').disabled;
160
+ $('discovery-notice').querySelector('details').open=notice.rpcIssues===true;
161
+ $('discovery-network-details').textContent=w.chains.filter(c=>c.environment==='mainnet'&&KiraView.coverageGap(c)).map(c=>{
162
+ const reasons=[];
163
+ if(!c.rpc_available)reasons.push('balance reads failed; retry holdings, then review RPC settings if this continues');
164
+ if(!c.complete)reasons.push(({disabled:'token discovery off',missing_credential:'indexer credential unavailable',unsupported:'indexer unsupported',provider_error:'indexer request failed'})[c.discovery_status]||'token discovery incomplete');
165
+ if(c.registry_complete===false&&c.rpc_available)reasons.push('Mint Club balances partly unchecked; retry holdings'+(c.registry_errors||c.balance_errors?' ('+(c.registry_errors||0)+' asset checks and '+(c.balance_errors||0)+' balance reads failed)':''));
166
+ if(c.candidate_balance_errors>0)reasons.push(c.candidate_balance_errors+' token balance reads failed; missing balances remain unknown');
167
+ return c.name+': '+reasons.join('; ');
168
+ }).join('\n');
145
169
  }
146
170
  function icon(asset){
147
171
  if(typeof asset.symbol!=='string'||!asset.symbol)return ['', '?'];
@@ -50,6 +50,7 @@
50
50
  <div id="wallet-actions" class="wallet-actions" hidden><button id="refresh-wallet" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M3 11a9 9 0 1 1 2.6 7M3 5v6h6"/></svg>Refresh holdings</button><button id="refresh-prices" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 17l5-5 4 3 7-10M15 5h5v5"/></svg>Refresh prices</button><button id="edit-tags" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 20l4-1L20 7l-3-3L5 16zM14 7l3 3"/></svg>Edit names</button><button id="view-history" class="quiet-button" type="button"><svg class="ui-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"/></svg>Saved analyses</button></div>
51
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
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 id="discovery-notice" class="discovery-notice" aria-labelledby="discovery-notice-title" hidden><strong id="discovery-notice-title"></strong><p id="discovery-notice-text" role="status"></p><div class="discovery-actions"><button id="discovery-refresh" class="quiet-button" type="button" hidden>Refresh holdings</button><button id="discovery-settings" class="quiet-button" type="button" hidden>Connect token discovery</button></div><details><summary>Network coverage details</summary><p id="discovery-network-details"></p></details></section>
53
54
  <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
55
  <div id="holdings-filter-note" class="holdings-filter-note" role="status" hidden></div><div id="asset-groups"></div>
55
56
  <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>
@@ -120,7 +121,8 @@
120
121
  <label>Wallet name<input id="new-wallet-tag" required maxlength="200" placeholder="My research wallet" autocomplete="off"></label>
121
122
  <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>
122
123
  <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>
123
- <p class="form-note">Public RPC checks known assets and Mint Club. Broader token discovery may be incomplete. Research can consume provider allowance.</p>
124
+ <section id="wallet-discovery-warning" class="discovery-notice" aria-label="Token discovery connection"><strong id="wallet-discovery-warning-title">Checking token discovery</strong><p id="wallet-discovery-warning-text">An indexer is needed to find other ERC20 holdings. Public RPC alone checks native balances and Mint Club assets.</p><button id="wallet-discovery-settings" class="quiet-button" type="button">Connect token discovery</button></section>
125
+ <p class="form-note">Research can consume provider allowance. No signature is requested.</p>
124
126
  <button id="wallet-register" class="primary-button" type="submit" disabled>Add and research</button>
125
127
  </div>
126
128
  <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>
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
  let localSession=null, jobList=[], jobsSignature='', localPolling=false;
3
+ let discoverySettingsRevision='';
3
4
  const pendingActions=new Set();
4
5
  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
6
  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'};
@@ -28,6 +29,7 @@ function refreshControlState(){
28
29
  $(id).disabled=(sample&&id.startsWith('refresh'))||active||(id==='refresh-prices'&&!currentWallet()?.analysed_at);
29
30
  $(id).title=sample?'Sample research cannot contact providers.':active?'A research job is already active for this wallet.':'';
30
31
  }
32
+ $('discovery-refresh').disabled=$('refresh-wallet').disabled;
31
33
  if(typeof renderWatching==='function')renderWatching();
32
34
  }
33
35
  let jobsPage=1, jobsConnected=true, lastJobsPoll=null;
@@ -74,13 +76,15 @@ async function pollJobs(){
74
76
  if(!localSession){const response=await fetch('/api/session',{cache:'no-store',signal:AbortSignal.timeout(5000)});if(!response.ok)return;localSession=await response.json();}
75
77
  refreshControlState();if(!localSession.controls){renderJobs();return;}
76
78
  const jobs=await localAPI('/api/jobs');jobList=jobs;const reconnected=!jobsConnected;jobsConnected=true;lastJobsPoll=new Date().toISOString();
79
+ const setting=jobs.filter(j=>j.operation==='settings.discovery'&&j.state==='succeeded').map(j=>j.job_id+':'+j.updated_at).sort().join(',');
80
+ if(setting!==discoverySettingsRevision){discoverySettingsRevision=setting;if(typeof loadSetupReadiness==='function')await loadSetupReadiness();}
77
81
  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);}
78
82
  refreshControlState();
79
83
  }catch{jobsConnected=false;renderJobs();}
80
84
  finally{localPolling=false;}
81
85
  }
82
86
  for(const button of document.querySelectorAll('[data-close-dialog]'))button.addEventListener('click',()=>button.closest('dialog').close());
83
- function openAdd(){if(!localSession?.controls)return;if(!watchingSubmission)$('wallet-form-error').textContent='';if(typeof renderWatching==='function')renderWatching();$('wallet-dialog').showModal();}
87
+ function openAdd(){if(!localSession?.controls)return;if(!watchingSubmission)$('wallet-form-error').textContent='';if(typeof renderWatching==='function')renderWatching();$('wallet-dialog').showModal();if(typeof loadSetupReadiness==='function')loadSetupReadiness();}
84
88
  $('open-wallet').addEventListener('click',openAdd);$('empty-add-wallet').addEventListener('click',openAdd);
85
89
  function formAction(form,error,action){
86
90
  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);
@@ -104,7 +108,10 @@ $('refresh-prices').addEventListener('click',async()=>{try{await submitOperation
104
108
  $('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();});
105
109
  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();});
106
110
  $('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);}});
107
- $('open-settings').addEventListener('click',async()=>{try{const config=await localAPI('/api/settings');$('rpc-mode').value=config.rpc.mode;$('rpc-custom-only').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='';if(typeof KiraSelect!=='undefined')KiraSelect.refresh();$('settings-dialog').showModal();}catch(error){toast(error.message);}});
111
+ $('open-settings').addEventListener('click',async()=>{try{const config=await localAPI('/api/settings');$('rpc-mode').value=config.rpc.mode;$('rpc-custom-only').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='';if(typeof KiraSelect!=='undefined')KiraSelect.refresh();$('settings-dialog').showModal();if(typeof loadSetupReadiness==='function')loadSetupReadiness();}catch(error){toast(error.message);}});
112
+ $('discovery-settings').addEventListener('click',()=>$('open-settings').click());
113
+ $('wallet-discovery-settings').addEventListener('click',()=>{$('wallet-dialog').close();$('open-settings').click();});
114
+ $('discovery-refresh').addEventListener('click',()=>$('refresh-wallet').click());
108
115
  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-custom-only').checked,chains});});
109
116
  formAction($('discovery-form'),$('settings-error'),async()=>{await submitOperation('settings.discovery',{provider:$('discovery-provider').value,key_env:$('discovery-key').value});if(typeof loadSetupReadiness==='function')await loadSetupReadiness();});
110
117
  $('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);}});
@@ -15,7 +15,7 @@ function renderSetupPath() {
15
15
  const discovery=setupReadiness?.discovery_key_available===true,configured=setupReadiness?.discovery_configured===true,ai=typeof agentState!=='undefined'&&!!agentState?.config;
16
16
  $('setup-path').hidden=state.demo||!localSession?.controls||(wallet&&analysed&&discovery&&ai);
17
17
  $('home-empty').hidden=wallet||!$('setup-path').hidden;
18
- $('setup-path').innerHTML='<details '+(!wallet?'open':'')+'><summary>'+ (wallet?'Finish your setup':'Set up your first wallet')+'</summary><div class="section-heading"><h2>'+ (wallet?'Finish your setup':'Start with one wallet')+'</h2><span class="section-note">Your keys stay yours</span></div><ol class="setup-checklist">'+
18
+ $('setup-path').innerHTML='<details '+(!wallet||!discovery?'open':'')+'><summary>'+ (wallet?'Finish your setup':'Set up your first wallet')+'</summary><div class="section-heading"><h2>'+ (wallet?'Finish your setup':'Start with one wallet')+'</h2><span class="section-note">Your keys stay yours</span></div><ol class="setup-checklist">'+
19
19
  '<li><span class="setup-check">'+(wallet?'✓':'1')+'</span><div><strong>Add a public wallet</strong><p>'+(wallet?'Saved locally.':'Paste an EVM address or choose a browser wallet. No signature or seed phrase.')+'</p></div><button class="quiet-button" id="setup-wallet" type="button">'+(wallet?'Add another':'Add wallet')+'</button></li>'+
20
20
  '<li><span class="setup-check">'+(discovery?'✓':'2')+'</span><div><strong>Connect token discovery</strong><p>'+(!setupReadiness?(setupUnavailable?'Could not check the connection. Retrying…':'Checking the discovery connection…'):discovery?'Indexer key detected. Actual chain coverage is confirmed by research results.':configured?'Indexer selected, but its key is unavailable. Use a private environment reference.':'Public RPC checks known assets. Connect an indexer for broader ERC20 discovery, or continue with limited coverage.')+'</p></div><button class="quiet-button" id="setup-discovery" type="button">Review connection</button></li>'+
21
21
  '<li><span class="setup-check">'+(analysed?'✓':'3')+'</span><div><strong>Read your first report</strong><p>'+(analysed?'Saved balances, prices and coverage are ready.':'Adding a wallet starts one explicit research job. Follow its progress in Activity; gaps are shown in the report.')+'</p></div><a class="quiet-button" href="#/activity">View Activity</a></li>'+
@@ -26,7 +26,7 @@ function renderSetupPath() {
26
26
  }
27
27
  async function loadSetupReadiness(){
28
28
  if(setupLoading||!localSession?.controls)return false;setupLoading=true;
29
- try{setupReadiness=await localAPI('/api/onboarding');setupUnavailable=false;setupRetryAfter=0;renderSetupPath();return true;}catch{setupReadiness=null;setupUnavailable=true;setupRetryAfter=Date.now()+5000;renderSetupPath();return false;}finally{setupLoading=false;}
29
+ try{setupReadiness=await localAPI('/api/onboarding');setupUnavailable=false;setupRetryAfter=0;renderSetupPath();if(typeof renderWatching==='function')renderWatching();if(typeof currentWallet==='function'&&currentWallet())renderDiscoveryCoverage(currentWallet());return true;}catch{setupReadiness=null;setupUnavailable=true;setupRetryAfter=Date.now()+5000;renderSetupPath();return false;}finally{setupLoading=false;}
30
30
  }
31
31
  window.addEventListener('hashchange',()=>{if(selectedView==='home')renderHomeTokens();});
32
32
  const setupStartup=setInterval(async()=>{if(state&&localSession?.controls&&!setupReadiness&&Date.now()>=setupRetryAfter){await loadSetupReadiness();renderHomeTokens();}},1000);
@@ -49,3 +49,5 @@ body{background:#fcfbf8;color:#292733}main{background:#fcfbf8}.sidebar{backgroun
49
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
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
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}
52
+
53
+ .discovery-notice{margin:22px 0;padding:20px 22px;border:1px solid #e8d8b4;border-radius:12px;background:#fffaf0;color:#534426;line-height:1.6}.discovery-notice>strong{display:block;font-size:14px;font-weight:600}.discovery-notice p{margin:7px 0 0;font-size:13px}.discovery-actions{display:flex;gap:10px;margin-top:14px}.discovery-notice>.quiet-button{margin-top:12px}.discovery-notice details{margin-top:13px;font-size:12px}.discovery-notice summary{cursor:pointer;font-weight:500}.discovery-notice details p{white-space:pre-line;overflow-wrap:anywhere;font-size:12px}.discovery-notice .quiet-button{background:#fff;border-color:#e8d8b4}.discovery-notice [hidden]{display:none!important}@media(max-width:620px){.discovery-notice{padding:16px}.discovery-actions{flex-wrap:wrap}}
@@ -125,7 +125,11 @@ function renderWatching() {
125
125
  $('wallet-existing').hidden = !valid || !existing;
126
126
  $('wallet-existing-link').href = '#/wallet/'+address.toLowerCase();
127
127
  $('wallet-register').disabled = !!submission || !permitted || !valid || !!existing;
128
- $('wallet-register').textContent = watchingSubmitting ? 'Saving wallet…' : 'Add and research';
128
+ const discovery=typeof setupReadiness!=='undefined'?setupReadiness:null,ready=discovery?.discovery_key_available===true;
129
+ $('wallet-discovery-warning').hidden=ready||!!submission||state?.demo===true;
130
+ $('wallet-discovery-warning-title').textContent=!discovery?'Token discovery is not verified':discovery.discovery_configured?'Token discovery needs a connection':'Token discovery is off';
131
+ $('wallet-discovery-warning-text').textContent=discovery?.discovery_configured?'The selected indexer credential is unavailable. Connect it before researching, or continue with native and Mint Club assets only.':'Other ERC20 holdings may be missing. Connect an indexer before researching, or continue with native and Mint Club assets only.';
132
+ $('wallet-register').textContent = watchingSubmitting ? 'Saving wallet…' : ready||state?.demo?'Add and research':'Add with limited discovery';
129
133
  if (browser && $('wallet-dialog').open && watchingLastStatus === 'requesting' && current.status === 'accounts')
130
134
  $('wallet-accounts').querySelector('button')?.focus();
131
135
  watchingLastStatus = current.status;
@@ -17,6 +17,27 @@
17
17
  return { rows: rows.slice((page - 1) * pageSize, page * pageSize), count: rows.length, page, pages };
18
18
  }
19
19
  const active = job => ['queued', 'running'].includes(job.state);
20
+ const rpcGap = chain => chain.rpc_available===false||chain.registry_complete===false||chain.candidate_balance_errors>0;
21
+ const coverageGap = chain => !chain.complete||rpcGap(chain);
22
+ function discoveryNotice(wallet, readiness = null) {
23
+ const chains=(wallet?.chains||[]).filter(c=>c.environment==='mainnet');
24
+ const gaps=chains.filter(coverageGap);
25
+ if(!wallet?.analysed_at||!gaps.length)return null;
26
+ const missing=chains.some(c=>!c.complete);
27
+ const disabled=(readiness?.discovery_provider==='none'&&chains.every(c=>!c.complete))||(!readiness&&chains.every(c=>c.discovery_status==='disabled'));
28
+ const noKey=readiness?.discovery_configured&&!readiness.discovery_key_available;
29
+ const rpc=chains.filter(rpcGap);
30
+ if(rpc.length){
31
+ const connection=missing&&(disabled||noKey);
32
+ return {title:'Some balances could not be checked',
33
+ message:'On-chain checks did not finish on '+rpc.length+(rpc.length===1?' network.':' networks.')+' Missing balances are unknown, so this portfolio is partial. Wait a moment, then retry holdings. If errors continue, review RPC settings or use a different provider.'+(connection?' Token discovery also needs a connection to find other ERC20 holdings.':''),
34
+ action:'refresh',settings:true,rpcIssues:true,refreshLabel:'Retry holdings',settingsLabel:connection?'Review connections':'Review RPC settings'};
35
+ }
36
+ if(missing&&disabled)return {title:'Token discovery is off',message:'This analysis checks native balances and Mint Club assets only. Other ERC20 holdings may be missing. Connect token discovery, then refresh holdings.',action:'settings'};
37
+ if(missing&&noKey)return {title:'Token discovery needs a connection',message:'The selected indexer credential is unavailable. Review its connection, then refresh holdings. This portfolio is partial.',action:'settings'};
38
+ if(missing&&readiness?.discovery_key_available&&chains.some(c=>['disabled','missing_credential'].includes(c.discovery_status)))return {title:'Refresh holdings to discover other tokens',message:'Your discovery connection is now available. This saved analysis predates it and remains partial until you refresh holdings.',action:'refresh'};
39
+ return {title:'Your portfolio is partial',message:'Some token discovery or on-chain checks did not complete. Missing holdings remain unknown. Review the network details below before refreshing holdings.',action:readiness?.discovery_key_available?'refresh':'settings'};
40
+ }
20
41
  function jobPresentation(job, now = Date.now(), connected = true) {
21
42
  const labels = { queued: 'Queued', running: 'Running', succeeded: 'Completed', partial: 'Saved with gaps', interrupted: 'Interrupted', failed: 'Failed', cancelled: 'Stopped' };
22
43
  const start = job.state === 'queued' ? job.updated_at || job.created_at : job.started_at || job.created_at;
@@ -37,7 +58,7 @@
37
58
  const h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60), s = seconds % 60;
38
59
  return (h ? h + 'h ' : '') + m + 'm ' + s + 's';
39
60
  }
40
- const api = { catalog, pageSize, active, jobPresentation, duration };
61
+ const api = { catalog, pageSize, active, jobPresentation, duration, discoveryNotice, coverageGap };
41
62
  if (typeof module !== 'undefined' && module.exports) module.exports = api;
42
63
  else root.KiraView = api;
43
64
  })(typeof globalThis !== 'undefined' ? globalThis : this);
package/wallet.py CHANGED
@@ -41,7 +41,9 @@ def atomic(path, data):
41
41
  temp = path.with_suffix(path.suffix+'.tmp')
42
42
  text = data if isinstance(data, str) else json.dumps(data, indent=2, ensure_ascii=False)+'\n'
43
43
  # Providers occasionally include credentialed URLs in an error response.
44
- for secret in h.secrets().values():
44
+ try:values=h.secrets().values()
45
+ except ValueError:values=[]
46
+ for secret in values:
45
47
  if secret:
46
48
  text=text.replace(secret,'[redacted]')
47
49
  temp.write_text(text)
@@ -92,9 +94,10 @@ def discover(wallet, n, folder):
92
94
  cached=read(evidence)
93
95
  if cached['wallet'].lower()==wallet.lower() and cached['complete']:
94
96
  return cached
95
- row = {'wallet':wallet,'chain_id':cid,'observed_at':h.now(),'complete':False,'tokens':[],'native':[], 'pages':[],'source':None}
97
+ row = {'wallet':wallet,'chain_id':cid,'observed_at':h.now(),'complete':False,'tokens':[],'native':[], 'pages':[],'source':None,'discovery_status':'incomplete'}
96
98
  cfg=load_config()
97
- key=h.secrets().get('ALCHEMY_CUSTOM_APY_KEY') if cfg['discovery']['provider']=='alchemy' else None
99
+ try:key=h.secrets().get('ALCHEMY_CUSTOM_APY_KEY') if cfg['discovery']['provider']=='alchemy' else None
100
+ except ValueError:key=None
98
101
  if cid in ALCHEMY and key:
99
102
  row['source']='Alchemy Portfolio Tokens By Wallet'
100
103
  network=ALCHEMY[cid]
@@ -147,7 +150,14 @@ def discover(wallet, n, folder):
147
150
  else:
148
151
  row['error']=response
149
152
  else:
150
- row['error']={'message':'No general ERC20 indexer configured for this network. Mint Club registry and native balances are queried independently.'}
153
+ status='disabled' if cfg['discovery']['provider']=='none' else 'missing_credential' if cid in ALCHEMY and not key else 'unsupported'
154
+ row['discovery_status']=status
155
+ messages={'disabled':'Token discovery is off. Configure an indexer to discover other ERC20 holdings.',
156
+ 'missing_credential':'The selected indexer credential is unavailable. Configure its private environment reference, then refresh holdings.',
157
+ 'unsupported':'No general ERC20 indexer is available for this network.'}
158
+ row['error']={'message':messages[status]+' Mint Club registry and native balances are queried independently.'}
159
+ if row['complete']:row['discovery_status']='checked'
160
+ elif row['source']:row['discovery_status']='provider_error'
151
161
  row['tokens']=list({t['address'].lower():t for t in row['tokens']}.values())
152
162
  atomic(evidence,row)
153
163
  return row
@@ -228,6 +238,8 @@ def finish(wallet, folder, networks, discovered):
228
238
  'rpc_endpoint_indices':c.get('rpc_endpoint_indices',[]),
229
239
  'general_erc20_discovery':'indexer_checked' if d['complete'] else 'incomplete',
230
240
  'indexer_source':d['source'],'indexer_pages':len(d['pages']),'indexer_complete':d['complete'],
241
+ 'discovery_status':d.get('discovery_status','checked' if d['complete'] else 'incomplete'),
242
+ 'candidate_balance_errors':len(c.get('balance_errors',[])),
231
243
  'positive_erc20_count_discovered':sum(t['chain_id']==n['chain_id'] and t['token_type']=='ERC20' for t in tokens),
232
244
  'mintclub_inventory_scan':None,'mintclub_registry_scan':c.get('registry_scan'),'notes':[]}
233
245
  if not d['complete']:row['notes'].append('General token discovery incomplete. See discovery evidence; this is not proof of no holdings.')
@@ -264,7 +276,7 @@ def finish(wallet, folder, networks, discovered):
264
276
  'dex_tokens_with_liquidity_evidence':sum(t['dex_liquidity_found'] for t in tokens),'mintclub_tokens':len(minted),
265
277
  'mintclub_tokens_with_nonzero_reserve':sum(t['mintclub']['funded'] for t in minted),
266
278
  'mintclub_registry_assets_checked':sum(c.get('mintclub_registry_scan',{}).get('checked',0) if c.get('mintclub_registry_scan') else 0 for c in coverage)}
267
- gaps=any(not c['indexer_complete'] or not (c.get('mintclub_registry_scan') or {}).get('complete') or c['rpc_status']!='available' for c in coverage)
279
+ gaps=any(not c['indexer_complete'] or not (c.get('mintclub_registry_scan') or {}).get('complete') or c['rpc_status']!='available' or c['candidate_balance_errors'] for c in coverage)
268
280
  result={'schema_version':1,'wallet_address':wallet['address'],'tags':wallet['tags'],'compiled_at':h.now(),'status':'completed_with_coverage_gaps' if gaps else 'completed',
269
281
  'scope':'Direct ERC20, registered Mint Club ERC1155 and native holdings across all configured Mint Club EVM networks. Testnets are separate.',
270
282
  'counts':counts,'coverage':coverage,'tokens':tokens,'nested_curve_independent_redemption_estimates':[], 'price_references':refs,