kira-wallet 0.2.1 → 0.2.3

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.
@@ -11,12 +11,12 @@ import sys
11
11
  sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
12
12
  import research as h
13
13
  from model import dex_price, within, number
14
- from curve_pricing import enrich, resolve
14
+ from curve_pricing import enrich, resolve, index_prices
15
15
  from native_assets import market_metadata
16
16
  from market_records import collected_markets, canonical_token
17
17
  from chain_lifecycle import retired
18
18
  from token_images import refresh_catalog
19
- DEX_CHAINS={1:'ethereum',8453:'base',10:'optimism',42161:'arbitrum',137:'polygon',56:'bsc',43114:'avalanche',81457:'blast',130:'unichain',7777777:'zora',33139:'apechain',177:'hashkey',8217:'kaia',7560:'cyber',109:'shibarium',4663:'robinhood'}
19
+ from wallet import DEX as DEX_CHAINS, retain_price_evidence
20
20
 
21
21
  parser=argparse.ArgumentParser();parser.add_argument('--wallet',help='Registered wallet address or tag. Omit to refresh all registered snapshots.')
22
22
  args=parser.parse_args()
@@ -59,10 +59,10 @@ for w in selected:
59
59
  chain=DEX_CHAINS.get(chain_id)
60
60
  if chain:requests.append('https://api.dexscreener.com/token-pairs/v1/'+chain+'/'+address)
61
61
  def get(url):return {'source':url,'observed_at':h.now(),'response':h.fetch(url)}
62
- fetched=list(ThreadPoolExecutor(max_workers=5).map(get,requests))
62
+ with ThreadPoolExecutor(max_workers=5) as executor:fetched=list(executor.map(get,requests))
63
63
  pairs=[]
64
64
  for r in fetched:
65
- response=r['response'];pairs.extend(response if isinstance(response,list) else response.get('pairs') or [])
65
+ response=r['response'];pairs.extend(response if isinstance(response,list) else (response.get('pairs') or []) if isinstance(response,dict) else [])
66
66
  prices=[]
67
67
  for t in snapshot['tokens']:
68
68
  candidates=[p for p in pairs if p['chainId']==DEX_CHAINS.get(t['chain_id']) and p['baseToken']['address'].lower()==t['token_address'].lower()]
@@ -76,6 +76,12 @@ for w in selected:
76
76
  shadow={'token_address':address,'dex_pools':[{'reported_price_usd':p.get('priceUsd'),'paired_tokens':[p['baseToken'],p['quoteToken']],'reported_liquidity':p.get('liquidity'),'source_url':p['url']} for p in options]}
77
77
  p=dex_price(shadow,h.now())
78
78
  if p:prices.append({'chain_id':chain_id,'address':address,**p,'reserve_symbol':symbol})
79
+ priced={(p['chain_id'],p['address'].lower()) for p in prices}
80
+ mainnets={c['chain_id'] for c in snapshot.get('coverage',[]) if c.get('environment')=='mainnet'}
81
+ indexed=index_prices([(t['chain_id'],t['token_address']) for t in snapshot['tokens']
82
+ if t['chain_id'] in mainnets and t.get('token_type','ERC20')=='ERC20' and not t.get('mintclub')
83
+ and (t['chain_id'],t['token_address'].lower()) not in priced],evidence_folder)
84
+ prices.extend(indexed['tokens'])
79
85
  curve_input=evidence_folder/'curve-refresh-input.json';h.save(curve_input,snapshot)
80
86
  try:node=subprocess.run(['node',str(Path(__file__).with_name('refresh-curve-prices.cjs')),str(curve_input)],capture_output=True,text=True,timeout=180)
81
87
  except subprocess.TimeoutExpired:rpc={'tokens':[],'errors':[{'code':'curve_budget','message':'Fresh curve checks reached their bound. Previous usable quotes are retained.'}]}
@@ -142,30 +148,33 @@ for w in selected:
142
148
  output['tokens']=list(partial.values())
143
149
  # Preserve historical quotes on a provider gap, after fresh reserve derivation.
144
150
  # Fresh unfunded/unreliable evidence must invalidate an old estimate.
145
- published={(p['chain_id'],p['address'].lower()):p for p in output['tokens']}
146
- retained=0
147
- for p in previous_market.get('tokens',[]):
148
- key=(p['chain_id'],p['address'].lower());current=published.get(key)
149
- if number(p.get('usd')) is None or p.get('quality') in ('unfunded','unreliable'):continue
150
- if current and (number(current.get('usd')) is not None or current.get('quality') in ('unfunded','unreliable')):continue
151
- published[key]={**p,'retained_from_previous':True};retained+=1
152
- output['tokens']=list(published.values());output['retained_price_records']=retained
151
+ output=retain_price_evidence(output,previous_market)
152
+ retained=sum(bool(p.get('retained_from_previous')) for p in output['tokens'])
153
+ output['retained_price_records']=retained
153
154
  if retained:output['note']+=f' {retained} token price references retain their earlier observation times because fresh USD was unavailable.'
