kira-wallet 0.1.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/THIRD_PARTY_NOTICES.md +13 -0
  4. package/bin/kira.cjs +8 -0
  5. package/curve_pricing.py +86 -0
  6. package/docs/AGENT_TOOLS.md +53 -0
  7. package/docs/CLI.md +144 -0
  8. package/docs/JOB_CONTRACT.md +111 -0
  9. package/docs/KIRA_JOBS_BUILD_REPORT.md +92 -0
  10. package/docs/KIRA_REDESIGN_REPORT.md +125 -0
  11. package/docs/ONBOARDING_CHAT_CONTRACT.md +36 -0
  12. package/docs/RELEASE.md +45 -0
  13. package/docs/WALLET_CAPABILITIES.md +98 -0
  14. package/docs/character/brand-generation.json +20 -0
  15. package/docs/character/workspace-generation.json +29 -0
  16. package/docs/screenshots/kira-jobs-desktop.jpg +0 -0
  17. package/kira_agent.py +280 -0
  18. package/kira_cli.py +263 -0
  19. package/kira_config.py +98 -0
  20. package/kira_jobs.py +504 -0
  21. package/kira_tools.py +104 -0
  22. package/networks.json +214 -0
  23. package/onchain.cjs +34 -0
  24. package/package.json +78 -0
  25. package/pipeline-onchain.cjs +284 -0
  26. package/research.py +39 -0
  27. package/sources/job-envelope-v1.schema.json +469 -0
  28. package/sources/job-request-v1.schema.json +352 -0
  29. package/sources/mintclub-bond-abi.json +1457 -0
  30. package/sources/rpc-candidates.json +294 -0
  31. package/sources/uniswap-deployments.json +18261 -0
  32. package/viewer/README.md +119 -0
  33. package/viewer/chain_images.py +21 -0
  34. package/viewer/details.py +119 -0
  35. package/viewer/model.py +216 -0
  36. package/viewer/refresh-curve-prices.cjs +35 -0
  37. package/viewer/refresh_prices.py +97 -0
  38. package/viewer/server.py +153 -0
  39. package/viewer/static/agent.css +100 -0
  40. package/viewer/static/agent.js +203 -0
  41. package/viewer/static/app.js +252 -0
  42. package/viewer/static/favicon.svg +1 -0
  43. package/viewer/static/index.html +139 -0
  44. package/viewer/static/jobs.js +113 -0
  45. package/viewer/static/kira-attention.png +0 -0
  46. package/viewer/static/kira-explain.png +0 -0
  47. package/viewer/static/kira-logo.png +0 -0
  48. package/viewer/static/kira-research.png +0 -0
  49. package/viewer/static/kira-review.png +0 -0
  50. package/viewer/static/kira.png +0 -0
  51. package/viewer/static/style.css +51 -0
  52. package/viewer/static/watching-model.js +129 -0
  53. package/viewer/static/watching.js +167 -0
  54. package/viewer/static/workspace-model.js +43 -0
  55. package/viewer/static/workspace.css +463 -0
  56. package/viewer/static/workspace.js +102 -0
  57. package/viewer/token_images.py +137 -0
  58. package/wallet.py +389 -0
