@gibs/bridge-client 1.12.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/dist/cache.d.ts +9 -0
- package/dist/cache.js +17 -0
- package/dist/chain-read.d.ts +27 -0
- package/dist/chain-read.js +31 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +78 -0
- package/dist/endpoints.d.ts +99 -0
- package/dist/endpoints.js +175 -0
- package/dist/image.d.ts +46 -0
- package/dist/image.js +83 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/networks.d.ts +30 -0
- package/dist/networks.js +72 -0
- package/dist/token-metadata.d.ts +21 -0
- package/dist/token-metadata.js +245 -0
- package/package.json +76 -0
package/dist/cache.d.ts
ADDED
package/dist/cache.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class Cache {
|
|
2
|
+
ttl;
|
|
3
|
+
cache = new Map();
|
|
4
|
+
constructor(ttl = 1000 * 60) {
|
|
5
|
+
this.ttl = ttl;
|
|
6
|
+
}
|
|
7
|
+
clearStale() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
for (const k of this.cache.keys()) {
|
|
10
|
+
const { time } = this.cache.get(k);
|
|
11
|
+
if (now - time > this.ttl) {
|
|
12
|
+
this.cache.delete(k);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return now;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Hex } from 'viem';
|
|
2
|
+
/** Inputs identifying an ERC-20 allowance to read: which token, owner, and spender on which chain. */
|
|
3
|
+
export type ApprovalParameters = {
|
|
4
|
+
/** The ERC-20 token contract address. */
|
|
5
|
+
token: Hex;
|
|
6
|
+
/** The address allowed to spend the owner's tokens. */
|
|
7
|
+
spender: Hex;
|
|
8
|
+
/** The chain the token lives on. */
|
|
9
|
+
chainId: number;
|
|
10
|
+
/** The token owner whose allowance is being read. */
|
|
11
|
+
account: Hex;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Reads an ERC-20 allowance for the given owner/spender pair on a chain.
|
|
15
|
+
*
|
|
16
|
+
* The native asset (`zeroAddress`) has no allowance concept, so it returns
|
|
17
|
+
* `maxUint256` (always sufficient). A missing or malformed owner address throws,
|
|
18
|
+
* matching the Svelte source — callers that may lack an account must guard first.
|
|
19
|
+
*
|
|
20
|
+
* Ported verbatim from `chain-read.svelte.ts`; the only change is sourcing
|
|
21
|
+
* {@link clientFromChain} from the framework-agnostic `wallet/clientFromChain`
|
|
22
|
+
* module instead of the Svelte `input.svelte` rune store.
|
|
23
|
+
*
|
|
24
|
+
* @param params The token, spender, chain, and owner to read.
|
|
25
|
+
* @returns The allowance in token base units.
|
|
26
|
+
*/
|
|
27
|
+
export declare const checkAllowance: ({ token, spender, chainId, account }: ApprovalParameters) => Promise<bigint>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { erc20Abi, isAddress, maxUint256, zeroAddress } from 'viem';
|
|
2
|
+
import { clientFromChain } from './client.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads an ERC-20 allowance for the given owner/spender pair on a chain.
|
|
5
|
+
*
|
|
6
|
+
* The native asset (`zeroAddress`) has no allowance concept, so it returns
|
|
7
|
+
* `maxUint256` (always sufficient). A missing or malformed owner address throws,
|
|
8
|
+
* matching the Svelte source — callers that may lack an account must guard first.
|
|
9
|
+
*
|
|
10
|
+
* Ported verbatim from `chain-read.svelte.ts`; the only change is sourcing
|
|
11
|
+
* {@link clientFromChain} from the framework-agnostic `wallet/clientFromChain`
|
|
12
|
+
* module instead of the Svelte `input.svelte` rune store.
|
|
13
|
+
*
|
|
14
|
+
* @param params The token, spender, chain, and owner to read.
|
|
15
|
+
* @returns The allowance in token base units.
|
|
16
|
+
*/
|
|
17
|
+
export const checkAllowance = async ({ token, spender, chainId, account }) => {
|
|
18
|
+
if (token === zeroAddress) {
|
|
19
|
+
return maxUint256;
|
|
20
|
+
}
|
|
21
|
+
const client = clientFromChain(chainId);
|
|
22
|
+
if (!account || !isAddress(account)) {
|
|
23
|
+
throw new Error('No account');
|
|
24
|
+
}
|
|
25
|
+
return await client.readContract({
|
|
26
|
+
address: token,
|
|
27
|
+
abi: erc20Abi,
|
|
28
|
+
functionName: 'allowance',
|
|
29
|
+
args: [account, spender],
|
|
30
|
+
});
|
|
31
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PublicClient } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* The request-batching setting for a chain's client: `false` to disable batching
|
|
4
|
+
* for chains that reject batch bodies (see {@link chainsWithoutRpcBatching}), or
|
|
5
|
+
* `undefined` to use the transport's default batching.
|
|
6
|
+
*
|
|
7
|
+
* @param chainId - the chain to resolve the batch setting for
|
|
8
|
+
* @returns `false` when batching must be disabled, otherwise `undefined`
|
|
9
|
+
*/
|
|
10
|
+
export declare const rpcBatchSetting: (chainId: number) => false | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Returns a viem {@link PublicClient} for a chain, reusing the cached instance
|
|
13
|
+
* while the chain's endpoint list is unchanged. The cache key is derived from the
|
|
14
|
+
* chain id plus its urls, so editing the list yields a fresh client and lets the
|
|
15
|
+
* previous transport be garbage collected.
|
|
16
|
+
*
|
|
17
|
+
* The url source is the user-editable endpoint table; when it has no entry — or
|
|
18
|
+
* an EMPTY one — the chain's own defaults are used instead. The empty case is not
|
|
19
|
+
* hypothetical: the endpoint editor lets a user delete every entry for a chain,
|
|
20
|
+
* and an empty array is truthy, so a `||` fallback here quietly built a client
|
|
21
|
+
* with no transports at all and every read on that chain failed until the list
|
|
22
|
+
* was restored.
|
|
23
|
+
*
|
|
24
|
+
* @param chainId - the chain to build a client for
|
|
25
|
+
* @returns the cached or freshly built public client
|
|
26
|
+
*/
|
|
27
|
+
export declare const clientFromChain: (chainId: number) => PublicClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { chainKey, clientCache, clientFromChain as buildClient } from '@gibs/common/client';
|
|
2
|
+
import * as _ from 'lodash-es';
|
|
3
|
+
import { bsc } from 'viem/chains';
|
|
4
|
+
import * as endpoints from './endpoints.js';
|
|
5
|
+
import { evmChainsById } from './networks.js';
|
|
6
|
+
/**
|
|
7
|
+
* Chains whose public nodes reject BATCH remote procedure call requests (an
|
|
8
|
+
* array-wrapped body). viem otherwise wraps even a single request in an array
|
|
9
|
+
* when batching is on, which those nodes refuse — so every call fails and is
|
|
10
|
+
* retried, burning through requests. Their clients are built with batching
|
|
11
|
+
* disabled so requests go out as plain objects the node accepts. Binance Smart
|
|
12
|
+
* Chain's `publicnode` endpoint is the known offender.
|
|
13
|
+
*/
|
|
14
|
+
const chainsWithoutRpcBatching = new Set([bsc.id]);
|
|
15
|
+
/**
|
|
16
|
+
* The request-batching setting for a chain's client: `false` to disable batching
|
|
17
|
+
* for chains that reject batch bodies (see {@link chainsWithoutRpcBatching}), or
|
|
18
|
+
* `undefined` to use the transport's default batching.
|
|
19
|
+
*
|
|
20
|
+
* @param chainId - the chain to resolve the batch setting for
|
|
21
|
+
* @returns `false` when batching must be disabled, otherwise `undefined`
|
|
22
|
+
*/
|
|
23
|
+
export const rpcBatchSetting = (chainId) => chainsWithoutRpcBatching.has(chainId) ? false : undefined;
|
|
24
|
+
/**
|
|
25
|
+
* A chain's own default endpoints, used when the editable table has no entry.
|
|
26
|
+
*
|
|
27
|
+
* @param chainId - the chain to look up
|
|
28
|
+
* @returns the chain's default endpoints (possibly empty)
|
|
29
|
+
*/
|
|
30
|
+
const defaultUrlsFor = (chainId) => {
|
|
31
|
+
if (!chainId) {
|
|
32
|
+
throw new Error('chainId is required');
|
|
33
|
+
}
|
|
34
|
+
const chain = evmChainsById.get(chainId);
|
|
35
|
+
if (!chain) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const { http } = chain.rpcUrls.default;
|
|
39
|
+
return [...(http ?? [])];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Returns a viem {@link PublicClient} for a chain, reusing the cached instance
|
|
43
|
+
* while the chain's endpoint list is unchanged. The cache key is derived from the
|
|
44
|
+
* chain id plus its urls, so editing the list yields a fresh client and lets the
|
|
45
|
+
* previous transport be garbage collected.
|
|
46
|
+
*
|
|
47
|
+
* The url source is the user-editable endpoint table; when it has no entry — or
|
|
48
|
+
* an EMPTY one — the chain's own defaults are used instead. The empty case is not
|
|
49
|
+
* hypothetical: the endpoint editor lets a user delete every entry for a chain,
|
|
50
|
+
* and an empty array is truthy, so a `||` fallback here quietly built a client
|
|
51
|
+
* with no transports at all and every read on that chain failed until the list
|
|
52
|
+
* was restored.
|
|
53
|
+
*
|
|
54
|
+
* @param chainId - the chain to build a client for
|
|
55
|
+
* @returns the cached or freshly built public client
|
|
56
|
+
*/
|
|
57
|
+
export const clientFromChain = (chainId) => {
|
|
58
|
+
const stored = endpoints.get(chainId);
|
|
59
|
+
const urls = _.compact(stored?.length ? stored : defaultUrlsFor(chainId));
|
|
60
|
+
const key = chainKey(chainId, urls);
|
|
61
|
+
const existing = clientCache.get(chainId);
|
|
62
|
+
if (existing && existing.key === key) {
|
|
63
|
+
return existing.client;
|
|
64
|
+
}
|
|
65
|
+
const chain = evmChainsById.get(chainId);
|
|
66
|
+
if (!chain) {
|
|
67
|
+
// Previously this searched every chain viem ships and asserted a hit with a
|
|
68
|
+
// `!`. That answered for chains this application does not support, and did
|
|
69
|
+
// so badly: there are no endpoints for them, so the client was built with
|
|
70
|
+
// none and silently fell back to viem's own public node — or, for a chain
|
|
71
|
+
// viem had never heard of, died on an unreadable `undefined.id` several
|
|
72
|
+
// frames away. Both are worse than saying so here.
|
|
73
|
+
throw new Error(`Chain ${chainId} is not one of the supported networks.`);
|
|
74
|
+
}
|
|
75
|
+
const client = buildClient({ chain, urls, batch: rpcBatchSetting(chainId) });
|
|
76
|
+
clientCache.set(chainId, { key, client });
|
|
77
|
+
return client;
|
|
78
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-editable table of remote procedure call endpoints, per chain.
|
|
3
|
+
*
|
|
4
|
+
* This is the single source every read in the application resolves through, so
|
|
5
|
+
* that replacing a blocked node in one place moves every balance read, every
|
|
6
|
+
* receipt poll and every contract call at once.
|
|
7
|
+
*
|
|
8
|
+
* It is a plain external store — `subscribe` plus `getSnapshot` — rather than a
|
|
9
|
+
* hook, so it can be consumed by React through `useSyncExternalStore`, by another
|
|
10
|
+
* framework, or by nothing at all. The React binding lives with the application;
|
|
11
|
+
* nothing here imports a rendering library.
|
|
12
|
+
*/
|
|
13
|
+
/** One chain's identifier paired with its endpoint list. */
|
|
14
|
+
export type EndpointEntry = [chainId: number, urls: string[]];
|
|
15
|
+
/**
|
|
16
|
+
* The minimal shape of browser storage this module needs.
|
|
17
|
+
*
|
|
18
|
+
* Injectable rather than reaching for `localStorage` directly: the store then
|
|
19
|
+
* works under a test runner, on a server, and in a host that keeps preferences
|
|
20
|
+
* somewhere other than the browser — and a test can assert what was persisted
|
|
21
|
+
* without installing a global.
|
|
22
|
+
*/
|
|
23
|
+
export type EndpointStorage = {
|
|
24
|
+
getItem(key: string): string | null;
|
|
25
|
+
setItem(key: string, value: string): void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The storage key holding the endpoint table.
|
|
29
|
+
*
|
|
30
|
+
* The literal must not change: users have their own nodes saved under it, and a
|
|
31
|
+
* rename silently reverts every one of them to the defaults on the next load.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ENDPOINTS_STORAGE_KEY = "rpcs";
|
|
34
|
+
/** The bridge's production chains. */
|
|
35
|
+
export declare const productionSeed: EndpointEntry[];
|
|
36
|
+
/** The bridge's test chains, for a build that is not production. */
|
|
37
|
+
export declare const testnetSeed: EndpointEntry[];
|
|
38
|
+
/**
|
|
39
|
+
* Replaces the seed table and the persistence target.
|
|
40
|
+
*
|
|
41
|
+
* Both fields are optional so a host can override one without restating the
|
|
42
|
+
* other. The loaded table is discarded either way, so the next read rebuilds
|
|
43
|
+
* from the new seed merged with whatever is stored — which means this is safe to
|
|
44
|
+
* call after something has already read, not only at startup.
|
|
45
|
+
*
|
|
46
|
+
* @param options.seed - the entries to start from when storage holds nothing
|
|
47
|
+
* @param options.storage - where the table is persisted, or null for nowhere
|
|
48
|
+
*/
|
|
49
|
+
export declare const configureEndpoints: ({ seed: nextSeed, storage: nextStorage, }: {
|
|
50
|
+
seed?: EndpointEntry[];
|
|
51
|
+
storage?: EndpointStorage | null;
|
|
52
|
+
}) => void;
|
|
53
|
+
/** Serializes one chain's list the way it is persisted: `chainId,url,url`. */
|
|
54
|
+
export declare const key: (chain: number, list: string[]) => string;
|
|
55
|
+
/**
|
|
56
|
+
* Parses a stored blob back into a table, merged onto the seeded entries.
|
|
57
|
+
*
|
|
58
|
+
* @param entry - the persisted blob, or null when nothing is stored
|
|
59
|
+
* @returns the merged table, or null when there was nothing to parse
|
|
60
|
+
*/
|
|
61
|
+
export declare const parse: (entry: string | null) => Map<number, string[]> | null;
|
|
62
|
+
/** Serializes every entry, one chain per line. */
|
|
63
|
+
export declare const stringify: () => string;
|
|
64
|
+
/**
|
|
65
|
+
* Subscribes to changes in the endpoint table.
|
|
66
|
+
*
|
|
67
|
+
* @param listener - called after any mutation
|
|
68
|
+
* @returns the unsubscribe function
|
|
69
|
+
*/
|
|
70
|
+
export declare const subscribe: (listener: () => void) => (() => void);
|
|
71
|
+
/**
|
|
72
|
+
* The current table.
|
|
73
|
+
*
|
|
74
|
+
* The identity is stable until something changes it — every mutating accessor
|
|
75
|
+
* replaces the map rather than mutating in place — which is what lets a caller
|
|
76
|
+
* use this directly as a `useSyncExternalStore` snapshot without re-rendering
|
|
77
|
+
* forever.
|
|
78
|
+
*/
|
|
79
|
+
export declare const getSnapshot: () => Map<number, string[]>;
|
|
80
|
+
/** The url list for a chain, or null when it has no entry. */
|
|
81
|
+
export declare const get: (chain: number) => string[] | null;
|
|
82
|
+
/** All `[chainId, urls]` entries as a fresh array. */
|
|
83
|
+
export declare const entries: () => [number, string[]][];
|
|
84
|
+
/** Replaces a chain's url list, persists it, and notifies subscribers. */
|
|
85
|
+
export declare const setEndpoints: (chain: number, list: string[]) => void;
|
|
86
|
+
/** Replaces the url at index `i`; a no-op when the value is unchanged. */
|
|
87
|
+
export declare const update: (chain: number, i: number, url: string) => void;
|
|
88
|
+
/** Removes the url at index `i`. */
|
|
89
|
+
export declare const remove: (chain: number, i: number) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Applies `addition` to a chain's url list and stores the result, keeping only
|
|
92
|
+
* the first item if it is empty (the leading "add new" slot) and dropping every
|
|
93
|
+
* other empty. A no-op when the filtered list is unchanged.
|
|
94
|
+
*/
|
|
95
|
+
export declare const add: (chain: number, addition?: (urls: string[]) => string[]) => void;
|
|
96
|
+
/** Whether `list` already contains the chain's canonical default endpoint. */
|
|
97
|
+
export declare const hasDefault: (chain: number, list: string[]) => boolean;
|
|
98
|
+
/** Appends the chain's canonical default endpoints back onto its list. */
|
|
99
|
+
export declare const restoreDefault: (chain: number) => void;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { chainsMetadata } from '@gibs/bridge-sdk/chains';
|
|
2
|
+
import { Chains, toChain } from '@gibs/bridge-sdk/config';
|
|
3
|
+
import * as _ from 'lodash-es';
|
|
4
|
+
/**
|
|
5
|
+
* The storage key holding the endpoint table.
|
|
6
|
+
*
|
|
7
|
+
* The literal must not change: users have their own nodes saved under it, and a
|
|
8
|
+
* rename silently reverts every one of them to the defaults on the next load.
|
|
9
|
+
*/
|
|
10
|
+
export const ENDPOINTS_STORAGE_KEY = 'rpcs';
|
|
11
|
+
/** Builds a seed table from the bridge metadata for the given chains. */
|
|
12
|
+
const seedFor = (chains) => chains.map((chain) => [Number(chain), [...chainsMetadata[chain].rpcUrls.default.http]]);
|
|
13
|
+
/** The bridge's production chains. */
|
|
14
|
+
export const productionSeed = seedFor([Chains.PLS, Chains.ETH, Chains.BNB]);
|
|
15
|
+
/** The bridge's test chains, for a build that is not production. */
|
|
16
|
+
export const testnetSeed = seedFor([Chains.V4PLS, Chains.SEP]);
|
|
17
|
+
/** The seed the next load starts from. */
|
|
18
|
+
let seed = productionSeed;
|
|
19
|
+
/** Where the table is persisted, or null when there is nowhere. */
|
|
20
|
+
let storage = typeof localStorage === 'undefined' ? null : localStorage;
|
|
21
|
+
/** The live table, or null while it has not been loaded yet. */
|
|
22
|
+
let value = null;
|
|
23
|
+
const listeners = new Set();
|
|
24
|
+
const emit = () => {
|
|
25
|
+
for (const listener of listeners) {
|
|
26
|
+
listener();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Replaces the seed table and the persistence target.
|
|
31
|
+
*
|
|
32
|
+
* Both fields are optional so a host can override one without restating the
|
|
33
|
+
* other. The loaded table is discarded either way, so the next read rebuilds
|
|
34
|
+
* from the new seed merged with whatever is stored — which means this is safe to
|
|
35
|
+
* call after something has already read, not only at startup.
|
|
36
|
+
*
|
|
37
|
+
* @param options.seed - the entries to start from when storage holds nothing
|
|
38
|
+
* @param options.storage - where the table is persisted, or null for nowhere
|
|
39
|
+
*/
|
|
40
|
+
export const configureEndpoints = ({ seed: nextSeed, storage: nextStorage, }) => {
|
|
41
|
+
if (nextSeed !== undefined) {
|
|
42
|
+
seed = nextSeed;
|
|
43
|
+
}
|
|
44
|
+
if (nextStorage !== undefined) {
|
|
45
|
+
storage = nextStorage;
|
|
46
|
+
}
|
|
47
|
+
value = null;
|
|
48
|
+
emit();
|
|
49
|
+
};
|
|
50
|
+
/** Serializes one chain's list the way it is persisted: `chainId,url,url`. */
|
|
51
|
+
export const key = (chain, list) => `${chain},${list.join(',')}`;
|
|
52
|
+
/**
|
|
53
|
+
* Parses a stored blob back into a table, merged onto the seeded entries.
|
|
54
|
+
*
|
|
55
|
+
* @param entry - the persisted blob, or null when nothing is stored
|
|
56
|
+
* @returns the merged table, or null when there was nothing to parse
|
|
57
|
+
*/
|
|
58
|
+
export const parse = (entry) => {
|
|
59
|
+
if (!entry) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const stored = entry.split('\n').map((row) => {
|
|
63
|
+
const [chain, ...list] = row.split(',');
|
|
64
|
+
return [Number(chain), _.compact(list)];
|
|
65
|
+
});
|
|
66
|
+
return new Map([...seed, ...stored]);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Reads the persisted table, or null when there is nothing usable to read.
|
|
70
|
+
*
|
|
71
|
+
* Any failure falls back to the seeded defaults rather than propagating: a
|
|
72
|
+
* corrupt or unreadable entry must not be the reason the application cannot
|
|
73
|
+
* reach a chain at all.
|
|
74
|
+
*/
|
|
75
|
+
const readStored = () => {
|
|
76
|
+
if (!storage) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return parse(storage.getItem(ENDPOINTS_STORAGE_KEY));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.error('unable to read the stored endpoint list', error);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/** The table, loaded from storage the first time it is asked for. */
|
|
88
|
+
const loaded = () => {
|
|
89
|
+
if (!value) {
|
|
90
|
+
value = readStored() ?? new Map(seed);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
};
|
|
94
|
+
/** Serializes every entry, one chain per line. */
|
|
95
|
+
export const stringify = () => [...loaded().entries()].map(([chainId, list]) => key(chainId, list)).join('\n');
|
|
96
|
+
/**
|
|
97
|
+
* Subscribes to changes in the endpoint table.
|
|
98
|
+
*
|
|
99
|
+
* @param listener - called after any mutation
|
|
100
|
+
* @returns the unsubscribe function
|
|
101
|
+
*/
|
|
102
|
+
export const subscribe = (listener) => {
|
|
103
|
+
listeners.add(listener);
|
|
104
|
+
return () => {
|
|
105
|
+
listeners.delete(listener);
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* The current table.
|
|
110
|
+
*
|
|
111
|
+
* The identity is stable until something changes it — every mutating accessor
|
|
112
|
+
* replaces the map rather than mutating in place — which is what lets a caller
|
|
113
|
+
* use this directly as a `useSyncExternalStore` snapshot without re-rendering
|
|
114
|
+
* forever.
|
|
115
|
+
*/
|
|
116
|
+
export const getSnapshot = () => loaded();
|
|
117
|
+
/** The url list for a chain, or null when it has no entry. */
|
|
118
|
+
export const get = (chain) => loaded().get(chain) ?? null;
|
|
119
|
+
/** All `[chainId, urls]` entries as a fresh array. */
|
|
120
|
+
export const entries = () => [...loaded().entries()];
|
|
121
|
+
/** Replaces a chain's url list, persists it, and notifies subscribers. */
|
|
122
|
+
export const setEndpoints = (chain, list) => {
|
|
123
|
+
const next = new Map(loaded());
|
|
124
|
+
next.set(chain, list);
|
|
125
|
+
value = next;
|
|
126
|
+
storage?.setItem(ENDPOINTS_STORAGE_KEY, stringify());
|
|
127
|
+
emit();
|
|
128
|
+
};
|
|
129
|
+
/** Replaces the url at index `i`; a no-op when the value is unchanged. */
|
|
130
|
+
export const update = (chain, i, url) => {
|
|
131
|
+
const list = get(chain) ?? [];
|
|
132
|
+
if (list[i] === url) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const next = list.slice(0);
|
|
136
|
+
next[i] = url;
|
|
137
|
+
setEndpoints(chain, next);
|
|
138
|
+
};
|
|
139
|
+
/** Removes the url at index `i`. */
|
|
140
|
+
export const remove = (chain, i) => {
|
|
141
|
+
const next = (get(chain) ?? []).slice(0);
|
|
142
|
+
next.splice(i, 1);
|
|
143
|
+
setEndpoints(chain, next);
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Applies `addition` to a chain's url list and stores the result, keeping only
|
|
147
|
+
* the first item if it is empty (the leading "add new" slot) and dropping every
|
|
148
|
+
* other empty. A no-op when the filtered list is unchanged.
|
|
149
|
+
*/
|
|
150
|
+
export const add = (chain, addition = (urls) => [''].concat(urls)) => {
|
|
151
|
+
const before = get(chain) ?? [];
|
|
152
|
+
const list = addition(before.slice(0));
|
|
153
|
+
const filteredList = list.filter((val, i) => {
|
|
154
|
+
// only the first item in the list may be empty
|
|
155
|
+
if (!val && !i) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return !!val;
|
|
159
|
+
});
|
|
160
|
+
if (_.isEqual(before, filteredList)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
setEndpoints(chain, filteredList);
|
|
164
|
+
};
|
|
165
|
+
/** Whether `list` already contains the chain's canonical default endpoint. */
|
|
166
|
+
export const hasDefault = (chain, list) => {
|
|
167
|
+
const defaultUrl = chainsMetadata[toChain(chain)].rpcUrls.default.http[0];
|
|
168
|
+
if (!defaultUrl)
|
|
169
|
+
return false;
|
|
170
|
+
return list.includes(defaultUrl);
|
|
171
|
+
};
|
|
172
|
+
/** Appends the chain's canonical default endpoints back onto its list. */
|
|
173
|
+
export const restoreDefault = (chain) => {
|
|
174
|
+
add(chain, (l) => l.concat(chainsMetadata[toChain(chain)].rpcUrls.default.http));
|
|
175
|
+
};
|
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain-logo resolution, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Two image services are involved and neither covers every chain a route can
|
|
5
|
+
* start from, which is why this is a module rather than an inline template
|
|
6
|
+
* string: gib.show keys on real chain ids, while the non-Ethereum origins reach
|
|
7
|
+
* us under Relay's own pseudo identifiers (Solana, Bitcoin, Tron, The Open
|
|
8
|
+
* Network, Eclipse). Asking gib.show for one of those returns a 404, and a 404
|
|
9
|
+
* in an `<img src>` is drawn by the browser as a broken-image glyph — so the
|
|
10
|
+
* mistake surfaces as a visual blemish on the token pill rather than as an
|
|
11
|
+
* error anyone would go looking for.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Relay's public chain-icon service, keyed on Relay's own chain identifiers —
|
|
15
|
+
* the authority for the ids it invented.
|
|
16
|
+
*
|
|
17
|
+
* The `light` variant is the full-colour brand mark (the pair is light/dark
|
|
18
|
+
* BACKGROUND, not a monochrome inversion), so one variant serves both of our
|
|
19
|
+
* themes. Not every id is populated — The Open Network currently answers 403 —
|
|
20
|
+
* which is why callers must tolerate a badge that fails to load.
|
|
21
|
+
*
|
|
22
|
+
* @param chainId - a Relay chain identifier
|
|
23
|
+
* @returns the icon url
|
|
24
|
+
*/
|
|
25
|
+
export declare const relayChainIconUrl: (chainId: number) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the logo url for a chain, choosing the service that can actually
|
|
28
|
+
* serve it.
|
|
29
|
+
*
|
|
30
|
+
* A chain we bundle the mark for wins outright ({@link bundledChainLogoUri}).
|
|
31
|
+
* Otherwise Ethereum-Virtual-Machine chains go to gib.show, honouring the
|
|
32
|
+
* configured image root (so a self-hosted deployment is respected) rather than
|
|
33
|
+
* hard-coding the public host, and everything else is a Relay identifier and
|
|
34
|
+
* goes to Relay.
|
|
35
|
+
*
|
|
36
|
+
* @param chainId - the decimal chain id, Relay's pseudo identifiers included
|
|
37
|
+
* @param displayPx - the CSS pixel size the logo renders at; when supplied,
|
|
38
|
+
* gib.show is asked for twice that in webp per its retina guidance. Ignored
|
|
39
|
+
* for a Relay-served icon and for a bundled asset, neither of which takes
|
|
40
|
+
* sizing parameters.
|
|
41
|
+
* @returns an image url for the chain
|
|
42
|
+
*/
|
|
43
|
+
export declare const chainLogoUrl: ({ chainId, displayPx, }: {
|
|
44
|
+
chainId: number;
|
|
45
|
+
displayPx?: number;
|
|
46
|
+
}) => string;
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as imageLinks from '@gibs/bridge-sdk/image-links';
|
|
2
|
+
import { ecosystemByChainId, relayChainIds } from '@gibs/bridge-sdk/relay-quote';
|
|
3
|
+
/**
|
|
4
|
+
* Chain-logo resolution, in one place.
|
|
5
|
+
*
|
|
6
|
+
* Two image services are involved and neither covers every chain a route can
|
|
7
|
+
* start from, which is why this is a module rather than an inline template
|
|
8
|
+
* string: gib.show keys on real chain ids, while the non-Ethereum origins reach
|
|
9
|
+
* us under Relay's own pseudo identifiers (Solana, Bitcoin, Tron, The Open
|
|
10
|
+
* Network, Eclipse). Asking gib.show for one of those returns a 404, and a 404
|
|
11
|
+
* in an `<img src>` is drawn by the browser as a broken-image glyph — so the
|
|
12
|
+
* mistake surfaces as a visual blemish on the token pill rather than as an
|
|
13
|
+
* error anyone would go looking for.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Relay's public chain-icon service, keyed on Relay's own chain identifiers —
|
|
17
|
+
* the authority for the ids it invented.
|
|
18
|
+
*
|
|
19
|
+
* The `light` variant is the full-colour brand mark (the pair is light/dark
|
|
20
|
+
* BACKGROUND, not a monochrome inversion), so one variant serves both of our
|
|
21
|
+
* themes. Not every id is populated — The Open Network currently answers 403 —
|
|
22
|
+
* which is why callers must tolerate a badge that fails to load.
|
|
23
|
+
*
|
|
24
|
+
* @param chainId - a Relay chain identifier
|
|
25
|
+
* @returns the icon url
|
|
26
|
+
*/
|
|
27
|
+
export const relayChainIconUrl = (chainId) => `https://assets.relay.link/icons/${chainId}/light.png`;
|
|
28
|
+
/**
|
|
29
|
+
* Chains we ship the mark for ourselves, because neither service will serve it.
|
|
30
|
+
*
|
|
31
|
+
* The Open Network is the only entry today: gib.show does not know Relay's
|
|
32
|
+
* pseudo id, and Relay's own service answers 403 for it — the one chain its
|
|
33
|
+
* authority does not actually cover. Between the two, the badge had no source at
|
|
34
|
+
* all and rendered as nothing via the hide-on-error path, which reads as a
|
|
35
|
+
* missing chain rather than a missing image.
|
|
36
|
+
*
|
|
37
|
+
* The paths are RELATIVE (no leading slash): the reference deployment publishes
|
|
38
|
+
* to the InterPlanetary File System, where a leading slash resolves against the
|
|
39
|
+
* gateway root instead of the deployed directory.
|
|
40
|
+
*
|
|
41
|
+
* A CONSUMER CONTRACT: this package names the asset but cannot ship it. An
|
|
42
|
+
* application using these urls must serve a file at each path from its own
|
|
43
|
+
* static root, or the badge silently renders as nothing. The application-side
|
|
44
|
+
* check is `ui/src/lib/tokens/bundledChainLogo.test.ts`.
|
|
45
|
+
*
|
|
46
|
+
* This is checked BEFORE either service, so an entry here is the last word — the
|
|
47
|
+
* point is to stop depending on a service that has already been observed to fail
|
|
48
|
+
* for the chain.
|
|
49
|
+
*/
|
|
50
|
+
const bundledChainLogoUri = {
|
|
51
|
+
[relayChainIds.ton]: 'images/networks/ton.png',
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Resolves the logo url for a chain, choosing the service that can actually
|
|
55
|
+
* serve it.
|
|
56
|
+
*
|
|
57
|
+
* A chain we bundle the mark for wins outright ({@link bundledChainLogoUri}).
|
|
58
|
+
* Otherwise Ethereum-Virtual-Machine chains go to gib.show, honouring the
|
|
59
|
+
* configured image root (so a self-hosted deployment is respected) rather than
|
|
60
|
+
* hard-coding the public host, and everything else is a Relay identifier and
|
|
61
|
+
* goes to Relay.
|
|
62
|
+
*
|
|
63
|
+
* @param chainId - the decimal chain id, Relay's pseudo identifiers included
|
|
64
|
+
* @param displayPx - the CSS pixel size the logo renders at; when supplied,
|
|
65
|
+
* gib.show is asked for twice that in webp per its retina guidance. Ignored
|
|
66
|
+
* for a Relay-served icon and for a bundled asset, neither of which takes
|
|
67
|
+
* sizing parameters.
|
|
68
|
+
* @returns an image url for the chain
|
|
69
|
+
*/
|
|
70
|
+
export const chainLogoUrl = ({ chainId, displayPx, }) => {
|
|
71
|
+
const bundled = bundledChainLogoUri[chainId];
|
|
72
|
+
if (bundled !== undefined) {
|
|
73
|
+
return bundled;
|
|
74
|
+
}
|
|
75
|
+
if (ecosystemByChainId(chainId) !== 'evm') {
|
|
76
|
+
return relayChainIconUrl(chainId);
|
|
77
|
+
}
|
|
78
|
+
const base = imageLinks.network(chainId);
|
|
79
|
+
if (displayPx === undefined) {
|
|
80
|
+
return base;
|
|
81
|
+
}
|
|
82
|
+
return `${base}?w=${displayPx * 2}&h=${displayPx * 2}&format=webp`;
|
|
83
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side machinery for talking to the chains the bridge supports.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is framework-free. It is the layer between `@gibs/bridge-sdk`,
|
|
5
|
+
* which knows about pathways, quotes and fees but performs no input or output,
|
|
6
|
+
* and an application, which renders. Endpoint resolution, public clients, chain
|
|
7
|
+
* reads, token metadata, caching and logo resolution all live here so that a
|
|
8
|
+
* second application does not have to rebuild them — and so that the rules they
|
|
9
|
+
* encode (which node answers for a chain, which image service knows a chain's
|
|
10
|
+
* mark) have exactly one home.
|
|
11
|
+
*
|
|
12
|
+
* Subpath exports are the intended way in — `@gibs/bridge-client/client`,
|
|
13
|
+
* `/endpoints`, `/image` and so on — so that importing one does not pull the
|
|
14
|
+
* rest. This barrel exists for consumers that would rather not think about it.
|
|
15
|
+
*/
|
|
16
|
+
export { Cache } from './cache.js';
|
|
17
|
+
export { checkAllowance, type ApprovalParameters } from './chain-read.js';
|
|
18
|
+
export { clientFromChain, rpcBatchSetting } from './client.js';
|
|
19
|
+
export { add, configureEndpoints, ENDPOINTS_STORAGE_KEY, entries, get, getSnapshot, hasDefault, key, parse, productionSeed, remove, restoreDefault, setEndpoints, stringify, subscribe, testnetSeed, update, type EndpointEntry, type EndpointStorage, } from './endpoints.js';
|
|
20
|
+
export { chainLogoUrl, relayChainIconUrl } from './image.js';
|
|
21
|
+
export { evmChains, evmChainsById, isSupportedEvmChain } from './networks.js';
|
|
22
|
+
export { getBridgeTokenMetadata, getStoreKey, getStoreSize, getTokenAddressFromBridge, getTokenChainIdFromBridge, getTokenMetadata, loadTokenMetadata, parseChainScopedAddress, parseTokenKey, type ChainScopedAddress, } from './token-metadata.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side machinery for talking to the chains the bridge supports.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is framework-free. It is the layer between `@gibs/bridge-sdk`,
|
|
5
|
+
* which knows about pathways, quotes and fees but performs no input or output,
|
|
6
|
+
* and an application, which renders. Endpoint resolution, public clients, chain
|
|
7
|
+
* reads, token metadata, caching and logo resolution all live here so that a
|
|
8
|
+
* second application does not have to rebuild them — and so that the rules they
|
|
9
|
+
* encode (which node answers for a chain, which image service knows a chain's
|
|
10
|
+
* mark) have exactly one home.
|
|
11
|
+
*
|
|
12
|
+
* Subpath exports are the intended way in — `@gibs/bridge-client/client`,
|
|
13
|
+
* `/endpoints`, `/image` and so on — so that importing one does not pull the
|
|
14
|
+
* rest. This barrel exists for consumers that would rather not think about it.
|
|
15
|
+
*/
|
|
16
|
+
export { Cache } from './cache.js';
|
|
17
|
+
export { checkAllowance } from './chain-read.js';
|
|
18
|
+
export { clientFromChain, rpcBatchSetting } from './client.js';
|
|
19
|
+
export { add, configureEndpoints, ENDPOINTS_STORAGE_KEY, entries, get, getSnapshot, hasDefault, key, parse, productionSeed, remove, restoreDefault, setEndpoints, stringify, subscribe, testnetSeed, update, } from './endpoints.js';
|
|
20
|
+
export { chainLogoUrl, relayChainIconUrl } from './image.js';
|
|
21
|
+
export { evmChains, evmChainsById, isSupportedEvmChain } from './networks.js';
|
|
22
|
+
export { getBridgeTokenMetadata, getStoreKey, getStoreSize, getTokenAddressFromBridge, getTokenChainIdFromBridge, getTokenMetadata, loadTokenMetadata, parseChainScopedAddress, parseTokenKey, } from './token-metadata.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Chain } from 'viem';
|
|
2
|
+
/**
|
|
3
|
+
* Every Ethereum-Virtual-Machine chain this client can build a public client for.
|
|
4
|
+
*
|
|
5
|
+
* The leading entries are the bridge's own chains; the rest are the routing
|
|
6
|
+
* destinations reachable through the aggregators. Ordering is preserved because
|
|
7
|
+
* chain pickers render it directly.
|
|
8
|
+
*
|
|
9
|
+
* Solana and Bitcoin are deliberately absent. They are supported ORIGINS, but
|
|
10
|
+
* they are not Ethereum-style chains: they are keyed by a genesis hash and by
|
|
11
|
+
* their own string rather than by a number, and nothing here can build a client
|
|
12
|
+
* for them. Anything wallet-facing that needs them keys off the aggregator's
|
|
13
|
+
* chain identifiers instead.
|
|
14
|
+
*/
|
|
15
|
+
export declare const evmChains: readonly Chain[];
|
|
16
|
+
/**
|
|
17
|
+
* Lookup from a chain id to its viem chain definition, for supported chains only.
|
|
18
|
+
*
|
|
19
|
+
* Keyed as `string | number` because callers hold ids that have been through a
|
|
20
|
+
* url, a wallet, or a quote response, and a lookup that silently misses on a
|
|
21
|
+
* numeric-versus-string mismatch reads as "chain not supported".
|
|
22
|
+
*/
|
|
23
|
+
export declare const evmChainsById: Map<string | number, Chain>;
|
|
24
|
+
/**
|
|
25
|
+
* Whether this client can reach a chain over Ethereum-style remote procedure calls.
|
|
26
|
+
*
|
|
27
|
+
* @param chainId - the chain id to test
|
|
28
|
+
* @returns true when the chain has a definition here
|
|
29
|
+
*/
|
|
30
|
+
export declare const isSupportedEvmChain: (chainId: number) => boolean;
|
package/dist/networks.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// NAMED imports, deliberately, and never `import * as chains from 'viem/chains'`.
|
|
2
|
+
// A namespace import that is read dynamically (`Object.values`, or indexing by a
|
|
3
|
+
// computed key) defeats tree-shaking outright: all ~700 chain definitions viem
|
|
4
|
+
// ships get emitted to answer a question about the few dozen below, which
|
|
5
|
+
// measured 255 kB of an application entry chunk. Naming each one keeps only what
|
|
6
|
+
// is named.
|
|
7
|
+
import { abstract, arbitrum, aurora, avalanche, base, berachain, blast, boba, bsc, celo, cronos, cronoszkEVM, fantom, fraxtal, fuse, gnosis, gravity, immutableZkEvm, linea, mainnet, mantle, optimism, polygon, polygonZkEvm, pulsechain, pulsechainV4, rootstock, scroll, sei, sepolia, sonic, taiko, unichain, worldchain, zksync, } from 'viem/chains';
|
|
8
|
+
/**
|
|
9
|
+
* Every Ethereum-Virtual-Machine chain this client can build a public client for.
|
|
10
|
+
*
|
|
11
|
+
* The leading entries are the bridge's own chains; the rest are the routing
|
|
12
|
+
* destinations reachable through the aggregators. Ordering is preserved because
|
|
13
|
+
* chain pickers render it directly.
|
|
14
|
+
*
|
|
15
|
+
* Solana and Bitcoin are deliberately absent. They are supported ORIGINS, but
|
|
16
|
+
* they are not Ethereum-style chains: they are keyed by a genesis hash and by
|
|
17
|
+
* their own string rather than by a number, and nothing here can build a client
|
|
18
|
+
* for them. Anything wallet-facing that needs them keys off the aggregator's
|
|
19
|
+
* chain identifiers instead.
|
|
20
|
+
*/
|
|
21
|
+
export const evmChains = [
|
|
22
|
+
mainnet,
|
|
23
|
+
pulsechain,
|
|
24
|
+
sepolia,
|
|
25
|
+
pulsechainV4,
|
|
26
|
+
bsc,
|
|
27
|
+
arbitrum,
|
|
28
|
+
base,
|
|
29
|
+
blast,
|
|
30
|
+
avalanche,
|
|
31
|
+
polygon,
|
|
32
|
+
scroll,
|
|
33
|
+
optimism,
|
|
34
|
+
linea,
|
|
35
|
+
zksync,
|
|
36
|
+
polygonZkEvm,
|
|
37
|
+
gnosis,
|
|
38
|
+
fantom,
|
|
39
|
+
fuse,
|
|
40
|
+
boba,
|
|
41
|
+
unichain,
|
|
42
|
+
aurora,
|
|
43
|
+
sei,
|
|
44
|
+
immutableZkEvm,
|
|
45
|
+
sonic,
|
|
46
|
+
gravity,
|
|
47
|
+
taiko,
|
|
48
|
+
cronos,
|
|
49
|
+
cronoszkEVM,
|
|
50
|
+
fraxtal,
|
|
51
|
+
abstract,
|
|
52
|
+
rootstock,
|
|
53
|
+
celo,
|
|
54
|
+
worldchain,
|
|
55
|
+
mantle,
|
|
56
|
+
berachain,
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Lookup from a chain id to its viem chain definition, for supported chains only.
|
|
60
|
+
*
|
|
61
|
+
* Keyed as `string | number` because callers hold ids that have been through a
|
|
62
|
+
* url, a wallet, or a quote response, and a lookup that silently misses on a
|
|
63
|
+
* numeric-versus-string mismatch reads as "chain not supported".
|
|
64
|
+
*/
|
|
65
|
+
export const evmChainsById = new Map(evmChains.map((chain) => [chain.id, chain]));
|
|
66
|
+
/**
|
|
67
|
+
* Whether this client can reach a chain over Ethereum-style remote procedure calls.
|
|
68
|
+
*
|
|
69
|
+
* @param chainId - the chain id to test
|
|
70
|
+
* @returns true when the chain has a definition here
|
|
71
|
+
*/
|
|
72
|
+
export const isSupportedEvmChain = (chainId) => evmChainsById.has(chainId);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TokenMetadata } from '@gibs/bridge-sdk/types';
|
|
2
|
+
import type { Hex } from 'viem';
|
|
3
|
+
export type ChainScopedAddress = `${number}:${Hex}`;
|
|
4
|
+
export declare function getStoreKey(chainId: number, tokenAddress: string): ChainScopedAddress;
|
|
5
|
+
export declare function parseChainScopedAddress(key: ChainScopedAddress): {
|
|
6
|
+
chainId: number;
|
|
7
|
+
address: Hex;
|
|
8
|
+
};
|
|
9
|
+
export declare function parseTokenKey(key: string): {
|
|
10
|
+
chainId: number;
|
|
11
|
+
address: Hex;
|
|
12
|
+
};
|
|
13
|
+
export declare function getTokenMetadata(chainId: number, tokenAddress: string): TokenMetadata | null;
|
|
14
|
+
export declare function loadTokenMetadata(tokens: Array<{
|
|
15
|
+
chainId: number;
|
|
16
|
+
address: Hex;
|
|
17
|
+
}>): Promise<void>;
|
|
18
|
+
export declare function getStoreSize(): number;
|
|
19
|
+
export declare function getTokenAddressFromBridge(bridge: any): string;
|
|
20
|
+
export declare function getTokenChainIdFromBridge(bridge: any): number;
|
|
21
|
+
export declare function getBridgeTokenMetadata(bridge: any): TokenMetadata | null;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { nativeTokenName, nativeTokenSymbol, toChain } from '@gibs/bridge-sdk/config';
|
|
2
|
+
import * as _ from 'lodash-es';
|
|
3
|
+
import { erc20Abi, erc20Abi_bytes32, hexToString, zeroAddress } from 'viem';
|
|
4
|
+
import { clientFromChain } from './client.js';
|
|
5
|
+
import { evmChainsById } from './networks.js';
|
|
6
|
+
const bytes32Whitelist = new Set();
|
|
7
|
+
// In-memory store for token metadata (loaded once, no persistence)
|
|
8
|
+
const tokenMetadataStore = new Map();
|
|
9
|
+
// Generate store key
|
|
10
|
+
export function getStoreKey(chainId, tokenAddress) {
|
|
11
|
+
return `${chainId}:${tokenAddress.toLowerCase()}`;
|
|
12
|
+
}
|
|
13
|
+
// Parse a ChainScopedAddress into its components
|
|
14
|
+
export function parseChainScopedAddress(key) {
|
|
15
|
+
const [chainIdStr, address] = key.split(':');
|
|
16
|
+
const chainId = parseInt(chainIdStr ?? '', 10);
|
|
17
|
+
if (isNaN(chainId)) {
|
|
18
|
+
throw new Error(`Invalid chain ID in ChainScopedAddress: ${chainIdStr}`);
|
|
19
|
+
}
|
|
20
|
+
if (!address || !address.startsWith('0x')) {
|
|
21
|
+
throw new Error(`Invalid address in ChainScopedAddress: ${address}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
chainId,
|
|
25
|
+
address: address,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Parse a string key (chainId:address format) into its components with proper types
|
|
29
|
+
export function parseTokenKey(key) {
|
|
30
|
+
const [chainIdStr, address] = key.split(':');
|
|
31
|
+
const chainId = parseInt(chainIdStr ?? '', 10);
|
|
32
|
+
if (isNaN(chainId)) {
|
|
33
|
+
throw new Error(`Invalid chain ID in token key: ${chainIdStr}`);
|
|
34
|
+
}
|
|
35
|
+
if (!address) {
|
|
36
|
+
throw new Error(`Invalid address in token key: ${address}`);
|
|
37
|
+
}
|
|
38
|
+
// Ensure address is properly formatted as Hex
|
|
39
|
+
const hexAddress = address.startsWith('0x') ? address : `0x${address}`;
|
|
40
|
+
return {
|
|
41
|
+
chainId,
|
|
42
|
+
address: hexAddress,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// Get native token metadata
|
|
46
|
+
function getNativeTokenMetadata(chainId) {
|
|
47
|
+
const chain = evmChainsById.get(chainId);
|
|
48
|
+
// Safely get native token info from SDK
|
|
49
|
+
let name = chain?.nativeCurrency?.name || 'Ether';
|
|
50
|
+
let symbol = chain?.nativeCurrency?.symbol || 'ETH';
|
|
51
|
+
const decimals = chain?.nativeCurrency?.decimals || 18;
|
|
52
|
+
// Try to get more specific info from SDK if available
|
|
53
|
+
try {
|
|
54
|
+
const chainKey = toChain(chainId);
|
|
55
|
+
const sdkName = nativeTokenName[chainKey];
|
|
56
|
+
const sdkSymbol = nativeTokenSymbol[chainKey];
|
|
57
|
+
if (sdkName) {
|
|
58
|
+
name = sdkName;
|
|
59
|
+
}
|
|
60
|
+
if (sdkSymbol) {
|
|
61
|
+
symbol = sdkSymbol;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Fallback to viem chain info
|
|
66
|
+
}
|
|
67
|
+
return { name, symbol, decimals };
|
|
68
|
+
}
|
|
69
|
+
// Batch fetch token metadata for multiple tokens on the same chain
|
|
70
|
+
async function batchFetchTokenMetadata(chainId, tokenAddresses) {
|
|
71
|
+
const results = new Map();
|
|
72
|
+
if (tokenAddresses.length === 0)
|
|
73
|
+
return results;
|
|
74
|
+
// Get the chain and client
|
|
75
|
+
const chain = evmChainsById.get(chainId);
|
|
76
|
+
if (!chain) {
|
|
77
|
+
throw new Error(`Chain not found for chainId: ${chainId}`);
|
|
78
|
+
}
|
|
79
|
+
const client = clientFromChain(chainId);
|
|
80
|
+
// Separate native tokens from ERC20 tokens
|
|
81
|
+
const nativeTokens = [];
|
|
82
|
+
const erc20Tokens = [];
|
|
83
|
+
tokenAddresses.forEach((address) => {
|
|
84
|
+
if (address === zeroAddress || address.toLowerCase() === zeroAddress.toLowerCase()) {
|
|
85
|
+
nativeTokens.push(address);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
erc20Tokens.push(address);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
// Handle native tokens
|
|
92
|
+
const nativeMetadata = getNativeTokenMetadata(chainId);
|
|
93
|
+
nativeTokens.forEach((address) => {
|
|
94
|
+
results.set(address.toLowerCase(), nativeMetadata);
|
|
95
|
+
});
|
|
96
|
+
// Handle ERC20 tokens with batch multicall
|
|
97
|
+
if (erc20Tokens.length === 0) {
|
|
98
|
+
// console.log(`No ERC20 tokens to process for chain ${chainId}`)
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
// console.log(`Processing ${erc20Tokens.length} ERC20 tokens for chain ${chainId}:`, erc20Tokens)
|
|
102
|
+
const addressChunks = _.chunk(erc20Tokens, 100);
|
|
103
|
+
const methods = ['symbol', 'name', 'decimals'];
|
|
104
|
+
const addressChunksToCallChunks = (addressChunks, abi) => addressChunks.map((chunk) => chunk.map((address) => methods.map((functionName) => ({
|
|
105
|
+
functionName,
|
|
106
|
+
address,
|
|
107
|
+
abi: abi(address),
|
|
108
|
+
allowFailure: true,
|
|
109
|
+
args: [],
|
|
110
|
+
}))));
|
|
111
|
+
// Try with standard ERC20 ABI first
|
|
112
|
+
const chunkedCalls = addressChunksToCallChunks(addressChunks, (address) => bytes32Whitelist.has(getStoreKey(chainId, address)) ? erc20Abi_bytes32 : erc20Abi);
|
|
113
|
+
const fallback = [];
|
|
114
|
+
for (const chunk of chunkedCalls) {
|
|
115
|
+
// console.log(`Executing multicall for chain ${chainId} with ${_.flatten(chunk).length} contracts`)
|
|
116
|
+
try {
|
|
117
|
+
const batchResults = await client.multicall({
|
|
118
|
+
allowFailure: true,
|
|
119
|
+
contracts: _.flatten(chunk),
|
|
120
|
+
});
|
|
121
|
+
// console.log(`Multicall results for chain ${chainId}:`, batchResults.length, 'results')
|
|
122
|
+
_.chunk(batchResults, 3).forEach((batch, index) => {
|
|
123
|
+
const tokenAddress = erc20Tokens[index];
|
|
124
|
+
const symbol = batch[0].result;
|
|
125
|
+
const name = batch[1].result;
|
|
126
|
+
const decimals = Number(batch[2].result);
|
|
127
|
+
// console.log(`Token ${tokenAddress} results:`, { symbol, name, decimals, errors: [batch[0].error, batch[1].error, batch[2].error] })
|
|
128
|
+
if (name && symbol && !isNaN(decimals)) {
|
|
129
|
+
results.set(tokenAddress.toLowerCase(), { name, symbol, decimals });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.log(`Adding ${tokenAddress} to fallback due to missing data`);
|
|
133
|
+
fallback.push(tokenAddress);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.error(`Multicall failed for chain ${chainId}:`, error);
|
|
139
|
+
// Add all tokens to fallback if multicall fails completely
|
|
140
|
+
fallback.push(...erc20Tokens);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (fallback.length === 0) {
|
|
144
|
+
return results;
|
|
145
|
+
}
|
|
146
|
+
console.log(`Processing ${fallback.length} tokens in fallback with bytes32 ABI for chain ${chainId}:`, fallback);
|
|
147
|
+
const fallbackAddressChunks = _.chunk(fallback, 100);
|
|
148
|
+
const fallbackChunkedCalls = addressChunksToCallChunks(fallbackAddressChunks, () => erc20Abi_bytes32);
|
|
149
|
+
for (const chunk of fallbackChunkedCalls) {
|
|
150
|
+
try {
|
|
151
|
+
console.log(`Executing fallback multicall for chain ${chainId} with ${_.flatten(chunk).length} contracts`);
|
|
152
|
+
const batchResults = await client.multicall({
|
|
153
|
+
allowFailure: false,
|
|
154
|
+
contracts: _.flatten(chunk),
|
|
155
|
+
});
|
|
156
|
+
console.log(`Fallback multicall results for chain ${chainId}:`, batchResults.length, 'results');
|
|
157
|
+
_.chunk(batchResults, 3).forEach((batch, index) => {
|
|
158
|
+
const tokenAddress = fallback[index];
|
|
159
|
+
const symbol = batch[0];
|
|
160
|
+
const name = batch[1];
|
|
161
|
+
const decimals = Number(batch[2]);
|
|
162
|
+
const symbolString = hexToString(symbol);
|
|
163
|
+
const nameString = hexToString(name);
|
|
164
|
+
console.log(`Fallback token ${tokenAddress} results:`, {
|
|
165
|
+
symbolString,
|
|
166
|
+
nameString,
|
|
167
|
+
decimals,
|
|
168
|
+
});
|
|
169
|
+
results.set(tokenAddress.toLowerCase(), {
|
|
170
|
+
name: nameString,
|
|
171
|
+
symbol: symbolString,
|
|
172
|
+
decimals,
|
|
173
|
+
});
|
|
174
|
+
// Add to bytes32 whitelist for future optimization
|
|
175
|
+
bytes32Whitelist.add(getStoreKey(chainId, tokenAddress));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error(`Fallback multicall failed for chain ${chainId}:`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return results;
|
|
183
|
+
}
|
|
184
|
+
// Get token metadata synchronously (from store only)
|
|
185
|
+
export function getTokenMetadata(chainId, tokenAddress) {
|
|
186
|
+
const storeKey = getStoreKey(chainId, tokenAddress);
|
|
187
|
+
return tokenMetadataStore.get(storeKey) || null;
|
|
188
|
+
}
|
|
189
|
+
// Load token metadata for multiple tokens (called once on page load)
|
|
190
|
+
export async function loadTokenMetadata(tokens) {
|
|
191
|
+
// Group tokens by chain ID for batching
|
|
192
|
+
const tokensByChain = new Map();
|
|
193
|
+
tokens.forEach(({ chainId, address }) => {
|
|
194
|
+
if (!tokensByChain.has(chainId)) {
|
|
195
|
+
tokensByChain.set(chainId, []);
|
|
196
|
+
}
|
|
197
|
+
tokensByChain.get(chainId).push(address);
|
|
198
|
+
});
|
|
199
|
+
// Batch fetch for each chain
|
|
200
|
+
const batchPromises = Array.from(tokensByChain.entries()).map(async ([chainId, addresses]) => {
|
|
201
|
+
try {
|
|
202
|
+
// console.log(`Loading token metadata for chain ${chainId} (${addresses.length} tokens):`, addresses)
|
|
203
|
+
const results = await batchFetchTokenMetadata(chainId, addresses);
|
|
204
|
+
// console.log(`Loaded ${results.size} token metadata results for chain ${chainId}:`, Array.from(results.entries()))
|
|
205
|
+
// Store the results
|
|
206
|
+
results.forEach((metadata, address) => {
|
|
207
|
+
if (metadata) {
|
|
208
|
+
const storeKey = getStoreKey(chainId, address);
|
|
209
|
+
tokenMetadataStore.set(storeKey, metadata);
|
|
210
|
+
// console.log(`Stored metadata for ${storeKey}:`, metadata)
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error(`Failed to load tokens for chain ${chainId}:`, error);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
await Promise.allSettled(batchPromises);
|
|
219
|
+
}
|
|
220
|
+
// Get store size
|
|
221
|
+
export function getStoreSize() {
|
|
222
|
+
return tokenMetadataStore.size;
|
|
223
|
+
}
|
|
224
|
+
// Utility functions for bridge transactions
|
|
225
|
+
export function getTokenAddressFromBridge(bridge) {
|
|
226
|
+
return (bridge.originationToken?.address ||
|
|
227
|
+
bridge.destinationToken?.address ||
|
|
228
|
+
bridge.originationTokenAddress ||
|
|
229
|
+
bridge.destinationTokenAddress ||
|
|
230
|
+
'');
|
|
231
|
+
}
|
|
232
|
+
export function getTokenChainIdFromBridge(bridge) {
|
|
233
|
+
return Number(bridge.originationToken?.chainId ||
|
|
234
|
+
bridge.destinationToken?.chainId ||
|
|
235
|
+
bridge.originationTokenChainId ||
|
|
236
|
+
bridge.destinationTokenChainId ||
|
|
237
|
+
1);
|
|
238
|
+
}
|
|
239
|
+
export function getBridgeTokenMetadata(bridge) {
|
|
240
|
+
const address = getTokenAddressFromBridge(bridge);
|
|
241
|
+
const chainId = getTokenChainIdFromBridge(bridge);
|
|
242
|
+
if (!address)
|
|
243
|
+
return null;
|
|
244
|
+
return getTokenMetadata(chainId, address);
|
|
245
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gibs/bridge-client",
|
|
3
|
+
"version": "1.12.0",
|
|
4
|
+
"description": "Browser-side data loading, caching and endpoint resolution for the Gibs Finance bridge",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"gibs-source": "./src/index.ts",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./cache": {
|
|
18
|
+
"gibs-source": "./src/cache.ts",
|
|
19
|
+
"types": "./dist/cache.d.ts",
|
|
20
|
+
"default": "./dist/cache.js"
|
|
21
|
+
},
|
|
22
|
+
"./image": {
|
|
23
|
+
"gibs-source": "./src/image.ts",
|
|
24
|
+
"types": "./dist/image.d.ts",
|
|
25
|
+
"default": "./dist/image.js"
|
|
26
|
+
},
|
|
27
|
+
"./networks": {
|
|
28
|
+
"gibs-source": "./src/networks.ts",
|
|
29
|
+
"types": "./dist/networks.d.ts",
|
|
30
|
+
"default": "./dist/networks.js"
|
|
31
|
+
},
|
|
32
|
+
"./endpoints": {
|
|
33
|
+
"gibs-source": "./src/endpoints.ts",
|
|
34
|
+
"types": "./dist/endpoints.d.ts",
|
|
35
|
+
"default": "./dist/endpoints.js"
|
|
36
|
+
},
|
|
37
|
+
"./client": {
|
|
38
|
+
"gibs-source": "./src/client.ts",
|
|
39
|
+
"types": "./dist/client.d.ts",
|
|
40
|
+
"default": "./dist/client.js"
|
|
41
|
+
},
|
|
42
|
+
"./chain-read": {
|
|
43
|
+
"gibs-source": "./src/chain-read.ts",
|
|
44
|
+
"types": "./dist/chain-read.d.ts",
|
|
45
|
+
"default": "./dist/chain-read.js"
|
|
46
|
+
},
|
|
47
|
+
"./token-metadata": {
|
|
48
|
+
"gibs-source": "./src/token-metadata.ts",
|
|
49
|
+
"types": "./dist/token-metadata.d.ts",
|
|
50
|
+
"default": "./dist/token-metadata.js"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "npx tsc",
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:watch": "vitest"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [],
|
|
62
|
+
"author": "",
|
|
63
|
+
"license": "ISC",
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/lodash-es": "^4.17.12",
|
|
66
|
+
"@types/node": "^24",
|
|
67
|
+
"typescript": "~5.7.2",
|
|
68
|
+
"vitest": "^3.1.1"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@gibs/bridge-sdk": "^1.12.0",
|
|
72
|
+
"@gibs/common": "^1.12.0",
|
|
73
|
+
"lodash-es": "^4.17.21",
|
|
74
|
+
"viem": "^2.46.3"
|
|
75
|
+
}
|
|
76
|
+
}
|