kira-wallet 0.1.8 → 0.1.9
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/docs/FREE_RPC.md +35 -1
- package/docs/ONBOARDING_CHAT_CONTRACT.md +23 -0
- package/docs/RELEASE.md +28 -4
- package/kira_agent.py +34 -1
- package/kira_jobs.py +19 -2
- package/kira_research_tools.py +2 -1
- package/onchain.cjs +12 -5
- package/package.json +1 -1
- package/pipeline-onchain.cjs +4 -2
- package/rpc-pool.cjs +37 -7
- package/sources/rpc-candidates.json +16 -7
- package/viewer/details.py +4 -2
- package/viewer/static/app.js +6 -1
- package/viewer/static/jobs.js +7 -3
- package/viewer/static/workspace-model.js +8 -1
- package/wallet.py +2 -0
package/docs/FREE_RPC.md
CHANGED
|
@@ -7,7 +7,10 @@ effort and can throttle requests or omit historical state.
|
|
|
7
7
|
The reviewed default catalog adds alternatives on Ethereum, Base, BNB Chain,
|
|
8
8
|
Polygon, Arbitrum and Optimism. PublicNode is the initial preference. Reserves
|
|
9
9
|
are selected per chain rather than downloaded from an untrusted runtime list.
|
|
10
|
-
|
|
10
|
+
Shibarium and Puppynet use the current official Shib endpoints. Cyber mainnet
|
|
11
|
+
adds the official Superchain registry endpoint. Unverified replacements are
|
|
12
|
+
never accepted just because an endpoint returns HTTP 200. Some configured
|
|
13
|
+
networks can remain unavailable; their missing balances stay unknown.
|
|
11
14
|
|
|
12
15
|
## Request behavior
|
|
13
16
|
|
|
@@ -110,3 +113,34 @@ Primary references: [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898),
|
|
|
110
113
|
[Circle contracts](https://developers.circle.com/stablecoins/usdc-contract-addresses),
|
|
111
114
|
[Tether contracts](https://tether.to/en/supported-protocols/),
|
|
112
115
|
[Uniswap Arbitrum deployments](https://developers.uniswap.org/docs/protocols/v3/deployments/v3-arbitrum-deployments).
|
|
116
|
+
|
|
117
|
+
## Recorded route evidence and remaining limits
|
|
118
|
+
|
|
119
|
+
Chain coverage includes `rpc_routes`: bounded aggregate logical endpoint outcomes
|
|
120
|
+
with public/custom class, endpoint index, method, outcome and count. Failures
|
|
121
|
+
can include HTTP or RPC error codes. A successful read can follow failed public
|
|
122
|
+
attempts; configuring public first does not prove every read was public.
|
|
123
|
+
Cooldown skips, queue timeouts and successes are distinct. The receipt contains
|
|
124
|
+
no endpoint URLs, keys, request arguments or balances. Cached checkpoints retain
|
|
125
|
+
their original evidence and routes; a resume is not a fresh observation.
|
|
126
|
+
|
|
127
|
+
A live October 10 check identified DNS failures on the old Shib endpoints,
|
|
128
|
+
Ham, Over and Cyber testnet. Current official Shib endpoints returned the correct
|
|
129
|
+
chain identities and supported hash-qualified balance reads. Cyber mainnet's
|
|
130
|
+
registry endpoint also passed. The official Cyber testnet replacement failed
|
|
131
|
+
certificate validation, so it was not added. Ham and Over have no verified
|
|
132
|
+
replacement in this catalog. This is an observation, not a claim that their
|
|
133
|
+
chains permanently stopped. Their failed checks remain visible.
|
|
134
|
+
|
|
135
|
+
Base public balance reads work independently of arbitrary token discovery.
|
|
136
|
+
The keyless Base inventory remains selected common contracts and Mint Club.
|
|
137
|
+
Blockscout's current documentation deprecates per-instance APIs in favor of its
|
|
138
|
+
keyed PRO API. Kira does not promise keyless Base enumeration through that route.
|
|
139
|
+
The former 1RPC Base reserve returned HTTP 410 during block lookup and was
|
|
140
|
+
removed. PublicNode and dRPC remain verified Base candidates. The official Base
|
|
141
|
+
public endpoint returned HTTP 403 in this test and was not silently substituted.
|
|
142
|
+
|
|
143
|
+
Sources: [Shib network setup](https://docs.shib.io/get-started/deploy-contracts),
|
|
144
|
+
[Superchain registry](https://github.com/ethereum-optimism/superchain-registry/blob/main/chainList.toml),
|
|
145
|
+
[Ham network information](https://docs.ham.fun/docs/network-information/),
|
|
146
|
+
[Blockscout API migration](https://docs.blockscout.com/devs/apis).
|
|
@@ -98,3 +98,26 @@ native default-model response checks with no wallet data. The provider save
|
|
|
98
98
|
used a synthetic local key reference and made no provider research request.
|
|
99
99
|
Real extension approval, personal OWS passphrases and live holdings coverage
|
|
100
100
|
are separate operator acceptance. Published 0.1.4 snapshots are not rewritten.
|
|
101
|
+
|
|
102
|
+
## Recorded value and history interpretation
|
|
103
|
+
|
|
104
|
+
`recorded_valuation` summarizes the full recorded mainnet inventory before any
|
|
105
|
+
holdings pagination. Native, Mint Club and other-token categories are disjoint.
|
|
106
|
+
Compact pages retain `exit_quote_recorded` when quote outputs are deferred.
|
|
107
|
+
A true flag requires reading the token details before judging the historical
|
|
108
|
+
quote. A false flag describes this snapshot, not the existence of an exit route.
|
|
109
|
+
Each category carries its own priced-position count and price observation range.
|
|
110
|
+
The all-priced total includes all three categories; it is never a Mint Club-only
|
|
111
|
+
denominator. Missing category prices remain null. Spot sums at separate, possibly
|
|
112
|
+
stale or indicative price times are not floors on current wealth or sale proceeds.
|
|
113
|
+
|
|
114
|
+
Snapshot comparisons state whether both quantities were observed. Missing earlier
|
|
115
|
+
or later quantities cannot establish or rule out receipts or disposals. Wider
|
|
116
|
+
discovery can explain newly visible records; causal history remains unknown.
|
|
117
|
+
Even equal endpoint quantities do not exclude intervening transfers. These facts
|
|
118
|
+
and rules reach both supported native providers. Generated prose is not rewritten
|
|
119
|
+
by keyword filters, so actual response quality still needs scoped verification.
|
|
120
|
+
`comparison_summary` supplies exact full-comparison counts by chain before
|
|
121
|
+
pagination. Missing earlier quantities, missing later quantities, equal and
|
|
122
|
+
different observations stay separate. These counts cover recorded non-native
|
|
123
|
+
token identities; they do not count native balances or infer transfer activity.
|
package/docs/RELEASE.md
CHANGED
|
@@ -94,12 +94,12 @@ python3 scripts/prepare-release.py
|
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
`prepare-release.py` scans the package boundary and creates
|
|
97
|
-
`dist/kira-wallet-0.1.
|
|
97
|
+
`dist/kira-wallet-0.1.9.tgz`. The ignored `dist/release.json` records its source
|
|
98
98
|
commit, SHA-256 and member count. It never publishes. After reviewing that
|
|
99
99
|
manifest, the operator's final command from the project root is:
|
|
100
100
|
|
|
101
101
|
```sh
|
|
102
|
-
npm publish ./dist/kira-wallet-0.1.
|
|
102
|
+
npm publish ./dist/kira-wallet-0.1.9.tgz --access public
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Use the operator's npm account and complete authentication or OTP personally.
|
|
@@ -123,9 +123,9 @@ with untouched settings fields while preserving edited drafts.
|
|
|
123
123
|
|
|
124
124
|
The operator published **0.1.7** on October 9, 2026. Its archive is immutable.
|
|
125
125
|
|
|
126
|
-
##
|
|
126
|
+
## Published 0.1.8 behavior
|
|
127
127
|
|
|
128
|
-
The
|
|
128
|
+
The operator published **0.1.8** on October 9, 2026. Its archive is immutable.
|
|
129
129
|
It queues local connection imports during research, lets settings and price
|
|
130
130
|
updates preempt enrichment, preserves explicit cancellation, and checkpoints
|
|
131
131
|
wallet balance batches and market verification at fixed blocks. A 180-second
|
|
@@ -169,3 +169,27 @@ establish that native extension acceptance passed. Those actions still require
|
|
|
169
169
|
the operator's device handoff. Signing and trading remain outside Kira. Human-submitted OWS creation stores an encrypted wallet in the separate local OWS vault.
|
|
170
170
|
|
|
171
171
|
See [current UI evidence](https://github.com/realproject7/kira-wallet/blob/codex/kira-release/docs/KIRA_UI_REFINEMENT.md).
|
|
172
|
+
|
|
173
|
+
## Proposed 0.1.9 behavior
|
|
174
|
+
|
|
175
|
+
Version 0.1.9 permits a saved report's price update during detailed research,
|
|
176
|
+
with duplicate queued/running price feedback and existing automatic continuation.
|
|
177
|
+
Network empty states distinguish pending coverage, incomplete checks, completed
|
|
178
|
+
empty records and actual filter exclusion. Native facts include full-inventory
|
|
179
|
+
category valuation and explicit unknown activity causes. Public RPC configuration
|
|
180
|
+
updates verified Shib/Cyber routes and removes a failed Base reserve. Sanitized
|
|
181
|
+
route outcomes distinguish public-only acceptance from custom fallback. Bounded
|
|
182
|
+
route telemetry retains aggregate outcomes when detailed records reach their cap.
|
|
183
|
+
Compact agent facts retain historical quote presence even when outputs are deferred.
|
|
184
|
+
Snapshot comparisons provide exact full-comparison and per-chain counts before
|
|
185
|
+
pagination so native agents do not need to tally large position pages manually.
|
|
186
|
+
|
|
187
|
+
Publication remains an operator action. Post-publication acceptance uses two
|
|
188
|
+
clean installations of the same verified registry bytes: public/free only first,
|
|
189
|
+
then a clean reinstall with authorized Alchemy discovery and public-first backup.
|
|
190
|
+
Each lane begins without previous holdings, identity caches or RPC health.
|
|
191
|
+
Compare exact chain/contract identities, observations, coverage, route classes,
|
|
192
|
+
failures and runtime. Separate changed observation times from discovery effects.
|
|
193
|
+
The procedure does not treat partial free discovery as exhaustive or stale spot
|
|
194
|
+
sums as wealth floors. Private wallets, financial screenshots and local tickets
|
|
195
|
+
stay outside the package and public review.
|
package/kira_agent.py
CHANGED
|
@@ -30,6 +30,7 @@ SYSTEM = ('You are Kira, a careful wallet research partner. Answer in the langua
|
|
|
30
30
|
'A bounded summary is not contradictory new evidence. pools_omitted means recorded pool details were left out of this response, not absent from the record. '
|
|
31
31
|
'Use token_read before judging or retracting market details when pools are omitted. Testnet assets are excluded from portfolio valuation; do not infer a zero market price. '
|
|
32
32
|
'assets_omitted and next_offset describe a page, not missing holdings. Use wallet_read with offset and limit for more holdings, and token_read for detailed evidence. '
|
|
33
|
+
'Compact pages defer exit_quote and curve details. exit_quote_recorded true means a validated historical quote exists in the record even if its output is omitted here. Use token_read before judging execution evidence or claiming that a quote is absent. False means no validated quote in this snapshot, not that no exit route exists. '
|
|
33
34
|
'For a complete inventory or portfolio comparison, follow next_offset for each approved wallet using limit 100. If the tool budget prevents this, state which pages remain unread. Inventory pages defer curve and exit details to token_read. '
|
|
34
35
|
'rpc_pending or rpc_status pending means research has not reached that network. It is not an RPC outage. Only an explicit unavailable status proves a failed RPC check. '
|
|
35
36
|
'Compare curve state only for the same chain and contract using comparable reserve, supply, next-mint price and royalty fields. A newer block or different wallet-sized burn output alone does not prove the curve moved. If comparable fields are unchanged, say no change was demonstrated; if missing, say the comparison is unknown. '
|
|
@@ -49,6 +50,10 @@ SYSTEM = ('You are Kira, a careful wallet research partner. Answer in the langua
|
|
|
49
50
|
'In normalized wallet facts, chain complete means general ERC20 discovery was checked. In job chains, status complete means an on-chain checkpoint finished. These are different stages. Neither flag alone proves all holdings are known. registry_complete and candidate_balance_errors describe separate read coverage. An indexer check and unavailable RPC are separate coverage facts, not conflicting records. '
|
|
50
51
|
'An explicit chains.native_balance of zero is an observed native balance at its recorded time and block; null is unknown. Empty positive-holding inventories do not erase these observations. '
|
|
51
52
|
'For saved-analysis comparisons, use snapshots_compare and read both snapshots. coverage_changed compares full evidence, including times and blocks, not just network status flags. price_references_changed may include changed reference values or times, not balances. '
|
|
53
|
+
'comparison_summary gives exact full-comparison totals and per-chain counts before pagination. Use these counts instead of manually counting position pages. It covers recorded non-native tokens, not native balances, transfer history or unobserved discovery candidates. '
|
|
54
|
+
'A newly visible or absent token record is not a receipt or disposal. If either balance observation is missing, the change and its cause are unknown. Broader discovery can explain visibility but cannot establish or rule out receipts, transfers or intervening activity. Even equal observed balances do not prove no intervening transfers. '
|
|
55
|
+
'recorded_valuation covers all recorded mainnet positions of a wallet, including positions omitted from this page. Its all_priced_positions total includes native assets, Mint Club assets and other priced tokens. Use the exact category total and denominator for category exposure or concentration, never relabel a whole-wallet sum as Mint Club exposure. '
|
|
56
|
+
'These are estimated recorded priced subsets at their separate price observation times. Missing prices, stale prices and indicative curve spot prices mean the sum is not a floor, minimum wealth, lower bound on current value or guaranteed sale proceeds. Do not sum independent burn quotes as cash proceeds. '
|
|
52
57
|
'Snapshot reads are normalized evidence. Compare chain observations and snapshot_price_references; never claim these are the only raw fields that changed. '
|
|
53
58
|
'Your personality is a sharp, composed crypto analyst: concise, quietly confident, practical and a little dry. '
|
|
54
59
|
'Talk like a knowledgeable person sitting beside the user. Lead with the actual finding or decision. '
|
|
@@ -115,6 +120,7 @@ def projection(root, config, *, token_id=None, limit=12, offset=0, wallet_key=No
|
|
|
115
120
|
if config['scope'] == 'wallet':
|
|
116
121
|
wallets = [w for w in wallets if w['key'] == config['wallet']]
|
|
117
122
|
if len(wallets) != 1: raise JobError('wallet_missing','The approved wallet is no longer registered. Review your context settings.')
|
|
123
|
+
wallets=[{**w,'recorded_valuation':recorded_valuation(w['assets'])} for w in wallets]
|
|
118
124
|
if token_id is not None:
|
|
119
125
|
wallets=[{**wallet,'assets':[a for a in wallet['assets'] if a['id']==token_id]} for wallet in wallets]
|
|
120
126
|
if wallet_key is not None: wallets=[w for w in wallets if w['key']==wallet_key]
|
|
@@ -141,17 +147,43 @@ def projection(root, config, *, token_id=None, limit=12, offset=0, wallet_key=No
|
|
|
141
147
|
return result
|
|
142
148
|
|
|
143
149
|
def compact_wallet_facts(rows):
|
|
144
|
-
asset_fields=('id','chain_id','address','symbol','name','environment','balance','is_native','value_usd','balance_observed_at','price','pools','pools_recorded','pools_omitted')
|
|
150
|
+
asset_fields=('id','chain_id','address','symbol','name','environment','balance','is_native','value_usd','balance_observed_at','price','pools','pools_recorded','pools_omitted','exit_quote_recorded')
|
|
145
151
|
return [{**row,'assets':[{key:asset[key] for key in asset_fields if key in asset and
|
|
146
152
|
(asset[key] is not None or key in ('value_usd','price'))}
|
|
147
153
|
for asset in row['assets']]} for row in rows]
|
|
148
154
|
|
|
155
|
+
def recorded_valuation(assets):
|
|
156
|
+
"""Explicit disjoint categories over the full recorded inventory, before paging."""
|
|
157
|
+
from decimal import Decimal, InvalidOperation, localcontext
|
|
158
|
+
groups={'native':[], 'mintclub':[], 'other_tokens':[]}
|
|
159
|
+
unpriced=0
|
|
160
|
+
for asset in assets:
|
|
161
|
+
if asset.get('environment')!='mainnet':continue
|
|
162
|
+
try:value=Decimal(str(asset.get('value_usd')))
|
|
163
|
+
except InvalidOperation:unpriced+=1;continue
|
|
164
|
+
if not value.is_finite() or value<0:unpriced+=1;continue
|
|
165
|
+
category='native' if asset.get('is_native') else 'mintclub' if asset.get('curve_state') or asset.get('exit_route')=='mintclub_burn' else 'other_tokens'
|
|
166
|
+
groups[category].append((value,(asset.get('price') or {}).get('observed_at')))
|
|
167
|
+
def summarize(rows):
|
|
168
|
+
times=[time for _,time in rows if time]
|
|
169
|
+
with localcontext() as context:
|
|
170
|
+
context.prec=100
|
|
171
|
+
value=format(sum((value for value,_ in rows),Decimal(0)),'f') if rows else None
|
|
172
|
+
return {'estimated_value_usd':value,'priced_position_count':len(rows),
|
|
173
|
+
'price_observed_at':{'from':min(times,default=None),'to':max(times,default=None)},
|
|
174
|
+
'price_time_unknown_count':sum(time is None for _,time in rows)}
|
|
175
|
+
return {'scope':'Full recorded mainnet inventory, before pagination. Categories are disjoint.',
|
|
176
|
+
'all_priced_positions':summarize([row for rows in groups.values() for row in rows]),
|
|
177
|
+
'categories':{name:summarize(rows) for name,rows in groups.items()},'unpriced_mainnet_count':unpriced,
|
|
178
|
+
'interpretation':'Estimated recorded priced subset at separate observation times. Not a floor on current wealth or realizable proceeds. Missing values remain unknown; indicative prices are not executable quotes.'}
|
|
179
|
+
|
|
149
180
|
def wallet_facts(wallets, *, pool_budget=40_000):
|
|
150
181
|
def pick(value, names): return {k:value.get(k) for k in names}
|
|
151
182
|
rows = []
|
|
152
183
|
for wallet in wallets:
|
|
153
184
|
row = pick(wallet,('address','tags','name','snapshot_id','analysed_at','prices_at','balance_observed_at','known_value_usd','unpriced_count','status'))
|
|
154
185
|
if not wallet.get('analysed_at'): row['known_value_usd']=None
|
|
186
|
+
row['recorded_valuation']=wallet.get('recorded_valuation') or recorded_valuation(wallet['assets'])
|
|
155
187
|
row['chains'] = [pick(c,('id','name','environment','complete','rpc_available','rpc_status','market_pending','market_errors','native_symbol','native_balance','native_observed_at','native_block_number','registry_block_number','discovery_status','registry_complete','registry_phase','rpc_pending','candidate_balance_errors','candidate_deferred')) for c in wallet['chains']]
|
|
156
188
|
row['snapshot_price_references'] = wallet.get('snapshot_price_references',{})
|
|
157
189
|
row['assets'] = []
|
|
@@ -159,6 +191,7 @@ def wallet_facts(wallets, *, pool_budget=40_000):
|
|
|
159
191
|
item = pick(asset,('id','chain_id','address','symbol','name','environment','balance','is_native','value_usd','balance_observed_at'))
|
|
160
192
|
item['price'] = pick(asset['price'],('usd','basis','quality','observed_at')) if asset.get('price') else None
|
|
161
193
|
item['exit_quote'] = asset.get('exit_quote')
|
|
194
|
+
item['exit_quote_recorded'] = asset.get('exit_quote') is not None
|
|
162
195
|
item['exit_route'] = asset.get('exit_route')
|
|
163
196
|
item['markets'] = asset.get('links',[])
|
|
164
197
|
pools=asset.get('market_pools',[]);item['pools']=[];item['pools_recorded']=len(pools)
|
package/kira_jobs.py
CHANGED
|
@@ -364,17 +364,34 @@ class JobStore:
|
|
|
364
364
|
if a is not None and b is not None and a.get('wallet_balance') is not None and b.get('wallet_balance') is not None:
|
|
365
365
|
try:
|
|
366
366
|
with localcontext() as context:
|
|
367
|
-
context.prec=100
|
|
367
|
+
context.prec=100
|
|
368
|
+
first,last=Decimal(a['wallet_balance']),Decimal(b['wallet_balance'])
|
|
369
|
+
if first.is_finite() and last.is_finite():delta=str(last-first)
|
|
368
370
|
except InvalidOperation:pass
|
|
369
371
|
changes.append({'chain_id':identity[0],'address':identity[1],'token_id':identity[2],
|
|
370
372
|
'symbol':(b or a).get('symbol'),'before_balance':a.get('wallet_balance') if a else None,
|
|
371
373
|
'after_balance':b.get('wallet_balance') if b else None,'balance_delta':delta,
|
|
374
|
+
'balance_comparison':'unknown' if delta is None else 'unchanged_observations' if Decimal(delta)==0 else 'different_observations',
|
|
375
|
+
'activity_cause':'unknown_without_transfer_history',
|
|
372
376
|
'presence':'both' if a and b else 'new_record' if b else 'absent_record',
|
|
373
377
|
'price_references_changed':(a or {}).get('indexer_price_references')!=(b or {}).get('indexer_price_references'),
|
|
374
378
|
'valuation_method_changed':bool((a or {}).get('mintclub'))!=bool((b or {}).get('mintclub'))})
|
|
379
|
+
def counts(rows):
|
|
380
|
+
return {'position_count':len(rows),
|
|
381
|
+
'new_record_count':sum(r['presence']=='new_record' for r in rows),
|
|
382
|
+
'absent_record_count':sum(r['presence']=='absent_record' for r in rows),
|
|
383
|
+
'missing_before_balance_count':sum(r['before_balance'] is None for r in rows),
|
|
384
|
+
'missing_after_balance_count':sum(r['after_balance'] is None for r in rows),
|
|
385
|
+
'unchanged_observation_count':sum(r['balance_comparison']=='unchanged_observations' for r in rows),
|
|
386
|
+
'different_observation_count':sum(r['balance_comparison']=='different_observations' for r in rows),
|
|
387
|
+
'unknown_comparison_count':sum(r['balance_comparison']=='unknown' for r in rows)}
|
|
388
|
+
summary={'scope':'full_recorded_non_native_token_comparison_before_pagination',
|
|
389
|
+
'totals':counts(changes),
|
|
390
|
+
'chains':[{'chain_id':cid,**counts([r for r in changes if r['chain_id']==cid])}
|
|
391
|
+
for cid in sorted({r['chain_id'] for r in changes})]}
|
|
375
392
|
return {'schema_version':1,'before':before,'after':after,'wallet':new['wallet_address'],
|
|
376
393
|
'coverage_changed':old.get('coverage')!=new.get('coverage'),'price_references_changed':old.get('price_references')!=new.get('price_references'),
|
|
377
|
-
'positions':changes,'note':'Absent records are unknown, not zero. Price overlays have separate observations and are not balance changes. Coverage changes compare full evidence, including observation times and blocks; this alone does not prove the set of checked networks changed. Positions compare direct token records. Read both snapshots to compare native balances.'}
|
|
394
|
+
'comparison_summary':summary,'positions':changes,'note':'Absent records are unknown, not zero. Missing prior or later observations cannot establish or rule out receipts, disposals or transfers. Broader discovery can explain visibility, but activity and its cause remain unknown without transfer history. Even equal observed balances do not exclude intervening activity. Price overlays have separate observations and are not balance changes. Coverage changes compare full evidence, including observation times and blocks; this alone does not prove the set of checked networks changed. Positions compare direct token records. Read both snapshots to compare native balances.'}
|
|
378
395
|
|
|
379
396
|
def snapshots(self, wallet):
|
|
380
397
|
key = self.wallet(wallet)['address_key']
|
package/kira_research_tools.py
CHANGED
|
@@ -60,11 +60,12 @@ class ResearchTools:
|
|
|
60
60
|
if identity not in visible:raise JobError('snapshot_scope','This analysis is outside the current conversation scope.')
|
|
61
61
|
result=self.store.snapshot(identity)
|
|
62
62
|
if result.get('wallet_address','').lower() not in approved:raise JobError('snapshot_scope','This analysis does not belong to the approved wallet.')
|
|
63
|
-
from kira_agent import wallet_facts, compact_wallet_facts
|
|
63
|
+
from kira_agent import wallet_facts, compact_wallet_facts, recorded_valuation
|
|
64
64
|
from model import project_wallet
|
|
65
65
|
entry=self.store.wallet(result['wallet_address'])
|
|
66
66
|
entry={**entry,'latest_snapshot':{'directory':identity}}
|
|
67
67
|
wallet=project_wallet(entry,result,self.root);count=len(wallet['assets'])
|
|
68
|
+
wallet['recorded_valuation']=recorded_valuation(wallet['assets'])
|
|
68
69
|
wallet['assets']=wallet['assets'][offset:offset+limit]
|
|
69
70
|
rows=compact_wallet_facts(wallet_facts([wallet],pool_budget=0))
|
|
70
71
|
rows[0].update(asset_count=count,assets_omitted=count-len(wallet['assets']),offset=offset,
|
package/onchain.cjs
CHANGED
|
@@ -9,21 +9,27 @@ const runtime=JSON.parse(resolved.stdout);
|
|
|
9
9
|
const root=runtime.data_root;
|
|
10
10
|
const networks=JSON.parse(fs.readFileSync(path.join(assets,'sources/rpc-candidates.json')));
|
|
11
11
|
const bondAbi=JSON.parse(fs.readFileSync(path.join(assets,'sources/mintclub-bond-abi.json')));
|
|
12
|
-
const {RpcPool}=require('./rpc-pool.cjs');
|
|
12
|
+
const {RpcPool,RouteRecorder}=require('./rpc-pool.cjs');
|
|
13
13
|
const pool=new RpcPool({publicUrls:networks.flatMap(n=>n.public_rpc||[]),healthFile:runtime.health_file});
|
|
14
14
|
function endpoints(chainId){return runtime.endpoints[String(chainId)] || [];}
|
|
15
15
|
const endpointReads=new WeakMap();
|
|
16
|
+
const endpointOutcomes=new WeakMap();
|
|
16
17
|
const clientState=new WeakMap();
|
|
17
|
-
function resilientTransport(chainId,urls,transport=viem.http,onSuccess=()=>{}) {
|
|
18
|
-
return pool.transport(chainId,urls,transport,onSuccess);
|
|
18
|
+
function resilientTransport(chainId,urls,transport=viem.http,onSuccess=()=>{},onOutcome=()=>{}) {
|
|
19
|
+
return pool.transport(chainId,urls,transport,onSuccess,onOutcome);
|
|
19
20
|
}
|
|
20
21
|
function client(chainId,url) {
|
|
21
22
|
const urls=endpoints(chainId);
|
|
22
23
|
const index=url==null?0:urls.indexOf(url);
|
|
23
24
|
const ordered=url==null?urls:index<0?[url]:urls.slice(index);
|
|
24
25
|
const used=new Set();
|
|
26
|
+
const outcomes=new RouteRecorder();
|
|
25
27
|
const state={chainId,last:null};
|
|
26
|
-
const c=viem.createPublicClient({transport:resilientTransport(chainId,ordered,viem.http,(i,endpoint)=>{used.add(urls.indexOf(ordered[i]));state.last=endpoint;})
|
|
28
|
+
const c=viem.createPublicClient({transport:resilientTransport(chainId,ordered,viem.http,(i,endpoint)=>{used.add(urls.indexOf(ordered[i]));state.last=endpoint;},(i,event)=>{
|
|
29
|
+
const endpoint=urls.indexOf(ordered[i]),endpointClass=pool.publicUrls.has(ordered[i])?'public':'custom';
|
|
30
|
+
outcomes.record(endpoint,endpointClass,event);
|
|
31
|
+
})});
|
|
32
|
+
endpointOutcomes.set(c,outcomes);
|
|
27
33
|
clientState.set(c,state);
|
|
28
34
|
endpointReads.set(c,used);return c;
|
|
29
35
|
}
|
|
@@ -52,4 +58,5 @@ function safeError(error){
|
|
|
52
58
|
return diagnostic;
|
|
53
59
|
}
|
|
54
60
|
function rpcEndpointsUsed(c){return [...(endpointReads.get(c)||[])].filter(i=>i>=0).sort((a,b)=>a-b);}
|
|
55
|
-
|
|
61
|
+
function rpcRoutes(c){return endpointOutcomes.get(c)?.snapshot()||[];}
|
|
62
|
+
module.exports={viem,root,assets,networks,bondAbi,endpoints,client,connect,snapshotBlock,resilientTransport,rpcEndpointsUsed,rpcRoutes,serialize,safeError};
|
package/package.json
CHANGED
package/pipeline-onchain.cjs
CHANGED
|
@@ -41,9 +41,10 @@ async function parallel(items, workers, fn) {
|
|
|
41
41
|
}));
|
|
42
42
|
return results;
|
|
43
43
|
}
|
|
44
|
-
async function connect(n) {
|
|
44
|
+
async function connect(n, onClient=()=>{}) {
|
|
45
45
|
if(!h.endpoints(n.chain_id).length)throw new Error('No RPC endpoint configured; public fallback is disabled.');
|
|
46
46
|
const c=h.client(n.chain_id);
|
|
47
|
+
onClient(c);
|
|
47
48
|
if(await c.getChainId()!==n.chain_id)throw new Error('RPC chain identity mismatch');
|
|
48
49
|
endpointIdentity.set(c,h.rpcEndpointsUsed(c)[0]??0);return c;
|
|
49
50
|
}
|
|
@@ -149,7 +150,7 @@ async function scanChain(input, n, output, baseline=false) {
|
|
|
149
150
|
let checkpoint=fs.existsSync(checkpointFile)?JSON.parse(fs.readFileSync(checkpointFile)):null;
|
|
150
151
|
if(checkpoint&&(checkpoint.wallet!==input.wallet.toLowerCase()||checkpoint.chain_id!==n.chain_id||checkpoint.fingerprint!==fingerprint&&(!checkpoint.candidates||checkpoint.candidates.some(a=>!candidates.includes(a)))))
|
|
151
152
|
throw new Error('Saved balance checkpoint does not match this research request. Start a new holdings analysis.');
|
|
152
|
-
const c = await connect(n), snapshot = await h.snapshotBlock(c,checkpoint),block=snapshot.number;
|
|
153
|
+
const c = await connect(n,c=>{rpcClient=c;}), snapshot = await h.snapshotBlock(c,checkpoint),block=snapshot.number;
|
|
153
154
|
checkpoint=checkpoint||{wallet:input.wallet.toLowerCase(),chain_id:n.chain_id,block_number:block.toString(),block_hash:snapshot.hash,observed_at:stamp(),balances:{}};
|
|
154
155
|
checkpoint.fingerprint=fingerprint;checkpoint.candidates=candidates;row.candidate_fingerprint=fingerprint;
|
|
155
156
|
row.preferred_endpoint_index=endpointIdentity.get(c);
|
|
@@ -233,6 +234,7 @@ async function scanChain(input, n, output, baseline=false) {
|
|
|
233
234
|
row.status = row.registry_scan.complete && errors.length===0?'complete':'partial';
|
|
234
235
|
} catch(e) {row.error=h.safeError(e);}
|
|
235
236
|
row.rpc_endpoint_indices=h.rpcEndpointsUsed(rpcClient);
|
|
237
|
+
row.rpc_routes=h.rpcRoutes(rpcClient);
|
|
236
238
|
row.endpoint_index=row.rpc_endpoint_indices.length===1?row.rpc_endpoint_indices[0]:null;
|
|
237
239
|
write(file,row);
|
|
238
240
|
emitChain(row);
|
package/rpc-pool.cjs
CHANGED
|
@@ -64,6 +64,26 @@ function blockTag(request) {
|
|
|
64
64
|
const tag = request.params?.[1];
|
|
65
65
|
return ['eth_call','eth_getBalance','eth_getCode'].includes(request.method) && typeof tag === 'string' && /^0x[0-9a-f]+$/i.test(tag) ? tag.toLowerCase() : null;
|
|
66
66
|
}
|
|
67
|
+
class RouteRecorder {
|
|
68
|
+
constructor() { this.rows=new Map();this.overflow=new Map(); }
|
|
69
|
+
record(index, endpointClass, event) {
|
|
70
|
+
if(!['public','custom'].includes(endpointClass)||!['succeeded','failed','skipped','queue_timeout'].includes(event.outcome))return;
|
|
71
|
+
const row={endpoint_index:Number.isInteger(index)?index:null,endpoint_class:endpointClass,
|
|
72
|
+
method:/^[a-zA-Z0-9_]{1,64}$/.test(event.method||'')?event.method:'other',outcome:event.outcome};
|
|
73
|
+
if(['transport','throttle','size','method','contract','cooldown'].includes(event.reason))row.reason=event.reason;
|
|
74
|
+
for(const name of ['http_status','rpc_code'])if(Number.isSafeInteger(event[name]))row[name]=event[name];
|
|
75
|
+
const key=JSON.stringify(row);
|
|
76
|
+
let saved=this.rows.get(key);
|
|
77
|
+
if(!saved && this.rows.size<96){saved={...row,count:0};this.rows.set(key,saved);}
|
|
78
|
+
if(!saved){
|
|
79
|
+
const bucket=endpointClass+':'+event.outcome;
|
|
80
|
+
saved=this.overflow.get(bucket);
|
|
81
|
+
if(!saved){saved={endpoint_class:endpointClass,outcome:event.outcome,aggregated_overflow:true,count:0};this.overflow.set(bucket,saved);}
|
|
82
|
+
}
|
|
83
|
+
saved.count=Math.min(Number.MAX_SAFE_INTEGER,saved.count+1);
|
|
84
|
+
}
|
|
85
|
+
snapshot(){return [...this.rows.values(),...this.overflow.values()].map(row=>({...row}));}
|
|
86
|
+
}
|
|
67
87
|
class RpcPool {
|
|
68
88
|
constructor({publicUrls = [], healthFile = null, interval = 2000, deadline = 12000, attemptTimeout = 4000, cooldown = 60000} = {}) {
|
|
69
89
|
this.publicUrls = new Set(publicUrls); this.healthFile = healthFile;
|
|
@@ -123,13 +143,13 @@ class RpcPool {
|
|
|
123
143
|
finally { clearTimeout(timer); signal.removeEventListener('abort', abort); }
|
|
124
144
|
} finally { release(); }
|
|
125
145
|
}
|
|
126
|
-
async run(chain, choices, request, transport, options, onSuccess, signal, deadlineAt) {
|
|
146
|
+
async run(chain, choices, request, transport, options, onSuccess, signal, deadlineAt, onOutcome) {
|
|
127
147
|
let last = failure('RPC providers are temporarily unavailable. Retry holdings shortly.');
|
|
128
148
|
let attempts = 0;
|
|
129
149
|
for (const [index, url] of choices.entries()) {
|
|
130
150
|
if (signal.aborted) throw signal.reason;
|
|
131
151
|
const state = this.state(chain, url, transport, options);
|
|
132
|
-
if (state.until > Date.now() || (state.methods.get(request.method) || 0) > Date.now()) {last=state.error||last;continue;}
|
|
152
|
+
if (state.until > Date.now() || (state.methods.get(request.method) || 0) > Date.now()) {onOutcome(index,{method:request.method,outcome:'skipped',reason:'cooldown'});last=state.error||last;continue;}
|
|
133
153
|
// An endpoint attempt includes its queue, cold verification and family pacing.
|
|
134
154
|
// Public outages must leave time for the personal backup's actual read.
|
|
135
155
|
const remaining=deadlineAt-Date.now();
|
|
@@ -140,11 +160,13 @@ class RpcPool {
|
|
|
140
160
|
signal.addEventListener('abort',abort,{once:true});
|
|
141
161
|
const timer=budget<remaining?setTimeout(()=>controller.abort(failure('RPC endpoint attempt timed out.', 'TimeoutError')),budget):null;
|
|
142
162
|
let release;
|
|
163
|
+
let attempted=false;
|
|
143
164
|
try {
|
|
144
165
|
release=await state.gate.acquire(controller.signal);
|
|
145
166
|
// A request ahead of us may have cooled this endpoint while we waited.
|
|
146
|
-
if (state.until > Date.now() || (state.methods.get(request.method) || 0) > Date.now()) {last=state.error||last;continue;}
|
|
167
|
+
if (state.until > Date.now() || (state.methods.get(request.method) || 0) > Date.now()) {onOutcome(index,{method:request.method,outcome:'skipped',reason:'cooldown'});last=state.error||last;continue;}
|
|
147
168
|
if (++attempts > 4) break;
|
|
169
|
+
attempted=true;
|
|
148
170
|
const timeout=attempts===1?this.attemptTimeout:Math.min(this.attemptTimeout,2000);
|
|
149
171
|
if (!state.verified) {
|
|
150
172
|
const id = await this.send(state, {method:'eth_chainId'}, controller.signal, timeout);
|
|
@@ -168,11 +190,18 @@ class RpcPool {
|
|
|
168
190
|
throw error;
|
|
169
191
|
}
|
|
170
192
|
onSuccess(index, state); state.strikes = 0;state.error=null;
|
|
193
|
+
onOutcome(index,{method:request.method,outcome:'succeeded'});
|
|
171
194
|
if (state.until) { state.until = 0; this.persist(); }
|
|
172
195
|
return result;
|
|
173
196
|
} catch (error) {
|
|
174
|
-
if (signal.aborted) throw signal.reason;
|
|
175
197
|
last = error;state.error=error; const kind = classify(error);
|
|
198
|
+
const event={method:request.method,outcome:attempted?'failed':'queue_timeout',reason:kind};
|
|
199
|
+
for(let cause=error,depth=0;cause&&depth<10;cause=cause.cause,depth++){
|
|
200
|
+
if(Number.isInteger(cause.status))event.http_status=cause.status;
|
|
201
|
+
if(Number.isInteger(cause.code))event.rpc_code=cause.code;
|
|
202
|
+
}
|
|
203
|
+
onOutcome(index,event);
|
|
204
|
+
if (signal.aborted) throw signal.reason;
|
|
176
205
|
if (kind === 'contract' || kind === 'size') throw error;
|
|
177
206
|
if (kind === 'method') state.methods.set(request.method, Date.now() + 300000);
|
|
178
207
|
else {
|
|
@@ -184,7 +213,7 @@ class RpcPool {
|
|
|
184
213
|
}
|
|
185
214
|
throw last;
|
|
186
215
|
}
|
|
187
|
-
transport(chain, urls, transport = viem.http, onSuccess = () => {}) {
|
|
216
|
+
transport(chain, urls, transport = viem.http, onSuccess = () => {}, onOutcome = () => {}) {
|
|
188
217
|
if (!urls.length) throw failure('No configured RPC endpoint for this chain.');
|
|
189
218
|
const pool = this;
|
|
190
219
|
// Leave time for a personal backup after two public providers. Preserve explicit custom-first order.
|
|
@@ -203,7 +232,8 @@ class RpcPool {
|
|
|
203
232
|
if (external?.aborted) abort(); else external?.addEventListener('abort', abort, {once:true});
|
|
204
233
|
const deadlineAt=Date.now()+pool.deadline;
|
|
205
234
|
const timer = setTimeout(() => controller.abort(failure('RPC read deadline exceeded. Retry holdings.', 'TimeoutError')), pool.deadline);
|
|
206
|
-
const promise = pool.run(chain, choices.map(row=>row.url), request, transport, options, (index,state)=>onSuccess(choices[index].index,state), controller.signal,deadlineAt
|
|
235
|
+
const promise = pool.run(chain, choices.map(row=>row.url), request, transport, options, (index,state)=>onSuccess(choices[index].index,state), controller.signal,deadlineAt,
|
|
236
|
+
(index,event)=>onOutcome(choices[index].index,event)).then(value => {
|
|
207
237
|
if (hash) { if (pool.cache.size >= 512) pool.cache.delete(pool.cache.keys().next().value); pool.cache.set(key, {value,expires:Date.now()+300000}); }
|
|
208
238
|
return value;
|
|
209
239
|
}).finally(() => { clearTimeout(timer); external?.removeEventListener('abort', abort); if (pool.pending.get(key) === promise) pool.pending.delete(key); });
|
|
@@ -213,4 +243,4 @@ class RpcPool {
|
|
|
213
243
|
});
|
|
214
244
|
}
|
|
215
245
|
}
|
|
216
|
-
module.exports = {RpcPool, classify};
|
|
246
|
+
module.exports = {RpcPool, RouteRecorder, classify};
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"chain_id_source": "https://github.com/wevm/viem/blob/main/src/chains/definitions/base.ts",
|
|
28
28
|
"public_rpc": [
|
|
29
29
|
"https://base-rpc.publicnode.com",
|
|
30
|
-
"https://base.drpc.org"
|
|
31
|
-
"https://public.1rpc.io/base"
|
|
30
|
+
"https://base.drpc.org"
|
|
32
31
|
],
|
|
33
32
|
"explorer": "https://basescan.org",
|
|
34
33
|
"public_rpc_sources": [
|
|
@@ -143,9 +142,12 @@
|
|
|
143
142
|
"mintclub_bond_address": "0xc5a076cad94176c2996B32d8466Be1cE757FAa27",
|
|
144
143
|
"chain_id_source": "https://github.com/wevm/viem/blob/main/src/chains/definitions/shibarium.ts",
|
|
145
144
|
"public_rpc": [
|
|
146
|
-
"https://rpc.
|
|
145
|
+
"https://rpc.shibarium.shib.io"
|
|
147
146
|
],
|
|
148
|
-
"explorer": "https://shibariumscan.io"
|
|
147
|
+
"explorer": "https://shibariumscan.io",
|
|
148
|
+
"public_rpc_sources": [
|
|
149
|
+
"https://docs.shib.io/get-started/deploy-contracts"
|
|
150
|
+
]
|
|
149
151
|
},
|
|
150
152
|
{
|
|
151
153
|
"network": "cyber",
|
|
@@ -155,9 +157,13 @@
|
|
|
155
157
|
"mintclub_bond_address": "0xc5a076cad94176c2996B32d8466Be1cE757FAa27",
|
|
156
158
|
"chain_id_source": "https://github.com/wevm/viem/blob/main/src/chains/definitions/cyber.ts",
|
|
157
159
|
"public_rpc": [
|
|
160
|
+
"https://rpc.cyber.co",
|
|
158
161
|
"https://cyber.alt.technology"
|
|
159
162
|
],
|
|
160
|
-
"explorer": "https://cyberscan.co"
|
|
163
|
+
"explorer": "https://cyberscan.co",
|
|
164
|
+
"public_rpc_sources": [
|
|
165
|
+
"https://github.com/ethereum-optimism/superchain-registry/blob/main/chainList.toml"
|
|
166
|
+
]
|
|
161
167
|
},
|
|
162
168
|
{
|
|
163
169
|
"network": "ham",
|
|
@@ -299,9 +305,12 @@
|
|
|
299
305
|
"mintclub_bond_address": "0x5dfA75b0185efBaEF286E80B847ce84ff8a62C2d",
|
|
300
306
|
"chain_id_source": "https://github.com/wevm/viem/blob/main/src/chains/definitions/shibariumTestnet.ts",
|
|
301
307
|
"public_rpc": [
|
|
302
|
-
"https://puppynet.
|
|
308
|
+
"https://rpc.puppynet.shib.io"
|
|
303
309
|
],
|
|
304
|
-
"explorer": "https://puppyscan.shib.io"
|
|
310
|
+
"explorer": "https://puppyscan.shib.io",
|
|
311
|
+
"public_rpc_sources": [
|
|
312
|
+
"https://docs.shib.io/get-started/deploy-contracts"
|
|
313
|
+
]
|
|
305
314
|
},
|
|
306
315
|
{
|
|
307
316
|
"network": "avalanchefuji",
|
package/viewer/details.py
CHANGED
|
@@ -35,6 +35,8 @@ def coverage_status(wallet, coverage):
|
|
|
35
35
|
if not coverage: return 'Not researched'
|
|
36
36
|
if coverage.get('rpc_pending') or coverage.get('rpc_status') == 'pending': return 'Research pending'
|
|
37
37
|
if not coverage.get('rpc_available'): return 'RPC unavailable'
|
|
38
|
+
if coverage.get('registry_phase')=='deferred' or type(coverage.get('candidate_deferred')) is int and coverage['candidate_deferred']>0: return 'Research pending'
|
|
39
|
+
if any(type(coverage.get(k)) is int and coverage[k]>0 for k in ('candidate_balance_errors','balance_errors','registry_errors')) or coverage.get('registry_complete') is False: return 'Incomplete on-chain checks'
|
|
38
40
|
if not coverage.get('complete'): return 'Incomplete discovery'
|
|
39
41
|
return 'Researched'
|
|
40
42
|
|
|
@@ -87,7 +89,7 @@ def project_details(wallets, root):
|
|
|
87
89
|
row.update({'asset': asset, 'coverage': coverage_status(wallet, coverage),
|
|
88
90
|
'balance_observed_at':asset.get('balance_observed_at') if asset else None,
|
|
89
91
|
'price_observed_at':(asset.get('price') or {}).get('observed_at') if asset else None,
|
|
90
|
-
'status': 'Held' if asset else 'No holding recorded' if coverage
|
|
92
|
+
'status': 'Held' if asset else 'No holding recorded' if coverage_status(wallet,coverage)=='Researched' else 'Unknown'})
|
|
91
93
|
rows.append(row)
|
|
92
94
|
token['wallets'] = rows
|
|
93
95
|
held_rows = [r for r in rows if r['asset']]
|
|
@@ -107,7 +109,7 @@ def project_details(wallets, root):
|
|
|
107
109
|
held = [a for a in wallet['assets'] if a['chain_id'] == chain_id]
|
|
108
110
|
account = wallet_identity(wallet)
|
|
109
111
|
value = total_value(held)
|
|
110
|
-
if not held and coverage
|
|
112
|
+
if not held and coverage_status(wallet,coverage)=='Researched' and network['environment'] == 'mainnet': value = 0
|
|
111
113
|
account.update({'position_count': len(held), 'value_usd': value,
|
|
112
114
|
'unpriced_count': sum(a.get('value_usd') is None for a in held),
|
|
113
115
|
'coverage': coverage_status(wallet, coverage), 'has_holdings': bool(held)})
|
package/viewer/static/app.js
CHANGED
|
@@ -267,10 +267,15 @@ function aggregateRow(a){
|
|
|
267
267
|
}
|
|
268
268
|
function renderNetworkTokens(network){
|
|
269
269
|
const search=tokenSearch.trim().toLowerCase();
|
|
270
|
-
const
|
|
270
|
+
const recorded=state.details.tokens.filter(a=>a.chain_id===network.id);
|
|
271
|
+
const assets=recorded.filter(a=>(minimum===0||(a.value_usd!=null&&a.value_usd>=minimum))&&(!search||[a.symbol,a.name,a.address||'native'].some(s=>s.toLowerCase().includes(search))));
|
|
271
272
|
$('network-visible-count').textContent=assets.length;
|
|
272
273
|
$('network-token-body').innerHTML=assets.map(aggregateRow).join('');
|
|
273
274
|
$('network-token-table').hidden=assets.length===0;$('network-token-empty').hidden=assets.length>0;
|
|
275
|
+
if(!assets.length){
|
|
276
|
+
const empty=KiraView.networkEmpty(network,recorded.length);
|
|
277
|
+
$('network-token-empty').innerHTML=`<h3>${escapeHTML(empty.title)}</h3><p>${escapeHTML(empty.message)}</p>${empty.activity?'<a class="quiet-button" href="#/activity">View Activity</a>':''}`;
|
|
278
|
+
}
|
|
274
279
|
bindImages();
|
|
275
280
|
}
|
|
276
281
|
function renderDetail(){
|
package/viewer/static/jobs.js
CHANGED
|
@@ -38,9 +38,13 @@ function refreshControlState(){
|
|
|
38
38
|
$('service-mode').textContent=enabled?'Local controls':'Watch only';
|
|
39
39
|
const sample=state?.demo===true;
|
|
40
40
|
for(const id of ['open-wallet','empty-add-wallet','refresh-wallet','refresh-prices']){
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const prices=id==='refresh-prices';
|
|
42
|
+
const active=id.startsWith('refresh')&&jobList.find(j=>KiraView.active(j)&&
|
|
43
|
+
(j.input.wallet||j.input.address)?.toLowerCase()===selectedWallet&&(!prices||j.operation==='prices.refresh'));
|
|
44
|
+
const missing=prices&&!currentWallet()?.analysed_at;
|
|
45
|
+
$(id).disabled=(sample&&id.startsWith('refresh'))||!!active||missing;
|
|
46
|
+
$(id).textContent=prices?(active?.state==='queued'?'Prices queued…':active?'Refreshing prices…':'Refresh prices'):$(id).textContent;
|
|
47
|
+
$(id).title=sample?'Sample research cannot contact providers.':active?(prices?'A price refresh is already '+active.state+' for this wallet.':'A research job is already active for this wallet.'):missing?'Wait for the first saved report before refreshing prices.':prices?'Update prices for the saved report. Detailed research pauses briefly and resumes automatically.':'';
|
|
44
48
|
}
|
|
45
49
|
$('discovery-refresh').disabled=$('refresh-wallet').disabled;
|
|
46
50
|
if(typeof renderWatching==='function')renderWatching();
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
const active = job => ['queued', 'running'].includes(job.state);
|
|
20
20
|
const rpcGap = chain => (chain.rpc_available===false&&!chain.rpc_pending)||(chain.registry_complete===false&&!chain.rpc_pending&&chain.registry_phase!=='deferred')||chain.candidate_balance_errors>0;
|
|
21
21
|
const coverageGap = chain => !chain.complete||rpcGap(chain)||chain.candidate_deferred>0||chain.market_pending||chain.market_errors>0;
|
|
22
|
+
function networkEmpty(network, recordedCount) {
|
|
23
|
+
if(recordedCount)return {title:'No tokens match these filters',message:'Lower the minimum value or clear your search to see recorded holdings.',activity:false};
|
|
24
|
+
const rows=network.wallets||[];
|
|
25
|
+
if(!rows.length||rows.some(r=>['Research pending','Awaiting analysis','Not researched'].includes(r.coverage)))return {title:'Research is pending on this network',message:'No holdings have been recorded here yet. Check Activity for progress or resume saved work. Missing records do not mean zero holdings.',activity:true};
|
|
26
|
+
if(rows.some(r=>r.coverage!=='Researched'))return {title:'No holdings recorded; coverage is incomplete',message:'Some checks could not establish holdings on this network. Review Activity and retry holdings when ready. Missing records remain unknown.',activity:true};
|
|
27
|
+
return {title:'No positive holdings recorded',message:'The completed checks found no positive holdings. Coverage can still omit tokens. Refresh holdings to check again.',activity:false};
|
|
28
|
+
}
|
|
22
29
|
function discoveryNotice(wallet, readiness = null) {
|
|
23
30
|
const chains=(wallet?.chains||[]).filter(c=>c.environment==='mainnet');
|
|
24
31
|
const gaps=chains.filter(coverageGap);
|
|
@@ -62,7 +69,7 @@
|
|
|
62
69
|
const h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60), s = seconds % 60;
|
|
63
70
|
return (h ? h + 'h ' : '') + m + 'm ' + s + 's';
|
|
64
71
|
}
|
|
65
|
-
const api = { catalog, pageSize, active, jobPresentation, duration, discoveryNotice, coverageGap };
|
|
72
|
+
const api = { catalog, pageSize, active, jobPresentation, duration, discoveryNotice, coverageGap, networkEmpty };
|
|
66
73
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
67
74
|
else root.KiraView = api;
|
|
68
75
|
})(typeof globalThis !== 'undefined' ? globalThis : this);
|
package/wallet.py
CHANGED
|
@@ -256,6 +256,7 @@ def initial_result(wallet, folder, networks, discovered, chains, *, phase='basel
|
|
|
256
256
|
'rpc_status':c.get('rpc_status','pending'),'native_symbol':NATIVE.get(n['chain_id'],'ETH'),
|
|
257
257
|
'native_balance':c.get('native_balance'),'native_observed_at':c.get('observed_at'),
|
|
258
258
|
'native_block_number':c.get('block_number'),'native_block_hash':c.get('block_hash'),
|
|
259
|
+
'rpc_endpoint_indices':c.get('rpc_endpoint_indices',[]),'rpc_routes':c.get('rpc_routes',[]),
|
|
259
260
|
'general_erc20_discovery':'indexer_checked' if d.get('complete') else 'incomplete',
|
|
260
261
|
'indexer_source':d.get('source'),'indexer_complete':d.get('complete',False),'indexer_pages':len(d.get('pages',[])),
|
|
261
262
|
'discovery_status':d.get('discovery_status','pending'),'candidate_balance_errors':len(c.get('balance_errors',[])),
|
|
@@ -361,6 +362,7 @@ def finish(wallet, folder, networks, discovered):
|
|
|
361
362
|
'native_observed_at':c['observed_at'],'native_block_number':c.get('block_number'),
|
|
362
363
|
'native_block_hash':c.get('block_hash'),
|
|
363
364
|
'rpc_endpoint_indices':c.get('rpc_endpoint_indices',[]),
|
|
365
|
+
'rpc_routes':c.get('rpc_routes',[]),
|
|
364
366
|
'general_erc20_discovery':'indexer_checked' if d['complete'] else 'incomplete',
|
|
365
367
|
'indexer_source':d['source'],'indexer_pages':len(d['pages']),'indexer_complete':d['complete'],
|
|
366
368
|
'discovery_status':d.get('discovery_status','checked' if d['complete'] else 'incomplete'),
|