154
155
  from sell_quotes import enrich as enrich_sell_quotes
155
156
  output=enrich_sell_quotes(snapshot,output,evidence_folder/'sell-quotes',previous=previous_market)
156
157
  try:refresh_catalog([snapshot],h.ROOT,native_images=native_market['images'],market_metadata=fetched)
157
158
  except (OSError,ValueError):output['image_refresh_status']='unavailable'
158
- if os.environ.get('KIRA_JOB_SNAPSHOT'):
159
- from kira_jobs import atomic
160
- job_folder=(h.ROOT/os.environ['KIRA_JOB_SNAPSHOT']).resolve()
161
- if not job_folder.is_relative_to((h.ROOT/'snapshots').resolve()):raise ValueError('Job path must be within snapshots/.')
162
- overlay=job_folder/'prices.json'
163
- output['snapshot_id']=w['latest_snapshot']['directory']
164
- missing_native=any(c.get('environment')=='mainnet' and float(c.get('native_balance') or 0)>0 and c.get('native_symbol')+'_USD' not in fresh_native for c in snapshot.get('coverage',[]))
165
- output['job_result_status']='completed_with_coverage_gaps' if output.get('market_discovery_status')=='partial' or missing_native or retained or output.get('reserve_price_status')=='partial' or (output.get('reserve_price_graph') or {}).get('errors') or rpc.get('errors') or any('error' in p for p in rpc['tokens']) or any(isinstance(r['response'],dict) and 'transport_error' in r['response'] for r in fetched) else 'completed'
166
- atomic(overlay,output)
167
- atomic(job_folder/'price-publication.json',{'schema_version':1,'snapshot_id':w['latest_snapshot']['directory'],
168
- 'overlay':str(overlay.relative_to(h.ROOT)),'projection':str(destination.relative_to(h.ROOT)),
169
- 'status':output['job_result_status']})
159
+ from kira_jobs import atomic
160
+ job_folder=evidence_folder.resolve()
161
+ if not job_folder.is_relative_to((h.ROOT/'snapshots').resolve()):raise ValueError('Job path must be within snapshots/.')
162
+ overlay=job_folder/'prices.json'
163
+ output['snapshot_id']=w['latest_snapshot']['directory']
164
+ missing_native=any(c.get('environment')=='mainnet' and float(c.get('native_balance') or 0)>0 and c.get('native_symbol')+'_USD' not in fresh_native for c in snapshot.get('coverage',[]))
165
+ # Name each gap so a partial price job explains itself.
166
+ output['job_result_gaps']=[name for name,found in (
167
+ ('market_discovery_partial',output.get('market_discovery_status')=='partial'),
168
+ ('native_price_missing',missing_native),
169
+ ('earlier_prices_retained',bool(retained)),
170
+ ('index_price_missing',indexed['status']=='partial'),
171
+ ('reserve_pricing_partial',output.get('reserve_price_status')=='partial' or bool((output.get('reserve_price_graph') or {}).get('errors'))),
172
+ ('curve_reads_incomplete',bool(rpc.get('errors')) or any('error' in p for p in rpc['tokens'])),
173
+ ('provider_request_failed',any(isinstance(r['response'],dict) and 'transport_error' in r['response'] for r in fetched))) if found]
174
+ output['job_result_status']='completed_with_coverage_gaps' if output['job_result_gaps'] else 'completed'
175
+ atomic(overlay,output)
176
+ if os.environ.get('KIRA_JOB_SNAPSHOT'):atomic(job_folder/'price-publication.json',{'schema_version':1,'snapshot_id':w['latest_snapshot']['directory'],
177
+ 'overlay':str(overlay.relative_to(h.ROOT)),'projection':str(destination.relative_to(h.ROOT)),
178
+ 'status':output['job_result_status']})
170
179
  temporary.write_text(json.dumps(output,indent=2)+'\n');os.replace(temporary,destination)
171
180
  print(json.dumps({'wallet':w['address'],'tag':w['tags'],'price_records':len(output['tokens']),'observed_at':output['observed_at'],'errors':sum(isinstance(r['response'],dict) and 'transport_error' in r['response'] for r in fetched)}))