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/kira_ows.py
CHANGED
|
@@ -10,7 +10,7 @@ import shutil
|
|
|
10
10
|
import subprocess
|
|
11
11
|
import threading
|
|
12
12
|
import uuid
|
|
13
|
-
from kira_jobs import JobError, JobStore, atomic, fields, identifier
|
|
13
|
+
from kira_jobs import JobError, JobStore, atomic, digest, fields, identifier
|
|
14
14
|
|
|
15
15
|
class OwsStore:
|
|
16
16
|
def __init__(self, root, vault=None, runner=None):
|
|
@@ -104,8 +104,14 @@ class OwsStore:
|
|
|
104
104
|
address=wallet['accounts'][0]['address'];store=JobStore(self.root)
|
|
105
105
|
registered=next((w for w in json.loads((self.root/'wallets.json').read_text())['wallets'] if w['address_key']==address.lower()),None)
|
|
106
106
|
job=None
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
registration_key=str(uuid.uuid5(uuid.UUID(key),'ows-register'))
|
|
108
|
+
# Pre-intent receipts from older versions may already own a queued job.
|
|
109
|
+
if not registered or receipt.get('registration_requested') or any(j['key_hash']==digest(registration_key) for j in store.list()):
|
|
110
|
+
# Record intent before admission: submit can register the wallet
|
|
111
|
+
# before this process has saved its job ID or started a worker.
|
|
112
|
+
if not receipt.get('registration_requested'):
|
|
113
|
+
receipt['registration_requested']=True;atomic(path,receipt)
|
|
114
|
+
job=store.submit({'schema_version':1,'operation':'wallet.add','input':{'address':address,'tag':tag},'idempotency_key':registration_key})
|
|
109
115
|
if job['state']=='queued':store.launch()
|
|
110
116
|
connection={'source':'ows','wallet_id':wallet['id'],'name':wallet['name'],'address':address,'tag':(registered.get('tags') or [tag])[0] if registered else tag}
|
|
111
117
|
atomic(self.session,connection);self.session.chmod(0o600)
|
package/kira_records.py
CHANGED
|
@@ -4,166 +4,6 @@ import re
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from kira_jobs import JobError
|
|
6
6
|
|
|
7
|
-
def read_receipt(name, arguments, result, delivered=True):
|
|
8
|
-
receipt={'name':name,'arguments':arguments,'status':'completed' if delivered and not (isinstance(result,dict) and result.get('error')) else 'failed'}
|
|
9
|
-
if name=='portfolio_read' and isinstance(result,dict) and isinstance(result.get('wallets'),list):
|
|
10
|
-
receipt['wallet_pages']=[read_receipt('inventory_page',{'wallet':w['address'],'offset':w.get('offset',0)},w,receipt['status']=='completed') for w in result['wallets']]
|
|
11
|
-
if name=='snapshot_read' and isinstance(result,dict) and isinstance(result.get('wallets'),list) and len(result['wallets'])==1:
|
|
12
|
-
receipt['snapshot_id']=result.get('snapshot_id')
|
|
13
|
-
result=result['wallets'][0]
|
|
14
|
-
if isinstance(result,dict):
|
|
15
|
-
for key in ('offset','next_offset','asset_count','position_count','snapshot_id','query_scope'):
|
|
16
|
-
if key in result:receipt[key]=result[key]
|
|
17
|
-
for key in ('assets','positions'):
|
|
18
|
-
if isinstance(result.get(key),list):receipt['page_end']=result.get('offset',0)+len(result[key])
|
|
19
|
-
return receipt
|
|
20
|
-
|
|
21
|
-
def read_completion(message, receipts, tools):
|
|
22
|
-
"""Validate declared plans and literal requests against delivered receipts."""
|
|
23
|
-
ranking=re.search(r'\btop[- ]*(\d+|one|two|three|four|five|ten)\b|상위\s*(\d+|다섯|열)',message,re.I)
|
|
24
|
-
requested_rank=None
|
|
25
|
-
if ranking:
|
|
26
|
-
raw=next(v for v in ranking.groups() if v).lower()
|
|
27
|
-
requested_rank=int(raw) if raw.isdigit() else {'one':1,'two':2,'three':3,'four':4,'five':5,'ten':10,'다섯':5,'열':10}[raw]
|
|
28
|
-
names=set(re.findall(r'\b(?:holdings_query|portfolio_read|wallet_read|snapshot_read|snapshots_compare|token_read|job_read|job_list|snapshots_list)\b',message))
|
|
29
|
-
literal_names=names.copy()
|
|
30
|
-
decoder=json.JSONDecoder();requested=[]
|
|
31
|
-
for match in re.finditer(r'\{',message):
|
|
32
|
-
try:value,_=decoder.raw_decode(message[match.start():])
|
|
33
|
-
except ValueError:continue
|
|
34
|
-
if isinstance(value,dict) and value.get('name',value.get('kira_tool')) in names and isinstance(value.get('arguments'),dict):
|
|
35
|
-
requested.append((value.get('name',value.get('kira_tool')),value['arguments']))
|
|
36
|
-
for receipt in receipts:
|
|
37
|
-
plan=receipt.get('arguments')
|
|
38
|
-
if receipt['name']=='records_read' and isinstance(plan,dict) and isinstance(plan.get('reads'),list):
|
|
39
|
-
for read in plan['reads']:
|
|
40
|
-
if isinstance(read,dict) and isinstance(read.get('name'),str) and isinstance(read.get('arguments'),dict):
|
|
41
|
-
names.add(read['name']);requested.append((read['name'],read['arguments']))
|
|
42
|
-
if not requested_rank and (not names or not requested and not re.search(r'\b(?:read|call|run|use|execute)\b|읽|호출|실행|수행|\+',message,re.I)):return None
|
|
43
|
-
expanded=[]
|
|
44
|
-
for receipt in receipts:
|
|
45
|
-
expanded.extend(receipt.get('reads',[]) if receipt['name']=='records_read' else [receipt])
|
|
46
|
-
expanded.extend(page for r in list(expanded) for page in r.get('wallet_pages',[]))
|
|
47
|
-
completed=[r for r in expanded if r['status']=='completed']
|
|
48
|
-
missing=[name for name in sorted(names) if not any(r['name']==name for r in completed)]
|
|
49
|
-
if tools and 'wallet_read' in literal_names:
|
|
50
|
-
context,approved=tools.allowed()
|
|
51
|
-
wallets=context['wallets']
|
|
52
|
-
selected=wallets if re.search(r'\b(?:all|each|every)\b|모든|각각|전부',message,re.I) else [w for w in wallets if w['address'].lower() in message.lower() or any(tag.lower() in message.lower() for tag in w['tags'])]
|
|
53
|
-
actual=set()
|
|
54
|
-
for receipt in completed:
|
|
55
|
-
if receipt['name']=='wallet_read':
|
|
56
|
-
try:actual.add(tools.wallet(receipt['arguments']['wallet'],approved))
|
|
57
|
-
except JobError:pass
|
|
58
|
-
missing.extend('wallet_read '+w['name'] for w in selected if w['address'].lower() not in actual)
|
|
59
|
-
if 'snapshot_read' in names:
|
|
60
|
-
snapshot_requested=set(re.findall(r'snapshots/[A-Za-z0-9_./-]+',message))
|
|
61
|
-
actual={r['arguments'].get('snapshot_id') for r in completed if r['name']=='snapshot_read'}
|
|
62
|
-
missing.extend('snapshot_read '+identity for identity in sorted(snapshot_requested-actual))
|
|
63
|
-
# Literal JSON read plans are checked by argument, including chain identity.
|
|
64
|
-
# This is a bounded parser, not a claim to understand every natural request.
|
|
65
|
-
from kira_research_tools import CATALOG
|
|
66
|
-
specifications={row['name']:row for row in CATALOG}
|
|
67
|
-
specifications['inventory_page']={'arguments':{'wallet':None},'optional_arguments':{'offset':None}}
|
|
68
|
-
def matches(name,wanted,receipt):
|
|
69
|
-
if receipt['name']!=name:return False
|
|
70
|
-
if not isinstance(wanted,dict) or not isinstance(receipt.get('arguments'),dict):return False
|
|
71
|
-
spec=specifications.get(name)
|
|
72
|
-
if spec is None:return False
|
|
73
|
-
required=set(spec['arguments']);allowed=required|set(spec.get('optional_arguments',{}))
|
|
74
|
-
if any(not required<=set(args) or set(args)-allowed for args in (wanted,receipt['arguments'])):return False
|
|
75
|
-
defaults={'offset':0,'limit':100 if name=='wallet_read' else 20} if name in ('wallet_read','snapshot_read','snapshots_compare') else {}
|
|
76
|
-
if name=='inventory_page':defaults={'offset':0}
|
|
77
|
-
wanted={**defaults,**wanted};actual={**defaults,**receipt.get('arguments',{})}
|
|
78
|
-
if set(wanted)!=set(actual):return False
|
|
79
|
-
for key,value in wanted.items():
|
|
80
|
-
other=actual.get(key)
|
|
81
|
-
if key=='minimum_value_usd':
|
|
82
|
-
from decimal import Decimal, InvalidOperation
|
|
83
|
-
try:
|
|
84
|
-
left,right=Decimal(str(value)),Decimal(str(other))
|
|
85
|
-
if not left.is_finite() or not right.is_finite() or left!=right:return False
|
|
86
|
-
except InvalidOperation:return False
|
|
87
|
-
continue
|
|
88
|
-
if key=='wallet' and tools:
|
|
89
|
-
try:
|
|
90
|
-
_,approved=tools.allowed();value=tools.wallet(value,approved);other=tools.wallet(other,approved)
|
|
91
|
-
except JobError:return False
|
|
92
|
-
if isinstance(value,str) and key in ('address','wallet'):value=value.lower();other=other.lower() if isinstance(other,str) else other
|
|
93
|
-
if type(value) is not type(other) or value!=other:return False
|
|
94
|
-
return True
|
|
95
|
-
# A delivered retry fulfils the same request. Keep failed attempts in the
|
|
96
|
-
# original receipts, but do not call a recovered request incomplete.
|
|
97
|
-
failed=[{'name':r['name'],'arguments':r.get('arguments',{})} for r in expanded
|
|
98
|
-
if r['status']=='failed' and r['name']!='inventory_page'
|
|
99
|
-
and not any(matches(r['name'],r.get('arguments',{}),ok) for ok in completed)]
|
|
100
|
-
missing.extend(name+' '+json.dumps(args,sort_keys=True) for name,args in requested if not any(matches(name,args,r) for r in completed))
|
|
101
|
-
if 'token_read' in names and not any(name=='token_read' for name,_ in requested):
|
|
102
|
-
addresses=set(re.findall(r'0x[0-9a-fA-F]{40}',message))
|
|
103
|
-
chains={int(value) for value in re.findall(r'\bchain(?:_id)?\s*[:=]?\s*(\d+)\b',message,re.I)}
|
|
104
|
-
for address in addresses:
|
|
105
|
-
relevant=[r for r in completed if r['name']=='token_read' and str(r['arguments'].get('address','')).lower()==address.lower()]
|
|
106
|
-
if not relevant:missing.append('token_read '+address)
|
|
107
|
-
for chain in chains:
|
|
108
|
-
if not any(r['arguments'].get('chain_id')==chain for r in relevant):missing.append('token_read '+str(chain)+':'+address)
|
|
109
|
-
unread=[]
|
|
110
|
-
exhaustive=bool(re.search(r'\b(?:complete|full)\s+inventory\b|\ball\s+(?:holdings|assets|positions)\b|전체\s*(?:보유|자산|목록)',message,re.I))
|
|
111
|
-
groups={}
|
|
112
|
-
for r in expanded:
|
|
113
|
-
if r['name'] not in ('wallet_read','snapshot_read','snapshots_compare','inventory_page','holdings_query'):continue
|
|
114
|
-
if r['name']=='holdings_query' and (r.get('query_scope') or {}).get('mode')!='search':continue
|
|
115
|
-
if not isinstance(r.get('arguments'),dict):continue
|
|
116
|
-
if r['status']=='failed' and any(matches(r['name'],r.get('arguments',{}),ok) for ok in completed):continue
|
|
117
|
-
if r['name']=='inventory_page' and not exhaustive:continue
|
|
118
|
-
identity={k:v for k,v in r['arguments'].items() if k not in ('offset','limit')}
|
|
119
|
-
if 'wallet' in identity and tools:
|
|
120
|
-
try:identity['wallet']=tools.wallet(identity['wallet'],tools.allowed()[1])
|
|
121
|
-
except JobError:pass
|
|
122
|
-
name='wallet_read' if r['name']=='inventory_page' else r['name']
|
|
123
|
-
groups.setdefault((name,json.dumps(identity,sort_keys=True),r.get('snapshot_id')),[]).append(r)
|
|
124
|
-
for (name,identity,snapshot),pages in groups.items():
|
|
125
|
-
offset=0;seen=set();finished=False
|
|
126
|
-
while offset not in seen:
|
|
127
|
-
seen.add(offset);page=next((r for r in pages if r['status']=='completed' and r['arguments'].get('offset',0)==offset and 'next_offset' in r),None)
|
|
128
|
-
if not page:break
|
|
129
|
-
if page['next_offset'] is None:finished=True;break
|
|
130
|
-
if type(page['next_offset']) is not int or page['next_offset']<=offset:break
|
|
131
|
-
offset=page['next_offset']
|
|
132
|
-
if not finished and (exhaustive or any(r.get('next_offset') is not None for r in pages)):
|
|
133
|
-
unread.append({'name':name,'arguments':{**json.loads(identity),'offset':offset},'snapshot_id':snapshot})
|
|
134
|
-
if requested_rank:
|
|
135
|
-
from decimal import Decimal
|
|
136
|
-
ranked=[r for r in completed if r['name']=='holdings_query' and (r.get('query_scope') or {}).get('mode')=='rank']
|
|
137
|
-
rank_pages={};approved_wallets=None
|
|
138
|
-
for r in ranked:
|
|
139
|
-
q=r['query_scope']
|
|
140
|
-
if not q.get('full_inventory_scanned') or not q.get('complete'):continue
|
|
141
|
-
identity={'environment':'mainnet','group_by':'asset','query':'',
|
|
142
|
-
**{k:v for k,v in r['arguments'].items() if k not in ('offset','limit')}}
|
|
143
|
-
identity['query']=identity['query'].strip().casefold()
|
|
144
|
-
if 'minimum_value_usd' in identity:identity['minimum_value_usd']=Decimal(str(identity['minimum_value_usd']))
|
|
145
|
-
if 'wallet' in identity and tools:
|
|
146
|
-
try:
|
|
147
|
-
if approved_wallets is None:approved_wallets=tools.allowed()[1]
|
|
148
|
-
identity['wallet']=tools.wallet(identity['wallet'],approved_wallets)
|
|
149
|
-
except JobError:continue
|
|
150
|
-
rank_pages.setdefault((tuple(sorted(identity.items())),q['result_count']),[]).append(q)
|
|
151
|
-
rank_complete=False
|
|
152
|
-
for (_,available),pages in rank_pages.items():
|
|
153
|
-
covered=0
|
|
154
|
-
for page in sorted(pages,key=lambda q:q['offset']):
|
|
155
|
-
if page['offset']>covered:break
|
|
156
|
-
covered=max(covered,page['offset']+page['returned_count'])
|
|
157
|
-
if covered>=min(requested_rank,available):rank_complete=True;break
|
|
158
|
-
if not rank_complete:
|
|
159
|
-
missing.append('holdings_query rank for the requested top '+str(requested_rank))
|
|
160
|
-
for r in completed:
|
|
161
|
-
q=r.get('query_scope')
|
|
162
|
-
if q and not q.get('complete'):missing.append('holdings_query result detail was truncated')
|
|
163
|
-
missing.extend('holdings_query has unread matching records' for r in unread if r['name']=='holdings_query')
|
|
164
|
-
if exhaustive:missing.extend(r['name']+' unread offset '+str(r['arguments']['offset']) for r in unread)
|
|
165
|
-
return {'status':'partial' if missing or failed else 'complete','missing':list(dict.fromkeys(missing)),'failed':failed,'unread_pages':unread,'completed_reads':sum(r['name']!='inventory_page' for r in completed)}
|
|
166
|
-
|
|
167
7
|
def scoped_wallets(root,config):
|
|
168
8
|
import sys
|
|
169
9
|
sys.path.insert(0,str(Path(__file__).resolve().parent/'viewer'))
|
|
@@ -209,7 +49,7 @@ def projection(root, config, *, token_id=None, limit=12, offset=0, wallet_key=No
|
|
|
209
49
|
def compact_wallet_facts(rows):
|
|
210
50
|
asset_fields=('id','chain_id','address','display_identity','symbol','name','environment','balance','is_native','value_usd','historical_value_usd','balance_observed_at','price','pools','pools_recorded','pools_omitted','exit_quote_recorded','archived')
|
|
211
51
|
return [{**row,'assets':[{key:asset[key] for key in asset_fields if key in asset and
|
|
212
|
-
(asset[key] is not None or key in ('value_usd','price'))}
|
|
52
|
+
(asset[key] is not None or key in ('address','value_usd','price'))}
|
|
213
53
|
for asset in row['assets']]} for row in rows]
|
|
214
54
|
|
|
215
55
|
def recorded_valuation(assets):
|
package/kira_research_tools.py
CHANGED
|
@@ -11,7 +11,7 @@ CATALOG = [
|
|
|
11
11
|
{'name':'wallet_read','arguments':{'wallet':'registered address or exact name'},'optional_arguments':{'offset':'nonnegative integer, default 0','limit':'integer 1 to 100, default 100'},'description':'Read a compact holdings page of one approved wallet. Follow next_offset until null for the complete recorded inventory. Use token_read for curve, market and exit details.'},
|
|
12
12
|
{'name':'holdings_query','arguments':{'mode':'search, rank or aggregate'},'optional_arguments':{'query':'Token symbol, name, exact contract or native. Required for search. Use one query per token.','wallet':'Optional registered wallet name or address; omit for every approved wallet.','chain_id':'Optional positive chain ID','environment':'mainnet (default), testnet or all','group_by':'asset (default: sum priced positions by chain and contract) or position','minimum_value_usd':'Optional nonnegative USD minimum per position; unknowns excluded and counted.','offset':'nonnegative integer, default 0','limit':'integer 1 to 25, default 10'},'description':'Search names, rank top N or aggregate the ENTIRE approved saved inventory on the host. No inventory pagination or jobs needed. Returns exact counts, values, ambiguity and bounded detailed matches. Use search for symbol-only questions, rank for top-N questions and aggregate for counts or sums.'},
|
|
13
13
|
{'name':'token_read','arguments':{'chain_id':'positive integer','address':'contract address or native'},'optional_arguments':{'minimum_value_usd':'Nonnegative USD number or decimal string. Filter by recorded mainnet value; unknowns stay separate.'},'description':'Read this asset across ALL approved wallets in one request, including saved native observations. Use the optional minimum for value-threshold questions and the exact returned position_count. No wallet inventory pagination is needed.'},
|
|
14
|
-
{'name':'snapshots_list','arguments':{'wallet':'registered address or exact name'},'description':'List
|
|
14
|
+
{'name':'snapshots_list','arguments':{'wallet':'registered address or exact name'},'optional_arguments':{'kind':'holdings (default), prices or all'},'description':'List saved holdings analyses or immutable price observations for an approved wallet.'},
|
|
15
15
|
{'name':'snapshot_read','arguments':{'snapshot_id':'saved snapshot ID'},'optional_arguments':{'offset':'nonnegative integer, default 0','limit':'integer 1 to 100, default 20'},'description':'Read a compact page of historical evidence for an approved wallet.'},
|
|
16
16
|
{'name':'snapshots_compare','arguments':{'before':'saved snapshot ID','after':'saved snapshot ID'},'optional_arguments':{'offset':'nonnegative integer, default 0','limit':'integer 1 to 100, default 20'},'description':'Compare a page of recorded positions of the same approved wallet.'},
|
|
17
17
|
{'name':'job_list','arguments':{},'description':'Read research jobs only for approved wallets.'},
|
|
@@ -62,21 +62,22 @@ class ResearchTools:
|
|
|
62
62
|
|
|
63
63
|
def snapshot(self, identity, approved, offset=0, limit=20):
|
|
64
64
|
# Only published historical analyses listed for this scope may be read.
|
|
65
|
-
visible={row['snapshot_id'] for key in approved for row in self.store.snapshots(key)}
|
|
65
|
+
visible={row['snapshot_id'] for key in approved for row in self.store.snapshots(key,kind='all')}
|
|
66
66
|
if identity not in visible:raise JobError('snapshot_scope','This analysis is outside the current conversation scope.')
|
|
67
67
|
result=self.store.snapshot(identity)
|
|
68
68
|
if result.get('wallet_address','').lower() not in approved:raise JobError('snapshot_scope','This analysis does not belong to the approved wallet.')
|
|
69
69
|
from kira_records import wallet_facts, compact_wallet_facts, recorded_valuation
|
|
70
70
|
from model import project_wallet
|
|
71
71
|
entry=self.store.wallet(result['wallet_address'])
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
observation=result.get('price_observation')
|
|
73
|
+
entry={**entry,'latest_snapshot':{'directory':observation['holdings_snapshot'] if observation else identity}}
|
|
74
|
+
wallet=project_wallet(entry,result,self.root,market=result.get('price_overlay'));count=len(wallet['assets'])
|
|
74
75
|
wallet['recorded_valuation']=recorded_valuation(wallet['assets'])
|
|
75
76
|
wallet['assets']=wallet['assets'][offset:offset+limit]
|
|
76
77
|
rows=compact_wallet_facts(wallet_facts([wallet],pool_budget=0))
|
|
77
78
|
rows[0].update(asset_count=count,assets_omitted=count-len(wallet['assets']),offset=offset,
|
|
78
79
|
next_offset=offset+len(wallet['assets']) if offset+len(wallet['assets'])<count else None)
|
|
79
|
-
return {'snapshot_id':identity,'wallets':rows,'note':'Historical recorded evidence. Prices and balances retain their observation times; curve backing is not executable cash. Follow next_offset for more holdings.'}
|
|
80
|
+
return {'snapshot_id':identity,**({'price_observation':observation} if observation else {}),'wallets':rows,'note':'Historical recorded evidence. Prices and balances retain their observation times; curve backing is not executable cash. Follow next_offset for more holdings.'}
|
|
80
81
|
|
|
81
82
|
def job(self, job, approved):
|
|
82
83
|
if job['operation'] not in ('wallet.add','wallet.refresh','prices.refresh'):
|
|
@@ -159,7 +160,7 @@ class ResearchTools:
|
|
|
159
160
|
return {'identity':identity,'positions':positions,'position_count':len(positions),'unfiltered_position_count':total,
|
|
160
161
|
'unpriced_position_count':unpriced,'outside_usd_scope_count':outside,'curve_comparison':compare_curve_states(positions),
|
|
161
162
|
'note':'An absent record is unknown, not a verified zero holding.'}
|
|
162
|
-
if name=='snapshots_list':return self.store.snapshots(self.wallet(arguments['wallet'],approved))
|
|
163
|
+
if name=='snapshots_list':return self.store.snapshots(self.wallet(arguments['wallet'],approved),arguments.get('kind','holdings'))
|
|
163
164
|
if name=='snapshot_read':return self.snapshot(arguments['snapshot_id'],approved,arguments.get('offset',0),arguments.get('limit',20))
|
|
164
165
|
if name=='snapshots_compare':
|
|
165
166
|
self.snapshot(arguments['before'],approved);self.snapshot(arguments['after'],approved)
|
package/kira_service.py
CHANGED
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
import time
|
|
6
6
|
import uuid
|
|
7
|
-
from kira_jobs import JobStore, JobError, atomic, fields
|
|
7
|
+
from kira_jobs import JobStore, JobError, atomic, fields, STATE_FILTERS
|
|
8
8
|
from kira_state import StateCache, page, page_bounds
|
|
9
9
|
from kira_research_tools import ResearchTools, CATALOG
|
|
10
10
|
|
|
@@ -18,11 +18,16 @@ for row in CATALOG:
|
|
|
18
18
|
schema={'type':'integer','minimum':1 if name in ('chain_id','limit') else 0} if name in ('chain_id','offset','limit') else dict(STRING)
|
|
19
19
|
if name=='minimum_value_usd':schema={'type':['string','number']}
|
|
20
20
|
if name=='limit':schema['maximum']=25 if row['name']=='holdings_query' else 100
|
|
21
|
+
if row['name']=='snapshots_list' and name=='kind':schema['enum']=['holdings','prices','all']
|
|
21
22
|
properties[name]={**schema,'description':description}
|
|
22
23
|
if row['name'] in ('portfolio_read','job_list','snapshots_list'):
|
|
23
24
|
properties.update(offset=INTEGER,limit={'type':'integer','minimum':1,'maximum':100})
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
summary=row['description']
|
|
26
|
+
if row['name']=='job_list':
|
|
27
|
+
summary='Read a newest-first page of durable jobs. Optionally filter by state or registered wallet.'
|
|
28
|
+
properties.update(state={'type':'string','enum':list(STATE_FILTERS),'description':'Default all. Groups: active (queued, running), succeeded (succeeded, partial), attention (interrupted, failed). The other values match one exact state.'},
|
|
29
|
+
wallet={**STRING,'description':'registered address or exact name'})
|
|
30
|
+
READ_TOOLS.append({'name':row['name'],'description':summary,
|
|
26
31
|
'inputSchema':{'type':'object','properties':properties,'required':list(row['arguments']),'additionalProperties':False},
|
|
27
32
|
'annotations':{'readOnlyHint':True,'destructiveHint':False,'idempotentHint':True,'openWorldHint':False}})
|
|
28
33
|
|
|
@@ -63,6 +68,8 @@ def validate(arguments,schema):
|
|
|
63
68
|
field=schema['properties'][name];types=field['type'] if isinstance(field['type'],list) else [field['type']]
|
|
64
69
|
if not any((type(value) in kinds[k] if isinstance(kinds[k],tuple) else type(value) is kinds[k]) for k in types):
|
|
65
70
|
raise JobError('invalid_input','Invalid argument type: '+name)
|
|
71
|
+
if 'enum' in field and value not in field['enum']:
|
|
72
|
+
raise JobError('invalid_input','Choose '+name+' from: '+', '.join(map(str,field['enum']))+'.')
|
|
66
73
|
if isinstance(value,str) and (len(value)>field.get('maxLength',500) or len(value)<field.get('minLength',0)):
|
|
67
74
|
raise JobError('invalid_input','Use a bounded identifier: '+name)
|
|
68
75
|
if type(value) in (int,float) and (value<field.get('minimum',value) or value>field.get('maximum',value)):
|
|
@@ -122,7 +129,7 @@ class Application:
|
|
|
122
129
|
return {'schema_version':1,'dashboard':state['dashboard'],**page(rows,offset,limit,key='wallets'),
|
|
123
130
|
'data_revision':self.views.revision,'note':'Saved summaries only. Use wallet_read for holdings pages and holdings_query for search, ranking and aggregates.'}
|
|
124
131
|
if name=='snapshots_list':
|
|
125
|
-
return page(self.jobs.snapshots(arguments['wallet']),arguments.get('offset',0),arguments.get('limit',100),key='snapshots')
|
|
132
|
+
return page(self.jobs.snapshots(arguments['wallet'],arguments.get('kind','holdings')),arguments.get('offset',0),arguments.get('limit',100),key='snapshots')
|
|
126
133
|
return self.records.call(name,arguments,str(uuid.uuid4()))
|
|
127
134
|
|
|
128
135
|
def select(self,value):
|
package/market_records.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Market identity and public routes. Pool existence never establishes a price."""
|
|
2
|
+
from decimal import Decimal, InvalidOperation
|
|
2
3
|
import json
|
|
4
|
+
import math
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
import re
|
|
5
7
|
from urllib.parse import quote, urlsplit
|
|
@@ -9,6 +11,29 @@ NETWORKS={n['chain_id']:n['network'] for n in json.loads((Path(__file__).parent/
|
|
|
9
11
|
UNISWAP_CHAINS={1:'ethereum',10:'optimism',56:'bnb',137:'polygon',8453:'base',42161:'arbitrum',43114:'avalanche',81457:'blast',130:'unichain',7777777:'zora',4663:'robinhood'}
|
|
10
12
|
|
|
11
13
|
|
|
14
|
+
def number(value):
|
|
15
|
+
try:
|
|
16
|
+
n = float(Decimal(str(value)))
|
|
17
|
+
return n if math.isfinite(n) and n >= 0 else None
|
|
18
|
+
except (InvalidOperation, TypeError, ValueError, OverflowError): return None
|
|
19
|
+
|
|
20
|
+
MIN_PRICE_LIQUIDITY_USD=1000
|
|
21
|
+
|
|
22
|
+
def checked_price(price, pools=()):
|
|
23
|
+
"""A spot quote needs a meaningful observed market, including saved quotes."""
|
|
24
|
+
if not price or price.get('basis')!='DEX market':return price
|
|
25
|
+
liquidity=number(price.get('liquidity_usd'))
|
|
26
|
+
if liquidity is None:
|
|
27
|
+
pool=next((p for p in pools if p.get('source_url')==price.get('source')),None)
|
|
28
|
+
liquidity=number((pool.get('reported_liquidity') or {}).get('usd')) if pool else None
|
|
29
|
+
base=number((pool.get('reported_liquidity') or {}).get('base')) if pool else None
|
|
30
|
+
usd=number(price.get('usd'))
|
|
31
|
+
if liquidity and base is not None and usd is not None and base*usd/liquidity>0.98:
|
|
32
|
+
return {**price,'usd':None,'quality':'unreliable','basis':'Insufficient market liquidity','liquidity_usd':liquidity}
|
|
33
|
+
if liquidity is None or liquidity<MIN_PRICE_LIQUIDITY_USD:
|
|
34
|
+
return {**price,'usd':None,'quality':'unreliable','basis':'Insufficient market liquidity','liquidity_usd':liquidity}
|
|
35
|
+
return {**price,'liquidity_usd':liquidity}
|
|
36
|
+
|
|
12
37
|
def market_url(chain_id, pool):
|
|
13
38
|
"""Choose a known venue route without changing the recorded data source."""
|
|
14
39
|
venue=str(pool.get('venue','')).lower()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kira-wallet",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Local wallet research for your agent session and a human-controlled web workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"skills/kira-wallet"
|
|
105
105
|
],
|
|
106
106
|
"scripts": {
|
|
107
|
-
"test": "python3 -m unittest discover -s . -p 'test_*.py' && python3 -m unittest discover -s viewer -p 'test_*.py' && node test-onchain.cjs && node test-onchain-continuation.cjs && node test-asset-review.cjs && node test-cashout-quotes.cjs && node test-cashout-routes.cjs && node --check viewer/static/app.js && node --check viewer/static/jobs.js && node test-workspace.cjs && node test-scan-ux.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/report.js && node test-report-ui.cjs && node test-holdings-ui.cjs && node test-settings-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/selects.js && node --check site/demo.js && node test-rpc-pool.cjs && node test-zero-verification.cjs && node test-market-discovery.cjs && node test-sell-quotes.cjs && node test-external-ui.cjs"
|
|
107
|
+
"test": "python3 -m unittest discover -s . -p 'test_*.py' && python3 -m unittest discover -s viewer -p 'test_*.py' && node test-onchain.cjs && node test-onchain-continuation.cjs && node test-asset-review.cjs && node test-cashout-quotes.cjs && node test-cashout-routes.cjs && node --check viewer/static/app.js && node --check viewer/static/jobs.js && node test-workspace.cjs && node test-scan-ux.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/report.js && node test-report-ui.cjs && node test-holdings-ui.cjs && node test-settings-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/selects.js && node --check site/demo.js && node test-rpc-pool.cjs && node test-zero-verification.cjs && node test-market-discovery.cjs && node test-sell-quotes.cjs && node test-external-ui.cjs && node test-refresh-curve-prices.cjs"
|
|
108
108
|
},
|
|
109
109
|
"dependencies": {
|
|
110
110
|
"viem": "2.57.2"
|
package/sell_quotes.py
CHANGED
|
@@ -68,7 +68,7 @@ def candidates(snapshot, market):
|
|
|
68
68
|
if len(pair) != 2 or address not in pair:
|
|
69
69
|
continue
|
|
70
70
|
price = output_price(token['chain_id'], next(iter(pair - {address})), market)
|
|
71
|
-
if not price
|
|
71
|
+
if not price:
|
|
72
72
|
continue
|
|
73
73
|
pools.append(pool)
|
|
74
74
|
if not pools:
|
|
@@ -12,8 +12,9 @@ and MCP operations.
|
|
|
12
12
|
|
|
13
13
|
## Start with the existing workspace
|
|
14
14
|
|
|
15
|
-
1. Run `kira --version`, then `kira info --json` and inspect
|
|
16
|
-
`
|
|
15
|
+
1. Run `kira --version`, then `kira info --json` and inspect `data_dir` and
|
|
16
|
+
`research_connection.status`: `ready` means Alchemy is connected;
|
|
17
|
+
`connection_required` means new research cannot start yet. The commands below require 0.2.0 or later. If installation is
|
|
17
18
|
part of the request, use `npm install -g kira-wallet` or the supplied
|
|
18
19
|
prepared package archive. Requirements are Node.js 22+, Python 3.11+ and
|
|
19
20
|
POSIX; macOS is verified.
|
|
@@ -44,8 +45,12 @@ kira config show --json
|
|
|
44
45
|
|
|
45
46
|
An existing private environment file can be referenced with
|
|
46
47
|
`kira config import-env --file /absolute/private/rpc.env --key-env ALCHEMY_API_KEY --json`.
|
|
47
|
-
Use the human-supplied path and name. This does not copy keys.
|
|
48
|
-
|
|
48
|
+
Use the human-supplied path and name. This does not copy keys. It also selects
|
|
49
|
+
Alchemy for token discovery and network reads, so a separate
|
|
50
|
+
`kira discovery alchemy` step is not needed. Configuration commands with
|
|
51
|
+
`--json` return jobs; check completion before claiming readiness. A finished
|
|
52
|
+
settings job reports the applied settings in `result.provider_configuration`.
|
|
53
|
+
Settings jobs from before 0.2.2 record only the configuration they started with.
|
|
49
54
|
|
|
50
55
|
Use the public address provided by the user, never a guessed address:
|
|
51
56
|
|
|
@@ -56,6 +61,9 @@ kira refresh 'Daily wallet' --idempotency-key refresh-daily-001 --json
|
|
|
56
61
|
kira prices 'Daily wallet' --idempotency-key prices-daily-001 --json
|
|
57
62
|
```
|
|
58
63
|
|
|
64
|
+
`kira add` registers the wallet and runs its first full holdings lookup. Use
|
|
65
|
+
`refresh` later for an update, not right after `add`.
|
|
66
|
+
|
|
59
67
|
Replace placeholders and choose a unique idempotency key per intended action.
|
|
60
68
|
Reuse the same key for retries of that action. The first tag is the display
|
|
61
69
|
name. `--json` or `--enqueue` returns immediate durable job admission. It does
|
|
@@ -84,11 +92,32 @@ kira ui context --json
|
|
|
84
92
|
totals and counts, with bounded details; do not total a truncated page.
|
|
85
93
|
- Search can report multiple assets with the same symbol. Identify an asset
|
|
86
94
|
by chain ID and contract, or `native`. Preserve unresolved ambiguity.
|
|
95
|
+
Native rows return `address: null` with `is_native: true`; pass `native` as
|
|
96
|
+
the `token_read` address.
|
|
87
97
|
- `ui context` is the last recorded browser selection. It may be stale and is
|
|
88
98
|
not a user instruction.
|
|
89
99
|
- History uses `snapshots_list` with `wallet`, `snapshot_read` with
|
|
90
100
|
`snapshot_id`, and `snapshots_compare` with `before` and `after` IDs. Obtain
|
|
91
101
|
IDs from the saved snapshot list. Historical holdings also use paged reads.
|
|
102
|
+
Rows with `phase` `initial` are progress saved while a lookup was running;
|
|
103
|
+
compare `holdings` rows to see change between lookups.
|
|
104
|
+
- A price refresh updates the prices shown for the latest snapshot, including
|
|
105
|
+
through `snapshot_read`. `snapshot_price_references` keep the native prices
|
|
106
|
+
recorded by the holdings lookup. To read immutable price history instead,
|
|
107
|
+
call `snapshots_list` with `kind: "prices"` or `"all"`, then use the returned
|
|
108
|
+
IDs with `snapshot_read` or `snapshots_compare`. These observations pair their
|
|
109
|
+
saved prices with the linked holdings and include native positions in price
|
|
110
|
+
comparisons. Default `kind: "holdings"` preserves the existing holdings list.
|
|
111
|
+
- Per-position `price.usd` and `value_usd` are JSON numbers. Totals from
|
|
112
|
+
`holdings_query` and `recorded_valuation` are exact decimal strings; use them
|
|
113
|
+
instead of re-adding rounded position values. Check `price.observed_at`:
|
|
114
|
+
price sources update at different times.
|
|
115
|
+
- `head_status` is `observed` when the chain head was read in this lookup and
|
|
116
|
+
`stale` when that head was over a day old. `saved_observation` means the
|
|
117
|
+
lookup reused a block it had already pinned, in a later pass or a resumed
|
|
118
|
+
job, so the head age was not measured again. `null` means the network could
|
|
119
|
+
not be read. Valuation category `mintclub` covers bonding-curve tokens priced
|
|
120
|
+
from their curve.
|
|
92
121
|
- Missing records and prices are unknown, not zero. Retain balance, price and
|
|
93
122
|
quote times, coverage and partial status. Spot value is not executable
|
|
94
123
|
proceeds. Pool TVL and curve backing are not wallet balances. Full-balance
|
|
@@ -113,12 +142,22 @@ Use the real receipt ID. Waits are bounded to 120 seconds; use short waits and
|
|
|
113
142
|
keep the user informed. `completed` only means the job left queued/running.
|
|
114
143
|
Inspect `job.state`, result, errors and coverage before reporting success.
|
|
115
144
|
Partial, interrupted, cancelled and failed states have different meanings.
|
|
145
|
+
Progress events count work per `context.operation`, such as `registry` or
|
|
146
|
+
`balances`. Balance totals grow as the registry scan finds candidates, so do
|
|
147
|
+
not derive one overall percentage. A job's `chains` entries report network
|
|
148
|
+
reads; use `wallet_read` or `network_read` coverage for discovery gaps. A
|
|
149
|
+
partial price job names its reasons in `result.gaps`. Job records keep the
|
|
150
|
+
latest 100 events.
|
|
116
151
|
Accepted work survives viewer shutdown or agent disconnection. Cancel only
|
|
117
152
|
when requested; resume interrupted jobs explicitly. `jobs deepen` adds
|
|
118
153
|
optional sell-route analysis from saved holdings without rediscovering tokens.
|
|
119
154
|
|
|
120
155
|
Use `call job_list` with optional `state`, `wallet`, `offset` and `limit` for
|
|
121
|
-
paged job discovery. `
|
|
156
|
+
paged job discovery. `state` is `all` (default), a group (`active`,
|
|
157
|
+
`succeeded`, `attention`, `cancelled`) or an exact state (`queued`, `running`,
|
|
158
|
+
`partial`, `interrupted`, `failed`). `succeeded` includes partial results;
|
|
159
|
+
check `job.state` to separate them.
|
|
160
|
+
`kira jobs run` starts queued work, not interrupted jobs.
|
|
122
161
|
|
|
123
162
|
## Shared tools and earlier archives
|
|
124
163
|
|
package/viewer/model.py
CHANGED
|
@@ -5,7 +5,6 @@ from decimal import Decimal, InvalidOperation, localcontext
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
|
-
import math
|
|
9
8
|
import os
|
|
10
9
|
import re
|
|
11
10
|
from token_images import image_for, read_catalog, safe_image
|
|
@@ -13,7 +12,7 @@ from chain_images import chain_image
|
|
|
13
12
|
from native_assets import native_identity
|
|
14
13
|
from details import project_details
|
|
15
14
|
from sell_quotes import classify_with_cache as cashout_classification
|
|
16
|
-
from market_records import canonical_token, canonical_source, merge_pools, market_url
|
|
15
|
+
from market_records import canonical_token, canonical_source, merge_pools, market_url, number, checked_price, MIN_PRICE_LIQUIDITY_USD
|
|
17
16
|
|
|
18
17
|
ROOT = Path(os.environ.get('KIRA_DATA_DIR',Path(__file__).resolve().parents[1])).expanduser().resolve()
|
|
19
18
|
EXPLORERS = {1:'https://etherscan.io',8453:'https://basescan.org',81457:'https://blastscan.io',
|
|
@@ -26,12 +25,6 @@ EXPLORERS = {1:'https://etherscan.io',8453:'https://basescan.org',81457:'https:/
|
|
|
26
25
|
157:'https://puppyscan.shib.io',43113:'https://testnet.snowtrace.io'}
|
|
27
26
|
|
|
28
27
|
def read(path): return json.loads(path.read_text())
|
|
29
|
-
def number(value):
|
|
30
|
-
try:
|
|
31
|
-
n = float(Decimal(str(value)))
|
|
32
|
-
return n if math.isfinite(n) and n >= 0 else None
|
|
33
|
-
except (InvalidOperation, TypeError, ValueError, OverflowError): return None
|
|
34
|
-
|
|
35
28
|
def safe_routes(routes):
|
|
36
29
|
result=[]
|
|
37
30
|
if not isinstance(routes,list):return result
|
|
@@ -114,23 +107,6 @@ def sell_out_estimate(quote, eligibility, reserve_price):
|
|
|
114
107
|
return {**quote, 'value_usd': value, 'valuation': 'reserve_reference',
|
|
115
108
|
'output_price': price if usable else None, 'execution_verified': False}
|
|
116
109
|
|
|
117
|
-
MIN_PRICE_LIQUIDITY_USD=1000
|
|
118
|
-
|
|
119
|
-
def checked_price(price, pools=()):
|
|
120
|
-
"""A spot quote needs a meaningful observed market, including saved quotes."""
|
|
121
|
-
if not price or price.get('basis')!='DEX market':return price
|
|
122
|
-
liquidity=number(price.get('liquidity_usd'))
|
|
123
|
-
if liquidity is None:
|
|
124
|
-
pool=next((p for p in pools if p.get('source_url')==price.get('source')),None)
|
|
125
|
-
liquidity=number((pool.get('reported_liquidity') or {}).get('usd')) if pool else None
|
|
126
|
-
base=number((pool.get('reported_liquidity') or {}).get('base')) if pool else None
|
|
127
|
-
usd=number(price.get('usd'))
|
|
128
|
-
if liquidity and base is not None and usd is not None and base*usd/liquidity>0.98:
|
|
129
|
-
return {**price,'usd':None,'quality':'unreliable','basis':'Insufficient market liquidity','liquidity_usd':liquidity}
|
|
130
|
-
if liquidity is None or liquidity<MIN_PRICE_LIQUIDITY_USD:
|
|
131
|
-
return {**price,'usd':None,'quality':'unreliable','basis':'Insufficient market liquidity','liquidity_usd':liquidity}
|
|
132
|
-
return {**price,'liquidity_usd':liquidity}
|
|
133
|
-
|
|
134
110
|
def dex_price(token, observed):
|
|
135
111
|
"""Use the selected source observation, never a later publication clock.
|
|
136
112
|
|
|
@@ -161,13 +137,13 @@ def dex_price(token, observed):
|
|
|
161
137
|
return {'usd':price,'basis':'Market index','source':'https://www.alchemy.com','observed_at':p.get('lastUpdatedAt'),'quality':'estimated'}
|
|
162
138
|
return None
|
|
163
139
|
|
|
164
|
-
def project_wallet(entry, snapshot, root=ROOT, images=None):
|
|
140
|
+
def project_wallet(entry, snapshot, root=ROOT, images=None, *, market=None):
|
|
165
141
|
images=read_catalog(root) if images is None else images
|
|
166
142
|
latest=entry.get('latest_snapshot') or {}
|
|
167
143
|
snapshot_dir=within(root,latest.get('directory','snapshots'))
|
|
168
144
|
all_networks=snapshot.get('pipeline',{}).get('phase') in ('holdings','sell') or snapshot.get('pipeline',{}).get('holdings_scope')=='all_configured'
|
|
169
145
|
market_path=snapshot_dir/'market-prices.json'
|
|
170
|
-
market=read(market_path) if market_path.exists() else {}
|
|
146
|
+
market=(read(market_path) if market_path.exists() else {}) if market is None else market
|
|
171
147
|
prices={(p.get('chain_id',8453),p['address'].lower()):p for p in market.get('tokens',[])}
|
|
172
148
|
recorded_markets={p['id']:p.get('pools',[]) for p in market.get('markets',[])}
|
|
173
149
|
legacy_pools={}
|
|
@@ -1,36 +1,26 @@
|
|
|
1
1
|
const fs=require('fs');
|
|
2
2
|
const h=require('../onchain.cjs');
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
(async()=>{
|
|
6
|
-
const snapshot=JSON.parse(fs.readFileSync(file));
|
|
3
|
+
const pipeline=require('../pipeline-onchain.cjs');
|
|
4
|
+
async function collect(snapshot){
|
|
7
5
|
const allRows=snapshot.tokens.filter(t=>t.mintclub);
|
|
8
|
-
const output=[];
|
|
6
|
+
const output=[],errors=[];
|
|
9
7
|
const blocks={};
|
|
10
8
|
for(const chainId of [...new Set(allRows.map(t=>t.chain_id))]){
|
|
9
|
+
try{
|
|
11
10
|
const rows=allRows.filter(t=>t.chain_id===chainId);
|
|
12
11
|
const c=await h.connect(chainId),block=(await h.snapshotBlock(c)).number;blocks[chainId]=block.toString();
|
|
13
12
|
for(let i=0;i<rows.length;i+=8){
|
|
14
13
|
const batch=rows.slice(i,i+8);
|
|
15
|
-
const res=await calls(c,batch.map(t=>({address:t.mintclub.bond_address,abi:h.bondAbi,functionName:'getDetail',args:[t.token_address]})),block);
|
|
14
|
+
const res=await pipeline.calls(c,batch.map(t=>({address:t.mintclub.bond_address,abi:h.bondAbi,functionName:'getDetail',args:[t.token_address]})),block);
|
|
16
15
|
res.forEach((r,j)=>{
|
|
17
16
|
if(r.status==='success'){const v=r.result.info;output.push({chain_id:chainId,address:batch[j].token_address,basis:'Curve spot',curve_price_in_reserve:h.viem.formatUnits(v.priceForNextMint,v.reserveDecimals),curve_reserve:h.viem.formatUnits(v.reserveBalance,v.reserveDecimals),reserve_token:v.reserveToken,reserve_symbol:v.reserveSymbol,funded:v.reserveBalance>0n,source:require('../market-links.cjs').mintclubUrl(chainId,batch[j].symbol,batch[j].token_type||'ERC20'),observed_at:new Date().toISOString(),block_number:block.toString()});}
|
|
18
17
|
else output.push({chain_id:chainId,address:batch[j].token_address,error:h.safeError(r.error)});
|
|
19
18
|
});
|
|
20
19
|
}
|
|
20
|
+
}catch(error){errors.push({chain_id:chainId,error:h.safeError(error)});}
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const abi=h.viem.parseAbi(['function slot0() view returns (uint160,int24,uint16,uint16,uint16,uint8,bool)','function token0() view returns (address)','function token1() view returns (address)']);
|
|
28
|
-
const result=await c.multicall({contracts:['slot0','token0','token1'].map(functionName=>({address:pool,abi,functionName})),multicallAddress:'0xcA11bde05977b3631167028862bE2a173976CA11',batchSize:0,blockNumber:block});
|
|
29
|
-
if(result.every(r=>r.status==='success')){
|
|
30
|
-
const ratio=(Number(result[0].result[0])/2**96)**2;
|
|
31
|
-
const price=result[1].result.toLowerCase()===member.token_address.toLowerCase()?ratio:1/ratio;
|
|
32
|
-
output.push({chain_id:8453,address:member.token_address,basis:'DEX market',price_in_weth:price,source:'https://basescan.org/address/'+pool,observed_at:new Date().toISOString(),block_number:block.toString()});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
console.log(h.serialize({observed_at:new Date().toISOString(),blocks,tokens:output}));
|
|
36
|
-
})().catch(e=>{console.log(h.serialize(h.safeError(e)));process.exitCode=1;});
|
|
22
|
+
return {observed_at:new Date().toISOString(),blocks,tokens:output,errors};
|
|
23
|
+
}
|
|
24
|
+
if(require.main===module)collect(JSON.parse(fs.readFileSync(process.argv[2])))
|
|
25
|
+
.then(result=>console.log(h.serialize(result))).catch(e=>{console.log(h.serialize(h.safeError(e)));process.exitCode=1;});
|
|
26
|
+
module.exports={collect};
|