solid-drift 0.15.0 → 0.16.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.
package/README.md CHANGED
@@ -1094,6 +1094,37 @@ const connect = createConnectButton(() => btn, { strength: 0.35 });
1094
1094
 
1095
1095
  Returns `{ copyTick, chainPulse, status }`. `status()` is `"idle"`, `"ticking"` (check visible), or `"pulsing"` (ring expanding). Call `chainPulse()` after a successful connection or network switch. Under reduced motion there is no magnetic pull or scale; `copyTick()` and `chainPulse()` still show their overlays statically.
1096
1096
 
1097
+ ### Web3 data layer
1098
+
1099
+ A zero-dependency read layer for chain and market data as signals: public RPC and API endpoints over `fetch`, with user-swappable endpoints. Every network primitive shares the `{ data, error, status, retry, abort }` shape, is SSR-safe (nothing fetches on the server), and polls with error backoff. Defaults are conservative because public endpoints are rate-limited. This is read-only: transaction signing stays with wallet libraries.
1100
+
1101
+ ```tsx
1102
+ import {
1103
+ createPoll, createTokenPrice, createPriceChange, createPriceCompare,
1104
+ createGasPrice, createBalance, createTxReceipt, createBlockNumber,
1105
+ createChainlinkPrice, createNFTMetadata, createENS, createIdenticon,
1106
+ createChain, CHAINS, shortenAddress, isAddress, formatUnits, parseUnits,
1107
+ } from "solid-drift";
1108
+ ```
1109
+
1110
+ **Polling infra.** `createPoll(fetcher, options?)` fetches immediately (unless `immediate: false`), then on `interval` (default 30s). On error the interval multiplies by `backoff` (default 2) up to `maxInterval` (default 5min) and resets on the next success. Returns `{ data, error, status, retry, abort }`; `status()` is `"idle"`, `"loading"`, `"success"`, or `"error"`.
1111
+
1112
+ **Pure helpers.** `isAddress(value)` checks `0x` + 40 hex chars. `shortenAddress(address, chars = 4)` renders `0xd8dA…6045` and passes invalid input through. `formatUnits(value, decimals = 18)` formats wei-style bigints as decimal strings without float artifacts; `parseUnits(value, decimals = 18)` parses them back and throws on invalid input. `CHAINS` maps seven chain ids (Ethereum, Optimism, BNB Chain, Polygon, Base, Arbitrum One, Sepolia) to name, currency, decimals, explorer, and a public RPC; `createChain(id)` looks one up as a reactive accessor (`undefined` for unknown ids).
1113
+
1114
+ **Market.** `createTokenPrice(tokenId, options?)` polls CoinGecko's public API (default 60s; swap `endpoint` or `vsCurrency`) and exposes `price()` and `change24h()`. `createPriceChange(source, options?)` samples any numeric signal on change and on `sampleMs` (default 60s), keeps a rolling `windowMs` (default 1h), and reports the percent change between the first and last sample; `reset()` clears the window. `createPriceCompare(a, b)` compares two price signals with `ratio()`, `diffPercent()`, and `leader()` (`"a"`, `"b"`, or `"tie"`).
1115
+
1116
+ **Chain (JSON-RPC).** `createGasPrice(options?)` reads `eth_gasPrice` every 15s as `{ wei, gwei }`. `createBalance(address, options?)` reads the native balance every 20s, or an ERC20 `balanceOf` when `token` is set, exposing `balance()` (bigint) and `formatted()`. `createTxReceipt(hash, options?)` polls every 4s until the receipt lands, then stops on its own; `mined()` mirrors that and `receipt()` carries `transactionHash`, `blockNumber`, `success`, and `gasUsed`. `createBlockNumber(options?)` polls the latest block every 12s as a chain-health heartbeat. `createChainlinkPrice(feed, options?)` reads a Chainlink `AggregatorV3Interface` feed on-chain (`decimals()` once, then `latestRoundData()` every 30s). All take an `endpoint` option defaulting to a public mainnet RPC.
1117
+
1118
+ **Identity and NFTs.** `createNFTMetadata(contract, tokenId, options?)` fetches `tokenURI` on-chain, resolves the JSON (one-shot with `retry`), rewrites `ipfs://` through a gateway, and exposes `metadata()` (`name`, `description`, `image`, `attributes`, `raw`) plus `image()`. `createENS(address, options?)` reverse-resolves an address through the public ENS registry (one-shot with `retry`); `name()` is `undefined` when no name is set. `createIdenticon(address, options?)` renders a deterministic mirrored-grid SVG avatar as a data URI, pure computation, works on the server.
1119
+
1120
+ ```tsx
1121
+ const { price, change24h } = createTokenPrice("ethereum");
1122
+ const { change } = createPriceChange(price);
1123
+ const { formatted } = createBalance("0xd8dA…6045");
1124
+ const { mined, receipt } = createTxReceipt("0x5c50…f7b");
1125
+ const avatar = createIdenticon("0xd8dA…6045");
1126
+ ```
1127
+
1097
1128
  ### `createToast(options?)`
1098
1129
 
1099
1130
  A signal-native toast queue with choreographed lifecycle. The primitive owns timing and state; you own the rendering, so no component opinions leak into your design system. Each toast moves through `"entering"` to `"visible"` to `"leaving"` to removed on the shared animation clock: bind `state` to CSS classes or drift values for enter/exit motion without any timers of your own.
package/dist/index.d.ts CHANGED
@@ -34,3 +34,5 @@ export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer,
34
34
  export type { StreamRevealStatus, StreamRevealOptions, StreamRevealControls, AgentState, AgentStateTransition, AgentStateOptions, AgentStateControls, DriftSpecPrimitive, DriftSpecStep, DriftSpec, SpecPlayerStatus, SpecPlayerControls, } from "./ai.js";
35
35
  export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
36
36
  export type { TxState, TxStatusInput, TxLifecycleOptions, TxLifecycleControls, TickerOptions, TickerControls, MintRevealStatus, MintRevealOptions, MintRevealControls, ConnectButtonOptions, ConnectButtonStatus, ConnectButtonControls, } from "./web3.js";
