@gibs/common 1.6.27
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/package.json +27 -0
- package/src/cache.ts +48 -0
- package/src/client.ts +59 -0
- package/src/erc20.ts +41 -0
- package/src/index.ts +5 -0
- package/src/multicall.ts +67 -0
- package/src/serialize.ts +28 -0
- package/src/types.ts +11 -0
- package/tsconfig.json +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gibs/common",
|
|
3
|
+
"version": "1.6.27",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"access": "public",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./erc20": "./src/erc20.ts",
|
|
10
|
+
"./cache": "./src/cache.ts",
|
|
11
|
+
"./serialize": "./src/serialize.ts",
|
|
12
|
+
"./types": "./src/types.ts",
|
|
13
|
+
"./multicall": "./src/multicall.ts",
|
|
14
|
+
"./client": "./src/client.ts"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"description": "",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"lodash": "^4.17.21",
|
|
25
|
+
"viem": "^2.31.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsonAnyStringify } from "./serialize.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Memoize a function result and return a single value
|
|
5
|
+
* @param fn the function to memoize
|
|
6
|
+
* @param max the maximum number of values to cache
|
|
7
|
+
* @returns the memoized function
|
|
8
|
+
*/
|
|
9
|
+
export const maxMemoize = <A extends unknown[], B>(fn: (...a: A) => B, max = 1024) => {
|
|
10
|
+
const cache = new Map<string, B>()
|
|
11
|
+
return (...a: A) => {
|
|
12
|
+
const key = JSON.stringify(a, jsonAnyStringify)
|
|
13
|
+
const existing = cache.get(key)
|
|
14
|
+
if (cache.has(key)) {
|
|
15
|
+
return existing
|
|
16
|
+
}
|
|
17
|
+
for (const k of cache.keys()) {
|
|
18
|
+
if (cache.size < max) break
|
|
19
|
+
cache.delete(k)
|
|
20
|
+
}
|
|
21
|
+
const result = fn(...a)
|
|
22
|
+
cache.set(key, result)
|
|
23
|
+
return result
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hour1 = 1000 * 60 * 60
|
|
28
|
+
/**
|
|
29
|
+
* Memoize a function result and return a single value
|
|
30
|
+
* @param fn the function to memoize
|
|
31
|
+
* @param ttl the time to live for the cache
|
|
32
|
+
* @returns the memoized function
|
|
33
|
+
*/
|
|
34
|
+
export const ttlMemoizeSingle = <A extends unknown[], B>(fn: (...a: A) => B, ttl = hour1) => {
|
|
35
|
+
let cache: {
|
|
36
|
+
timestamp: number
|
|
37
|
+
result: B
|
|
38
|
+
} | null = null
|
|
39
|
+
return (...a: A) => {
|
|
40
|
+
const now = Date.now()
|
|
41
|
+
if (cache && cache.timestamp + ttl > now) {
|
|
42
|
+
return cache.result
|
|
43
|
+
}
|
|
44
|
+
const result = fn(...a)
|
|
45
|
+
cache = { timestamp: now, result }
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Chain, type PublicClient, createPublicClient, fallback, http, webSocket } from "viem"
|
|
2
|
+
|
|
3
|
+
export const defaultBatchConfig = {
|
|
4
|
+
batch: {
|
|
5
|
+
wait: 10,
|
|
6
|
+
batchSize: 32,
|
|
7
|
+
},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const chainKey = (chainId: number, urls: string[]) => {
|
|
11
|
+
return `${chainId},${urls.join(',')}`.toLowerCase()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const clientCache = new Map<number, {
|
|
15
|
+
key: string
|
|
16
|
+
client: PublicClient
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
export const clientFromChain = ({
|
|
20
|
+
chain,
|
|
21
|
+
urls,
|
|
22
|
+
}: {
|
|
23
|
+
chain: Chain
|
|
24
|
+
urls: string[]
|
|
25
|
+
}): PublicClient => {
|
|
26
|
+
const key = chainKey(chain.id, urls)
|
|
27
|
+
const existing = clientCache.get(chain.id)
|
|
28
|
+
if (existing && existing.key === key) {
|
|
29
|
+
return existing.client
|
|
30
|
+
}
|
|
31
|
+
const transport = !urls?.length
|
|
32
|
+
? http()
|
|
33
|
+
: fallback(
|
|
34
|
+
urls.map((rpc) =>
|
|
35
|
+
rpc.startsWith('http')
|
|
36
|
+
? http(rpc, {
|
|
37
|
+
...defaultBatchConfig,
|
|
38
|
+
})
|
|
39
|
+
: webSocket(rpc, {
|
|
40
|
+
keepAlive: true,
|
|
41
|
+
reconnect: true,
|
|
42
|
+
retryDelay: 250,
|
|
43
|
+
retryCount: 10,
|
|
44
|
+
timeout: 4_000,
|
|
45
|
+
...defaultBatchConfig,
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
{ rank: true },
|
|
49
|
+
)
|
|
50
|
+
const client = createPublicClient({
|
|
51
|
+
chain,
|
|
52
|
+
transport,
|
|
53
|
+
})
|
|
54
|
+
clientCache.set(chain.id, {
|
|
55
|
+
key,
|
|
56
|
+
client,
|
|
57
|
+
})
|
|
58
|
+
return client
|
|
59
|
+
}
|
package/src/erc20.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Chain,
|
|
3
|
+
type PublicClient,
|
|
4
|
+
type Hex,
|
|
5
|
+
type Abi,
|
|
6
|
+
erc20Abi,
|
|
7
|
+
erc20Abi_bytes32,
|
|
8
|
+
getAddress,
|
|
9
|
+
} from 'viem'
|
|
10
|
+
import _ from 'lodash'
|
|
11
|
+
import type { Erc20Metadata } from './types.js'
|
|
12
|
+
import { multicallRead } from './multicall.js'
|
|
13
|
+
|
|
14
|
+
export const erc20MetadataCalls = [
|
|
15
|
+
{ functionName: 'name' },
|
|
16
|
+
{ functionName: 'symbol' },
|
|
17
|
+
{ functionName: 'decimals' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export const multicallErc20 = _.memoize(
|
|
21
|
+
async ({ client, target, chain }: { client: PublicClient; target: Hex; chain: Chain }) => {
|
|
22
|
+
const options = {
|
|
23
|
+
chain: chain,
|
|
24
|
+
client: client,
|
|
25
|
+
abi: erc20Abi,
|
|
26
|
+
target,
|
|
27
|
+
calls: erc20MetadataCalls,
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return await multicallRead<Erc20Metadata>(options)
|
|
31
|
+
} catch {
|
|
32
|
+
return await multicallRead<Erc20Metadata>({
|
|
33
|
+
...options,
|
|
34
|
+
abi: erc20Abi_bytes32,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
({ target, chain }) => {
|
|
39
|
+
return !target ? chain.id : getAddress(target, chain.id)
|
|
40
|
+
},
|
|
41
|
+
)
|
package/src/index.ts
ADDED
package/src/multicall.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Chain,
|
|
3
|
+
type PublicClient,
|
|
4
|
+
type Hex,
|
|
5
|
+
type Abi,
|
|
6
|
+
getContract,
|
|
7
|
+
encodeFunctionData,
|
|
8
|
+
decodeFunctionResult,
|
|
9
|
+
multicall3Abi,
|
|
10
|
+
} from 'viem'
|
|
11
|
+
import _ from 'lodash'
|
|
12
|
+
import type { Call } from './types.js'
|
|
13
|
+
|
|
14
|
+
export const multicallRead = async <T>({
|
|
15
|
+
chain,
|
|
16
|
+
client,
|
|
17
|
+
abi,
|
|
18
|
+
calls,
|
|
19
|
+
target,
|
|
20
|
+
}: {
|
|
21
|
+
chain: Chain
|
|
22
|
+
client: PublicClient
|
|
23
|
+
abi: Abi
|
|
24
|
+
calls: Call[]
|
|
25
|
+
target?: Hex
|
|
26
|
+
}) => {
|
|
27
|
+
const multicall = getContract({
|
|
28
|
+
abi: multicall3Abi,
|
|
29
|
+
address: chain.contracts!.multicall3!.address!,
|
|
30
|
+
// this seems like a bug in viem, it should be able to infer the type of the client
|
|
31
|
+
client: client as any,
|
|
32
|
+
}) as any
|
|
33
|
+
const arg = calls.map((call) => ({
|
|
34
|
+
callData: encodeFunctionData({
|
|
35
|
+
abi: call.abi || abi,
|
|
36
|
+
functionName: call.functionName,
|
|
37
|
+
// this seems like a bug in viem, it should be able to infer the type of the args
|
|
38
|
+
args: (call.args || []) as any,
|
|
39
|
+
}),
|
|
40
|
+
allowFailure: call.allowFailure || false,
|
|
41
|
+
target: (call.target || target) as Hex,
|
|
42
|
+
}))
|
|
43
|
+
let reads: null | Awaited<ReturnType<typeof multicall.read.aggregate3>> = null
|
|
44
|
+
try {
|
|
45
|
+
reads = await multicall.read.aggregate3([arg])
|
|
46
|
+
if (!reads) throw reads
|
|
47
|
+
const r = reads
|
|
48
|
+
return calls.map((call, i) =>
|
|
49
|
+
call.allowFailure
|
|
50
|
+
? r[i].success
|
|
51
|
+
? decodeFunctionResult({
|
|
52
|
+
abi: call.abi || abi,
|
|
53
|
+
functionName: call.functionName,
|
|
54
|
+
data: r[i].returnData,
|
|
55
|
+
})
|
|
56
|
+
: r[i].returnData
|
|
57
|
+
: decodeFunctionResult({
|
|
58
|
+
abi: call.abi || abi,
|
|
59
|
+
functionName: call.functionName,
|
|
60
|
+
data: r[i].returnData,
|
|
61
|
+
}),
|
|
62
|
+
) as T
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(client.chain?.id, target, calls, reads)
|
|
65
|
+
throw err
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/serialize.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
const BIGINT_TYPE = 'bigint' as const
|
|
3
|
+
type SerializedBigInt = {
|
|
4
|
+
__type__: typeof BIGINT_TYPE
|
|
5
|
+
value: string
|
|
6
|
+
}
|
|
7
|
+
export const jsonAnyStringify = (key: string, value: unknown) => {
|
|
8
|
+
if (typeof value === BIGINT_TYPE) {
|
|
9
|
+
return {
|
|
10
|
+
__type__: BIGINT_TYPE,
|
|
11
|
+
value: (value as bigint).toString(),
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const isSerializedBigInt = (value: unknown): value is SerializedBigInt => {
|
|
18
|
+
return (
|
|
19
|
+
!!value && typeof value === 'object' && (value as SerializedBigInt).__type__ === BIGINT_TYPE
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const jsonAnyParse = (_key: string, value: unknown) => {
|
|
24
|
+
if (isSerializedBigInt(value)) {
|
|
25
|
+
return BigInt(value.value)
|
|
26
|
+
}
|
|
27
|
+
return value
|
|
28
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Abi, Hex } from "viem"
|
|
2
|
+
|
|
3
|
+
export type Call = {
|
|
4
|
+
allowFailure?: boolean
|
|
5
|
+
functionName: string
|
|
6
|
+
target?: Hex
|
|
7
|
+
abi?: Abi
|
|
8
|
+
args?: (bigint | Hex | Hex[] | bigint[] | string | string[])[]
|
|
9
|
+
}
|
|
10
|
+
export type Erc20Metadata = [string, string, number]
|
|
11
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"composite": true,
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"moduleResolution": "nodenext"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "src/**/*.js"]
|
|
16
|
+
}
|