nebula-ai-plugin-onchain 0.1.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 +26 -0
- package/abis/erc20.json +78 -0
- package/abis/factory.json +236 -0
- package/abis/gimo-pool.json +53 -0
- package/abis/multicall3.json +76 -0
- package/abis/quoter.json +193 -0
- package/abis/stog.json +58 -0
- package/abis/swap-router.json +565 -0
- package/abis/weth9.json +65 -0
- package/data/tokens.json +94 -0
- package/package.json +52 -0
- package/src/aave.ts +193 -0
- package/src/abis.ts +84 -0
- package/src/allowance.ts +77 -0
- package/src/analysis.ts +195 -0
- package/src/approval.ts +99 -0
- package/src/balances.ts +262 -0
- package/src/bybit.ts +118 -0
- package/src/constants.ts +102 -0
- package/src/defillama.ts +127 -0
- package/src/guidance.ts +23 -0
- package/src/index.ts +139 -0
- package/src/mint-block.ts +53 -0
- package/src/moe.ts +111 -0
- package/src/nansen.ts +85 -0
- package/src/policy.ts +213 -0
- package/src/quoter.ts +87 -0
- package/src/raw-logs.ts +49 -0
- package/src/risk.ts +79 -0
- package/src/simulate.ts +121 -0
- package/src/swap.ts +108 -0
- package/src/tokens.ts +232 -0
- package/src/tools/aave.ts +425 -0
- package/src/tools/account-balance.ts +67 -0
- package/src/tools/account.ts +111 -0
- package/src/tools/analysis.ts +371 -0
- package/src/tools/balance.ts +119 -0
- package/src/tools/blockchain.ts +95 -0
- package/src/tools/cex.ts +54 -0
- package/src/tools/defillama.ts +83 -0
- package/src/tools/generic.ts +213 -0
- package/src/tools/identity.ts +139 -0
- package/src/tools/moe.ts +245 -0
- package/src/tools/nansen.ts +71 -0
- package/src/tools/policy-show.ts +74 -0
- package/src/tools/risk.ts +134 -0
- package/src/tools/simulate-tx.ts +98 -0
- package/src/tools/swap-best.ts +218 -0
- package/src/tools/swap.ts +253 -0
- package/src/tools/tokens-info.ts +49 -0
- package/src/tools/transfer.ts +164 -0
- package/src/tools/wrap.ts +183 -0
- package/src/types.ts +53 -0
- package/src/wait-receipt.ts +34 -0
package/data/tokens.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Jaine Token List",
|
|
3
|
+
"timestamp": "2025-01-03T20:47:02.923Z",
|
|
4
|
+
"version": {
|
|
5
|
+
"major": 1,
|
|
6
|
+
"minor": 0,
|
|
7
|
+
"patch": 0
|
|
8
|
+
},
|
|
9
|
+
"tags": {},
|
|
10
|
+
"logoURI": "ipfs://QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir",
|
|
11
|
+
"keywords": ["jaine", "active"],
|
|
12
|
+
"tokens": [
|
|
13
|
+
{
|
|
14
|
+
"name": "stOG",
|
|
15
|
+
"address": "0x7bbc63d01ca42491c3e084c941c3e86e55951404",
|
|
16
|
+
"symbol": "st0G",
|
|
17
|
+
"decimals": 18,
|
|
18
|
+
"chainId": 16661,
|
|
19
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/0x7bbc63d01ca42491c3e084c941c3e86e55951404/stOG.svg"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "USDCe",
|
|
23
|
+
"address": "0x1f3aa82227281ca364bfb3d253b0f1af1da6473e",
|
|
24
|
+
"symbol": "USDCe",
|
|
25
|
+
"decimals": 6,
|
|
26
|
+
"chainId": 16661,
|
|
27
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/USDCe.svg"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "wstETH",
|
|
31
|
+
"address": "0x161a128567bf0c005b58211757f7e46eed983f02",
|
|
32
|
+
"symbol": "wstETH",
|
|
33
|
+
"decimals": 18,
|
|
34
|
+
"chainId": 16661,
|
|
35
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/wstETH.svg"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "PAI",
|
|
39
|
+
"address": "0x59ef6f3943bbdfe2fb19565037ac85071223e94c",
|
|
40
|
+
"symbol": "PAI",
|
|
41
|
+
"decimals": 9,
|
|
42
|
+
"chainId": 16661,
|
|
43
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/PAI.svg"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "wETH",
|
|
47
|
+
"address": "0x564770837ef8bbf077cfe54e5f6106538c815b22",
|
|
48
|
+
"symbol": "wETH",
|
|
49
|
+
"decimals": 18,
|
|
50
|
+
"chainId": 16661,
|
|
51
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/wETH.svg"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "oUSDT",
|
|
55
|
+
"address": "0x1217bfe6c773eec6cc4a38b5dc45b92292b6e189",
|
|
56
|
+
"symbol": "oUSDT",
|
|
57
|
+
"decimals": 6,
|
|
58
|
+
"chainId": 16661,
|
|
59
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/oUSDT.svg"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "LINK",
|
|
63
|
+
"address": "0x76159c2b43ff6f630193e37ec68452169914c1bb",
|
|
64
|
+
"symbol": "LINK",
|
|
65
|
+
"decimals": 18,
|
|
66
|
+
"chainId": 16661,
|
|
67
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/Link.svg"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "WBTC",
|
|
71
|
+
"address": "0x0555e30da8f98308edb960aa94c0db47230d2b9c",
|
|
72
|
+
"symbol": "wBTC",
|
|
73
|
+
"decimals": 8,
|
|
74
|
+
"chainId": 16661,
|
|
75
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/wBTC.png"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "cbBTC",
|
|
79
|
+
"address": "0xa5613ac7f1e83a68719b1398c8f6aaa25581db82",
|
|
80
|
+
"symbol": "CoinbaseBTC",
|
|
81
|
+
"decimals": 8,
|
|
82
|
+
"chainId": 16661,
|
|
83
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/cbBTC.svg"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "HaiO",
|
|
87
|
+
"address": "0xbCB868d3d0A823ced82B77DD72E4f7A3C720A40E",
|
|
88
|
+
"symbol": "HAIO",
|
|
89
|
+
"decimals": 9,
|
|
90
|
+
"chainId": 16661,
|
|
91
|
+
"logoURI": "https://raw.githubusercontent.com/Mantle-X/jaine-token-lists/main/assets/mainnet/HaiO_SVG.svg"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nebula-ai-plugin-onchain",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The Mantle on-chain tools for nebula: transfers, Agni + Merchant Moe swaps, Aave V3 lending, DeFiLlama discovery, and ERC-8004 identity — every write policy + simulation gated",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/rstfulzz/nebula",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rstfulzz/nebula.git",
|
|
11
|
+
"directory": "packages/plugin-onchain"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rstfulzz/nebula/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nebula",
|
|
18
|
+
"ai",
|
|
19
|
+
"agent",
|
|
20
|
+
"mantle",
|
|
21
|
+
"defi",
|
|
22
|
+
"swap",
|
|
23
|
+
"aave",
|
|
24
|
+
"agni",
|
|
25
|
+
"erc-8004",
|
|
26
|
+
"plugin"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"bun": ">=1.1"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"!src/**/*.test.ts",
|
|
37
|
+
"abis",
|
|
38
|
+
"data",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"main": "./src/index.ts",
|
|
42
|
+
"types": "./src/index.ts",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -b",
|
|
45
|
+
"test": "bun test"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"nebula-ai-core": "0.1.0",
|
|
49
|
+
"viem": "^2.21.55",
|
|
50
|
+
"zod": "^3.23.8"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/aave.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aave V3 adapter for Mantle. Pool verified live (getReservesList returns the
|
|
3
|
+
* supported markets). We expose supply / withdraw (writes, guarded by the
|
|
4
|
+
* policy + simulate pipeline) and a read-only position view (health factor).
|
|
5
|
+
*/
|
|
6
|
+
import { type Address, type PublicClient, erc20Abi, parseAbi } from 'viem'
|
|
7
|
+
import { MULTICALL3 } from './constants'
|
|
8
|
+
|
|
9
|
+
export const AAVE_V3_POOL_ABI = [
|
|
10
|
+
{
|
|
11
|
+
name: 'supply',
|
|
12
|
+
type: 'function',
|
|
13
|
+
stateMutability: 'nonpayable',
|
|
14
|
+
inputs: [
|
|
15
|
+
{ name: 'asset', type: 'address' },
|
|
16
|
+
{ name: 'amount', type: 'uint256' },
|
|
17
|
+
{ name: 'onBehalfOf', type: 'address' },
|
|
18
|
+
{ name: 'referralCode', type: 'uint16' },
|
|
19
|
+
],
|
|
20
|
+
outputs: [],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'withdraw',
|
|
24
|
+
type: 'function',
|
|
25
|
+
stateMutability: 'nonpayable',
|
|
26
|
+
inputs: [
|
|
27
|
+
{ name: 'asset', type: 'address' },
|
|
28
|
+
{ name: 'amount', type: 'uint256' },
|
|
29
|
+
{ name: 'to', type: 'address' },
|
|
30
|
+
],
|
|
31
|
+
outputs: [{ type: 'uint256' }],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'borrow',
|
|
35
|
+
type: 'function',
|
|
36
|
+
stateMutability: 'nonpayable',
|
|
37
|
+
inputs: [
|
|
38
|
+
{ name: 'asset', type: 'address' },
|
|
39
|
+
{ name: 'amount', type: 'uint256' },
|
|
40
|
+
{ name: 'interestRateMode', type: 'uint256' },
|
|
41
|
+
{ name: 'referralCode', type: 'uint16' },
|
|
42
|
+
{ name: 'onBehalfOf', type: 'address' },
|
|
43
|
+
],
|
|
44
|
+
outputs: [],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'repay',
|
|
48
|
+
type: 'function',
|
|
49
|
+
stateMutability: 'nonpayable',
|
|
50
|
+
inputs: [
|
|
51
|
+
{ name: 'asset', type: 'address' },
|
|
52
|
+
{ name: 'amount', type: 'uint256' },
|
|
53
|
+
{ name: 'interestRateMode', type: 'uint256' },
|
|
54
|
+
{ name: 'onBehalfOf', type: 'address' },
|
|
55
|
+
],
|
|
56
|
+
outputs: [{ type: 'uint256' }],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'getUserAccountData',
|
|
60
|
+
type: 'function',
|
|
61
|
+
stateMutability: 'view',
|
|
62
|
+
inputs: [{ name: 'user', type: 'address' }],
|
|
63
|
+
outputs: [
|
|
64
|
+
{ name: 'totalCollateralBase', type: 'uint256' },
|
|
65
|
+
{ name: 'totalDebtBase', type: 'uint256' },
|
|
66
|
+
{ name: 'availableBorrowsBase', type: 'uint256' },
|
|
67
|
+
{ name: 'currentLiquidationThreshold', type: 'uint256' },
|
|
68
|
+
{ name: 'ltv', type: 'uint256' },
|
|
69
|
+
{ name: 'healthFactor', type: 'uint256' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'getReservesList',
|
|
74
|
+
type: 'function',
|
|
75
|
+
stateMutability: 'view',
|
|
76
|
+
inputs: [],
|
|
77
|
+
outputs: [{ type: 'address[]' }],
|
|
78
|
+
},
|
|
79
|
+
] as const
|
|
80
|
+
|
|
81
|
+
/** Full-balance sentinel for Aave withdraw (and full-debt sentinel for repay). */
|
|
82
|
+
export const AAVE_MAX_WITHDRAW = (1n << 256n) - 1n
|
|
83
|
+
|
|
84
|
+
/** Aave V3 interest rate mode: 2 = variable (stable rate mode is deprecated). */
|
|
85
|
+
export const AAVE_VARIABLE_RATE = 2n
|
|
86
|
+
|
|
87
|
+
/** getReserveData returns the V3 ReserveData struct; we read the two rate fields. */
|
|
88
|
+
const AAVE_RESERVE_DATA_ABI = parseAbi([
|
|
89
|
+
'struct ReserveData { uint256 configuration; uint128 liquidityIndex; uint128 currentLiquidityRate; uint128 variableBorrowIndex; uint128 currentVariableBorrowRate; uint128 currentStableBorrowRate; uint40 lastUpdateTimestamp; uint16 id; address aTokenAddress; address stableDebtTokenAddress; address variableDebtTokenAddress; address interestRateStrategyAddress; uint128 accruedToTreasury; uint128 unbacked; uint128 isolationModeTotalDebt; }',
|
|
90
|
+
'function getReserveData(address asset) view returns (ReserveData)',
|
|
91
|
+
'function getReservesList() view returns (address[])',
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const RAY = 10n ** 27n
|
|
95
|
+
|
|
96
|
+
/** Aave rates are per-second APR scaled to RAY (1e27). APR% = rate / 1e27 * 100. */
|
|
97
|
+
export function rayToAprPct(rateRay: bigint): number {
|
|
98
|
+
return Number((rateRay * 1_000_000n) / RAY) / 10_000
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AaveMarket {
|
|
102
|
+
symbol: string
|
|
103
|
+
address: Address
|
|
104
|
+
supplyAprPct: number
|
|
105
|
+
variableBorrowAprPct: number
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Read every Aave V3 reserve on Mantle with its live supply + variable-borrow APR. */
|
|
109
|
+
export async function readAaveMarkets(client: PublicClient, pool: Address): Promise<AaveMarket[]> {
|
|
110
|
+
const reserves = (await client.readContract({
|
|
111
|
+
address: pool,
|
|
112
|
+
abi: AAVE_RESERVE_DATA_ABI,
|
|
113
|
+
functionName: 'getReservesList',
|
|
114
|
+
})) as readonly Address[]
|
|
115
|
+
|
|
116
|
+
// One Multicall3 batch instead of 2N parallel calls (public RPCs rate-limit
|
|
117
|
+
// a 20-call burst). For each reserve: getReserveData(pool) + symbol(asset).
|
|
118
|
+
const contracts = reserves.flatMap(asset => [
|
|
119
|
+
{
|
|
120
|
+
address: pool,
|
|
121
|
+
abi: AAVE_RESERVE_DATA_ABI,
|
|
122
|
+
functionName: 'getReserveData' as const,
|
|
123
|
+
args: [asset] as const,
|
|
124
|
+
},
|
|
125
|
+
{ address: asset, abi: erc20Abi, functionName: 'symbol' as const },
|
|
126
|
+
])
|
|
127
|
+
const results = await client.multicall({
|
|
128
|
+
contracts,
|
|
129
|
+
allowFailure: true,
|
|
130
|
+
multicallAddress: MULTICALL3,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return reserves.map((asset, i) => {
|
|
134
|
+
const dRes = results[i * 2]
|
|
135
|
+
const symRes = results[i * 2 + 1]
|
|
136
|
+
const d =
|
|
137
|
+
dRes?.status === 'success'
|
|
138
|
+
? (dRes.result as { currentLiquidityRate: bigint; currentVariableBorrowRate: bigint })
|
|
139
|
+
: { currentLiquidityRate: 0n, currentVariableBorrowRate: 0n }
|
|
140
|
+
const symbol = symRes?.status === 'success' ? (symRes.result as string) : '?'
|
|
141
|
+
return {
|
|
142
|
+
symbol,
|
|
143
|
+
address: asset,
|
|
144
|
+
supplyAprPct: rayToAprPct(d.currentLiquidityRate),
|
|
145
|
+
variableBorrowAprPct: rayToAprPct(d.currentVariableBorrowRate),
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface AaveAccount {
|
|
151
|
+
totalCollateralBase: bigint
|
|
152
|
+
totalDebtBase: bigint
|
|
153
|
+
availableBorrowsBase: bigint
|
|
154
|
+
liquidationThresholdBps: bigint
|
|
155
|
+
ltvBps: bigint
|
|
156
|
+
healthFactorRaw: bigint
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Human health factor — '∞ (no debt)' when there is no borrow, else 1e18-scaled. */
|
|
160
|
+
export function formatHealthFactor(raw: bigint): string {
|
|
161
|
+
if (raw >= AAVE_MAX_WITHDRAW) return '∞ (no debt)'
|
|
162
|
+
const whole = raw / 10n ** 18n
|
|
163
|
+
const frac = ((raw % 10n ** 18n) * 100n) / 10n ** 18n
|
|
164
|
+
return `${whole.toString()}.${frac.toString().padStart(2, '0')}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Aave base currency is USD with 8 decimals on this market. */
|
|
168
|
+
export function formatBaseUsd(base: bigint): string {
|
|
169
|
+
const whole = base / 10n ** 8n
|
|
170
|
+
const frac = ((base % 10n ** 8n) * 100n) / 10n ** 8n
|
|
171
|
+
return `$${whole.toString()}.${frac.toString().padStart(2, '0')}`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function readAaveAccount(
|
|
175
|
+
client: PublicClient,
|
|
176
|
+
pool: Address,
|
|
177
|
+
user: Address,
|
|
178
|
+
): Promise<AaveAccount> {
|
|
179
|
+
const d = (await client.readContract({
|
|
180
|
+
address: pool,
|
|
181
|
+
abi: AAVE_V3_POOL_ABI,
|
|
182
|
+
functionName: 'getUserAccountData',
|
|
183
|
+
args: [user],
|
|
184
|
+
})) as readonly bigint[]
|
|
185
|
+
return {
|
|
186
|
+
totalCollateralBase: d[0] ?? 0n,
|
|
187
|
+
totalDebtBase: d[1] ?? 0n,
|
|
188
|
+
availableBorrowsBase: d[2] ?? 0n,
|
|
189
|
+
liquidationThresholdBps: d[3] ?? 0n,
|
|
190
|
+
ltvBps: d[4] ?? 0n,
|
|
191
|
+
healthFactorRaw: d[5] ?? 0n,
|
|
192
|
+
}
|
|
193
|
+
}
|
package/src/abis.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed ABIs for plugin-onchain. Small ABIs use `parseAbi` inline so viem can
|
|
3
|
+
* infer arg/return types. The big vendored JSON ABIs (SwapRouter, Quoter,
|
|
4
|
+
* Factory) load via JSON import + `as Abi` cast — too large to inline,
|
|
5
|
+
* generated from the canonical AGNI testnet artifacts (bytecode-equivalent
|
|
6
|
+
* on mainnet, verified May 1 2026).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type Abi, parseAbi } from 'viem'
|
|
10
|
+
import factoryJson from '../abis/factory.json' with { type: 'json' }
|
|
11
|
+
import quoterJson from '../abis/quoter.json' with { type: 'json' }
|
|
12
|
+
import swapRouterJson from '../abis/swap-router.json' with { type: 'json' }
|
|
13
|
+
|
|
14
|
+
export const ERC20_ABI = parseAbi([
|
|
15
|
+
'function name() view returns (string)',
|
|
16
|
+
'function symbol() view returns (string)',
|
|
17
|
+
'function decimals() view returns (uint8)',
|
|
18
|
+
'function totalSupply() view returns (uint256)',
|
|
19
|
+
'function balanceOf(address account) view returns (uint256)',
|
|
20
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
21
|
+
'function transfer(address to, uint256 amount) returns (bool)',
|
|
22
|
+
'function approve(address spender, uint256 amount) returns (bool)',
|
|
23
|
+
'event Transfer(address indexed from, address indexed to, uint256 value)',
|
|
24
|
+
'event Approval(address indexed owner, address indexed spender, uint256 value)',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export const WETH9_ABI = parseAbi([
|
|
28
|
+
'function deposit() payable',
|
|
29
|
+
'function withdraw(uint256 wad)',
|
|
30
|
+
'function balanceOf(address account) view returns (uint256)',
|
|
31
|
+
'function totalSupply() view returns (uint256)',
|
|
32
|
+
'function transfer(address to, uint256 wad) returns (bool)',
|
|
33
|
+
'function approve(address spender, uint256 wad) returns (bool)',
|
|
34
|
+
'function name() view returns (string)',
|
|
35
|
+
'function symbol() view returns (string)',
|
|
36
|
+
'function decimals() view returns (uint8)',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
// `aggregate3` is `payable` on-chain but we only ever call it for batched
|
|
40
|
+
// reads (no msg.value). Marking it `view` here lets viem's `readContract`
|
|
41
|
+
// type-narrowing keep `aggregate3` callable; the runtime contract still
|
|
42
|
+
// accepts the call without msg.value.
|
|
43
|
+
export const MULTICALL3_ABI = parseAbi([
|
|
44
|
+
'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) view returns ((bool success, bytes returnData)[] returnData)',
|
|
45
|
+
'function getEthBalance(address addr) view returns (uint256 balance)',
|
|
46
|
+
'function getBlockNumber() view returns (uint256 blockNumber)',
|
|
47
|
+
])
|
|
48
|
+
|
|
49
|
+
export const SWAP_ROUTER_ABI = swapRouterJson as Abi
|
|
50
|
+
export const QUOTER_ABI = quoterJson as Abi
|
|
51
|
+
export const FACTORY_ABI = factoryJson as Abi
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Merchant Moe Liquidity Book quoter. `findBestPathFromAmountIn` scans LB pairs
|
|
55
|
+
* for the best route; the last element of `amounts` is the output. `binSteps`
|
|
56
|
+
* + `versions` + `route` feed directly into the router's swap `Path`.
|
|
57
|
+
*/
|
|
58
|
+
export const LB_QUOTER_ABI = parseAbi([
|
|
59
|
+
'struct Quote { address[] route; address[] pairs; uint256[] binSteps; uint8[] versions; uint128[] amounts; uint128[] virtualAmountsWithoutSlippage; uint128[] fees; }',
|
|
60
|
+
'function findBestPathFromAmountIn(address[] route, uint128 amountIn) view returns (Quote)',
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Merchant Moe Liquidity Book router. The `Path` struct carries the per-hop bin
|
|
65
|
+
* steps + pair versions (V1=0, V2=1, V2_1=2, V2_2=3) + token path returned by
|
|
66
|
+
* the quoter. Native legs go through WNATIVE (WMNT) automatically.
|
|
67
|
+
*/
|
|
68
|
+
export const LB_ROUTER_ABI = parseAbi([
|
|
69
|
+
'struct Path { uint256[] pairBinSteps; uint8[] versions; address[] tokenPath; }',
|
|
70
|
+
'function getWNATIVE() view returns (address)',
|
|
71
|
+
'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, Path path, address to, uint256 deadline) returns (uint256 amountOut)',
|
|
72
|
+
'function swapExactNATIVEForTokens(uint256 amountOutMin, Path path, address to, uint256 deadline) payable returns (uint256 amountOut)',
|
|
73
|
+
'function swapExactTokensForNATIVE(uint256 amountIn, uint256 amountOutMinNATIVE, Path path, address to, uint256 deadline) returns (uint256 amountOut)',
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
/** All known function fragments concatenated, for `analysis.decodeCalldata`. */
|
|
77
|
+
export const ALL_KNOWN_ABIS: Abi = [
|
|
78
|
+
...(SWAP_ROUTER_ABI as readonly unknown[]),
|
|
79
|
+
...(QUOTER_ABI as readonly unknown[]),
|
|
80
|
+
...(FACTORY_ABI as readonly unknown[]),
|
|
81
|
+
...(WETH9_ABI as readonly unknown[]),
|
|
82
|
+
...(ERC20_ABI as readonly unknown[]),
|
|
83
|
+
...(MULTICALL3_ABI as readonly unknown[]),
|
|
84
|
+
] as Abi
|
package/src/allowance.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ERC-20 allowance helper. Reads current allowance via Multicall3,
|
|
3
|
+
* sends approve(spender, maxUint256) only if insufficient. Used by
|
|
4
|
+
* `swap.execute` (Agni router) and `aave.supply` (Aave Pool).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getGasPriceWithFloor } from 'nebula-ai-core'
|
|
8
|
+
import {
|
|
9
|
+
type Address,
|
|
10
|
+
type PublicClient,
|
|
11
|
+
type WalletClient,
|
|
12
|
+
decodeFunctionResult,
|
|
13
|
+
encodeFunctionData,
|
|
14
|
+
getAddress,
|
|
15
|
+
maxUint256,
|
|
16
|
+
} from 'viem'
|
|
17
|
+
import { ERC20_ABI, MULTICALL3_ABI } from './abis'
|
|
18
|
+
import { MULTICALL3 } from './constants'
|
|
19
|
+
import { waitForReceipt } from './wait-receipt'
|
|
20
|
+
|
|
21
|
+
export async function readAllowance(opts: {
|
|
22
|
+
client: PublicClient
|
|
23
|
+
token: Address
|
|
24
|
+
owner: Address
|
|
25
|
+
spender: Address
|
|
26
|
+
}): Promise<bigint> {
|
|
27
|
+
const { client, token, owner, spender } = opts
|
|
28
|
+
const calls = [
|
|
29
|
+
{
|
|
30
|
+
target: token,
|
|
31
|
+
allowFailure: false,
|
|
32
|
+
callData: encodeFunctionData({
|
|
33
|
+
abi: ERC20_ABI,
|
|
34
|
+
functionName: 'allowance',
|
|
35
|
+
args: [owner, spender],
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
const results = (await client.readContract({
|
|
40
|
+
address: MULTICALL3,
|
|
41
|
+
abi: MULTICALL3_ABI,
|
|
42
|
+
functionName: 'aggregate3',
|
|
43
|
+
args: [calls],
|
|
44
|
+
})) as ReadonlyArray<{ success: boolean; returnData: `0x${string}` }>
|
|
45
|
+
return decodeFunctionResult({
|
|
46
|
+
abi: ERC20_ABI,
|
|
47
|
+
functionName: 'allowance',
|
|
48
|
+
data: results[0]!.returnData,
|
|
49
|
+
}) as bigint
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function ensureAllowance(opts: {
|
|
53
|
+
publicClient: PublicClient
|
|
54
|
+
walletClient: WalletClient
|
|
55
|
+
token: Address
|
|
56
|
+
owner: Address
|
|
57
|
+
spender: Address
|
|
58
|
+
amount: bigint
|
|
59
|
+
}): Promise<{ approved: boolean; txHash?: `0x${string}` }> {
|
|
60
|
+
const { publicClient, walletClient, token, owner, spender, amount } = opts
|
|
61
|
+
const current = await readAllowance({ client: publicClient, token, owner, spender })
|
|
62
|
+
if (current >= amount) return { approved: false }
|
|
63
|
+
const gasPrice = await getGasPriceWithFloor(publicClient)
|
|
64
|
+
const account = walletClient.account
|
|
65
|
+
if (!account) throw new Error('walletClient has no account; cannot approve')
|
|
66
|
+
const txHash = await walletClient.writeContract({
|
|
67
|
+
address: getAddress(token) as Address,
|
|
68
|
+
abi: ERC20_ABI,
|
|
69
|
+
functionName: 'approve',
|
|
70
|
+
args: [spender, maxUint256],
|
|
71
|
+
chain: walletClient.chain,
|
|
72
|
+
account,
|
|
73
|
+
gasPrice,
|
|
74
|
+
})
|
|
75
|
+
await waitForReceipt(publicClient, txHash)
|
|
76
|
+
return { approved: true, txHash }
|
|
77
|
+
}
|
package/src/analysis.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tx + contract analysis helpers. Tries our local ABI library first, falls
|
|
3
|
+
* back to the 4byte directory for unknown selectors with a canonical-first
|
|
4
|
+
* filter (longest, lowercase, simplest signature wins; spam filtered out).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { dirname, join } from 'node:path'
|
|
9
|
+
import { type AbiFunction, decodeFunctionData, parseAbiItem, toFunctionSelector } from 'viem'
|
|
10
|
+
import { ALL_KNOWN_ABIS } from './abis'
|
|
11
|
+
|
|
12
|
+
const KNOWN_ABIS = ALL_KNOWN_ABIS as readonly AbiFunction[]
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a selector → abi item map from KNOWN_ABIS. Multiple ABIs can include
|
|
16
|
+
* the same selector (e.g. `transfer` in both ERC-20 and W0G); we keep the
|
|
17
|
+
* first hit since they share the same signature anyway.
|
|
18
|
+
*/
|
|
19
|
+
const KNOWN_SELECTORS = (() => {
|
|
20
|
+
const map = new Map<string, AbiFunction>()
|
|
21
|
+
for (const item of KNOWN_ABIS) {
|
|
22
|
+
if ((item as AbiFunction).type !== 'function') continue
|
|
23
|
+
const fn = item as AbiFunction
|
|
24
|
+
try {
|
|
25
|
+
const sel = toFunctionSelector(fn).toLowerCase()
|
|
26
|
+
if (!map.has(sel)) map.set(sel, fn)
|
|
27
|
+
} catch {
|
|
28
|
+
// skip items that don't selector-encode (events, errors)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return map
|
|
32
|
+
})()
|
|
33
|
+
|
|
34
|
+
interface FourByteCacheFile {
|
|
35
|
+
version: 1
|
|
36
|
+
hits: Record<string, string> // selector → canonical signature
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fourByteCachePath(agentDir: string): string {
|
|
40
|
+
return join(agentDir, 'onchain', '4byte-cache.json')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadFourByteCache(agentDir: string): FourByteCacheFile {
|
|
44
|
+
const path = fourByteCachePath(agentDir)
|
|
45
|
+
if (!existsSync(path)) return { version: 1, hits: {} }
|
|
46
|
+
try {
|
|
47
|
+
const raw = readFileSync(path, 'utf8')
|
|
48
|
+
const parsed = JSON.parse(raw) as FourByteCacheFile
|
|
49
|
+
if (parsed?.version === 1 && parsed.hits) return parsed
|
|
50
|
+
return { version: 1, hits: {} }
|
|
51
|
+
} catch {
|
|
52
|
+
return { version: 1, hits: {} }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function saveFourByteCache(agentDir: string, cache: FourByteCacheFile): void {
|
|
57
|
+
const path = fourByteCachePath(agentDir)
|
|
58
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
59
|
+
writeFileSync(path, JSON.stringify(cache, null, 2))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface FourByteResult {
|
|
63
|
+
text_signature: string
|
|
64
|
+
hex_signature: string
|
|
65
|
+
bytes_signature: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pick the most "canonical" signature out of 4byte's results. Spam bots
|
|
70
|
+
* register PascalCase or all-caps names whose hashes collide with real
|
|
71
|
+
* selectors; filter those out, then prefer fewer args + shorter names.
|
|
72
|
+
*/
|
|
73
|
+
function pickCanonical(results: FourByteResult[]): string | null {
|
|
74
|
+
if (results.length === 0) return null
|
|
75
|
+
const scored = results
|
|
76
|
+
.map(r => {
|
|
77
|
+
const sig = r.text_signature
|
|
78
|
+
const fnName = sig.split('(')[0] ?? sig
|
|
79
|
+
const argList = sig.slice(fnName.length + 1, -1)
|
|
80
|
+
const argCount = argList.length === 0 ? 0 : argList.split(',').length
|
|
81
|
+
const looksLikeSpam =
|
|
82
|
+
/^[A-Z][a-zA-Z]*$/.test(fnName) || /[A-Z]{4,}/.test(fnName) || fnName.length > 32
|
|
83
|
+
return { sig, fnName, argCount, looksLikeSpam }
|
|
84
|
+
})
|
|
85
|
+
.filter(s => !s.looksLikeSpam)
|
|
86
|
+
.sort((a, b) => {
|
|
87
|
+
if (a.argCount !== b.argCount) return a.argCount - b.argCount
|
|
88
|
+
if (a.fnName.length !== b.fnName.length) return a.fnName.length - b.fnName.length
|
|
89
|
+
return a.fnName.localeCompare(b.fnName)
|
|
90
|
+
})
|
|
91
|
+
return scored[0]?.sig ?? null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function lookup4byte(selector: string): Promise<FourByteResult[]> {
|
|
95
|
+
const url = `https://www.4byte.directory/api/v1/signatures/?hex_signature=${selector}`
|
|
96
|
+
const ctrl = new AbortController()
|
|
97
|
+
const t = setTimeout(() => ctrl.abort(), 5000)
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(url, { signal: ctrl.signal })
|
|
100
|
+
if (!res.ok) return []
|
|
101
|
+
const json = (await res.json()) as { results?: FourByteResult[] }
|
|
102
|
+
return json.results ?? []
|
|
103
|
+
} catch {
|
|
104
|
+
return []
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(t)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface DecodedFunction {
|
|
111
|
+
name: string
|
|
112
|
+
signature: string
|
|
113
|
+
args: unknown[]
|
|
114
|
+
source: 'local' | '4byte' | 'cache'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function decodeCalldata(opts: {
|
|
118
|
+
data: `0x${string}`
|
|
119
|
+
agentDir: string
|
|
120
|
+
}): Promise<DecodedFunction | { selector: `0x${string}`; source: 'unknown' }> {
|
|
121
|
+
const { data, agentDir } = opts
|
|
122
|
+
if (data.length < 10) {
|
|
123
|
+
return { selector: '0x' as `0x${string}`, source: 'unknown' }
|
|
124
|
+
}
|
|
125
|
+
const selector = data.slice(0, 10).toLowerCase() as `0x${string}`
|
|
126
|
+
// Local ABI hit
|
|
127
|
+
const localHit = KNOWN_SELECTORS.get(selector)
|
|
128
|
+
if (localHit) {
|
|
129
|
+
try {
|
|
130
|
+
const decoded = decodeFunctionData({
|
|
131
|
+
abi: [localHit] as readonly [AbiFunction],
|
|
132
|
+
data,
|
|
133
|
+
})
|
|
134
|
+
return {
|
|
135
|
+
name: localHit.name,
|
|
136
|
+
signature: formatAbiFunction(localHit),
|
|
137
|
+
args: Array.isArray(decoded.args) ? [...decoded.args] : [],
|
|
138
|
+
source: 'local',
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// fall through to 4byte
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Cache hit
|
|
145
|
+
const cache = loadFourByteCache(agentDir)
|
|
146
|
+
const cached = cache.hits[selector]
|
|
147
|
+
if (cached) {
|
|
148
|
+
const args = tryDecodeWithSignature(cached, data)?.args ?? []
|
|
149
|
+
return {
|
|
150
|
+
name: cached.split('(')[0] ?? cached,
|
|
151
|
+
signature: cached,
|
|
152
|
+
args,
|
|
153
|
+
source: 'cache',
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 4byte fallback
|
|
157
|
+
const results = await lookup4byte(selector)
|
|
158
|
+
const canonical = pickCanonical(results)
|
|
159
|
+
if (!canonical) {
|
|
160
|
+
return { selector, source: 'unknown' }
|
|
161
|
+
}
|
|
162
|
+
cache.hits[selector] = canonical
|
|
163
|
+
saveFourByteCache(agentDir, cache)
|
|
164
|
+
const out = tryDecodeWithSignature(canonical, data)
|
|
165
|
+
return {
|
|
166
|
+
name: canonical.split('(')[0] ?? canonical,
|
|
167
|
+
signature: canonical,
|
|
168
|
+
args: out?.args ?? [],
|
|
169
|
+
source: '4byte',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function tryDecodeWithSignature(
|
|
174
|
+
sig: string,
|
|
175
|
+
data: `0x${string}`,
|
|
176
|
+
): { decoded: boolean; args: unknown[] } | null {
|
|
177
|
+
try {
|
|
178
|
+
const fn = parseAbiItem(`function ${sig}`) as AbiFunction
|
|
179
|
+
const decoded = decodeFunctionData({
|
|
180
|
+
abi: [fn] as readonly [AbiFunction],
|
|
181
|
+
data,
|
|
182
|
+
})
|
|
183
|
+
return {
|
|
184
|
+
decoded: true,
|
|
185
|
+
args: Array.isArray(decoded.args) ? [...decoded.args] : [],
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatAbiFunction(fn: AbiFunction): string {
|
|
193
|
+
const params = fn.inputs.map(p => p.type).join(',')
|
|
194
|
+
return `${fn.name}(${params})`
|
|
195
|
+
}
|