37
+ export { createPoll, shortenAddress, isAddress, formatUnits, parseUnits, CHAINS, createChain, createTokenPrice, createPriceChange, createPriceCompare, createGasPrice, createBalance, createTxReceipt, createBlockNumber, createChainlinkPrice, createNFTMetadata, createENS, createIdenticon, } from "./web3data.js";
38
+ export type { PollStatus, PollOptions, PollControls, ChainInfo, TokenPrice, TokenPriceOptions, PriceChangeOptions, GasPriceOptions, GasPriceData, BalanceOptions, BalanceData, TxReceiptData, TxReceiptOptions, BlockNumberOptions, ChainlinkPriceOptions, NFTMetadata, NFTMetadataOptions, ENSOptions, IdenticonOptions, } from "./web3data.js";
package/dist/index.js CHANGED
@@ -31,3 +31,4 @@ export { createKineticType, createScenePlayer, createShowreel, createCamera, cre
31
31
  export { createDrag, } from "./gesture.js";
32
32
  export { createStreamReveal, createAgentState, parseDriftSpec, createSpecPlayer, DriftSpecError, } from "./ai.js";
33
33
  export { createTxLifecycle, createTicker, createMintReveal, createConnectButton, } from "./web3.js";
34
+ export { createPoll, shortenAddress, isAddress, formatUnits, parseUnits, CHAINS, createChain, createTokenPrice, createPriceChange, createPriceCompare, createGasPrice, createBalance, createTxReceipt, createBlockNumber, createChainlinkPrice, createNFTMetadata, createENS, createIdenticon, } from "./web3data.js";
@@ -0,0 +1,323 @@
1
+ import { type Accessor } from "solid-js";
2
+ /**
3
+ * Web3 data layer: zero-dependency chain and market data as signals.
4
+ *
5
+ * Public RPC and API endpoints over fetch, with user-swappable endpoints.
6
+ * Every network primitive shares the `{ data, error, status, retry, abort }`
7
+ * shape and is SSR-safe (nothing fetches on the server).
8
+ *
9
+ * Honest limits: public endpoints are rate-limited, so default polling
10
+ * intervals are conservative. This is a read-only data layer: transaction
11
+ * signing stays with wallet libraries like wagmi.
12
+ */
13
+ export type PollStatus = "idle" | "loading" | "success" | "error";
14
+ export interface PollOptions {
15
+ /** Milliseconds between fetches. Default 30000. */
16
+ interval?: number;
17
+ /** Error backoff multiplier. Default 2. */
18
+ backoff?: number;
19
+ /** Backoff cap in milliseconds. Default 300000. */
20
+ maxInterval?: number;
21
+ /** Fetch immediately on creation. Default true. */
22
+ immediate?: boolean;
23
+ }
24
+ export interface PollControls<T> {
25
+ data: Accessor<T | undefined>;
26
+ error: Accessor<Error | null>;
27
+ status: Accessor<PollStatus>;
28
+ /** Fetch now and reset the backoff. */
29
+ retry: () => void;
30
+ /** Stop polling and abort any in-flight request. */
31
+ abort: () => void;
32
+ }
33
+ /**
34
+ * Backoff polling infrastructure for the data primitives.
35
+ *
36
+ * Fetches immediately (unless `immediate: false`), then on `interval`.
37
+ * On error the interval multiplies by `backoff` up to `maxInterval` and
38
+ * resets on the next success. Uses `setTimeout`, so background tabs get
39
+ * the browser's natural timer throttling instead of a busy rAF loop.
40
+ * SSR-safe: never fetches on the server.
41
+ *
42
+ * ```ts
43
+ * const { data, status, retry, abort } = createPoll(
44
+ * async (signal) => {
45
+ * const res = await fetch("https://api.example.com/price", { signal });
46
+ * return res.json();
47
+ * },
48
+ * { interval: 30000 },
49
+ * );
50
+ * ```
51
+ */
52
+ export declare function createPoll<T>(fetcher: (signal: AbortSignal) => Promise<T>, options?: PollOptions): PollControls<T>;
53
+ /**
54
+ * Shorten an EVM address: `0x1234567890abcdef...` becomes `0x1234…abcd`.
55
+ * Returns the input unchanged when it is not a valid address.
56
+ */
57
+ export declare function shortenAddress(address: string, chars?: number): string;
58
+ /** True for `0x` + 40 hex chars. Checksum-agnostic. */
59
+ export declare function isAddress(value: string): boolean;
60
+ /**
61
+ * Format a wei-style integer as a decimal string: `formatUnits(1500000000000000000n)`
62
+ * is `"1.5"`. Accepts bigint or integer strings. BigInt-safe, no floats.
63
+ */
64
+ export declare function formatUnits(value: bigint | string, decimals?: number): string;
65
+ /**
66
+ * Parse a decimal string into wei-style bigint: `parseUnits("1.5")` is
67
+ * `1500000000000000000n`. Throws on invalid input or too many decimals.
68
+ */
69
+ export declare function parseUnits(value: string, decimals?: number): bigint;
70
+ export interface ChainInfo {
71
+ id: number;
72
+ name: string;
73
+ currency: string;
74
+ decimals: number;
75
+ explorer: string;
76
+ rpc: string;
77
+ }
78
+ /** Registry of well-known EVM chains: id to name, currency, explorer, RPC. */
79
+ export declare const CHAINS: Record<number, ChainInfo>;
80
+ /**
81
+ * Look up a chain in `CHAINS` as an accessor. Unknown ids give `undefined`.
82
+ *
83
+ * ```ts
84
+ * const chain = createChain(() => 1)
85
+ * chain()?.explorer // "https://etherscan.io"
86
+ * ```
87
+ */
88
+ export declare function createChain(source: number | Accessor<number>): Accessor<ChainInfo | undefined>;
89
+ /**
90
+ * keccak256 of a byte array. Exported for tests; not part of the public API.
91
+ */
92
+ export declare function keccak256(data: Uint8Array): Uint8Array<ArrayBuffer>;
93
+ export interface TokenPrice {
94
+ /** Price in the quote currency. */
95
+ price: number;
96
+ /** 24h change percent, when the source reports it. */
97
+ change24h?: number;
98
+ }
99
+ export interface TokenPriceOptions extends PollOptions {
100
+ /** Quote currency id. Default "usd". */
101
+ vsCurrency?: string;
102
+ /** Price API base. Default CoinGecko public API. */
103
+ endpoint?: string;
104
+ }
105
+ /**
106
+ * Live token price as a signal, via CoinGecko's public API.
107
+ *
108
+ * The free endpoint is rate-limited; the default 60s interval is
109
+ * conservative on purpose. Pass your own `endpoint` (any base that
110
+ * answers `/simple/price?ids={id}&vs_currencies={vs}&include_24hr_change=true`).
111
+ *
112
+ * ```ts
113
+ * const { price, change24h, status } = createTokenPrice("ethereum");
114
+ * <Show when={status() === "success"}>
115
+ * ${(price() ?? 0).toFixed(2)} ({(change24h() ?? 0).toFixed(1)}%)
116
+ * </Show>
117
+ * ```
118
+ */
119
+ export declare function createTokenPrice(tokenId: string | Accessor<string>, options?: TokenPriceOptions): PollControls<TokenPrice> & {
120
+ price: Accessor<number | undefined>;
121
+ change24h: Accessor<number | undefined>;
122
+ };
123
+ export interface PriceChangeOptions {
124
+ /** Rolling window in ms. Default 3600000 (1h). */
125
+ windowMs?: number;
126
+ /** How often to sample the source in ms. Default 60000. */
127
+ sampleMs?: number;
128
+ }
129
+ /**
130
+ * Percent change of any numeric signal over a rolling window.
131
+ *
132
+ * Samples the source on each change and on `sampleMs`, keeps samples
133
+ * within `windowMs`, and reports `(last - first) / first * 100`.
134
+ * `undefined` until at least two samples exist. Signal-native, no network.
135
+ *
136
+ * ```ts
137
+ * const { price } = createTokenPrice("ethereum");
138
+ * const { change, reset } = createPriceChange(price);
139
+ * ```
140
+ */
141
+ export declare function createPriceChange(source: Accessor<number | undefined>, options?: PriceChangeOptions): {
142
+ change: Accessor<number | undefined>;
143
+ reset: () => void;
144
+ };
145
+ /**
146
+ * Compare two numeric signals: their ratio, percent difference, and which
147
+ * is larger. Any side `undefined` makes everything `undefined` until both
148
+ * have values. Signal-native, no network.
149
+ *
150
+ * ```ts
151
+ * const { price: eth } = createTokenPrice("ethereum");
152
+ * const { price: btc } = createTokenPrice("bitcoin");
153
+ * const { ratio, diffPercent, leader } = createPriceCompare(eth, btc);
154
+ * ```
155
+ */
156
+ export declare function createPriceCompare(a: Accessor<number | undefined>, b: Accessor<number | undefined>): {
157
+ ratio: Accessor<number | undefined>;
158
+ diffPercent: Accessor<number | undefined>;
159
+ leader: Accessor<"a" | "b" | "tie" | undefined>;
160
+ };
161
+ export interface GasPriceOptions extends PollOptions {
162
+ /** JSON-RPC endpoint. Default a public mainnet endpoint. */
163
+ endpoint?: string;
164
+ }
165
+ export interface GasPriceData {
166
+ wei: bigint;
167
+ gwei: number;
168
+ }
169
+ /**
170
+ * Current gas price over JSON-RPC (`eth_gasPrice`), as wei bigint and gwei.
171
+ * Default 15s polling. Swap `endpoint` for any chain.
172
+ */
173
+ export declare function createGasPrice(options?: GasPriceOptions): PollControls<GasPriceData> & {
174
+ wei: Accessor<bigint | undefined>;
175
+ gwei: Accessor<number | undefined>;
176
+ };
177
+ export interface BalanceOptions extends PollOptions {
178
+ /** JSON-RPC endpoint. Default a public mainnet endpoint. */
179
+ endpoint?: string;
180
+ /** ERC20 token contract. Omit for the native balance. */
181
+ token?: string;
182
+ /** Decimals for formatting. Default 18. */
183
+ decimals?: number;
184
+ }
185
+ export interface BalanceData {
186
+ balance: bigint;
187
+ formatted: string;
188
+ }
189
+ /**
190
+ * Token balance of an address: native (`eth_getBalance`) or ERC20
191
+ * (`balanceOf` via `eth_call`). Read-only; never signs.
192
+ *
193
+ * ```ts
194
+ * const { formatted } = createBalance("0xabc…", { token: "0xdef…" });
195
+ * ```
196
+ */
197
+ export declare function createBalance(address: string, options?: BalanceOptions): PollControls<BalanceData> & {
198
+ balance: Accessor<bigint | undefined>;
199
+ formatted: Accessor<string | undefined>;
200
+ };
201
+ export interface TxReceiptData {
202
+ transactionHash: string;
203
+ blockNumber: number;
204
+ /** true when `status` is 0x1, false when 0x0 (reverted). */
205
+ success: boolean;
206
+ gasUsed: bigint;
207
+ }
208
+ export interface TxReceiptOptions extends PollOptions {
209
+ /** JSON-RPC endpoint. Default a public mainnet endpoint. */
210
+ endpoint?: string;
211
+ }
212
+ /**
213
+ * Watch a transaction hash until its receipt lands. Polls every 4s and
214
+ * stops on its own once the receipt arrives; `mined()` mirrors that.
215
+ * Read-only confirmation; pairs with `createTxLifecycle` from web3.ts.
216
+ */
217
+ export declare function createTxReceipt(hash: string, options?: TxReceiptOptions): PollControls<TxReceiptData | null> & {
218
+ receipt: Accessor<TxReceiptData | null | undefined>;
219
+ mined: Accessor<boolean>;
220
+ };
221
+ export interface BlockNumberOptions extends PollOptions {
222
+ /** JSON-RPC endpoint. Default a public mainnet endpoint. */
223
+ endpoint?: string;
224
+ }
225
+ /**
226
+ * Latest block number over JSON-RPC. Default 12s polling.
227
+ * Handy as a chain-health heartbeat and a cache-busting ticker.
228
+ */
229
+ export declare function createBlockNumber(options?: BlockNumberOptions): PollControls<number> & {
230
+ blockNumber: Accessor<number | undefined>;
231
+ };
232
+ export interface ChainlinkPriceOptions extends PollOptions {
233
+ /** JSON-RPC endpoint. Default a public mainnet endpoint. */
234
+ endpoint?: string;
235
+ }
236
+ /**
237
+ * Read a Chainlink `AggregatorV3Interface` price feed on-chain:
238
+ * `decimals()` once, then `latestRoundData()` polled every 30s.
239
+ * Feed addresses live on the
240
+ * [Chainlink docs](https://docs.chain.link/data-feeds/price-feeds/addresses).
241
+ *
242
+ * ```ts
243
+ * // ETH / USD feed on mainnet
244
+ * const { price } = createChainlinkPrice("0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419");
245
+ * ```
246
+ */
247
+ export declare function createChainlinkPrice(feed: string, options?: ChainlinkPriceOptions): PollControls<number> & {
248
+ price: Accessor<number | undefined>;
249
+ };
250
+ export interface NFTMetadata {
251
+ name?: string;
252
+ description?: string;
253
+ image?: string;
254
+ attributes?: Array<Record<string, unknown>>;
255
+ raw: unknown;
256
+ }
257
+ export interface NFTMetadataOptions {
258
+ /** JSON-RPC endpoint for `tokenURI`. Default a public mainnet endpoint. */
259
+ endpoint?: string;
260
+ /** IPFS gateway base. Default "https://ipfs.io". */
261
+ gateway?: string;
262
+ }
263
+ /**
264
+ * Fetch an NFT's `tokenURI` on-chain and resolve its JSON metadata
265
+ * (one-shot, with `retry`). `ipfs://` URIs are rewritten through the
266
+ * gateway. Returns the parsed fields plus `raw` for anything custom.
267
+ *
268
+ * ```ts
269
+ * const { metadata, image, status, retry } = createNFTMetadata(
270
+ * "0xcontract…",
271
+ * 42,
272
+ * );
273
+ * ```
274
+ */
275
+ export declare function createNFTMetadata(contract: string, tokenId: string | number | bigint, options?: NFTMetadataOptions): {
276
+ data: Accessor<NFTMetadata | undefined>;
277
+ error: Accessor<Error | null>;
278
+ status: Accessor<PollStatus>;
279
+ retry: () => void;
280
+ abort: () => void;
281
+ metadata: Accessor<NFTMetadata | undefined>;
282
+ image: Accessor<string | undefined>;
283
+ };
284
+ export interface ENSOptions {
285
+ /** JSON-RPC endpoint. Default a public mainnet endpoint. */
286
+ endpoint?: string;
287
+ }
288
+ /**
289
+ * Reverse-resolve an address to its ENS name via the public ENS registry
290
+ * (one-shot, with `retry`). `undefined` when the address has no name set;
291
+ * errors (network, RPC) surface on `error`.
292
+ *
293
+ * ```ts
294
+ * const { name, status } = createENS("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
295
+ * ```
296
+ */
297
+ export declare function createENS(address: string, options?: ENSOptions): {
298
+ data: Accessor<string | undefined>;
299
+ error: Accessor<Error | null>;
300
+ status: Accessor<PollStatus>;
301
+ retry: () => void;
302
+ abort: () => void;
303
+ name: Accessor<string | undefined>;
304
+ };
305
+ export interface IdenticonOptions {
306
+ /** Pixel size of the square image. Default 64. */
307
+ size?: number;
308
+ /** Grid cells per side. Default 8. */
309
+ cells?: number;
310
+ /** Background color. Default "#f0f0f0". */
311
+ background?: string;
312
+ }
313
+ /**
314
+ * Deterministic identicon avatar for any address as a data URI: a mirrored
315
+ * random-walk grid in SVG, so the same address always renders the same
316
+ * image. Pure computation, works on the server, no network.
317
+ *
318
+ * ```tsx
319
+ * const avatar = createIdenticon("0xabc…");
320
+ * <img src={avatar()} alt="avatar" width={64} height={64} />;
321
+ * ```
322
+ */
323
+ export declare function createIdenticon(address: string | Accessor<string>, options?: IdenticonOptions): Accessor<string>;
@@ -0,0 +1,751 @@
1
+ import { createEffect, createSignal, onCleanup, } from "solid-js";
2
+ /**
3
+ * Backoff polling infrastructure for the data primitives.
4
+ *
5
+ * Fetches immediately (unless `immediate: false`), then on `interval`.
6
+ * On error the interval multiplies by `backoff` up to `maxInterval` and
7
+ * resets on the next success. Uses `setTimeout`, so background tabs get
8
+ * the browser's natural timer throttling instead of a busy rAF loop.
9
+ * SSR-safe: never fetches on the server.
10
+ *
11
+ * ```ts
12
+ * const { data, status, retry, abort } = createPoll(
13
+ * async (signal) => {
14
+ * const res = await fetch("https://api.example.com/price", { signal });
15
+ * return res.json();
16
+ * },
17
+ * { interval: 30000 },
18
+ * );
19
+ * ```
20
+ */
21
+ export function createPoll(fetcher, options = {}) {
22
+ const { interval = 30000, backoff = 2, maxInterval = 300000, immediate = true, } = options;
23
+ const [data, setData] = createSignal(undefined);
24
+ const [error, setError] = createSignal(null);
25
+ const [status, setStatus] = createSignal("idle");
26
+ let timer = null;
27
+ let aborter = null;
28
+ let delay = interval;
29
+ let stopped = false;
30
+ const clearTimer = () => {
31
+ if (timer !== null) {
32
+ clearTimeout(timer);
33
+ timer = null;
34
+ }
35
+ };
36
+ const run = async () => {
37
+ if (stopped || typeof window === "undefined")
38
+ return;
39
+ clearTimer();
40
+ aborter?.abort();
41
+ aborter = new AbortController();
42
+ const signal = aborter.signal;
43
+ setStatus("loading");
44
+ try {
45
+ const value = await fetcher(signal);
46
+ if (signal.aborted || stopped)
47
+ return;
48
+ setData(() => value);
49
+ setError(null);
50
+ setStatus("success");
51
+ delay = interval;
52
+ }
53
+ catch (e) {
54
+ if (signal.aborted || stopped)
55
+ return;
56
+ setError(e instanceof Error ? e : new Error(String(e)));
57
+ setStatus("error");
58
+ delay = Math.min(delay * backoff, maxInterval);
59
+ }
60
+ if (!stopped && typeof window !== "undefined") {
61
+ timer = setTimeout(() => void run(), delay);
62
+ }
63
+ };
64
+ const retry = () => {
65
+ stopped = false;
66
+ delay = interval;
67
+ void run();
68
+ };
69
+ const abort = () => {
70
+ stopped = true;
71
+ clearTimer();
72
+ aborter?.abort();
73
+ aborter = null;
74
+ };
75
+ if (typeof window !== "undefined" && immediate) {
76
+ void run();
77
+ }
78
+ onCleanup(abort);
79
+ return { data, error, status, retry, abort };
80
+ }
81
+ /**
82
+ * Shorten an EVM address: `0x1234567890abcdef...` becomes `0x1234…abcd`.
83
+ * Returns the input unchanged when it is not a valid address.
84
+ */
85
+ export function shortenAddress(address, chars = 4) {
86
+ if (!isAddress(address))
87
+ return address;
88
+ return `${address.slice(0, 2 + chars)}…${address.slice(-chars)}`;
89
+ }
90
+ /** True for `0x` + 40 hex chars. Checksum-agnostic. */
91
+ export function isAddress(value) {
92
+ return /^0x[0-9a-fA-F]{40}$/.test(value);
93
+ }
94
+ /**
95
+ * Format a wei-style integer as a decimal string: `formatUnits(1500000000000000000n)`
96
+ * is `"1.5"`. Accepts bigint or integer strings. BigInt-safe, no floats.
97
+ */
98
+ export function formatUnits(value, decimals = 18) {
99
+ const str = typeof value === "bigint" ? value.toString() : value;
100
+ if (!/^-?\d+$/.test(str)) {
101
+ throw new Error("formatUnits: value must be an integer string or bigint.");
102
+ }
103
+ const negative = str.startsWith("-");
104
+ const digits = negative ? str.slice(1) : str;
105
+ const padded = digits.padStart(decimals + 1, "0");
106
+ const int = decimals > 0 ? padded.slice(0, -decimals) : padded;
107
+ const frac = decimals > 0 ? padded.slice(-decimals).replace(/0+$/, "") : "";
108
+ const intClean = int.replace(/^0+(?=\d)/, "");
109
+ return (negative ? "-" : "") + intClean + (frac ? `.${frac}` : "");
110
+ }
111
+ /**
112
+ * Parse a decimal string into wei-style bigint: `parseUnits("1.5")` is
113
+ * `1500000000000000000n`. Throws on invalid input or too many decimals.
114
+ */
115
+ export function parseUnits(value, decimals = 18) {
116
+ const m = /^(-?)(\d*)(?:\.(\d*))?$/.exec(value.trim());
117
+ if (!m || (m[2] === "" && m[3] === "")) {
118
+ throw new Error("parseUnits: invalid decimal string.");
119
+ }
120
+ const fracPart = m[3] ?? "";
121
+ if (fracPart.length > decimals) {
122
+ throw new Error("parseUnits: too many decimal places.");
123
+ }
124
+ const int = (m[2] === "" ? "0" : m[2]) + fracPart.padEnd(decimals, "0");
125
+ const clean = int.replace(/^0+(?=\d)/, "");
126
+ return BigInt(`${m[1]}${clean}`);
127
+ }
128
+ /** Registry of well-known EVM chains: id to name, currency, explorer, RPC. */
129
+ export const CHAINS = {
130
+ 1: {
131
+ id: 1,
132
+ name: "Ethereum",
133
+ currency: "ETH",
134
+ decimals: 18,
135
+ explorer: "https://etherscan.io",
136
+ rpc: "https://ethereum.publicnode.com",
137
+ },
138
+ 10: {
139
+ id: 10,
140
+ name: "Optimism",
141
+ currency: "ETH",
142
+ decimals: 18,
143
+ explorer: "https://optimistic.etherscan.io",
144
+ rpc: "https://optimism.publicnode.com",
145
+ },
146
+ 56: {
147
+ id: 56,
148
+ name: "BNB Chain",
149
+ currency: "BNB",
150
+ decimals: 18,
151
+ explorer: "https://bscscan.com",
152
+ rpc: "https://bsc.publicnode.com",
153
+ },
154
+ 137: {
155
+ id: 137,
156
+ name: "Polygon",
157
+ currency: "POL",
158
+ decimals: 18,
159
+ explorer: "https://polygonscan.com",
160
+ rpc: "https://polygon-bor.publicnode.com",
161
+ },
162
+ 8453: {
163
+ id: 8453,
164
+ name: "Base",
165
+ currency: "ETH",
166
+ decimals: 18,
167
+ explorer: "https://basescan.org",
168
+ rpc: "https://base.publicnode.com",
169
+ },
170
+ 42161: {
171
+ id: 42161,
172
+ name: "Arbitrum One",
173
+ currency: "ETH",
174
+ decimals: 18,
175
+ explorer: "https://arbiscan.io",
176
+ rpc: "https://arbitrum-one.publicnode.com",
177
+ },
178
+ 11155111: {
179
+ id: 11155111,
180
+ name: "Sepolia",
181
+ currency: "ETH",
182
+ decimals: 18,
183
+ explorer: "https://sepolia.etherscan.io",
184
+ rpc: "https://ethereum-sepolia.publicnode.com",
185
+ },
186
+ };
187
+ /**
188
+ * Look up a chain in `CHAINS` as an accessor. Unknown ids give `undefined`.
189
+ *
190
+ * ```ts
191
+ * const chain = createChain(() => 1)
192
+ * chain()?.explorer // "https://etherscan.io"
193
+ * ```
194
+ */
195
+ export function createChain(source) {
196
+ const get = typeof source === "function" ? source : () => source;
197
+ return () => CHAINS[get()];
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // keccak256 (for ENS namehash). Compact BigInt implementation, no dependencies.
201
+ // ---------------------------------------------------------------------------
202
+ const KECCAK_RC = [
203
+ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an,
204
+ 0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n,
205
+ 0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an,
206
+ 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
207
+ 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n,
208
+ 0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n,
209
+ 0x000000000000800an, 0x800000008000000an, 0x8000000080008081n,
210
+ 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
211
+ ];
212
+ const KECCAK_ROT = [
213
+ [0, 36, 3, 41, 18],
214
+ [1, 44, 10, 45, 2],
215
+ [62, 6, 43, 15, 61],
216
+ [28, 55, 25, 21, 56],
217
+ [27, 20, 39, 8, 14],
218
+ ];
219
+ const MASK64 = 0xffffffffffffffffn;
220
+ function rotl64(v, n) {
221
+ if (n === 0)
222
+ return v;
223
+ return (((v << BigInt(n)) | (v >> BigInt(64 - n))) & MASK64);
224
+ }
225
+ function keccakF(state) {
226
+ for (let round = 0; round < 24; round++) {
227
+ const c = [0n, 0n, 0n, 0n, 0n];
228
+ for (let x = 0; x < 5; x++) {
229
+ for (let y = 0; y < 5; y++)
230
+ c[x] ^= state[x + 5 * y];
231
+ }
232
+ const d = [0n, 0n, 0n, 0n, 0n];
233
+ for (let x = 0; x < 5; x++) {
234
+ d[x] = c[(x + 4) % 5] ^ rotl64(c[(x + 1) % 5], 1);
235
+ }
236
+ for (let x = 0; x < 5; x++) {
237
+ for (let y = 0; y < 5; y++)
238
+ state[x + 5 * y] ^= d[x];
239
+ }
240
+ const b = new Array(25);
241
+ for (let x = 0; x < 5; x++) {
242
+ for (let y = 0; y < 5; y++) {
243
+ b[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(state[x + 5 * y], KECCAK_ROT[x][y]);
244
+ }
245
+ }
246
+ for (let x = 0; x < 5; x++) {
247
+ for (let y = 0; y < 5; y++) {
248
+ state[x + 5 * y] =
249
+ b[x + 5 * y] ^ ((~b[(x + 1) % 5 + 5 * y] & MASK64) & b[(x + 2) % 5 + 5 * y]);
250
+ }
251
+ }
252
+ state[0] ^= KECCAK_RC[round];
253
+ }
254
+ }
255
+ /**
256
+ * keccak256 of a byte array. Exported for tests; not part of the public API.
257
+ */
258
+ export function keccak256(data) {
259
+ const RATE = 136;
260
+ let total = data.length + 1;
261
+ total = total % RATE === 0 ? total + RATE : total + (RATE - (total % RATE));
262
+ const padded = new Uint8Array(total);
263
+ padded.set(data, 0);
264
+ padded[data.length] = 0x01;
265
+ padded[total - 1] |= 0x80;
266
+ const state = new Array(25).fill(0n);
267
+ for (let off = 0; off < total; off += RATE) {
268
+ for (let i = 0; i < RATE / 8; i++) {
269
+ let lane = 0n;
270
+ for (let j = 0; j < 8; j++) {
271
+ lane |= BigInt(padded[off + i * 8 + j]) << BigInt(8 * j);
272
+ }
273
+ state[i] ^= lane;
274
+ }
275
+ keccakF(state);
276
+ }
277
+ const out = new Uint8Array(32);
278
+ for (let i = 0; i < 4; i++) {
279
+ for (let j = 0; j < 8; j++) {
280
+ out[i * 8 + j] = Number((state[i] >> BigInt(8 * j)) & 0xffn);
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+ function bytesToHex(bytes) {
286
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
287
+ }
288
+ function namehash(name) {
289
+ let node = new Uint8Array(32);
290
+ if (name) {
291
+ const labels = name.split(".");
292
+ for (let i = labels.length - 1; i >= 0; i--) {
293
+ const combined = new Uint8Array(64);
294
+ combined.set(node, 0);
295
+ combined.set(keccak256(new TextEncoder().encode(labels[i])), 32);
296
+ node = keccak256(combined);
297
+ }
298
+ }
299
+ return `0x${bytesToHex(node)}`;
300
+ }
301
+ const DEFAULT_ENDPOINT = "https://ethereum.publicnode.com";
302
+ async function rpc(endpoint, method, params, signal) {
303
+ const res = await fetch(endpoint, {
304
+ method: "POST",
305
+ headers: { "content-type": "application/json" },
306
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
307
+ signal,
308
+ });
309
+ if (!res.ok) {
310
+ throw new Error(`RPC ${method} failed with HTTP ${res.status}.`);
311
+ }
312
+ const json = (await res.json());
313
+ if (json.error) {
314
+ throw new Error(`RPC ${method} error: ${json.error.message ?? "unknown"}.`);
315
+ }
316
+ return json.result;
317
+ }
318
+ function parseAbiString(hex) {
319
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
320
+ const len = parseInt(clean.slice(64, 128), 16);
321
+ const dataHex = clean.slice(128, 128 + len * 2);
322
+ const bytes = new Uint8Array(dataHex.length / 2);
323
+ for (let i = 0; i < bytes.length; i++) {
324
+ bytes[i] = parseInt(dataHex.slice(i * 2, i * 2 + 2), 16);
325
+ }
326
+ return new TextDecoder().decode(bytes);
327
+ }
328
+ /**
329
+ * Live token price as a signal, via CoinGecko's public API.
330
+ *
331
+ * The free endpoint is rate-limited; the default 60s interval is
332
+ * conservative on purpose. Pass your own `endpoint` (any base that
333
+ * answers `/simple/price?ids={id}&vs_currencies={vs}&include_24hr_change=true`).
334
+ *
335
+ * ```ts
336
+ * const { price, change24h, status } = createTokenPrice("ethereum");
337
+ * <Show when={status() === "success"}>
338
+ * ${(price() ?? 0).toFixed(2)} ({(change24h() ?? 0).toFixed(1)}%)
339
+ * </Show>
340
+ * ```
341
+ */
342
+ export function createTokenPrice(tokenId, options = {}) {
343
+ const { vsCurrency = "usd", endpoint = "https://api.coingecko.com/api/v3", ...poll } = options;
344
+ const getId = typeof tokenId === "function" ? tokenId : () => tokenId;
345
+ const p = createPoll(async (signal) => {
346
+ const res = await fetch(`${endpoint}/simple/price?ids=${encodeURIComponent(getId())}&vs_currencies=${encodeURIComponent(vsCurrency)}&include_24hr_change=true`, { signal });
347
+ if (!res.ok) {
348
+ throw new Error(`Price fetch failed with HTTP ${res.status}.`);
349
+ }
350
+ const json = (await res.json());
351
+ const row = json[getId()];
352
+ if (!row || typeof row[vsCurrency] !== "number") {
353
+ throw new Error(`No price returned for "${getId()}".`);
354
+ }
355
+ return {
356
+ price: row[vsCurrency],
357
+ change24h: row[`${vsCurrency}_24h_change`],
358
+ };
359
+ }, { interval: 60000, ...poll });
360
+ return {
361
+ ...p,
362
+ price: () => p.data()?.price,
363
+ change24h: () => p.data()?.change24h,
364
+ };
365
+ }
366
+ /**
367
+ * Percent change of any numeric signal over a rolling window.
368
+ *
369
+ * Samples the source on each change and on `sampleMs`, keeps samples
370
+ * within `windowMs`, and reports `(last - first) / first * 100`.
371
+ * `undefined` until at least two samples exist. Signal-native, no network.
372
+ *
373
+ * ```ts
374
+ * const { price } = createTokenPrice("ethereum");
375
+ * const { change, reset } = createPriceChange(price);
376
+ * ```
377
+ */
378
+ export function createPriceChange(source, options = {}) {
379
+ const { windowMs = 3600000, sampleMs = 60000 } = options;
380
+ const [samples, setSamples] = createSignal([]);
381
+ const take = () => {
382
+ const v = source();
383
+ if (v == null || typeof window === "undefined")
384
+ return;
385
+ const t = Date.now();
386
+ setSamples((prev) => [...prev.filter((s) => t - s.t <= windowMs), { t, v }]);
387
+ };
388
+ createEffect(() => {
389
+ source();
390
+ take();
391
+ });
392
+ if (typeof window !== "undefined") {
393
+ const id = setInterval(take, sampleMs);
394
+ onCleanup(() => clearInterval(id));
395
+ }
396
+ return {
397
+ change: () => {
398
+ const list = samples();
399
+ if (list.length < 2)
400
+ return undefined;
401
+ const first = list[0].v;
402
+ const last = list[list.length - 1].v;
403
+ if (first === 0)
404
+ return undefined;
405
+ return ((last - first) / Math.abs(first)) * 100;
406
+ },
407
+ reset: () => setSamples([]),
408
+ };
409
+ }
410
+ /**
411
+ * Compare two numeric signals: their ratio, percent difference, and which
412
+ * is larger. Any side `undefined` makes everything `undefined` until both
413
+ * have values. Signal-native, no network.
414
+ *
415
+ * ```ts
416
+ * const { price: eth } = createTokenPrice("ethereum");
417
+ * const { price: btc } = createTokenPrice("bitcoin");
418
+ * const { ratio, diffPercent, leader } = createPriceCompare(eth, btc);
419
+ * ```
420
+ */
421
+ export function createPriceCompare(a, b) {
422
+ return {
423
+ ratio: () => {
424
+ const x = a();
425
+ const y = b();
426
+ if (x == null || y == null || y === 0)
427
+ return undefined;
428
+ return x / y;
429
+ },
430
+ diffPercent: () => {
431
+ const x = a();
432
+ const y = b();
433
+ if (x == null || y == null || y === 0)
434
+ return undefined;
435
+ return ((x - y) / Math.abs(y)) * 100;
436
+ },
437
+ leader: () => {
438
+ const x = a();
439
+ const y = b();
440
+ if (x == null || y == null)
441
+ return undefined;
442
+ if (x === y)
443
+ return "tie";
444
+ return x > y ? "a" : "b";
445
+ },
446
+ };
447
+ }
448
+ /**
449
+ * Current gas price over JSON-RPC (`eth_gasPrice`), as wei bigint and gwei.
450
+ * Default 15s polling. Swap `endpoint` for any chain.
451
+ */
452
+ export function createGasPrice(options = {}) {
453
+ const { endpoint = DEFAULT_ENDPOINT, ...poll } = options;
454
+ const p = createPoll(async (signal) => {
455
+ const hex = await rpc(endpoint, "eth_gasPrice", [], signal);
456
+ const wei = BigInt(hex);
457
+ return { wei, gwei: Number(wei) / 1e9 };
458
+ }, { interval: 15000, ...poll });
459
+ return { ...p, wei: () => p.data()?.wei, gwei: () => p.data()?.gwei };
460
+ }
461
+ const BALANCE_OF_SELECTOR = "0x70a08231";
462
+ /**
463
+ * Token balance of an address: native (`eth_getBalance`) or ERC20
464
+ * (`balanceOf` via `eth_call`). Read-only; never signs.
465
+ *
466
+ * ```ts
467
+ * const { formatted } = createBalance("0xabc…", { token: "0xdef…" });
468
+ * ```
469
+ */
470
+ export function createBalance(address, options = {}) {
471
+ const { endpoint = DEFAULT_ENDPOINT, token, decimals = 18, ...poll } = options;
472
+ if (!isAddress(address)) {
473
+ throw new Error("createBalance: invalid address.");
474
+ }
475
+ const p = createPoll(async (signal) => {
476
+ let hex;
477
+ if (token) {
478
+ if (!isAddress(token)) {
479
+ throw new Error("createBalance: invalid token address.");
480
+ }
481
+ const data = BALANCE_OF_SELECTOR + address.slice(2).toLowerCase().padStart(64, "0");
482
+ hex = await rpc(endpoint, "eth_call", [{ to: token, data }, "latest"], signal);
483
+ }
484
+ else {
485
+ hex = await rpc(endpoint, "eth_getBalance", [address, "latest"], signal);
486
+ }
487
+ const balance = BigInt(hex);
488
+ return { balance, formatted: formatUnits(balance, decimals) };
489
+ }, { interval: 20000, ...poll });
490
+ return {
491
+ ...p,
492
+ balance: () => p.data()?.balance,
493
+ formatted: () => p.data()?.formatted,
494
+ };
495
+ }
496
+ /**
497
+ * Watch a transaction hash until its receipt lands. Polls every 4s and
498
+ * stops on its own once the receipt arrives; `mined()` mirrors that.
499
+ * Read-only confirmation; pairs with `createTxLifecycle` from web3.ts.
500
+ */
501
+ export function createTxReceipt(hash, options = {}) {
502
+ const { endpoint = DEFAULT_ENDPOINT, ...poll } = options;
503
+ const p = createPoll(async (signal) => {
504
+ const raw = await rpc(endpoint, "eth_getTransactionReceipt", [hash], signal);
505
+ if (!raw)
506
+ return null;
507
+ return {
508
+ transactionHash: raw.transactionHash,
509
+ blockNumber: parseInt(raw.blockNumber, 16),
510
+ success: raw.status === "0x1",
511
+ gasUsed: BigInt(raw.gasUsed),
512
+ };
513
+ }, { interval: 4000, ...poll });
514
+ createEffect(() => {
515
+ if (p.data() != null)
516
+ p.abort();
517
+ });
518
+ return { ...p, receipt: p.data, mined: () => p.data() != null };
519
+ }
520
+ /**
521
+ * Latest block number over JSON-RPC. Default 12s polling.
522
+ * Handy as a chain-health heartbeat and a cache-busting ticker.
523
+ */
524
+ export function createBlockNumber(options = {}) {
525
+ const { endpoint = DEFAULT_ENDPOINT, ...poll } = options;
526
+ const p = createPoll(async (signal) => {
527
+ const hex = await rpc(endpoint, "eth_blockNumber", [], signal);
528
+ return parseInt(hex, 16);
529
+ }, { interval: 12000, ...poll });
530
+ return { ...p, blockNumber: p.data };
531
+ }
532
+ const CHAINLINK_DECIMALS_SELECTOR = "0x313ce567";
533
+ const CHAINLINK_LATEST_SELECTOR = "0xfeaf968c";
534
+ /**
535
+ * Read a Chainlink `AggregatorV3Interface` price feed on-chain:
536
+ * `decimals()` once, then `latestRoundData()` polled every 30s.
537
+ * Feed addresses live on the
538
+ * [Chainlink docs](https://docs.chain.link/data-feeds/price-feeds/addresses).
539
+ *
540
+ * ```ts
541
+ * // ETH / USD feed on mainnet
542
+ * const { price } = createChainlinkPrice("0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419");
543
+ * ```
544
+ */
545
+ export function createChainlinkPrice(feed, options = {}) {
546
+ const { endpoint = DEFAULT_ENDPOINT, ...poll } = options;
547
+ if (!isAddress(feed)) {
548
+ throw new Error("createChainlinkPrice: invalid feed address.");
549
+ }
550
+ let decimals = null;
551
+ const p = createPoll(async (signal) => {
552
+ if (decimals === null) {
553
+ const raw = await rpc(endpoint, "eth_call", [{ to: feed, data: CHAINLINK_DECIMALS_SELECTOR }, "latest"], signal);
554
+ decimals = parseInt(raw, 16);
555
+ }
556
+ const raw = await rpc(endpoint, "eth_call", [{ to: feed, data: CHAINLINK_LATEST_SELECTOR }, "latest"], signal);
557
+ const answer = BigInt(`0x${raw.slice(2 + 64, 2 + 128)}`);
558
+ return Number(answer) / 10 ** decimals;
559
+ }, { interval: 30000, ...poll });
560
+ return { ...p, price: p.data };
561
+ }
562
+ /**
563
+ * Fetch an NFT's `tokenURI` on-chain and resolve its JSON metadata
564
+ * (one-shot, with `retry`). `ipfs://` URIs are rewritten through the
565
+ * gateway. Returns the parsed fields plus `raw` for anything custom.
566
+ *
567
+ * ```ts
568
+ * const { metadata, image, status, retry } = createNFTMetadata(
569
+ * "0xcontract…",
570
+ * 42,
571
+ * );
572
+ * ```
573
+ */
574
+ export function createNFTMetadata(contract, tokenId, options = {}) {
575
+ const { endpoint = DEFAULT_ENDPOINT, gateway = "https://ipfs.io" } = options;
576
+ if (!isAddress(contract)) {
577
+ throw new Error("createNFTMetadata: invalid contract address.");
578
+ }
579
+ const [data, setData] = createSignal(undefined);
580
+ const [error, setError] = createSignal(null);
581
+ const [status, setStatus] = createSignal("idle");
582
+ let aborter = null;
583
+ const toGateway = (uri) => uri.startsWith("ipfs://") ? `${gateway}/ipfs/${uri.slice(7)}` : uri;
584
+ const load = async () => {
585
+ if (typeof window === "undefined")
586
+ return;
587
+ aborter?.abort();
588
+ aborter = new AbortController();
589
+ const signal = aborter.signal;
590
+ setStatus("loading");
591
+ try {
592
+ const idHex = BigInt(tokenId).toString(16).padStart(64, "0");
593
+ const uriRaw = await rpc(endpoint, "eth_call", [{ to: contract, data: `0xc87b56dd${idHex}` }, "latest"], signal);
594
+ const uri = parseAbiString(uriRaw);
595
+ const res = await fetch(toGateway(uri), { signal });
596
+ if (!res.ok) {
597
+ throw new Error(`Metadata fetch failed with HTTP ${res.status}.`);
598
+ }
599
+ const json = (await res.json());
600
+ if (signal.aborted)
601
+ return;
602
+ setData({
603
+ name: typeof json.name === "string" ? json.name : undefined,
604
+ description: typeof json.description === "string" ? json.description : undefined,
605
+ image: typeof json.image === "string" ? toGateway(json.image) : undefined,
606
+ attributes: Array.isArray(json.attributes)
607
+ ? json.attributes
608
+ : undefined,
609
+ raw: json,
610
+ });
611
+ setError(null);
612
+ setStatus("success");
613
+ }
614
+ catch (e) {
615
+ if (signal.aborted)
616
+ return;
617
+ setError(e instanceof Error ? e : new Error(String(e)));
618
+ setStatus("error");
619
+ }
620
+ };
621
+ if (typeof window !== "undefined") {
622
+ void load();
623
+ }
624
+ onCleanup(() => aborter?.abort());
625
+ return {
626
+ data,
627
+ error,
628
+ status,
629
+ retry: () => void load(),
630
+ abort: () => aborter?.abort(),
631
+ metadata: data,
632
+ image: () => data()?.image,
633
+ };
634
+ }
635
+ const ENS_REGISTRY = "0x00000000000C2C1F563e148400aA1D00d43e5D";
636
+ const ENS_RESOLVER_SELECTOR = "0x0178b8bf";
637
+ const ENS_NAME_SELECTOR = "0x691f3431";
638
+ /**
639
+ * Reverse-resolve an address to its ENS name via the public ENS registry
640
+ * (one-shot, with `retry`). `undefined` when the address has no name set;
641
+ * errors (network, RPC) surface on `error`.
642
+ *
643
+ * ```ts
644
+ * const { name, status } = createENS("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
645
+ * ```
646
+ */
647
+ export function createENS(address, options = {}) {
648
+ const { endpoint = DEFAULT_ENDPOINT } = options;
649
+ if (!isAddress(address)) {
650
+ throw new Error("createENS: invalid address.");
651
+ }
652
+ const [data, setData] = createSignal(undefined);
653
+ const [error, setError] = createSignal(null);
654
+ const [status, setStatus] = createSignal("idle");
655
+ let aborter = null;
656
+ const load = async () => {
657
+ if (typeof window === "undefined")
658
+ return;
659
+ aborter?.abort();
660
+ aborter = new AbortController();
661
+ const signal = aborter.signal;
662
+ setStatus("loading");
663
+ try {
664
+ const node = namehash(`${address.slice(2).toLowerCase()}.addr.reverse`);
665
+ const resolverRaw = await rpc(endpoint, "eth_call", [
666
+ { to: ENS_REGISTRY, data: `${ENS_RESOLVER_SELECTOR}${node.slice(2)}` },
667
+ "latest",
668
+ ], signal);
669
+ const resolver = `0x${resolverRaw.slice(-40)}`;
670
+ if (/^0x0+$/.test(resolver)) {
671
+ if (!signal.aborted) {
672
+ setData(undefined);
673
+ setError(null);
674
+ setStatus("success");
675
+ }
676
+ return;
677
+ }
678
+ const nameRaw = await rpc(endpoint, "eth_call", [
679
+ { to: resolver, data: `${ENS_NAME_SELECTOR}${node.slice(2)}` },
680
+ "latest",
681
+ ], signal);
682
+ const name = parseAbiString(nameRaw);
683
+ if (signal.aborted)
684
+ return;
685
+ setData(name || undefined);
686
+ setError(null);
687
+ setStatus("success");
688
+ }
689
+ catch (e) {
690
+ if (signal.aborted)
691
+ return;
692
+ setError(e instanceof Error ? e : new Error(String(e)));
693
+ setStatus("error");
694
+ }
695
+ };
696
+ if (typeof window !== "undefined") {
697
+ void load();
698
+ }
699
+ onCleanup(() => aborter?.abort());
700
+ return {
701
+ data,
702
+ error,
703
+ status,
704
+ retry: () => void load(),
705
+ abort: () => aborter?.abort(),
706
+ name: data,
707
+ };
708
+ }
709
+ /**
710
+ * Deterministic identicon avatar for any address as a data URI: a mirrored
711
+ * random-walk grid in SVG, so the same address always renders the same
712
+ * image. Pure computation, works on the server, no network.
713
+ *
714
+ * ```tsx
715
+ * const avatar = createIdenticon("0xabc…");
716
+ * <img src={avatar()} alt="avatar" width={64} height={64} />;
717
+ * ```
718
+ */
719
+ export function createIdenticon(address, options = {}) {
720
+ const { size = 64, cells = 8, background = "#f0f0f0" } = options;
721
+ const get = typeof address === "function" ? address : () => address;
722
+ return () => {
723
+ const seed = keccak256(new TextEncoder().encode(get().toLowerCase()));
724
+ let s = (seed[0] << 24) | (seed[1] << 16) | (seed[2] << 8) | seed[3];
725
+ const rand = () => {
726
+ s |= 0;
727
+ s = (s + 0x6d2b79f5) | 0;
728
+ let t = Math.imul(s ^ (s >>> 15), 1 | s);
729
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
730
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
731
+ };
732
+ const color = `hsl(${Math.floor(rand() * 360)}, 65%, 55%)`;
733
+ const half = Math.ceil(cells / 2);
734
+ const unit = size / cells;
735
+ let rects = "";
736
+ for (let y = 0; y < cells; y++) {
737
+ for (let x = 0; x < half; x++) {
738
+ if (rand() > 0.5) {
739
+ const mirrorX = cells - 1 - x;
740
+ const mk = (rx) => `<rect x="${(rx * unit).toFixed(2)}" y="${(y * unit).toFixed(2)}" width="${unit.toFixed(2)}" height="${unit.toFixed(2)}" fill="${color}"/>`;
741
+ rects += mk(x);
742
+ if (mirrorX !== x)
743
+ rects += mk(mirrorX);
744
+ }
745
+ }
746
+ }
747
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">` +
748
+ `<rect width="${size}" height="${size}" fill="${background}"/>${rects}</svg>`;
749
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
750
+ };
751
+ }
package/package.json CHANGED
@@ -43,5 +43,5 @@
43
43
  },
44
44
  "type": "module",
45
45
  "types": "./dist/index.d.ts",
46
- "version": "0.15.0"
46
+ "version": "0.16.0"
47
47
  }