@perkos/util-tokens 1.0.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 +80 -0
- package/dist/index.d.mts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +195 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +169 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @perkos/util-tokens
|
|
2
|
+
|
|
3
|
+
ERC20 token detection utilities for reading on-chain token metadata. Essential for EIP-712 domain construction in payment signatures.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @perkos/util-tokens
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { detectTokenInfo, getChainId, getRpcUrl } from "@perkos/util-tokens";
|
|
15
|
+
|
|
16
|
+
// Detect token information from contract
|
|
17
|
+
const tokenInfo = await detectTokenInfo(
|
|
18
|
+
"0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
19
|
+
"avalanche"
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
console.log(tokenInfo);
|
|
23
|
+
// {
|
|
24
|
+
// address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
25
|
+
// name: "USD Coin",
|
|
26
|
+
// symbol: "USDC",
|
|
27
|
+
// decimals: 6,
|
|
28
|
+
// chainId: 43114
|
|
29
|
+
// }
|
|
30
|
+
|
|
31
|
+
// Get chain ID for a network
|
|
32
|
+
const chainId = getChainId("base-sepolia"); // 84532
|
|
33
|
+
|
|
34
|
+
// Get RPC URL
|
|
35
|
+
const rpcUrl = getRpcUrl("avalanche");
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Automatic Caching**: Token info is cached to avoid repeated RPC calls
|
|
41
|
+
- **Multi-Network Support**: Avalanche, Base, Celo (mainnet and testnets)
|
|
42
|
+
- **CAIP-2 Support**: Accepts both legacy and CAIP-2 network identifiers
|
|
43
|
+
- **TypeScript**: Full type definitions included
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### `detectTokenInfo(tokenAddress, network, config?)`
|
|
48
|
+
|
|
49
|
+
Detect token information from a contract address.
|
|
50
|
+
|
|
51
|
+
- `tokenAddress`: ERC20 token contract address
|
|
52
|
+
- `network`: Network name or CAIP-2 identifier
|
|
53
|
+
- `config`: Optional configuration with custom RPC URL
|
|
54
|
+
|
|
55
|
+
Returns `TokenInfo | null`
|
|
56
|
+
|
|
57
|
+
### `getChainId(network)`
|
|
58
|
+
|
|
59
|
+
Get chain ID for a network name.
|
|
60
|
+
|
|
61
|
+
### `getRpcUrl(network, customUrl?)`
|
|
62
|
+
|
|
63
|
+
Get RPC URL for a network.
|
|
64
|
+
|
|
65
|
+
### `clearTokenCache()`
|
|
66
|
+
|
|
67
|
+
Clear the token info cache.
|
|
68
|
+
|
|
69
|
+
## Supported Networks
|
|
70
|
+
|
|
71
|
+
- Avalanche (43114)
|
|
72
|
+
- Avalanche Fuji (43113)
|
|
73
|
+
- Base (8453)
|
|
74
|
+
- Base Sepolia (84532)
|
|
75
|
+
- Celo (42220)
|
|
76
|
+
- Celo Sepolia (11142220)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Address, PublicClient } from 'viem';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @perkos/token-detection
|
|
5
|
+
* ERC20 token detection utilities for reading on-chain token metadata
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Token information returned from detection
|
|
10
|
+
*/
|
|
11
|
+
interface TokenInfo {
|
|
12
|
+
address: Address;
|
|
13
|
+
name: string;
|
|
14
|
+
symbol: string;
|
|
15
|
+
decimals: number;
|
|
16
|
+
chainId: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Supported network names
|
|
20
|
+
*/
|
|
21
|
+
type NetworkName = "avalanche" | "avalanche-fuji" | "base" | "base-sepolia" | "celo" | "celo-sepolia";
|
|
22
|
+
/**
|
|
23
|
+
* Network configuration options
|
|
24
|
+
*/
|
|
25
|
+
interface NetworkConfig {
|
|
26
|
+
rpcUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert CAIP-2 network format to legacy format
|
|
30
|
+
* e.g., "eip155:43114" → "avalanche"
|
|
31
|
+
*/
|
|
32
|
+
declare function toLegacyNetwork(network: string): NetworkName;
|
|
33
|
+
/**
|
|
34
|
+
* Get chain ID for a network
|
|
35
|
+
*/
|
|
36
|
+
declare function getChainId(network: string): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get RPC URL for a network
|
|
39
|
+
*/
|
|
40
|
+
declare function getRpcUrl(network: string, customUrl?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Get public client for a network
|
|
43
|
+
*/
|
|
44
|
+
declare function getPublicClient(network: string, config?: NetworkConfig): PublicClient;
|
|
45
|
+
/**
|
|
46
|
+
* Detect token information from contract address
|
|
47
|
+
* Results are cached to avoid repeated RPC calls
|
|
48
|
+
*
|
|
49
|
+
* @param tokenAddress - The ERC20 token contract address
|
|
50
|
+
* @param network - Network name or CAIP-2 identifier
|
|
51
|
+
* @param config - Optional network configuration
|
|
52
|
+
* @returns Token information or null if detection fails
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const tokenInfo = await detectTokenInfo(
|
|
57
|
+
* "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
58
|
+
* "avalanche"
|
|
59
|
+
* );
|
|
60
|
+
* // => { name: "USD Coin", symbol: "USDC", decimals: 6, ... }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
declare function detectTokenInfo(tokenAddress: Address, network: string, config?: NetworkConfig): Promise<TokenInfo | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Clear the token info cache
|
|
66
|
+
*/
|
|
67
|
+
declare function clearTokenCache(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Get cached token info without RPC call
|
|
70
|
+
*/
|
|
71
|
+
declare function getCachedTokenInfo(tokenAddress: Address, network: string): TokenInfo | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Check if a network is valid
|
|
74
|
+
*/
|
|
75
|
+
declare function isValidNetwork(network: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Get all supported networks
|
|
78
|
+
*/
|
|
79
|
+
declare function getSupportedNetworks(): NetworkName[];
|
|
80
|
+
|
|
81
|
+
export { type NetworkConfig, type NetworkName, type TokenInfo, clearTokenCache, detectTokenInfo, getCachedTokenInfo, getChainId, getPublicClient, getRpcUrl, getSupportedNetworks, isValidNetwork, toLegacyNetwork };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Address, PublicClient } from 'viem';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @perkos/token-detection
|
|
5
|
+
* ERC20 token detection utilities for reading on-chain token metadata
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Token information returned from detection
|
|
10
|
+
*/
|
|
11
|
+
interface TokenInfo {
|
|
12
|
+
address: Address;
|
|
13
|
+
name: string;
|
|
14
|
+
symbol: string;
|
|
15
|
+
decimals: number;
|
|
16
|
+
chainId: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Supported network names
|
|
20
|
+
*/
|
|
21
|
+
type NetworkName = "avalanche" | "avalanche-fuji" | "base" | "base-sepolia" | "celo" | "celo-sepolia";
|
|
22
|
+
/**
|
|
23
|
+
* Network configuration options
|
|
24
|
+
*/
|
|
25
|
+
interface NetworkConfig {
|
|
26
|
+
rpcUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert CAIP-2 network format to legacy format
|
|
30
|
+
* e.g., "eip155:43114" → "avalanche"
|
|
31
|
+
*/
|
|
32
|
+
declare function toLegacyNetwork(network: string): NetworkName;
|
|
33
|
+
/**
|
|
34
|
+
* Get chain ID for a network
|
|
35
|
+
*/
|
|
36
|
+
declare function getChainId(network: string): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get RPC URL for a network
|
|
39
|
+
*/
|
|
40
|
+
declare function getRpcUrl(network: string, customUrl?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Get public client for a network
|
|
43
|
+
*/
|
|
44
|
+
declare function getPublicClient(network: string, config?: NetworkConfig): PublicClient;
|
|
45
|
+
/**
|
|
46
|
+
* Detect token information from contract address
|
|
47
|
+
* Results are cached to avoid repeated RPC calls
|
|
48
|
+
*
|
|
49
|
+
* @param tokenAddress - The ERC20 token contract address
|
|
50
|
+
* @param network - Network name or CAIP-2 identifier
|
|
51
|
+
* @param config - Optional network configuration
|
|
52
|
+
* @returns Token information or null if detection fails
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const tokenInfo = await detectTokenInfo(
|
|
57
|
+
* "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
58
|
+
* "avalanche"
|
|
59
|
+
* );
|
|
60
|
+
* // => { name: "USD Coin", symbol: "USDC", decimals: 6, ... }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
declare function detectTokenInfo(tokenAddress: Address, network: string, config?: NetworkConfig): Promise<TokenInfo | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Clear the token info cache
|
|
66
|
+
*/
|
|
67
|
+
declare function clearTokenCache(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Get cached token info without RPC call
|
|
70
|
+
*/
|
|
71
|
+
declare function getCachedTokenInfo(tokenAddress: Address, network: string): TokenInfo | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Check if a network is valid
|
|
74
|
+
*/
|
|
75
|
+
declare function isValidNetwork(network: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Get all supported networks
|
|
78
|
+
*/
|
|
79
|
+
declare function getSupportedNetworks(): NetworkName[];
|
|
80
|
+
|
|
81
|
+
export { type NetworkConfig, type NetworkName, type TokenInfo, clearTokenCache, detectTokenInfo, getCachedTokenInfo, getChainId, getPublicClient, getRpcUrl, getSupportedNetworks, isValidNetwork, toLegacyNetwork };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
clearTokenCache: () => clearTokenCache,
|
|
24
|
+
detectTokenInfo: () => detectTokenInfo,
|
|
25
|
+
getCachedTokenInfo: () => getCachedTokenInfo,
|
|
26
|
+
getChainId: () => getChainId,
|
|
27
|
+
getPublicClient: () => getPublicClient,
|
|
28
|
+
getRpcUrl: () => getRpcUrl,
|
|
29
|
+
getSupportedNetworks: () => getSupportedNetworks,
|
|
30
|
+
isValidNetwork: () => isValidNetwork,
|
|
31
|
+
toLegacyNetwork: () => toLegacyNetwork
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
var import_viem = require("viem");
|
|
35
|
+
var import_chains = require("viem/chains");
|
|
36
|
+
var ERC20_ABI = [
|
|
37
|
+
{
|
|
38
|
+
constant: true,
|
|
39
|
+
inputs: [],
|
|
40
|
+
name: "name",
|
|
41
|
+
outputs: [{ name: "", type: "string" }],
|
|
42
|
+
type: "function"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
constant: true,
|
|
46
|
+
inputs: [],
|
|
47
|
+
name: "symbol",
|
|
48
|
+
outputs: [{ name: "", type: "string" }],
|
|
49
|
+
type: "function"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
constant: true,
|
|
53
|
+
inputs: [],
|
|
54
|
+
name: "decimals",
|
|
55
|
+
outputs: [{ name: "", type: "uint8" }],
|
|
56
|
+
type: "function"
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
var CHAIN_IDS = {
|
|
60
|
+
avalanche: 43114,
|
|
61
|
+
"avalanche-fuji": 43113,
|
|
62
|
+
base: 8453,
|
|
63
|
+
"base-sepolia": 84532,
|
|
64
|
+
celo: 42220,
|
|
65
|
+
"celo-sepolia": 11142220,
|
|
66
|
+
// CAIP-2 format support
|
|
67
|
+
"eip155:43114": 43114,
|
|
68
|
+
"eip155:43113": 43113,
|
|
69
|
+
"eip155:8453": 8453,
|
|
70
|
+
"eip155:84532": 84532,
|
|
71
|
+
"eip155:42220": 42220,
|
|
72
|
+
"eip155:11142220": 11142220
|
|
73
|
+
};
|
|
74
|
+
var DEFAULT_RPC_URLS = {
|
|
75
|
+
avalanche: "https://api.avax.network/ext/bc/C/rpc",
|
|
76
|
+
"avalanche-fuji": "https://api.avax-test.network/ext/bc/C/rpc",
|
|
77
|
+
base: "https://mainnet.base.org",
|
|
78
|
+
"base-sepolia": "https://sepolia.base.org",
|
|
79
|
+
celo: "https://forno.celo.org",
|
|
80
|
+
"celo-sepolia": "https://forno.celo-sepolia.celo-testnet.org"
|
|
81
|
+
};
|
|
82
|
+
var CHAINS = {
|
|
83
|
+
avalanche: import_chains.avalanche,
|
|
84
|
+
"avalanche-fuji": import_chains.avalancheFuji,
|
|
85
|
+
base: import_chains.base,
|
|
86
|
+
"base-sepolia": import_chains.baseSepolia,
|
|
87
|
+
celo: import_chains.celo,
|
|
88
|
+
"celo-sepolia": import_chains.celoAlfajores
|
|
89
|
+
};
|
|
90
|
+
var CAIP2_TO_LEGACY = {
|
|
91
|
+
"eip155:43114": "avalanche",
|
|
92
|
+
"eip155:43113": "avalanche-fuji",
|
|
93
|
+
"eip155:8453": "base",
|
|
94
|
+
"eip155:84532": "base-sepolia",
|
|
95
|
+
"eip155:42220": "celo",
|
|
96
|
+
"eip155:11142220": "celo-sepolia"
|
|
97
|
+
};
|
|
98
|
+
var tokenInfoCache = /* @__PURE__ */ new Map();
|
|
99
|
+
function toLegacyNetwork(network) {
|
|
100
|
+
if (!network.includes(":")) {
|
|
101
|
+
return network;
|
|
102
|
+
}
|
|
103
|
+
return CAIP2_TO_LEGACY[network] || "avalanche";
|
|
104
|
+
}
|
|
105
|
+
function getChainId(network) {
|
|
106
|
+
return CHAIN_IDS[network] || CHAIN_IDS.avalanche;
|
|
107
|
+
}
|
|
108
|
+
function getRpcUrl(network, customUrl) {
|
|
109
|
+
if (customUrl) return customUrl;
|
|
110
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
111
|
+
return DEFAULT_RPC_URLS[legacyNetwork] || DEFAULT_RPC_URLS.avalanche;
|
|
112
|
+
}
|
|
113
|
+
function getPublicClient(network, config) {
|
|
114
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
115
|
+
const rpcUrl = getRpcUrl(network, config?.rpcUrl);
|
|
116
|
+
const chain = CHAINS[legacyNetwork] || {
|
|
117
|
+
id: getChainId(legacyNetwork),
|
|
118
|
+
name: legacyNetwork,
|
|
119
|
+
network: legacyNetwork,
|
|
120
|
+
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
|
|
121
|
+
rpcUrls: { default: { http: [rpcUrl] } }
|
|
122
|
+
};
|
|
123
|
+
return (0, import_viem.createPublicClient)({
|
|
124
|
+
chain,
|
|
125
|
+
transport: (0, import_viem.http)(rpcUrl)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function detectTokenInfo(tokenAddress, network, config) {
|
|
129
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
130
|
+
const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;
|
|
131
|
+
if (tokenInfoCache.has(cacheKey)) {
|
|
132
|
+
return tokenInfoCache.get(cacheKey);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const client = getPublicClient(legacyNetwork, config);
|
|
136
|
+
const chainId = getChainId(legacyNetwork);
|
|
137
|
+
const [name, symbol, decimals] = await Promise.all([
|
|
138
|
+
client.readContract({
|
|
139
|
+
address: tokenAddress,
|
|
140
|
+
abi: ERC20_ABI,
|
|
141
|
+
functionName: "name"
|
|
142
|
+
}),
|
|
143
|
+
client.readContract({
|
|
144
|
+
address: tokenAddress,
|
|
145
|
+
abi: ERC20_ABI,
|
|
146
|
+
functionName: "symbol"
|
|
147
|
+
}),
|
|
148
|
+
client.readContract({
|
|
149
|
+
address: tokenAddress,
|
|
150
|
+
abi: ERC20_ABI,
|
|
151
|
+
functionName: "decimals"
|
|
152
|
+
})
|
|
153
|
+
]);
|
|
154
|
+
const tokenInfo = {
|
|
155
|
+
address: tokenAddress,
|
|
156
|
+
name: name || "Unknown Token",
|
|
157
|
+
symbol: symbol || "UNKNOWN",
|
|
158
|
+
decimals: decimals || 18,
|
|
159
|
+
chainId
|
|
160
|
+
};
|
|
161
|
+
tokenInfoCache.set(cacheKey, tokenInfo);
|
|
162
|
+
return tokenInfo;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error("Failed to detect token info:", error);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function clearTokenCache() {
|
|
169
|
+
tokenInfoCache.clear();
|
|
170
|
+
}
|
|
171
|
+
function getCachedTokenInfo(tokenAddress, network) {
|
|
172
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
173
|
+
const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;
|
|
174
|
+
return tokenInfoCache.get(cacheKey);
|
|
175
|
+
}
|
|
176
|
+
function isValidNetwork(network) {
|
|
177
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
178
|
+
return legacyNetwork in CHAINS;
|
|
179
|
+
}
|
|
180
|
+
function getSupportedNetworks() {
|
|
181
|
+
return Object.keys(CHAINS);
|
|
182
|
+
}
|
|
183
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
184
|
+
0 && (module.exports = {
|
|
185
|
+
clearTokenCache,
|
|
186
|
+
detectTokenInfo,
|
|
187
|
+
getCachedTokenInfo,
|
|
188
|
+
getChainId,
|
|
189
|
+
getPublicClient,
|
|
190
|
+
getRpcUrl,
|
|
191
|
+
getSupportedNetworks,
|
|
192
|
+
isValidNetwork,
|
|
193
|
+
toLegacyNetwork
|
|
194
|
+
});
|
|
195
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @perkos/token-detection\n * ERC20 token detection utilities for reading on-chain token metadata\n */\n\nimport { createPublicClient, http, type Address, type PublicClient } from \"viem\";\nimport {\n avalanche,\n avalancheFuji,\n base,\n baseSepolia,\n celo,\n celoAlfajores,\n} from \"viem/chains\";\n\n/**\n * Token information returned from detection\n */\nexport interface TokenInfo {\n address: Address;\n name: string;\n symbol: string;\n decimals: number;\n chainId: number;\n}\n\n/**\n * Supported network names\n */\nexport type NetworkName =\n | \"avalanche\"\n | \"avalanche-fuji\"\n | \"base\"\n | \"base-sepolia\"\n | \"celo\"\n | \"celo-sepolia\";\n\n/**\n * Network configuration options\n */\nexport interface NetworkConfig {\n rpcUrl?: string;\n}\n\n/**\n * Standard ERC20 ABI (minimal - just what we need)\n */\nconst ERC20_ABI = [\n {\n constant: true,\n inputs: [],\n name: \"name\",\n outputs: [{ name: \"\", type: \"string\" }],\n type: \"function\",\n },\n {\n constant: true,\n inputs: [],\n name: \"symbol\",\n outputs: [{ name: \"\", type: \"string\" }],\n type: \"function\",\n },\n {\n constant: true,\n inputs: [],\n name: \"decimals\",\n outputs: [{ name: \"\", type: \"uint8\" }],\n type: \"function\",\n },\n] as const;\n\n/**\n * Chain ID mappings\n */\nconst CHAIN_IDS: Record<string, number> = {\n avalanche: 43114,\n \"avalanche-fuji\": 43113,\n base: 8453,\n \"base-sepolia\": 84532,\n celo: 42220,\n \"celo-sepolia\": 11142220,\n // CAIP-2 format support\n \"eip155:43114\": 43114,\n \"eip155:43113\": 43113,\n \"eip155:8453\": 8453,\n \"eip155:84532\": 84532,\n \"eip155:42220\": 42220,\n \"eip155:11142220\": 11142220,\n};\n\n/**\n * Default RPC URLs\n */\nconst DEFAULT_RPC_URLS: Record<NetworkName, string> = {\n avalanche: \"https://api.avax.network/ext/bc/C/rpc\",\n \"avalanche-fuji\": \"https://api.avax-test.network/ext/bc/C/rpc\",\n base: \"https://mainnet.base.org\",\n \"base-sepolia\": \"https://sepolia.base.org\",\n celo: \"https://forno.celo.org\",\n \"celo-sepolia\": \"https://forno.celo-sepolia.celo-testnet.org\",\n};\n\n/**\n * Chain definitions\n */\nconst CHAINS: Record<NetworkName, any> = {\n avalanche,\n \"avalanche-fuji\": avalancheFuji,\n base,\n \"base-sepolia\": baseSepolia,\n celo,\n \"celo-sepolia\": celoAlfajores,\n};\n\n/**\n * CAIP-2 to legacy network mapping\n */\nconst CAIP2_TO_LEGACY: Record<string, NetworkName> = {\n \"eip155:43114\": \"avalanche\",\n \"eip155:43113\": \"avalanche-fuji\",\n \"eip155:8453\": \"base\",\n \"eip155:84532\": \"base-sepolia\",\n \"eip155:42220\": \"celo\",\n \"eip155:11142220\": \"celo-sepolia\",\n};\n\n// Token info cache to avoid repeated RPC calls\nconst tokenInfoCache = new Map<string, TokenInfo>();\n\n/**\n * Convert CAIP-2 network format to legacy format\n * e.g., \"eip155:43114\" → \"avalanche\"\n */\nexport function toLegacyNetwork(network: string): NetworkName {\n if (!network.includes(\":\")) {\n return network as NetworkName;\n }\n return CAIP2_TO_LEGACY[network] || \"avalanche\";\n}\n\n/**\n * Get chain ID for a network\n */\nexport function getChainId(network: string): number {\n return CHAIN_IDS[network] || CHAIN_IDS.avalanche;\n}\n\n/**\n * Get RPC URL for a network\n */\nexport function getRpcUrl(network: string, customUrl?: string): string {\n if (customUrl) return customUrl;\n const legacyNetwork = toLegacyNetwork(network);\n return DEFAULT_RPC_URLS[legacyNetwork] || DEFAULT_RPC_URLS.avalanche;\n}\n\n/**\n * Get public client for a network\n */\nexport function getPublicClient(\n network: string,\n config?: NetworkConfig\n): PublicClient {\n const legacyNetwork = toLegacyNetwork(network);\n const rpcUrl = getRpcUrl(network, config?.rpcUrl);\n const chain = CHAINS[legacyNetwork] || {\n id: getChainId(legacyNetwork),\n name: legacyNetwork,\n network: legacyNetwork,\n nativeCurrency: { name: \"ETH\", symbol: \"ETH\", decimals: 18 },\n rpcUrls: { default: { http: [rpcUrl] } },\n };\n\n return createPublicClient({\n chain,\n transport: http(rpcUrl),\n }) as PublicClient;\n}\n\n/**\n * Detect token information from contract address\n * Results are cached to avoid repeated RPC calls\n *\n * @param tokenAddress - The ERC20 token contract address\n * @param network - Network name or CAIP-2 identifier\n * @param config - Optional network configuration\n * @returns Token information or null if detection fails\n *\n * @example\n * ```typescript\n * const tokenInfo = await detectTokenInfo(\n * \"0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E\",\n * \"avalanche\"\n * );\n * // => { name: \"USD Coin\", symbol: \"USDC\", decimals: 6, ... }\n * ```\n */\nexport async function detectTokenInfo(\n tokenAddress: Address,\n network: string,\n config?: NetworkConfig\n): Promise<TokenInfo | null> {\n const legacyNetwork = toLegacyNetwork(network);\n const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;\n\n // Check cache first\n if (tokenInfoCache.has(cacheKey)) {\n return tokenInfoCache.get(cacheKey)!;\n }\n\n try {\n const client = getPublicClient(legacyNetwork, config);\n const chainId = getChainId(legacyNetwork);\n\n // Read token info in parallel\n const [name, symbol, decimals] = await Promise.all([\n client.readContract({\n address: tokenAddress,\n abi: ERC20_ABI,\n functionName: \"name\",\n }) as Promise<string>,\n client.readContract({\n address: tokenAddress,\n abi: ERC20_ABI,\n functionName: \"symbol\",\n }) as Promise<string>,\n client.readContract({\n address: tokenAddress,\n abi: ERC20_ABI,\n functionName: \"decimals\",\n }) as Promise<number>,\n ]);\n\n const tokenInfo: TokenInfo = {\n address: tokenAddress,\n name: name || \"Unknown Token\",\n symbol: symbol || \"UNKNOWN\",\n decimals: decimals || 18,\n chainId,\n };\n\n // Cache the result\n tokenInfoCache.set(cacheKey, tokenInfo);\n\n return tokenInfo;\n } catch (error) {\n console.error(\"Failed to detect token info:\", error);\n return null;\n }\n}\n\n/**\n * Clear the token info cache\n */\nexport function clearTokenCache(): void {\n tokenInfoCache.clear();\n}\n\n/**\n * Get cached token info without RPC call\n */\nexport function getCachedTokenInfo(\n tokenAddress: Address,\n network: string\n): TokenInfo | undefined {\n const legacyNetwork = toLegacyNetwork(network);\n const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;\n return tokenInfoCache.get(cacheKey);\n}\n\n/**\n * Check if a network is valid\n */\nexport function isValidNetwork(network: string): boolean {\n const legacyNetwork = toLegacyNetwork(network);\n return legacyNetwork in CHAINS;\n}\n\n/**\n * Get all supported networks\n */\nexport function getSupportedNetworks(): NetworkName[] {\n return Object.keys(CHAINS) as NetworkName[];\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createPublicClient, http } from "viem";
|
|
3
|
+
import {
|
|
4
|
+
avalanche,
|
|
5
|
+
avalancheFuji,
|
|
6
|
+
base,
|
|
7
|
+
baseSepolia,
|
|
8
|
+
celo,
|
|
9
|
+
celoAlfajores
|
|
10
|
+
} from "viem/chains";
|
|
11
|
+
var ERC20_ABI = [
|
|
12
|
+
{
|
|
13
|
+
constant: true,
|
|
14
|
+
inputs: [],
|
|
15
|
+
name: "name",
|
|
16
|
+
outputs: [{ name: "", type: "string" }],
|
|
17
|
+
type: "function"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
constant: true,
|
|
21
|
+
inputs: [],
|
|
22
|
+
name: "symbol",
|
|
23
|
+
outputs: [{ name: "", type: "string" }],
|
|
24
|
+
type: "function"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
constant: true,
|
|
28
|
+
inputs: [],
|
|
29
|
+
name: "decimals",
|
|
30
|
+
outputs: [{ name: "", type: "uint8" }],
|
|
31
|
+
type: "function"
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
var CHAIN_IDS = {
|
|
35
|
+
avalanche: 43114,
|
|
36
|
+
"avalanche-fuji": 43113,
|
|
37
|
+
base: 8453,
|
|
38
|
+
"base-sepolia": 84532,
|
|
39
|
+
celo: 42220,
|
|
40
|
+
"celo-sepolia": 11142220,
|
|
41
|
+
// CAIP-2 format support
|
|
42
|
+
"eip155:43114": 43114,
|
|
43
|
+
"eip155:43113": 43113,
|
|
44
|
+
"eip155:8453": 8453,
|
|
45
|
+
"eip155:84532": 84532,
|
|
46
|
+
"eip155:42220": 42220,
|
|
47
|
+
"eip155:11142220": 11142220
|
|
48
|
+
};
|
|
49
|
+
var DEFAULT_RPC_URLS = {
|
|
50
|
+
avalanche: "https://api.avax.network/ext/bc/C/rpc",
|
|
51
|
+
"avalanche-fuji": "https://api.avax-test.network/ext/bc/C/rpc",
|
|
52
|
+
base: "https://mainnet.base.org",
|
|
53
|
+
"base-sepolia": "https://sepolia.base.org",
|
|
54
|
+
celo: "https://forno.celo.org",
|
|
55
|
+
"celo-sepolia": "https://forno.celo-sepolia.celo-testnet.org"
|
|
56
|
+
};
|
|
57
|
+
var CHAINS = {
|
|
58
|
+
avalanche,
|
|
59
|
+
"avalanche-fuji": avalancheFuji,
|
|
60
|
+
base,
|
|
61
|
+
"base-sepolia": baseSepolia,
|
|
62
|
+
celo,
|
|
63
|
+
"celo-sepolia": celoAlfajores
|
|
64
|
+
};
|
|
65
|
+
var CAIP2_TO_LEGACY = {
|
|
66
|
+
"eip155:43114": "avalanche",
|
|
67
|
+
"eip155:43113": "avalanche-fuji",
|
|
68
|
+
"eip155:8453": "base",
|
|
69
|
+
"eip155:84532": "base-sepolia",
|
|
70
|
+
"eip155:42220": "celo",
|
|
71
|
+
"eip155:11142220": "celo-sepolia"
|
|
72
|
+
};
|
|
73
|
+
var tokenInfoCache = /* @__PURE__ */ new Map();
|
|
74
|
+
function toLegacyNetwork(network) {
|
|
75
|
+
if (!network.includes(":")) {
|
|
76
|
+
return network;
|
|
77
|
+
}
|
|
78
|
+
return CAIP2_TO_LEGACY[network] || "avalanche";
|
|
79
|
+
}
|
|
80
|
+
function getChainId(network) {
|
|
81
|
+
return CHAIN_IDS[network] || CHAIN_IDS.avalanche;
|
|
82
|
+
}
|
|
83
|
+
function getRpcUrl(network, customUrl) {
|
|
84
|
+
if (customUrl) return customUrl;
|
|
85
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
86
|
+
return DEFAULT_RPC_URLS[legacyNetwork] || DEFAULT_RPC_URLS.avalanche;
|
|
87
|
+
}
|
|
88
|
+
function getPublicClient(network, config) {
|
|
89
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
90
|
+
const rpcUrl = getRpcUrl(network, config?.rpcUrl);
|
|
91
|
+
const chain = CHAINS[legacyNetwork] || {
|
|
92
|
+
id: getChainId(legacyNetwork),
|
|
93
|
+
name: legacyNetwork,
|
|
94
|
+
network: legacyNetwork,
|
|
95
|
+
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
|
|
96
|
+
rpcUrls: { default: { http: [rpcUrl] } }
|
|
97
|
+
};
|
|
98
|
+
return createPublicClient({
|
|
99
|
+
chain,
|
|
100
|
+
transport: http(rpcUrl)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function detectTokenInfo(tokenAddress, network, config) {
|
|
104
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
105
|
+
const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;
|
|
106
|
+
if (tokenInfoCache.has(cacheKey)) {
|
|
107
|
+
return tokenInfoCache.get(cacheKey);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const client = getPublicClient(legacyNetwork, config);
|
|
111
|
+
const chainId = getChainId(legacyNetwork);
|
|
112
|
+
const [name, symbol, decimals] = await Promise.all([
|
|
113
|
+
client.readContract({
|
|
114
|
+
address: tokenAddress,
|
|
115
|
+
abi: ERC20_ABI,
|
|
116
|
+
functionName: "name"
|
|
117
|
+
}),
|
|
118
|
+
client.readContract({
|
|
119
|
+
address: tokenAddress,
|
|
120
|
+
abi: ERC20_ABI,
|
|
121
|
+
functionName: "symbol"
|
|
122
|
+
}),
|
|
123
|
+
client.readContract({
|
|
124
|
+
address: tokenAddress,
|
|
125
|
+
abi: ERC20_ABI,
|
|
126
|
+
functionName: "decimals"
|
|
127
|
+
})
|
|
128
|
+
]);
|
|
129
|
+
const tokenInfo = {
|
|
130
|
+
address: tokenAddress,
|
|
131
|
+
name: name || "Unknown Token",
|
|
132
|
+
symbol: symbol || "UNKNOWN",
|
|
133
|
+
decimals: decimals || 18,
|
|
134
|
+
chainId
|
|
135
|
+
};
|
|
136
|
+
tokenInfoCache.set(cacheKey, tokenInfo);
|
|
137
|
+
return tokenInfo;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("Failed to detect token info:", error);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function clearTokenCache() {
|
|
144
|
+
tokenInfoCache.clear();
|
|
145
|
+
}
|
|
146
|
+
function getCachedTokenInfo(tokenAddress, network) {
|
|
147
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
148
|
+
const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;
|
|
149
|
+
return tokenInfoCache.get(cacheKey);
|
|
150
|
+
}
|
|
151
|
+
function isValidNetwork(network) {
|
|
152
|
+
const legacyNetwork = toLegacyNetwork(network);
|
|
153
|
+
return legacyNetwork in CHAINS;
|
|
154
|
+
}
|
|
155
|
+
function getSupportedNetworks() {
|
|
156
|
+
return Object.keys(CHAINS);
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
clearTokenCache,
|
|
160
|
+
detectTokenInfo,
|
|
161
|
+
getCachedTokenInfo,
|
|
162
|
+
getChainId,
|
|
163
|
+
getPublicClient,
|
|
164
|
+
getRpcUrl,
|
|
165
|
+
getSupportedNetworks,
|
|
166
|
+
isValidNetwork,
|
|
167
|
+
toLegacyNetwork
|
|
168
|
+
};
|
|
169
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @perkos/token-detection\n * ERC20 token detection utilities for reading on-chain token metadata\n */\n\nimport { createPublicClient, http, type Address, type PublicClient } from \"viem\";\nimport {\n avalanche,\n avalancheFuji,\n base,\n baseSepolia,\n celo,\n celoAlfajores,\n} from \"viem/chains\";\n\n/**\n * Token information returned from detection\n */\nexport interface TokenInfo {\n address: Address;\n name: string;\n symbol: string;\n decimals: number;\n chainId: number;\n}\n\n/**\n * Supported network names\n */\nexport type NetworkName =\n | \"avalanche\"\n | \"avalanche-fuji\"\n | \"base\"\n | \"base-sepolia\"\n | \"celo\"\n | \"celo-sepolia\";\n\n/**\n * Network configuration options\n */\nexport interface NetworkConfig {\n rpcUrl?: string;\n}\n\n/**\n * Standard ERC20 ABI (minimal - just what we need)\n */\nconst ERC20_ABI = [\n {\n constant: true,\n inputs: [],\n name: \"name\",\n outputs: [{ name: \"\", type: \"string\" }],\n type: \"function\",\n },\n {\n constant: true,\n inputs: [],\n name: \"symbol\",\n outputs: [{ name: \"\", type: \"string\" }],\n type: \"function\",\n },\n {\n constant: true,\n inputs: [],\n name: \"decimals\",\n outputs: [{ name: \"\", type: \"uint8\" }],\n type: \"function\",\n },\n] as const;\n\n/**\n * Chain ID mappings\n */\nconst CHAIN_IDS: Record<string, number> = {\n avalanche: 43114,\n \"avalanche-fuji\": 43113,\n base: 8453,\n \"base-sepolia\": 84532,\n celo: 42220,\n \"celo-sepolia\": 11142220,\n // CAIP-2 format support\n \"eip155:43114\": 43114,\n \"eip155:43113\": 43113,\n \"eip155:8453\": 8453,\n \"eip155:84532\": 84532,\n \"eip155:42220\": 42220,\n \"eip155:11142220\": 11142220,\n};\n\n/**\n * Default RPC URLs\n */\nconst DEFAULT_RPC_URLS: Record<NetworkName, string> = {\n avalanche: \"https://api.avax.network/ext/bc/C/rpc\",\n \"avalanche-fuji\": \"https://api.avax-test.network/ext/bc/C/rpc\",\n base: \"https://mainnet.base.org\",\n \"base-sepolia\": \"https://sepolia.base.org\",\n celo: \"https://forno.celo.org\",\n \"celo-sepolia\": \"https://forno.celo-sepolia.celo-testnet.org\",\n};\n\n/**\n * Chain definitions\n */\nconst CHAINS: Record<NetworkName, any> = {\n avalanche,\n \"avalanche-fuji\": avalancheFuji,\n base,\n \"base-sepolia\": baseSepolia,\n celo,\n \"celo-sepolia\": celoAlfajores,\n};\n\n/**\n * CAIP-2 to legacy network mapping\n */\nconst CAIP2_TO_LEGACY: Record<string, NetworkName> = {\n \"eip155:43114\": \"avalanche\",\n \"eip155:43113\": \"avalanche-fuji\",\n \"eip155:8453\": \"base\",\n \"eip155:84532\": \"base-sepolia\",\n \"eip155:42220\": \"celo\",\n \"eip155:11142220\": \"celo-sepolia\",\n};\n\n// Token info cache to avoid repeated RPC calls\nconst tokenInfoCache = new Map<string, TokenInfo>();\n\n/**\n * Convert CAIP-2 network format to legacy format\n * e.g., \"eip155:43114\" → \"avalanche\"\n */\nexport function toLegacyNetwork(network: string): NetworkName {\n if (!network.includes(\":\")) {\n return network as NetworkName;\n }\n return CAIP2_TO_LEGACY[network] || \"avalanche\";\n}\n\n/**\n * Get chain ID for a network\n */\nexport function getChainId(network: string): number {\n return CHAIN_IDS[network] || CHAIN_IDS.avalanche;\n}\n\n/**\n * Get RPC URL for a network\n */\nexport function getRpcUrl(network: string, customUrl?: string): string {\n if (customUrl) return customUrl;\n const legacyNetwork = toLegacyNetwork(network);\n return DEFAULT_RPC_URLS[legacyNetwork] || DEFAULT_RPC_URLS.avalanche;\n}\n\n/**\n * Get public client for a network\n */\nexport function getPublicClient(\n network: string,\n config?: NetworkConfig\n): PublicClient {\n const legacyNetwork = toLegacyNetwork(network);\n const rpcUrl = getRpcUrl(network, config?.rpcUrl);\n const chain = CHAINS[legacyNetwork] || {\n id: getChainId(legacyNetwork),\n name: legacyNetwork,\n network: legacyNetwork,\n nativeCurrency: { name: \"ETH\", symbol: \"ETH\", decimals: 18 },\n rpcUrls: { default: { http: [rpcUrl] } },\n };\n\n return createPublicClient({\n chain,\n transport: http(rpcUrl),\n }) as PublicClient;\n}\n\n/**\n * Detect token information from contract address\n * Results are cached to avoid repeated RPC calls\n *\n * @param tokenAddress - The ERC20 token contract address\n * @param network - Network name or CAIP-2 identifier\n * @param config - Optional network configuration\n * @returns Token information or null if detection fails\n *\n * @example\n * ```typescript\n * const tokenInfo = await detectTokenInfo(\n * \"0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E\",\n * \"avalanche\"\n * );\n * // => { name: \"USD Coin\", symbol: \"USDC\", decimals: 6, ... }\n * ```\n */\nexport async function detectTokenInfo(\n tokenAddress: Address,\n network: string,\n config?: NetworkConfig\n): Promise<TokenInfo | null> {\n const legacyNetwork = toLegacyNetwork(network);\n const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;\n\n // Check cache first\n if (tokenInfoCache.has(cacheKey)) {\n return tokenInfoCache.get(cacheKey)!;\n }\n\n try {\n const client = getPublicClient(legacyNetwork, config);\n const chainId = getChainId(legacyNetwork);\n\n // Read token info in parallel\n const [name, symbol, decimals] = await Promise.all([\n client.readContract({\n address: tokenAddress,\n abi: ERC20_ABI,\n functionName: \"name\",\n }) as Promise<string>,\n client.readContract({\n address: tokenAddress,\n abi: ERC20_ABI,\n functionName: \"symbol\",\n }) as Promise<string>,\n client.readContract({\n address: tokenAddress,\n abi: ERC20_ABI,\n functionName: \"decimals\",\n }) as Promise<number>,\n ]);\n\n const tokenInfo: TokenInfo = {\n address: tokenAddress,\n name: name || \"Unknown Token\",\n symbol: symbol || \"UNKNOWN\",\n decimals: decimals || 18,\n chainId,\n };\n\n // Cache the result\n tokenInfoCache.set(cacheKey, tokenInfo);\n\n return tokenInfo;\n } catch (error) {\n console.error(\"Failed to detect token info:\", error);\n return null;\n }\n}\n\n/**\n * Clear the token info cache\n */\nexport function clearTokenCache(): void {\n tokenInfoCache.clear();\n}\n\n/**\n * Get cached token info without RPC call\n */\nexport function getCachedTokenInfo(\n tokenAddress: Address,\n network: string\n): TokenInfo | undefined {\n const legacyNetwork = toLegacyNetwork(network);\n const cacheKey = `${tokenAddress.toLowerCase()}-${legacyNetwork}`;\n return tokenInfoCache.get(cacheKey);\n}\n\n/**\n * Check if a network is valid\n */\nexport function isValidNetwork(network: string): boolean {\n const legacyNetwork = toLegacyNetwork(network);\n return legacyNetwork in CHAINS;\n}\n\n/**\n * Get all supported networks\n */\nexport function getSupportedNetworks(): NetworkName[] {\n return Object.keys(CHAINS) as NetworkName[];\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@perkos/util-tokens",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ERC20 token detection utilities for reading on-chain token metadata",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"lint": "tsc --noEmit",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"clean": "rm -rf dist",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"erc20",
|
|
28
|
+
"token",
|
|
29
|
+
"detection",
|
|
30
|
+
"blockchain",
|
|
31
|
+
"eip-712",
|
|
32
|
+
"perkos"
|
|
33
|
+
],
|
|
34
|
+
"author": "PerkOS",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/PerkOS-xyz/pkg-util-tokens.git"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"viem": "^2.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.10.0",
|
|
45
|
+
"tsup": "^8.0.1",
|
|
46
|
+
"typescript": "^5.3.3",
|
|
47
|
+
"vitest": "^1.2.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|