kira-wallet 0.1.5 → 0.1.6
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 +11 -0
- package/docs/ALCHEMY_SETUP.md +95 -0
- package/docs/CLI.md +13 -4
- package/docs/FREE_RPC.md +85 -0
- package/docs/RELEASE.md +18 -16
- package/kira_cli.py +2 -1
- package/kira_config.py +6 -4
- package/kira_jobs.py +6 -2
- package/onchain.cjs +15 -24
- package/package.json +7 -3
- package/pipeline-onchain.cjs +24 -18
- package/rpc-pool.cjs +216 -0
- package/sources/job-request-v1.schema.json +6 -0
- package/sources/known-tokens.json +77 -0
- package/sources/rpc-candidates.json +46 -10
- package/viewer/refresh-curve-prices.cjs +2 -2
- package/viewer/static/app.js +16 -2
- package/viewer/static/index.html +14 -6
- package/viewer/static/jobs.js +41 -13
- package/viewer/static/report.js +1 -1
- package/viewer/static/watching.js +1 -1
- package/viewer/static/workspace-model.js +6 -5
- package/viewer/static/workspace.css +4 -0
- package/wallet.py +22 -7
package/README.md
CHANGED
|
@@ -140,3 +140,14 @@ From source, use `node bin/kira.cjs` in place of `kira`.
|
|
|
140
140
|
Original code and Kira character assets are available under the [MIT license](LICENSE).
|
|
141
141
|
Third-party artwork and references retain their own terms; see
|
|
142
142
|
[Third-party notices](THIRD_PARTY_NOTICES.md).
|
|
143
|
+
|
|
144
|
+
### Token coverage and optional Alchemy setup
|
|
145
|
+
|
|
146
|
+
Kira uses free public RPC with bounded fallback and checks a selected common-token
|
|
147
|
+
list alongside native assets and Mint Club. Other ERC20 holdings can be missing.
|
|
148
|
+
Incomplete results show recovery guidance on Home and the wallet page. Connect
|
|
149
|
+
your own Alchemy account for broader discovery on supported networks, then
|
|
150
|
+
refresh holdings. Public RPC remains the first choice for ordinary reads;
|
|
151
|
+
Alchemy is a backup and supplies the dedicated token inventory API. See the
|
|
152
|
+
[step-by-step Alchemy guide](docs/ALCHEMY_SETUP.md) and
|
|
153
|
+
[RPC behavior](docs/FREE_RPC.md).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Connect Alchemy to Kira
|
|
2
|
+
|
|
3
|
+
Kira works with free public RPC by default. It checks native assets, selected
|
|
4
|
+
common token contracts and the Mint Club registry. Other ERC20 holdings may
|
|
5
|
+
be missing. For broader token discovery, connect your own Alchemy account.
|
|
6
|
+
Kira runs these requests locally and does not operate a hosted relay.
|
|
7
|
+
|
|
8
|
+
## Create an Alchemy app
|
|
9
|
+
|
|
10
|
+
1. Sign in to the [Alchemy dashboard](https://dashboard.alchemy.com/).
|
|
11
|
+
A new account may already have a default app. Use it, or open your team
|
|
12
|
+
menu, Team Overview, Apps, then Create new app.
|
|
13
|
+
2. Name the app and select the mainnets you want to research. Check that the
|
|
14
|
+
app has Node API and Portfolio API access for those networks. Create it.
|
|
15
|
+
3. Copy the app API key. The Endpoints tab shows its network URLs. Kira's
|
|
16
|
+
combined Alchemy setup needs the key, so you do not need to copy each URL.
|
|
17
|
+
|
|
18
|
+
These steps follow Alchemy's [official API key guide](https://www.alchemy.com/docs/create-an-api-key),
|
|
19
|
+
checked on October 9, 2026. Dashboard wording can change.
|
|
20
|
+
|
|
21
|
+
## Connect the key locally
|
|
22
|
+
|
|
23
|
+
Create a private directory and file in your terminal:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
mkdir -p ~/.config/kira
|
|
27
|
+
touch ~/.config/kira/alchemy.env
|
|
28
|
+
chmod 600 ~/.config/kira/alchemy.env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Open the file in a local text editor. On macOS, you can use:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
open -e ~/.config/kira/alchemy.env
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Add this line, replacing `YOUR_KEY` inside the editor:
|
|
38
|
+
|
|
39
|
+
```dotenv
|
|
40
|
+
ALCHEMY_API_KEY=YOUR_KEY
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Save the file, then run:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
kira config import-env --file ~/.config/kira/alchemy.env
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For a non-default workspace, supply its existing `--data-dir` before `config`.
|
|
50
|
+
Kira stores the file reference. Credentials stay outside the browser, chat and
|
|
51
|
+
published analysis. Do not commit this file to a repository. A process
|
|
52
|
+
environment variable with the same name overrides the file value; check that
|
|
53
|
+
if an old key remains active.
|
|
54
|
+
|
|
55
|
+
## Check the connection and refresh
|
|
56
|
+
|
|
57
|
+
Open Workspace settings, then Manage data connection. Select Alchemy and use
|
|
58
|
+
"I added my key · check again". Save the connection. Finding the local key
|
|
59
|
+
does not prove the account, API or network is working.
|
|
60
|
+
|
|
61
|
+
For an existing wallet, choose Refresh this wallet's holdings or Refresh
|
|
62
|
+
holdings on its page. A new analysis confirms access and coverage. Old saved
|
|
63
|
+
analyses retain their original observations and warnings. Saving a connection
|
|
64
|
+
does not register a wallet or start research automatically.
|
|
65
|
+
|
|
66
|
+
## What uses the key
|
|
67
|
+
|
|
68
|
+
Ordinary balances prefer public RPC. After two failed or unavailable public
|
|
69
|
+
providers, Kira tries the configured custom backup within the read deadline.
|
|
70
|
+
Advanced settings can choose custom first or require custom only. Existing
|
|
71
|
+
explicit custom-only restrictions remain in force.
|
|
72
|
+
|
|
73
|
+
Alchemy token discovery uses the key directly. Standard public RPC cannot
|
|
74
|
+
enumerate arbitrary ERC20 holdings. Kira uses Alchemy's
|
|
75
|
+
[Tokens By Wallet endpoint](https://www.alchemy.com/docs/data/portfolio-apis/portfolio-api-endpoints/portfolio-api-endpoints/get-tokens-by-address),
|
|
76
|
+
then verifies discovered candidates with on-chain balance reads.
|
|
77
|
+
|
|
78
|
+
## If tokens are still missing
|
|
79
|
+
|
|
80
|
+
- **Local key unavailable:** confirm the file exists, its variable name matches
|
|
81
|
+
the connection settings, and you ran the import command in the right workspace.
|
|
82
|
+
- **Discovery failed:** check the app's enabled APIs and networks and Alchemy's
|
|
83
|
+
usage dashboard. Correct access or limits, then retry holdings.
|
|
84
|
+
- **Some balances could not be checked:** wait briefly and retry. Public and
|
|
85
|
+
custom providers both have outages and limits. Review Advanced read preferences
|
|
86
|
+
if a custom-only endpoint is unavailable.
|
|
87
|
+
- **Unsupported network:** broader discovery is unavailable there. Connecting
|
|
88
|
+
Alchemy cannot cover every network or every token. Known-contract reads remain
|
|
89
|
+
useful, and missing holdings remain unknown.
|
|
90
|
+
|
|
91
|
+
Alchemy has account allowances and throughput limits. Check the current
|
|
92
|
+
[plans](https://www.alchemy.com/docs/reference/pricing-plans) and API availability
|
|
93
|
+
before selecting a plan. Kira does not create an account, subscribe or upgrade
|
|
94
|
+
it. Public-first balance reads reduce ordinary use of the key; discovery still
|
|
95
|
+
consumes Alchemy allowance. No connection guarantees a complete token inventory.
|
package/docs/CLI.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install -g kira-wallet
|
|
|
10
10
|
kira setup
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The operator published npm version 0.1.
|
|
13
|
+
The operator published npm version 0.1.5. Version 0.1.6 is a preparation candidate. GitHub v0.1.3 remains
|
|
14
14
|
a draft; the previous archive and checksum are available from the
|
|
15
15
|
[0.1.2 GitHub release](https://github.com/realproject7/kira-wallet/releases/tag/v0.1.2).
|
|
16
16
|
Registry publication remains an operator action. The code and original character assets use the MIT license.
|
|
@@ -47,8 +47,8 @@ holdings. Missing coverage keeps the portfolio estimate partial. Existing snapsh
|
|
|
47
47
|
an indexer; a price refresh does not discover missing tokens.
|
|
48
48
|
|
|
49
49
|
Public RPC checks known assets and Mint Club. For broader ERC20 discovery,
|
|
50
|
-
open
|
|
51
|
-
|
|
50
|
+
open Custom RPC & token coverage, select Alchemy and follow its local key guide.
|
|
51
|
+
Published 0.1.5 uses one save for Alchemy RPC and discovery; chain overrides
|
|
52
52
|
remain under Advanced. Never paste the key into the browser. `kira doctor`
|
|
53
53
|
checks whether the configured key is available. Configuration alone does not
|
|
54
54
|
prove complete coverage; inspect the recorded results and gaps.
|
|
@@ -68,7 +68,7 @@ authentication. Verified adapter versions are Codex 0.158.x and Claude Code
|
|
|
68
68
|
verified. This is a native CLI connection, with no Kira account, API-key proxy or
|
|
69
69
|
hosted portfolio backend.
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
From published 0.1.5, setup opens the existing Account / Permissions / Ready
|
|
72
72
|
modal directly. Choose Codex or Claude, then Use default model or a readable
|
|
73
73
|
model choice. Codex choices come from bounded native metadata; Claude offers
|
|
74
74
|
Sonnet, Opus and Haiku aliases. A catalog is not proof of account access.
|
|
@@ -212,3 +212,12 @@ in session storage. It clears password fields immediately. Recovery keeps the
|
|
|
212
212
|
original encryption passphrase. No signing, key export or account deletion
|
|
213
213
|
endpoint is exposed. OWS absence does not block ordinary address or browser
|
|
214
214
|
wallet onboarding.
|
|
215
|
+
|
|
216
|
+
## Free reads and broader token coverage
|
|
217
|
+
|
|
218
|
+
Public RPC is preferred for ordinary reads, including when a custom backup is
|
|
219
|
+
configured. Advanced settings can select custom first or custom only. For CLI
|
|
220
|
+
configuration, use `kira rpc set --chain 8453 --url-env KIRA_BASE_RPC --priority
|
|
221
|
+
public_first`. Existing custom-only choices remain unchanged. Alchemy token
|
|
222
|
+
discovery uses the configured key directly. See [free RPC behavior](FREE_RPC.md)
|
|
223
|
+
and the [Alchemy setup guide](ALCHEMY_SETUP.md).
|
package/docs/FREE_RPC.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Free RPC reads and token coverage
|
|
2
|
+
|
|
3
|
+
Kira contacts public providers directly from the local research process. No
|
|
4
|
+
Kira relay or shared credential is required. Free public services are best
|
|
5
|
+
effort and can throttle requests or omit historical state.
|
|
6
|
+
|
|
7
|
+
The reviewed default catalog adds alternatives on Ethereum, Base, BNB Chain,
|
|
8
|
+
Polygon, Arbitrum and Optimism. PublicNode is the initial preference. Reserves
|
|
9
|
+
are selected per chain rather than downloaded from an untrusted runtime list.
|
|
10
|
+
The other supported networks retain their existing endpoints.
|
|
11
|
+
|
|
12
|
+
## Request behavior
|
|
13
|
+
|
|
14
|
+
- Chain identity is verified once per endpoint session and again after recovery.
|
|
15
|
+
- One request runs per endpoint. Provider families share one queue and a
|
|
16
|
+
conservative two-second interval across chains. Two chain scans run at once.
|
|
17
|
+
- A logical read has a twelve-second deadline including queues and verification.
|
|
18
|
+
The preferred HTTP attempt has four seconds, reserves at most two seconds.
|
|
19
|
+
Public attempts also have a total budget including verification and pacing,
|
|
20
|
+
leaving half the deadline for a configured personal backup.
|
|
21
|
+
Real fetches are aborted when a deadline expires. HTTP automatic retries are off.
|
|
22
|
+
- At most four endpoint attempts run for a read. Public-first configurations
|
|
23
|
+
reserve an attempt for the personal backup after two public providers.
|
|
24
|
+
- HTTP 429 and JSON-RPC backpressure cool an endpoint for at least sixty seconds.
|
|
25
|
+
Consecutive throttles increase this to at most thirty minutes. Retry-After
|
|
26
|
+
can extend the wait. Other transport failures have a short cooldown.
|
|
27
|
+
- Unsupported methods have a method-specific penalty. Contract reverts do not
|
|
28
|
+
cool an otherwise healthy endpoint. Oversized Multicalls can split; outages and
|
|
29
|
+
throttling cannot create a burst of smaller calls.
|
|
30
|
+
|
|
31
|
+
These are application limits, not promises about a provider's quota or uptime.
|
|
32
|
+
Cooldown observations survive sequential research subprocesses in a bounded
|
|
33
|
+
local cache. Custom endpoints and credentialed URLs are not written there.
|
|
34
|
+
Status screens and onboarding checks do not probe RPC providers.
|
|
35
|
+
|
|
36
|
+
## Snapshot consistency
|
|
37
|
+
|
|
38
|
+
Each chain analysis records a block number and hash. Balance and contract reads
|
|
39
|
+
keep that block. Before a fallback provider reads it, Kira verifies its header
|
|
40
|
+
has the same hash. Every evidence read then uses an EIP-1898 block-hash selector
|
|
41
|
+
with canonical checking. Providers that do not support it cannot supply that
|
|
42
|
+
snapshot's evidence. Conflicting or unavailable evidence stays incomplete.
|
|
43
|
+
No fallback silently substitutes `latest` for a fixed block.
|
|
44
|
+
|
|
45
|
+
Identical concurrent reads share work. Successful fixed-block reads have a
|
|
46
|
+
bounded cache keyed by chain, pinned hash, method and complete parameters,
|
|
47
|
+
including the wallet address. Failed reads are evicted and never become zero.
|
|
48
|
+
Wallet balances are not reused across wallets or new research processes.
|
|
49
|
+
Registry identity caching remains separate from balances.
|
|
50
|
+
|
|
51
|
+
## Default token inventory
|
|
52
|
+
|
|
53
|
+
Native balances and the selected issuer/deployment-backed contracts in
|
|
54
|
+
`sources/known-tokens.json` are checked before cold Mint Club enumeration.
|
|
55
|
+
The small list includes USDC on seven supported mainnets, USDT on Ethereum and
|
|
56
|
+
Avalanche, and WETH on Arbitrum. Decimals and quantities come from on-chain
|
|
57
|
+
reads. This list does not cover all common tokens or all wrapped/bridged variants.
|
|
58
|
+
|
|
59
|
+
The full Mint Club registry is still scanned. Public RPC cannot enumerate all
|
|
60
|
+
arbitrary ERC20 contracts held by an address. Discovery remains incomplete
|
|
61
|
+
without a working indexer on that network. Incomplete holdings show a persistent
|
|
62
|
+
notice on Home and the wallet page, with a path to
|
|
63
|
+
[Alchemy setup](ALCHEMY_SETUP.md) and an explicit holdings refresh.
|
|
64
|
+
|
|
65
|
+
## Read preferences
|
|
66
|
+
|
|
67
|
+
Public first is the default even when a custom connection is configured.
|
|
68
|
+
Advanced settings provide custom first and custom only. Configurations without
|
|
69
|
+
the new priority field use public first; an existing explicit custom-only choice
|
|
70
|
+
continues to exclude public endpoints. RPC priority does not change the dedicated
|
|
71
|
+
Alchemy discovery request.
|
|
72
|
+
|
|
73
|
+
CLI example using an existing local endpoint variable:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
kira rpc set --chain 8453 --url-env KIRA_BASE_RPC --priority public_first
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Primary references: [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898),
|
|
80
|
+
[PublicNode](https://www.publicnode.com/),
|
|
81
|
+
[dRPC limits](https://drpc.org/docs/howitworks/ratelimiting),
|
|
82
|
+
[1RPC networks](https://docs.1rpc.io/overview/supported-networks),
|
|
83
|
+
[Circle contracts](https://developers.circle.com/stablecoins/usdc-contract-addresses),
|
|
84
|
+
[Tether contracts](https://tether.to/en/supported-protocols/),
|
|
85
|
+
[Uniswap Arbitrum deployments](https://developers.uniswap.org/docs/protocols/v3/deployments/v3-arbitrum-deployments).
|
package/docs/RELEASE.md
CHANGED
|
@@ -44,19 +44,21 @@ RPC failures show affected networks, retry guidance and a settings action.
|
|
|
44
44
|
Failed candidate balance reads keep the saved result and valuation partial
|
|
45
45
|
even when general token discovery succeeded.
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
Published **0.1.5** opens the existing setup modal directly, adds Codex and
|
|
48
|
+
Claude model choices, exposes model and permission changes in Workspace settings,
|
|
49
|
+
and provides one Alchemy connection save. Its registry version was confirmed on
|
|
50
|
+
2026-10-09. Published package bytes remain immutable.
|
|
51
|
+
|
|
52
|
+
The next candidate is **0.1.6**. It adds a paced local RPC pool, core-chain public
|
|
53
|
+
alternatives, public-first custom backup, fixed-block hash checks and a small
|
|
54
|
+
known-token catalog. Home and wallet pages display incomplete coverage with an
|
|
55
|
+
Alchemy setup action. The settings guide explains account setup, private local
|
|
56
|
+
key import and an explicit holdings refresh. Missing or malformed discovery data
|
|
57
|
+
cannot become a completed empty inventory.
|
|
58
|
+
|
|
59
|
+
Version 0.1.6 requires review and operator registry publication. It is not yet
|
|
60
|
+
published. The showcase remains a separate deployment. See
|
|
61
|
+
[free RPC behavior](FREE_RPC.md) and [Alchemy setup](ALCHEMY_SETUP.md).
|
|
60
62
|
|
|
61
63
|
The operator published npm **0.1.2** on 2026-10-05. Its registry archive was
|
|
62
64
|
downloaded and matched the reviewed GitHub archive above, including SHA-256,
|
|
@@ -78,7 +80,7 @@ The earlier unpublished 0.1.2 candidate is preserved in the ignored
|
|
|
78
80
|
`dist/preserved-0.1.2-bad8f02/` directory. Use the current `dist/release.json`
|
|
79
81
|
and its matching archive for publication; do not mix manifests from candidates.
|
|
80
82
|
|
|
81
|
-
Keep GitHub v0.1.1, its tag and its released archive immutable. Versions 0.1.2, 0.1.3 and 0.1.
|
|
83
|
+
Keep GitHub v0.1.1, its tag and its released archive immutable. Versions 0.1.2, 0.1.3, 0.1.4 and 0.1.5 are also published and immutable. Do not rebuild them from later documentation
|
|
82
84
|
commits. Future package changes need a new version. Release preparation does not
|
|
83
85
|
publish a GitHub release, merge PR 19, promote the website or publish to npm.
|
|
84
86
|
Registry publication remains operator-only. The coding agent never publishes.
|
|
@@ -93,12 +95,12 @@ python3 scripts/prepare-release.py
|
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
`prepare-release.py` scans the package boundary and creates
|
|
96
|
-
`dist/kira-wallet-0.1.
|
|
98
|
+
`dist/kira-wallet-0.1.6.tgz`. The ignored `dist/release.json` records its source
|
|
97
99
|
commit, SHA-256 and member count. It never publishes. After reviewing that
|
|
98
100
|
manifest, the operator's final command from the project root is:
|
|
99
101
|
|
|
100
102
|
```sh
|
|
101
|
-
npm publish ./dist/kira-wallet-0.1.
|
|
103
|
+
npm publish ./dist/kira-wallet-0.1.6.tgz --access public
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
Use the operator's npm account and complete authentication or OTP personally.
|
package/kira_cli.py
CHANGED
|
@@ -172,7 +172,7 @@ def main():
|
|
|
172
172
|
imp=configsub.add_parser('import-env');imp.add_argument('--file',required=True);imp.add_argument('--key-env',default='ALCHEMY_API_KEY')
|
|
173
173
|
rpc=sub.add_parser('rpc');rpcsub=rpc.add_subparsers(dest='setting',required=True)
|
|
174
174
|
rpcsub.add_parser('public')
|
|
175
|
-
custom=rpcsub.add_parser('set');custom.add_argument('--chain',type=int,required=True);custom.add_argument('--url-env',required=True);fallback=custom.add_mutually_exclusive_group();fallback.add_argument('--public-fallback',action='store_true',default=None);fallback.add_argument('--custom-only',dest='public_fallback',action='store_false')
|
|
175
|
+
custom=rpcsub.add_parser('set');custom.add_argument('--chain',type=int,required=True);custom.add_argument('--url-env',required=True);custom.add_argument('--priority',choices=['public_first','custom_first']);fallback=custom.add_mutually_exclusive_group();fallback.add_argument('--public-fallback',action='store_true',default=None);fallback.add_argument('--custom-only',dest='public_fallback',action='store_false')
|
|
176
176
|
discovery=sub.add_parser('discovery');discovery.add_argument('provider',choices=['none','alchemy']);discovery.add_argument('--key-env',default='ALCHEMY_API_KEY');discovery.add_argument('--explorers',action='store_true')
|
|
177
177
|
args=parser.parse_args();root=Path(args.data_dir).expanduser().resolve();os.environ['KIRA_DATA_DIR']=str(root)
|
|
178
178
|
if sys.version_info<(3,11):raise ValueError('Python 3.11 or newer is required.')
|
|
@@ -239,6 +239,7 @@ def main():
|
|
|
239
239
|
elif args.setting=='public':cfg['rpc']['mode']='public'
|
|
240
240
|
else:
|
|
241
241
|
cfg['rpc'].update(mode='custom')
|
|
242
|
+
if args.priority:cfg['rpc']['priority']=args.priority
|
|
242
243
|
if args.public_fallback is not None:cfg['rpc']['allow_public_fallback']=args.public_fallback
|
|
243
244
|
cfg['rpc']['chains'][str(args.chain)]={'url_env':args.url_env}
|
|
244
245
|
# Validate before replacing the user's working configuration.
|
package/kira_config.py
CHANGED
|
@@ -16,7 +16,7 @@ def config_path():
|
|
|
16
16
|
return Path(os.environ.get('KIRA_CONFIG', data_root()/'.kira.local.json')).expanduser()
|
|
17
17
|
|
|
18
18
|
def default_config():
|
|
19
|
-
return {'schema_version':1,'rpc':{'mode':'public','allow_public_fallback':True,'chains':{}},
|
|
19
|
+
return {'schema_version':1,'rpc':{'mode':'public','allow_public_fallback':True,'priority':'public_first','chains':{}},
|
|
20
20
|
'discovery':{'provider':'none','key_env':'ALCHEMY_API_KEY','explorers':False}}
|
|
21
21
|
|
|
22
22
|
def provider_config(config, provider, key_env):
|
|
@@ -44,7 +44,8 @@ def load_config():
|
|
|
44
44
|
raise ValueError('Invalid RPC configuration.')
|
|
45
45
|
if not isinstance(discovery,dict) or discovery.get('provider') not in ('none','alchemy') or type(discovery.get('explorers',False)) is not bool:
|
|
46
46
|
raise ValueError('Invalid discovery configuration.')
|
|
47
|
-
if
|
|
47
|
+
if rpc.get('priority','public_first') not in ('public_first','custom_first'):raise ValueError('Invalid RPC priority.')
|
|
48
|
+
if set(rpc)-{'mode','allow_public_fallback','priority','chains'} or set(discovery)-{'provider','key_env','explorers'}:raise ValueError('Unknown provider configuration fields.')
|
|
48
49
|
if any(not isinstance(row,dict) or set(row)-{'url_env','alchemy_network'} for row in rpc['chains'].values()):raise ValueError('Unknown RPC endpoint fields.')
|
|
49
50
|
for name in [discovery.get('key_env'),*[row.get('url_env') if isinstance(row,dict) else None for row in rpc['chains'].values()]]:
|
|
50
51
|
if not isinstance(name,str) or not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*',name):raise ValueError('Credentials must use valid environment references.')
|
|
@@ -97,7 +98,8 @@ def endpoints(network,cfg=None):
|
|
|
97
98
|
# New setups default to automatic fallback. Preserve an explicit existing
|
|
98
99
|
# opt-out for operators who require custom providers only.
|
|
99
100
|
if not rpc['allow_public_fallback']:return urls
|
|
100
|
-
|
|
101
|
+
public=network.get('public_rpc') or []
|
|
102
|
+
urls=public+urls if rpc.get('priority','public_first')=='public_first' else urls+public
|
|
101
103
|
return list(dict.fromkeys(urls))
|
|
102
104
|
|
|
103
105
|
def runtime():
|
|
@@ -105,7 +107,7 @@ def runtime():
|
|
|
105
107
|
try: values=secret_values(cfg)
|
|
106
108
|
except ValueError: values={}
|
|
107
109
|
return {'endpoints':{str(n['chain_id']):endpoints(n,cfg) for n in networks},
|
|
108
|
-
'secrets':list(values.values()),'data_root':str(data_root())}
|
|
110
|
+
'secrets':list(values.values()),'data_root':str(data_root()),'health_file':str(data_root()/'cache/rpc-health.json')}
|
|
109
111
|
|
|
110
112
|
if __name__=='__main__':
|
|
111
113
|
# Internal child-process channel only. The user CLI never emits this payload.
|
package/kira_jobs.py
CHANGED
|
@@ -128,6 +128,7 @@ class JobStore:
|
|
|
128
128
|
raise JobError('invalid_envelope','Persisted job validation failed. Preserve the file for recovery.')
|
|
129
129
|
names={'wallet.add':['address','tag'],'wallet.refresh':['wallet'],'prices.refresh':['wallet'],'wallet.setTags':['wallet','tags'],
|
|
130
130
|
'settings.rpc':['mode','chains','allow_public_fallback'],'settings.discovery':['provider','key_env'],'settings.provider':['provider','key_env']}[job['operation']]
|
|
131
|
+
if job['operation']=='settings.rpc' and 'priority' in job['input']:names=names+['priority']
|
|
131
132
|
fields(job['input'],names)
|
|
132
133
|
wallet=job['input'].get('wallet') or job['input'].get('address')
|
|
133
134
|
if wallet is not None and (not isinstance(wallet,str) or not ADDRESS.fullmatch(wallet)):raise JobError('invalid_envelope','Persisted wallet identity is invalid.')
|
|
@@ -160,7 +161,9 @@ class JobStore:
|
|
|
160
161
|
raise JobError('invalid_settings','Choose a data provider and a local credential reference.')
|
|
161
162
|
return value.copy()
|
|
162
163
|
if operation == 'settings.rpc':
|
|
163
|
-
fields(value,['mode','chains','allow_public_fallback'])
|
|
164
|
+
fields(value,['mode','chains','allow_public_fallback']+(['priority'] if isinstance(value,dict) and 'priority' in value else []))
|
|
165
|
+
if value.get('priority','public_first') not in ('public_first','custom_first'):
|
|
166
|
+
raise JobError('invalid_settings','Choose a valid RPC priority.')
|
|
164
167
|
if value['mode'] not in ('public','custom') or type(value['allow_public_fallback']) is not bool or not isinstance(value['chains'],list) or len(value['chains'])>30:
|
|
165
168
|
raise JobError('invalid_settings','Choose a valid RPC mode and credential references.')
|
|
166
169
|
available={n['chain_id'] for n in read(ASSETS/'sources/rpc-candidates.json')}
|
|
@@ -171,7 +174,7 @@ class JobStore:
|
|
|
171
174
|
raise JobError('invalid_settings','Use a known chain and a unique environment variable reference.')
|
|
172
175
|
chains[str(chain['chain_id'])]={'url_env':chain['url_env']}
|
|
173
176
|
if value['mode']=='custom' and not chains:raise JobError('invalid_settings','Custom RPC requires at least one endpoint reference.')
|
|
174
|
-
return {'mode':value['mode'],'chains':chains,'allow_public_fallback':value['allow_public_fallback']}
|
|
177
|
+
return {'mode':value['mode'],'chains':chains,'allow_public_fallback':value['allow_public_fallback'],**({'priority':value['priority']} if 'priority' in value else {})}
|
|
175
178
|
if operation == 'settings.discovery':
|
|
176
179
|
fields(value,['provider','key_env'])
|
|
177
180
|
if value['provider'] not in ('none','alchemy') or not isinstance(value['key_env'],str) or not re.fullmatch(r'[A-Z_][A-Z0-9_]{0,99}',value['key_env']):raise JobError('invalid_settings','Use a supported indexer and environment variable reference.')
|
|
@@ -404,6 +407,7 @@ class JobStore:
|
|
|
404
407
|
config=load_config()
|
|
405
408
|
if job['operation']=='settings.rpc':
|
|
406
409
|
updated={**job['input'],'chains':{key:dict(row) for key,row in job['input']['chains'].items()}}
|
|
410
|
+
updated.setdefault('priority',config['rpc'].get('priority','public_first'))
|
|
407
411
|
for key,row in updated['chains'].items():
|
|
408
412
|
previous=config['rpc']['chains'].get(key,{})
|
|
409
413
|
if row['url_env']==previous.get('url_env') and previous.get('alchemy_network'):row['alchemy_network']=previous['alchemy_network']
|
package/onchain.cjs
CHANGED
|
@@ -9,41 +9,32 @@ 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');
|
|
13
|
+
const pool=new RpcPool({publicUrls:networks.flatMap(n=>n.public_rpc||[]),healthFile:runtime.health_file});
|
|
12
14
|
function endpoints(chainId){return runtime.endpoints[String(chainId)] || [];}
|
|
13
15
|
const endpointReads=new WeakMap();
|
|
16
|
+
const clientState=new WeakMap();
|
|
14
17
|
function resilientTransport(chainId,urls,transport=viem.http,onSuccess=()=>{}) {
|
|
15
|
-
|
|
16
|
-
const choices=urls.map((url,index)=>options=>{
|
|
17
|
-
const inner=transport(url,{timeout:8000,retryCount:0})(options);
|
|
18
|
-
let verified=null;
|
|
19
|
-
return {...inner,request:async request=>{
|
|
20
|
-
if(!verified)verified=inner.request({method:'eth_chainId'}).then(id=>{
|
|
21
|
-
if(Number(BigInt(id))!==chainId)throw new Error('RPC chain identity mismatch');
|
|
22
|
-
}).catch(error=>{verified=null;throw error;});
|
|
23
|
-
await verified;
|
|
24
|
-
const result=await inner.request(request);
|
|
25
|
-
if(request.method==='eth_chainId'&&Number(BigInt(result))!==chainId){verified=null;throw new Error('RPC chain identity mismatch');}
|
|
26
|
-
onSuccess(index);
|
|
27
|
-
return result;
|
|
28
|
-
}};
|
|
29
|
-
});
|
|
30
|
-
return viem.fallback(choices,{rank:false,retryCount:0});
|
|
18
|
+
return pool.transport(chainId,urls,transport,onSuccess);
|
|
31
19
|
}
|
|
32
20
|
function client(chainId,url) {
|
|
33
21
|
const urls=endpoints(chainId);
|
|
34
22
|
const index=url==null?0:urls.indexOf(url);
|
|
35
23
|
const ordered=url==null?urls:index<0?[url]:urls.slice(index);
|
|
36
24
|
const used=new Set();
|
|
37
|
-
const
|
|
25
|
+
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;})});
|
|
27
|
+
clientState.set(c,state);
|
|
38
28
|
endpointReads.set(c,used);return c;
|
|
39
29
|
}
|
|
40
30
|
async function connect(chainId){
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
const c=client(chainId);await c.getChainId();return c;
|
|
32
|
+
}
|
|
33
|
+
async function snapshotBlock(c){
|
|
34
|
+
const header=await c.getBlock({blockTag:'latest'}),state=clientState.get(c);
|
|
35
|
+
if(!state||header.number==null)throw new Error('Snapshot block identity unavailable.');
|
|
36
|
+
pool.pin(state.chainId,header.number,header.hash,state.last);
|
|
37
|
+
return {number:header.number,hash:header.hash};
|
|
47
38
|
}
|
|
48
39
|
const serialize=data=>JSON.stringify(data,(_,v)=>typeof v==='bigint'?v.toString():v,2);
|
|
49
40
|
function safeError(error){
|
|
@@ -60,4 +51,4 @@ function safeError(error){
|
|
|
60
51
|
return diagnostic;
|
|
61
52
|
}
|
|
62
53
|
function rpcEndpointsUsed(c){return [...(endpointReads.get(c)||[])].filter(i=>i>=0).sort((a,b)=>a-b);}
|
|
63
|
-
module.exports={viem,root,assets,networks,bondAbi,endpoints,client,connect,resilientTransport,rpcEndpointsUsed,serialize,safeError};
|
|
54
|
+
module.exports={viem,root,assets,networks,bondAbi,endpoints,client,connect,snapshotBlock,resilientTransport,rpcEndpointsUsed,serialize,safeError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kira-wallet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Manage your wallets with your own AI. Track tokens, research markets and refresh holdings in a local workspace.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -57,10 +57,14 @@
|
|
|
57
57
|
"ows_bridge.cjs",
|
|
58
58
|
"docs/KIRA_LIVE_QA.md",
|
|
59
59
|
"docs/KIRA_DISCOVERY_ACCEPTANCE.md",
|
|
60
|
-
"kira_models.py"
|
|
60
|
+
"kira_models.py",
|
|
61
|
+
"rpc-pool.cjs",
|
|
62
|
+
"sources/known-tokens.json",
|
|
63
|
+
"docs/ALCHEMY_SETUP.md",
|
|
64
|
+
"docs/FREE_RPC.md"
|
|
61
65
|
],
|
|
62
66
|
"scripts": {
|
|
63
|
-
"test": "python3 -m unittest discover -s . -p 'test_*.py' && python3 -m unittest discover -s viewer -p 'test_*.py' && node test-onchain.cjs && node --check viewer/static/app.js && node --check viewer/static/jobs.js && node test-workspace.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/agent.js && node test-agent-ui.cjs && node test-markdown.cjs && node --check viewer/static/report.js && node test-report-ui.cjs && node test-holdings-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/chat-workspace.js && node --check viewer/static/selects.js && node --check site/demo.js"
|
|
67
|
+
"test": "python3 -m unittest discover -s . -p 'test_*.py' && python3 -m unittest discover -s viewer -p 'test_*.py' && node test-onchain.cjs && node --check viewer/static/app.js && node --check viewer/static/jobs.js && node test-workspace.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/agent.js && node test-agent-ui.cjs && node test-markdown.cjs && node --check viewer/static/report.js && node test-report-ui.cjs && node test-holdings-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/chat-workspace.js && node --check viewer/static/selects.js && node --check site/demo.js && node test-rpc-pool.cjs"
|
|
64
68
|
},
|
|
65
69
|
"dependencies": {
|
|
66
70
|
"viem": "2.57.2"
|
package/pipeline-onchain.cjs
CHANGED
|
@@ -40,13 +40,10 @@ async function parallel(items, workers, fn) {
|
|
|
40
40
|
return results;
|
|
41
41
|
}
|
|
42
42
|
async function connect(n) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
catch (e) {error = e;}
|
|
48
|
-
}
|
|
49
|
-
throw error || new Error('No RPC endpoint configured; public fallback is disabled.');
|
|
43
|
+
if(!h.endpoints(n.chain_id).length)throw new Error('No RPC endpoint configured; public fallback is disabled.');
|
|
44
|
+
const c=h.client(n.chain_id);
|
|
45
|
+
if(await c.getChainId()!==n.chain_id)throw new Error('RPC chain identity mismatch');
|
|
46
|
+
endpointIdentity.set(c,h.rpcEndpointsUsed(c)[0]??0);return c;
|
|
50
47
|
}
|
|
51
48
|
function rpcFailure(error) {
|
|
52
49
|
let sizeLimit=false,transportFailure=false;
|
|
@@ -69,7 +66,7 @@ async function calls(c, contracts, block) {
|
|
|
69
66
|
return results;
|
|
70
67
|
} catch (e) {
|
|
71
68
|
const {sizeLimit,transportFailure}=rpcFailure(e);
|
|
72
|
-
if (!sizeLimit
|
|
69
|
+
if (!sizeLimit) return contracts.map(()=>({status:'failure',error:e}));
|
|
73
70
|
if (contracts.length > 1) {
|
|
74
71
|
const mid = Math.ceil(contracts.length/2);
|
|
75
72
|
return [...await calls(c,contracts.slice(0,mid),block), ...await calls(c,contracts.slice(mid),block)];
|
|
@@ -78,10 +75,10 @@ async function calls(c, contracts, block) {
|
|
|
78
75
|
catch (inner) {return [{status:'failure',error:inner}];}
|
|
79
76
|
}
|
|
80
77
|
}
|
|
81
|
-
async function batches(c, contracts, block, size=
|
|
78
|
+
async function batches(c, contracts, block, size=32) {
|
|
82
79
|
const plans = [];
|
|
83
80
|
for (let i=0; i<contracts.length; i+=size) plans.push(contracts.slice(i,i+size));
|
|
84
|
-
return (await parallel(plans,
|
|
81
|
+
return (await parallel(plans,1,b => calls(c,b,block))).flat();
|
|
85
82
|
}
|
|
86
83
|
async function registry(c, n, block) {
|
|
87
84
|
const count = Number(await c.readContract({address:n.mintclub_bond_address,abi:h.bondAbi,functionName:'tokenCount',blockNumber:block}));
|
|
@@ -125,20 +122,29 @@ async function scanChain(input, n, output) {
|
|
|
125
122
|
const row = {wallet:input.wallet,chain_id:n.chain_id,network:n.network,observed_at:stamp(),status:'unavailable',tokens:[],native_balance:null};
|
|
126
123
|
let rpcClient;
|
|
127
124
|
try {
|
|
128
|
-
const c = await connect(n),
|
|
125
|
+
const c = await connect(n), snapshot = await h.snapshotBlock(c),block=snapshot.number;rpcClient=c;
|
|
129
126
|
row.preferred_endpoint_index=endpointIdentity.get(c);
|
|
130
127
|
row.block_number = block;
|
|
128
|
+
row.block_hash = snapshot.hash;
|
|
131
129
|
row.native_balance = v.formatUnits(await c.getBalance({address:input.wallet,blockNumber:block}),18);
|
|
132
130
|
row.rpc_status = 'available';
|
|
131
|
+
// Check discovered and built-in candidates before expensive cold registry work.
|
|
132
|
+
const initial=new Map(input.candidates.filter(t=>t.chain_id===n.chain_id).map(t=>[t.address.toLowerCase(),{...t,token_type:'ERC20'}]));
|
|
133
|
+
const initialTokens=[...initial.values()];
|
|
134
|
+
const initialBalances=await batches(c,initialTokens.map(t=>({address:t.address,abi:erc20,functionName:'balanceOf',args:[input.wallet]})),block);
|
|
135
|
+
const firstReads=new Map(initialTokens.map((t,i)=>[t.address.toLowerCase(),initialBalances[i]]));
|
|
133
136
|
let reg;
|
|
134
137
|
try {reg = await registry(c,n,block);} catch (e) {row.registry_error = h.safeError(e); reg = {count:null,entries:[],errors:1,cache_reused:0};}
|
|
135
138
|
const plans = new Map();
|
|
136
|
-
for (const t of
|
|
139
|
+
for (const [key,t] of initial) plans.set(key,t);
|
|
137
140
|
for (const t of reg.entries.filter(t => t.address && t.token_type && t.token_type!=='unknown')) {
|
|
138
141
|
plans.set(t.address.toLowerCase(),{...plans.get(t.address.toLowerCase()),...t,registered_mintclub:true});
|
|
139
142
|
}
|
|
140
143
|
const tokens = [...plans.values()];
|
|
141
|
-
const
|
|
144
|
+
const remaining=tokens.filter(t=>!firstReads.has(t.address.toLowerCase())||t.token_type==='ERC1155');
|
|
145
|
+
const later=await batches(c,remaining.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);
|
|
146
|
+
remaining.forEach((t,i)=>firstReads.set(t.address.toLowerCase(),later[i]));
|
|
147
|
+
const balances=tokens.map(t=>firstReads.get(t.address.toLowerCase()));
|
|
142
148
|
const errors = []; let checked = 0, erc20Checked = 0, erc1155Checked = 0;
|
|
143
149
|
const held = [];
|
|
144
150
|
tokens.forEach((t,i) => {
|
|
@@ -192,7 +198,7 @@ function deployment(chain,protocol,name) {
|
|
|
192
198
|
return deployments.find(r=>r.chainId===chain && r.protocol===protocol && r.contract.toLowerCase()===name.toLowerCase())?.address;
|
|
193
199
|
}
|
|
194
200
|
async function dexChain(n, tokens) {
|
|
195
|
-
const c=await connect(n),block=await
|
|
201
|
+
const c=await connect(n),block=(await h.snapshotBlock(c)).number;
|
|
196
202
|
// Supplement DEX indexer discovery on Base with the canonical WETH/USDC factories.
|
|
197
203
|
if (n.chain_id===8453) {
|
|
198
204
|
const queries=[];
|
|
@@ -265,19 +271,19 @@ async function main() {
|
|
|
265
271
|
const input=JSON.parse(fs.readFileSync(inputPath));
|
|
266
272
|
if(!v.isAddress(input.wallet))throw new Error('Invalid wallet address');
|
|
267
273
|
if(mode==='scan') {
|
|
268
|
-
const rows=await parallel(input.networks,
|
|
274
|
+
const rows=await parallel(input.networks,2,n=>scanChain(input,n,outputPath));
|
|
269
275
|
write(path.join(outputPath,'onchain-summary.json'),{wallet:input.wallet,observed_at:stamp(),chains:rows});
|
|
270
276
|
}else if(mode==='dex') {
|
|
271
|
-
const result=await parallel(input.networks.filter(n=>input.tokens.some(t=>t.chain_id===n.chain_id)),
|
|
277
|
+
const result=await parallel(input.networks.filter(n=>input.tokens.some(t=>t.chain_id===n.chain_id)),2,async n=>{
|
|
272
278
|
const group=input.tokens.filter(t=>t.chain_id===n.chain_id);
|
|
273
279
|
try{return await dexChain(n,group);}catch(e){group.forEach(t=>t.dex_verification_error=h.safeError(e));return group;}
|
|
274
280
|
});
|
|
275
281
|
write(outputPath,{wallet:input.wallet,observed_at:stamp(),tokens:result.flat()});
|
|
276
282
|
}else if(mode==='reserves') {
|
|
277
|
-
const result=await parallel(input.networks.filter(n=>input.tokens.some(t=>t.chain_id===n.chain_id&&t.mintclub)),
|
|
283
|
+
const result=await parallel(input.networks.filter(n=>input.tokens.some(t=>t.chain_id===n.chain_id&&t.mintclub)),2,async n=>{
|
|
278
284
|
const rows=[];
|
|
279
285
|
try {
|
|
280
|
-
const c=await connect(n),block=await
|
|
286
|
+
const c=await connect(n),block=(await h.snapshotBlock(c)).number;
|
|
281
287
|
const queue=input.tokens.filter(t=>t.chain_id===n.chain_id&&t.mintclub).map(t=>t.token_address),seen=new Set();
|
|
282
288
|
for(let i=0;i<queue.length;i++) {
|
|
283
289
|
const address=queue[i],key=address.toLowerCase();
|