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.
- package/README.md +4 -2
- package/cashout.py +2 -0
- package/curve_pricing.py +38 -26
- package/docs/AGENT_TOOLS.md +4 -0
- package/docs/CLI.md +24 -6
- package/docs/RELEASE.md +70 -14
- package/kira_cli.py +25 -22
- package/kira_config.py +3 -2
- package/kira_jobs.py +77 -18
- package/kira_ows.py +9 -3
- package/kira_records.py +1 -161
- package/kira_research_tools.py +7 -6
- package/kira_service.py +11 -4
- package/market_records.py +25 -0
- package/package.json +2 -2
- package/sell_quotes.py +1 -1
- package/skills/kira-wallet/SKILL.md +44 -5
- package/viewer/model.py +3 -27
- package/viewer/refresh-curve-prices.cjs +11 -21
- package/viewer/refresh_prices.py +33 -24
package/viewer/refresh_prices.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
retained=
|
|
147
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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)}))
|