@@ -0,0 +1,284 @@
1
+ // Read-only wallet analysis. Credentials stay in the RPC helper process.
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const h = require('./onchain.cjs');
5
+ const v = h.viem;
6
+ const MULTICALL = '0xcA11bde05977b3631167028862bE2a173976CA11';
7
+ const erc20 = v.parseAbi([
8
+ 'function balanceOf(address) view returns (uint256)',
9
+ 'function decimals() view returns (uint8)',
10
+ 'function symbol() view returns (string)',
11
+ 'function name() view returns (string)',
12
+ ]);
13
+ const erc1155 = v.parseAbi(['function balanceOf(address,uint256) view returns (uint256)']);
14
+ const poolAbi = v.parseAbi([
15
+ 'function factory() view returns (address)', 'function token0() view returns (address)',
16
+ 'function token1() view returns (address)', 'function liquidity() view returns (uint128)',
17
+ 'function getReserves() view returns (uint112,uint112,uint32)',
18
+ 'function getSlot0(bytes32) view returns (uint160,int24,uint24,uint24)',
19
+ 'function getLiquidity(bytes32) view returns (uint128)',
20
+ 'function getPool(address,address,uint24) view returns (address)',
21
+ 'function getPool(address,address,bool) view returns (address)',
22
+ 'function getPair(address,address) view returns (address)',
23
+ ]);
24
+ const deployments = JSON.parse(fs.readFileSync(path.join(h.assets, 'sources/uniswap-deployments.json'))).records;
25
+ const stamp = () => new Date().toISOString();
26
+ const endpointIdentity = new WeakMap();
27
+ const emitChain = row => console.log(JSON.stringify({stage:'chain',chain_id:row.chain_id,status:row.status,endpoint_index:row.endpoint_index,
28
+ block_number:row.block_number==null?null:row.block_number.toString(),registry:row.registry_scan?.registry_count,
29
+ checked:row.registry_scan?.checked,held:row.tokens.length,cache_reused:row.registry_scan?.cache_reused}));
30
+ const write = (file, data) => {
31
+ fs.mkdirSync(path.dirname(file), {recursive:true});
32
+ fs.writeFileSync(file + '.tmp', h.serialize(data) + '\n');
33
+ fs.renameSync(file + '.tmp', file);
34
+ };
35
+ async function parallel(items, workers, fn) {
36
+ const results = new Array(items.length); let next = 0;
37
+ await Promise.all(Array.from({length:Math.min(workers, items.length)}, async () => {
38
+ for (;;) {const i = next++; if (i >= items.length) return; results[i] = await fn(items[i], i);}
39
+ }));
40
+ return results;
41
+ }
42
+ async function connect(n) {
43
+ const choices=h.endpoints(n.chain_id).map(url=>()=>h.client(n.chain_id,url));
44
+ let error;
45
+ for (const [index,create] of choices.entries()) {
46
+ try {const c = create(); if (await c.getChainId() !== n.chain_id) throw new Error('RPC chain identity mismatch'); endpointIdentity.set(c,index);return c;}
47
+ catch (e) {error = e;}
48
+ }
49
+ throw error || new Error('No RPC endpoint configured; public fallback is disabled.');
50
+ }
51
+ async function calls(c, contracts, block) {
52
+ if (!contracts.length) return [];
53
+ try {
54
+ return await c.multicall({contracts,blockNumber:block,batchSize:0,multicallAddress:MULTICALL});
55
+ } catch (e) {
56
+ if (['HttpRequestError','TimeoutError'].includes(e.name)) return contracts.map(()=>({status:'failure',error:e}));
57
+ if (contracts.length > 1) {
58
+ const mid = Math.ceil(contracts.length/2);
59
+ return [...await calls(c,contracts.slice(0,mid),block), ...await calls(c,contracts.slice(mid),block)];
60
+ }
61
+ try {return [{status:'success',result:await c.readContract({...contracts[0],blockNumber:block})}];}
62
+ catch (inner) {return [{status:'failure',error:inner}];}
63
+ }
64
+ }
65
+ async function batches(c, contracts, block, size=100) {
66
+ const plans = [];
67
+ for (let i=0; i<contracts.length; i+=size) plans.push(contracts.slice(i,i+size));
68
+ return (await parallel(plans,3,b => calls(c,b,block))).flat();
69
+ }
70
+ async function registry(c, n, block) {
71
+ const count = Number(await c.readContract({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'tokenCount',blockNumber:block}));
72
+ const cacheFile = path.join(h.root,'cache/mintclub-registry',`${n.chain_id}-${n.mintclub_bond_address.toLowerCase()}.json`);
73
+ let entries = [], cacheReused = 0;
74
+ if (fs.existsSync(cacheFile)) {
75
+ const cache = JSON.parse(fs.readFileSync(cacheFile));
76
+ if (cache.chain_id === n.chain_id && cache.bond_address.toLowerCase() === n.mintclub_bond_address.toLowerCase() && cache.entries.length <= count) {
77
+ const old = cache.entries;
78
+ const checks = old.length ? await calls(c,[0,old.length-1].map(i => ({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'tokens',args:[BigInt(i)]})),block) : [];
79
+ if (!old.length || checks.every((r,i) => r.status==='success' && r.result.toLowerCase()===old[i ? old.length-1 : 0].address.toLowerCase())) {
80
+ entries = old; cacheReused = old.length;
81
+ }
82
+ }
83
+ }
84
+ const start = entries.length;
85
+ const addresses = await batches(c,Array.from({length:count-start},(_,i) => ({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'tokens',args:[BigInt(start+i)]})),block);
86
+ const additions = addresses.map((r,i) => ({index:start+i,address:r.status==='success'?r.result:null,error:r.status==='success'?null:h.safeError(r.error)}));
87
+ const valid = additions.filter(t => t.address);
88
+ const decimals = await batches(c,valid.map(t => ({address:t.address,abi:erc20,functionName:'decimals'})),block);
89
+ valid.forEach((t,i) => {
90
+ const r = decimals[i];
91
+ t.decimals = r.status==='success'?r.result:null;
92
+ // The official Mint Club clones have fixed ERC20 decimals 18 or ERC1155 decimals 0.
93
+ t.token_type = t.decimals===18?'ERC20':t.decimals===0?'ERC1155':'unknown';
94
+ if (t.token_type==='unknown') t.error = r.status==='failure'?h.safeError(r.error):{message:'Unexpected Mint Club decimals'};
95
+ });
96
+ entries = [...entries,...additions];
97
+ if (entries.every(t => t.address && t.token_type!=='unknown' && !t.error)) {
98
+ write(cacheFile,{chain_id:n.chain_id,bond_address:n.mintclub_bond_address,observed_at:stamp(),block_number:block,entries});
99
+ }
100
+ return {count,entries,cache_reused:cacheReused,errors:entries.filter(t => t.error || !t.address).length};
101
+ }
102
+ async function scanChain(input, n, output) {
103
+ const file = path.join(output,`chain-${n.chain_id}.json`);
104
+ // A resumed run retains its fixed-block completed evidence, never wallet-independent balances.
105
+ if (fs.existsSync(file)) {
106
+ const previous = JSON.parse(fs.readFileSync(file));
107
+ if (previous.wallet.toLowerCase()===input.wallet.toLowerCase() && previous.status==='complete') {emitChain(previous);return previous;}
108
+ }
109
+ const row = {wallet:input.wallet,chain_id:n.chain_id,network:n.network,observed_at:stamp(),status:'unavailable',tokens:[],native_balance:null};
110
+ try {
111
+ const c = await connect(n), block = await c.getBlockNumber();
112
+ row.endpoint_index=endpointIdentity.get(c);
113
+ row.block_number = block;
114
+ row.native_balance = v.formatUnits(await c.getBalance({address:input.wallet,blockNumber:block}),18);
115
+ row.rpc_status = 'available';
116
+ let reg;
117
+ try {reg = await registry(c,n,block);} catch (e) {row.registry_error = h.safeError(e); reg = {count:null,entries:[],errors:1,cache_reused:0};}
118
+ const plans = new Map();
119
+ for (const t of input.candidates.filter(t => t.chain_id===n.chain_id)) plans.set(t.address.toLowerCase(), {...t,token_type:'ERC20'});
120
+ for (const t of reg.entries.filter(t => t.address && t.token_type && t.token_type!=='unknown')) {
121
+ plans.set(t.address.toLowerCase(),{...plans.get(t.address.toLowerCase()),...t,registered_mintclub:true});
122
+ }
123
+ const tokens = [...plans.values()];
124
+ const balances = await batches(c,tokens.map(t => ({address:t.address,abi:t.token_type==='ERC1155'?erc1155:erc20,functionName:'balanceOf',args:t.token_type==='ERC1155'?[input.wallet,0n]:[input.wallet]})),block);
125
+ const errors = []; let checked = 0, erc20Checked = 0, erc1155Checked = 0;
126
+ const held = [];
127
+ tokens.forEach((t,i) => {
128
+ const r = balances[i];
129
+ if (r.status==='failure') {errors.push({address:t.address,registered_mintclub:!!t.registered_mintclub,error:h.safeError(r.error)}); return;}
130
+ if (t.registered_mintclub) {checked++; if(t.token_type==='ERC20')erc20Checked++;else erc1155Checked++;}
131
+ if (r.result>0n) held.push({...t,balance_raw:r.result});
132
+ });
133
+ row.registry_scan = {registry_count:reg.count,registry_errors:reg.errors,checked,erc20_checked:erc20Checked,erc1155_checked:erc1155Checked,
134
+ cache_reused:reg.cache_reused,balance_errors:errors.filter(t=>t.registered_mintclub).length,
135
+ positive_balances:held.filter(t=>t.registered_mintclub).length,block_number:block,complete:reg.errors===0 && checked===reg.count,
136
+ source:'On-chain Mint Club bond tokenCount()/tokens(index), then direct wallet balance calls',bond_address:n.mintclub_bond_address};
137
+ row.balance_errors = errors;
138
+ for (const t of held) {
139
+ const item = {chain_id:n.chain_id,network:n.network,token_address:t.address,token_type:t.token_type,token_id:t.token_type==='ERC1155'?'0':null,
140
+ wallet_balance_raw:t.balance_raw,balance_block_number:block,balance_observed_at:stamp(),indexer_price_references:t.prices||[],mintclub:null,dex_pools:[]};
141
+ const meta = await calls(c,['decimals','symbol','name'].map(functionName=>({address:t.address,abi:erc20,functionName})),block);
142
+ item.decimals = meta[0].status==='success'?meta[0].result:t.decimals;
143
+ item.symbol = meta[1].status==='success'?meta[1].result:t.symbol||t.address.slice(0,10);
144
+ item.name = meta[2].status==='success'?meta[2].result:t.name||item.symbol;
145
+ item.wallet_balance = Number.isInteger(item.decimals)?v.formatUnits(t.balance_raw,item.decimals):null;
146
+ if (item.wallet_balance===null) item.metadata_error = 'Token decimals unavailable; quantity remains unknown.';
147
+ if (t.registered_mintclub) {
148
+ try {
149
+ const detail = await c.readContract({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'getDetail',args:[t.address],blockNumber:block});
150
+ const info = detail.info;
151
+ let refund=null,quoteError=null;
152
+ try {refund = await c.readContract({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'getRefundForTokens',args:[t.address,t.balance_raw],blockNumber:block});}
153
+ catch(e){quoteError=h.safeError(e);}
154
+ item.mintclub = {bond_address:n.mintclub_bond_address,reserve_token:info.reserveToken,reserve_symbol:info.reserveSymbol,reserve_decimals:info.reserveDecimals,
155
+ reserve_balance_raw:info.reserveBalance,reserve_balance:v.formatUnits(info.reserveBalance,info.reserveDecimals),funded:info.reserveBalance>0n,
156
+ price_for_next_mint_in_reserve_token:v.formatUnits(info.priceForNextMint,info.reserveDecimals),current_supply:v.formatUnits(info.currentSupply,item.decimals),
157
+ burn_royalty_bps:detail.burnRoyalty,wallet_full_burn:refund?{net_refund_raw:refund[0],net_refund:v.formatUnits(refund[0],info.reserveDecimals),
158
+ royalty_raw:refund[1],royalty:v.formatUnits(refund[1],info.reserveDecimals),asset:info.reserveSymbol,gas_included:false}:null,
159
+ wallet_full_burn_error:quoteError,block_number:block,observed_at:stamp(),source_url:`https://mint.club/token/${n.network}/${t.address}`};
160
+ } catch(e) {item.mintclub_error=h.safeError(e);}
161
+ }
162
+ row.tokens.push(item);
163
+ }
164
+ row.status = row.registry_scan.complete && errors.length===0?'complete':'partial';
165
+ row.observed_at=stamp();
166
+ } catch(e) {row.error=h.safeError(e);}
167
+ write(file,row);
168
+ emitChain(row);
169
+ return row;
170
+ }
171
+ function deployment(chain,protocol,name) {
172
+ return deployments.find(r=>r.chainId===chain && r.protocol===protocol && r.contract.toLowerCase()===name.toLowerCase())?.address;
173
+ }
174
+ async function dexChain(n, tokens) {
175
+ const c=await connect(n),block=await c.getBlockNumber();
176
+ // Supplement DEX indexer discovery on Base with the canonical WETH/USDC factories.
177
+ if (n.chain_id===8453) {
178
+ const queries=[];
179
+ for (const t of tokens.filter(t=>t.token_type!=='ERC1155')) for(const quote of ['0x4200000000000000000000000000000000000006','0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913']) {
180
+ if(t.token_address.toLowerCase()===quote.toLowerCase())continue;
181
+ for(const protocol of ['v2','v3','aerodrome']) {
182
+ const factory=protocol==='aerodrome'?'0x420DD381b31aEf6683db6B902084cB0FFECe40Da':deployment(8453,protocol,protocol==='v2'?'UniswapV2Factory':'UniswapV3Factory');
183
+ if(!factory)continue;
184
+ for(const value of protocol==='v3'?[100,500,3000,10000]:protocol==='aerodrome'?[false,true]:[null]) {
185
+ const args=protocol==='v2'?[t.token_address,quote]:[t.token_address,quote,value];
186
+ queries.push({t,quote,protocol,factory,call:{address:factory,abi:poolAbi,functionName:protocol==='v2'?'getPair':'getPool',args}});
187
+ }
188
+ }
189
+ }
190
+ const found=await batches(c,queries.map(q=>q.call),block,60);
191
+ queries.forEach((q,i)=>{
192
+ const r=found[i];
193
+ if(r.status==='success' && r.result!==v.zeroAddress && !q.t.dex_pools.some(p=>p.pool.toLowerCase()===r.result.toLowerCase())) {
194
+ q.t.dex_pools.push({pool:r.result,venue:q.protocol==='aerodrome'?'aerodrome':'uniswap',version_labels:[q.protocol],
195
+ paired_tokens:[{address:q.t.token_address,symbol:q.t.symbol},{address:q.quote,symbol:q.quote.toLowerCase().endsWith('0006')?'WETH':'USDC'}],
196
+ reported_liquidity:null,source_url:'https://basescan.org/address/'+r.result,factory_discovery:{factory:q.factory,block_number:block}});
197
+ }
198
+ });
199
+ for(const t of tokens)t.factory_discovery={candidate_count:queries.filter(q=>q.t===t).length,errors:queries.filter((q,i)=>q.t===t&&found[i].status==='failure').length,block_number:block};
200
+ }
201
+ for(const t of tokens) for(const p of t.dex_pools) {
202
+ if(!['uniswap','aerodrome'].includes(p.venue))continue;
203
+ try {
204
+ if(p.pool.length===66) {
205
+ const sv=deployment(n.chain_id,'v4','StateView');
206
+ if(!sv){p.rpc_verification={verification:'official_state_view_unavailable'};continue;}
207
+ const r=await calls(c,['getSlot0','getLiquidity'].map(functionName=>({address:sv,abi:poolAbi,functionName,args:[p.pool]})),block);
208
+ p.rpc_verification={verification:r[0].status==='success'&&r[0].result[0]>0n?'initialized_at_official_v4_state_view':'unverified',block_number:block,observed_at:stamp(),state_view:sv};
209
+ if(r[1].status==='success')p.factory_measurement={active_liquidity_verified:r[1].result>0n,state:{liquidity:r[1].result}};
210
+ continue;
211
+ }
212
+ const names=['factory','token0','token1',p.version_labels?.includes('v3')?'liquidity':'getReserves'];
213
+ const r=await calls(c,names.map(functionName=>({address:p.pool,abi:poolAbi,functionName})),block);
214
+ const state={};r.forEach((x,i)=>{if(x.status==='success')state[names[i]]=x.result;});
215
+ const factory=state.factory;
216
+ const recognized=typeof factory==='string' && (deployments.some(d=>d.chainId===n.chain_id&&d.contract.toLowerCase().includes('factory')&&d.address.toLowerCase()===factory.toLowerCase()) ||
217
+ (n.chain_id===8453&&p.venue==='aerodrome'&&factory.toLowerCase()==='0x420dd381b31aef6683db6b902084cb0ffece40da'));
218
+ let membership=false;
219
+ if(recognized&&state.token0&&state.token1) {
220
+ let check;
221
+ if(p.version_labels?.includes('v3')) {
222
+ const fee=await c.readContract({address:p.pool,abi:v.parseAbi(['function fee() view returns (uint24)']),functionName:'fee',blockNumber:block});
223
+ check={address:factory,abi:poolAbi,functionName:'getPool',args:[state.token0,state.token1,fee]};
224
+ } else if(p.venue==='aerodrome') {
225
+ const stable=await c.readContract({address:p.pool,abi:v.parseAbi(['function stable() view returns (bool)']),functionName:'stable',blockNumber:block});
226
+ check={address:factory,abi:poolAbi,functionName:'getPool',args:[state.token0,state.token1,stable]};
227
+ } else check={address:factory,abi:poolAbi,functionName:'getPair',args:[state.token0,state.token1]};
228
+ const mapped=await c.readContract({...check,blockNumber:block});membership=mapped.toLowerCase()===p.pool.toLowerCase();
229
+ }
230
+ p.rpc_verification={verification:membership?'official_factory_verified':'unverified',block_number:block,observed_at:stamp(),state};
231
+ const identity=[state.token0,state.token1].some(a=>a?.toLowerCase()===t.token_address.toLowerCase());
232
+ const active=membership&&identity&&(state.liquidity>0n || (state.getReserves?.[0]>0n&&state.getReserves?.[1]>0n));
233
+ p.factory_measurement={factory_verified:membership,active_liquidity_verified:!!active,state};
234
+ if(membership&&identity) {
235
+ const quote=[state.token0,state.token1].find(a=>a.toLowerCase()!==t.token_address.toLowerCase());
236
+ const q=await calls(c,[{address:quote,abi:erc20,functionName:'balanceOf',args:[p.pool]},{address:quote,abi:erc20,functionName:'decimals'},{address:quote,abi:erc20,functionName:'symbol'}],block);
237
+ if(q.every(x=>x.status==='success')) Object.assign(p.factory_measurement,{quote_token:quote,quote_symbol:q[2].result,quote_balance:v.formatUnits(q[0].result,q[1].result)});
238
+ }
239
+ }catch(e){p.rpc_verification={verification:'unverified',error:h.safeError(e)};}
240
+ }
241
+ return tokens;
242
+ }
243
+ async function main() {
244
+ const [mode,inputPath,outputPath]=process.argv.slice(2);
245
+ const input=JSON.parse(fs.readFileSync(inputPath));
246
+ if(!v.isAddress(input.wallet))throw new Error('Invalid wallet address');
247
+ if(mode==='scan') {
248
+ const rows=await parallel(input.networks,4,n=>scanChain(input,n,outputPath));
249
+ write(path.join(outputPath,'onchain-summary.json'),{wallet:input.wallet,observed_at:stamp(),chains:rows});
250
+ }else if(mode==='dex') {
251
+ const result=await parallel(input.networks.filter(n=>input.tokens.some(t=>t.chain_id===n.chain_id)),4,async n=>{
252
+ const group=input.tokens.filter(t=>t.chain_id===n.chain_id);
253
+ try{return await dexChain(n,group);}catch(e){group.forEach(t=>t.dex_verification_error=h.safeError(e));return group;}
254
+ });
255
+ write(outputPath,{wallet:input.wallet,observed_at:stamp(),tokens:result.flat()});
256
+ }else if(mode==='reserves') {
257
+ const result=await parallel(input.networks.filter(n=>input.tokens.some(t=>t.chain_id===n.chain_id&&t.mintclub)),4,async n=>{
258
+ const rows=[];
259
+ try {
260
+ const c=await connect(n),block=await c.getBlockNumber();
261
+ const queue=input.tokens.filter(t=>t.chain_id===n.chain_id&&t.mintclub).map(t=>t.token_address),seen=new Set();
262
+ for(let i=0;i<queue.length;i++) {
263
+ const address=queue[i],key=address.toLowerCase();
264
+ if(seen.has(key))continue;seen.add(key);
265
+ if(seen.size>128){rows.push({chain_id:n.chain_id,error:{message:'Reserve graph traversal limit reached'}});break;}
266
+ try {
267
+ const exists=await c.readContract({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'exists',args:[address],blockNumber:block});
268
+ if(!exists){rows.push({chain_id:n.chain_id,address,is_curve:false,block_number:block,observed_at:stamp()});continue;}
269
+ const d=await c.readContract({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'getDetail',args:[address],blockNumber:block});
270
+ const info=d.info;
271
+ rows.push({chain_id:n.chain_id,address,is_curve:true,symbol:info.symbol,price_in_reserve:v.formatUnits(info.priceForNextMint,info.reserveDecimals),
272
+ reserve_token:info.reserveToken,reserve_symbol:info.reserveSymbol,curve_reserve:v.formatUnits(info.reserveBalance,info.reserveDecimals),
273
+ funded:info.reserveBalance>0n,block_number:block,observed_at:stamp(),source:`https://mint.club/token/${n.network}/${address}`});
274
+ queue.push(info.reserveToken);
275
+ }catch(e){rows.push({chain_id:n.chain_id,address,error:h.safeError(e),block_number:block,observed_at:stamp()});}
276
+ }
277
+ }catch(e){rows.push({chain_id:n.chain_id,error:h.safeError(e)});}
278
+ return rows;
279
+ });
280
+ write(outputPath,{wallet:input.wallet,observed_at:stamp(),tokens:result.flat()});
281
+ }else throw new Error('Expected scan or dex mode');
282
+ }
283
+ if(require.main===module)main().catch(e=>{console.error(h.serialize(h.safeError(e)));process.exitCode=1;});
284
+ module.exports={parallel,calls,registry,connect};
package/research.py ADDED
@@ -0,0 +1,39 @@
1
+ """Read-only helpers for wallet liquidity research. Never print RPC credentials."""
2
+ from pathlib import Path
3
+ import datetime
4
+ import json
5
+ import re
6
+ import urllib.request
7
+ import urllib.error
8
+ from kira_config import ASSETS, data_root, secret_values, redact
9
+
10
+ ROOT = data_root()
11
+
12
+ def now():
13
+ return datetime.datetime.now(datetime.timezone.utc).isoformat()
14
+
15
+ def secrets():
16
+ return secret_values()
17
+
18
+ def fetch(url, body=None, timeout=20):
19
+ data = None if body is None else json.dumps(body).encode()
20
+ req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json', 'User-Agent': 'wallet-research'})
21
+ try:
22
+ with urllib.request.urlopen(req, timeout=timeout) as response:
23
+ return json.load(response)
24
+ except urllib.error.HTTPError as error:
25
+ return {'transport_error': {'status': error.code, 'message': redact(error.read().decode())[:2500]}}
26
+ except Exception as error:
27
+ return {'transport_error': {'type': type(error).__name__, 'message': redact(str(error))[:500]}}
28
+
29
+ def rpc(url, method, params):
30
+ return fetch(url, {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params})
31
+
32
+ def save(path, data):
33
+ path = Path(path)
34
+ path.parent.mkdir(parents=True, exist_ok=True)
35
+ path.write_text(json.dumps(data, indent=2) + '\n')
36
+
37
+ def alchemy_url(network):
38
+ key = secrets()['ALCHEMY_CUSTOM_APY_KEY']
39
+ return f'https://{network}.g.alchemy.com/v2/{key}'