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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chain.wrap` + `chain.unwrap` — native MNT ↔ WMNT via WETH9 deposit/withdraw.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ToolDef } from 'nebula-ai-core'
|
|
6
|
+
import { getGasPriceWithFloor } from 'nebula-ai-core'
|
|
7
|
+
import { type Abi, type Address, formatEther, parseEther } from 'viem'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { WETH9_ABI } from '../abis'
|
|
10
|
+
import { AGNI_BY_NETWORK, requireMainnet } from '../constants'
|
|
11
|
+
import { evaluatePolicy } from '../policy'
|
|
12
|
+
import { simulateContractWrite } from '../simulate'
|
|
13
|
+
import type { OnchainRuntimeContext } from '../types'
|
|
14
|
+
import { waitForReceipt } from '../wait-receipt'
|
|
15
|
+
|
|
16
|
+
const WrapSchema = z.object({
|
|
17
|
+
amount: z.string().min(1).describe('Amount of MNT to wrap (e.g. "0.05").'),
|
|
18
|
+
})
|
|
19
|
+
type WrapArgs = z.infer<typeof WrapSchema>
|
|
20
|
+
|
|
21
|
+
export function makeChainWrap(ctx: OnchainRuntimeContext): ToolDef<WrapArgs> {
|
|
22
|
+
return {
|
|
23
|
+
name: 'chain.wrap',
|
|
24
|
+
description:
|
|
25
|
+
'Wrap native MNT into WMNT (ERC-20). Calls WMNT.deposit() with msg.value. Required when the agent needs to swap with ERC-20 input on Agni.',
|
|
26
|
+
searchHint: 'wrap wmnt weth deposit erc20 mnt',
|
|
27
|
+
schema: WrapSchema,
|
|
28
|
+
handler: async args => {
|
|
29
|
+
try {
|
|
30
|
+
requireMainnet(ctx.network)
|
|
31
|
+
const wmnt = AGNI_BY_NETWORK[ctx.network]!.weth9
|
|
32
|
+
const account = ctx.walletClient.account
|
|
33
|
+
if (!account) {
|
|
34
|
+
return { ok: false, error: 'walletClient has no account; cannot wrap' }
|
|
35
|
+
}
|
|
36
|
+
const value = parseEther(args.amount)
|
|
37
|
+
// Policy + simulate, same as every other value-moving write.
|
|
38
|
+
if (ctx.policy) {
|
|
39
|
+
const verdict = evaluatePolicy(
|
|
40
|
+
{ kind: 'transfer', asset: 'native', amountRaw: value },
|
|
41
|
+
ctx.policy,
|
|
42
|
+
)
|
|
43
|
+
if (!verdict.allowed) {
|
|
44
|
+
return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const sim = await simulateContractWrite(ctx.publicClient, {
|
|
48
|
+
account: account.address,
|
|
49
|
+
address: wmnt as Address,
|
|
50
|
+
abi: WETH9_ABI as Abi,
|
|
51
|
+
functionName: 'deposit',
|
|
52
|
+
args: [],
|
|
53
|
+
value,
|
|
54
|
+
})
|
|
55
|
+
if (!sim.ok) return { ok: false, error: `pre-flight simulation reverted: ${sim.reason}` }
|
|
56
|
+
const gasPrice = await getGasPriceWithFloor(ctx.publicClient)
|
|
57
|
+
const txHash = await ctx.walletClient.writeContract({
|
|
58
|
+
address: wmnt as Address,
|
|
59
|
+
abi: WETH9_ABI,
|
|
60
|
+
functionName: 'deposit',
|
|
61
|
+
value,
|
|
62
|
+
chain: ctx.walletClient.chain,
|
|
63
|
+
account,
|
|
64
|
+
gasPrice,
|
|
65
|
+
})
|
|
66
|
+
const receipt = await waitForReceipt(ctx.publicClient, txHash)
|
|
67
|
+
const wmntBal = (await ctx.publicClient.readContract({
|
|
68
|
+
address: wmnt as Address,
|
|
69
|
+
abi: WETH9_ABI,
|
|
70
|
+
functionName: 'balanceOf',
|
|
71
|
+
args: [ctx.agentEoa],
|
|
72
|
+
})) as bigint
|
|
73
|
+
const nativeBal = await ctx.publicClient.getBalance({ address: ctx.agentEoa })
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
data: {
|
|
77
|
+
txHash,
|
|
78
|
+
blockNumber: Number(receipt.blockNumber),
|
|
79
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
80
|
+
wrappedAmount: args.amount,
|
|
81
|
+
wmntBalance: formatEther(wmntBal),
|
|
82
|
+
nativeBalance: formatEther(nativeBal),
|
|
83
|
+
status: receipt.status === 'success' ? 'success' : 'reverted',
|
|
84
|
+
simGasEstimate: sim.gas.toString(),
|
|
85
|
+
policyEnforced: ctx.policy != null,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return { ok: false, error: (e as Error).message.slice(0, 240) }
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const UnwrapSchema = z.object({
|
|
96
|
+
amount: z.string().min(1).describe('Amount of WMNT to unwrap, or "all" for entire WMNT balance.'),
|
|
97
|
+
})
|
|
98
|
+
type UnwrapArgs = z.infer<typeof UnwrapSchema>
|
|
99
|
+
|
|
100
|
+
export function makeChainUnwrap(ctx: OnchainRuntimeContext): ToolDef<UnwrapArgs> {
|
|
101
|
+
return {
|
|
102
|
+
name: 'chain.unwrap',
|
|
103
|
+
description:
|
|
104
|
+
'Unwrap WMNT back into native MNT. Calls WMNT.withdraw(amount). Pass "all" to unwrap entire balance.',
|
|
105
|
+
searchHint: 'unwrap wmnt native withdraw mnt',
|
|
106
|
+
schema: UnwrapSchema,
|
|
107
|
+
handler: async args => {
|
|
108
|
+
try {
|
|
109
|
+
requireMainnet(ctx.network)
|
|
110
|
+
const wmnt = AGNI_BY_NETWORK[ctx.network]!.weth9
|
|
111
|
+
const account = ctx.walletClient.account
|
|
112
|
+
if (!account) {
|
|
113
|
+
return { ok: false, error: 'walletClient has no account; cannot unwrap' }
|
|
114
|
+
}
|
|
115
|
+
let amountWei: bigint
|
|
116
|
+
if (args.amount === 'all') {
|
|
117
|
+
amountWei = (await ctx.publicClient.readContract({
|
|
118
|
+
address: wmnt as Address,
|
|
119
|
+
abi: WETH9_ABI,
|
|
120
|
+
functionName: 'balanceOf',
|
|
121
|
+
args: [ctx.agentEoa],
|
|
122
|
+
})) as bigint
|
|
123
|
+
if (amountWei === 0n) {
|
|
124
|
+
return { ok: false, error: 'no WMNT balance to unwrap' }
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
amountWei = parseEther(args.amount)
|
|
128
|
+
}
|
|
129
|
+
if (ctx.policy) {
|
|
130
|
+
const verdict = evaluatePolicy(
|
|
131
|
+
{ kind: 'transfer', asset: 'native', amountRaw: amountWei },
|
|
132
|
+
ctx.policy,
|
|
133
|
+
)
|
|
134
|
+
if (!verdict.allowed) {
|
|
135
|
+
return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const sim = await simulateContractWrite(ctx.publicClient, {
|
|
139
|
+
account: account.address,
|
|
140
|
+
address: wmnt as Address,
|
|
141
|
+
abi: WETH9_ABI as Abi,
|
|
142
|
+
functionName: 'withdraw',
|
|
143
|
+
args: [amountWei],
|
|
144
|
+
})
|
|
145
|
+
if (!sim.ok) return { ok: false, error: `pre-flight simulation reverted: ${sim.reason}` }
|
|
146
|
+
const gasPrice = await getGasPriceWithFloor(ctx.publicClient)
|
|
147
|
+
const txHash = await ctx.walletClient.writeContract({
|
|
148
|
+
address: wmnt as Address,
|
|
149
|
+
abi: WETH9_ABI,
|
|
150
|
+
functionName: 'withdraw',
|
|
151
|
+
args: [amountWei],
|
|
152
|
+
chain: ctx.walletClient.chain,
|
|
153
|
+
account,
|
|
154
|
+
gasPrice,
|
|
155
|
+
})
|
|
156
|
+
const receipt = await waitForReceipt(ctx.publicClient, txHash)
|
|
157
|
+
const wmntBal = (await ctx.publicClient.readContract({
|
|
158
|
+
address: wmnt as Address,
|
|
159
|
+
abi: WETH9_ABI,
|
|
160
|
+
functionName: 'balanceOf',
|
|
161
|
+
args: [ctx.agentEoa],
|
|
162
|
+
})) as bigint
|
|
163
|
+
const nativeBal = await ctx.publicClient.getBalance({ address: ctx.agentEoa })
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
data: {
|
|
167
|
+
txHash,
|
|
168
|
+
blockNumber: Number(receipt.blockNumber),
|
|
169
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
170
|
+
unwrappedAmount: formatEther(amountWei),
|
|
171
|
+
wmntBalance: formatEther(wmntBal),
|
|
172
|
+
nativeBalance: formatEther(nativeBal),
|
|
173
|
+
status: receipt.status === 'success' ? 'success' : 'reverted',
|
|
174
|
+
simGasEstimate: sim.gas.toString(),
|
|
175
|
+
policyEnforced: ctx.policy != null,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return { ok: false, error: (e as Error).message.slice(0, 240) }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types exported from plugin-onchain. The runtime context follows the
|
|
3
|
+
* same side-band pattern as `plugin-comms/src/index.ts:CommsRuntimeContext`:
|
|
4
|
+
* the harness builds it in `chat.tsx`, the plugin reads it via
|
|
5
|
+
* `(ctx as any).onchain`. Keeps PluginContext clean of plugin-specific fields.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { NebulaNetwork } from 'nebula-ai-core'
|
|
9
|
+
import type { Address } from 'viem'
|
|
10
|
+
import type { OnchainPolicy } from './policy'
|
|
11
|
+
|
|
12
|
+
export interface OnchainRuntimeContext {
|
|
13
|
+
agentEoa: Address
|
|
14
|
+
network: NebulaNetwork
|
|
15
|
+
/** Deterministic fund-control policy. When set, every write is checked before simulate/execute. */
|
|
16
|
+
policy?: OnchainPolicy
|
|
17
|
+
publicClient: import('viem').PublicClient
|
|
18
|
+
walletClient: import('viem').WalletClient
|
|
19
|
+
agentDir: string
|
|
20
|
+
/** iNFT mint block — used as floor for Transfer-event discovery scans. */
|
|
21
|
+
mintBlock: bigint
|
|
22
|
+
iNFT?: { contract: Address; tokenId: bigint }
|
|
23
|
+
/** Optional: brain provider/model for account.info bundling. */
|
|
24
|
+
brainProvider?: string | null
|
|
25
|
+
brainModel?: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TokenInfo {
|
|
29
|
+
address: Address
|
|
30
|
+
symbol: string
|
|
31
|
+
name?: string
|
|
32
|
+
decimals: number
|
|
33
|
+
/** Where this entry came from. */
|
|
34
|
+
source: 'cache' | 'list' | 'onchain' | 'native'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NativeBalance {
|
|
38
|
+
raw: string // bigint serialized as decimal string
|
|
39
|
+
formatted: string // human "1.234"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TokenBalance extends TokenInfo {
|
|
43
|
+
raw: string
|
|
44
|
+
formatted: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface BalanceSnapshot {
|
|
48
|
+
address: Address
|
|
49
|
+
native: NativeBalance
|
|
50
|
+
tokens: TokenBalance[]
|
|
51
|
+
/** Block at which the snapshot was taken. */
|
|
52
|
+
blockNumber: number
|
|
53
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper for viem's `waitForTransactionReceipt` with Mantle-mainnet-tuned
|
|
3
|
+
* polling. The default 4s poll occasionally times out on Mantle even when the
|
|
4
|
+
* tx lands in 1-2s; we bump to 1.5s with a generous retry budget. The
|
|
5
|
+
* helper also catches viem's `TransactionReceiptNotFoundError` (intermittent
|
|
6
|
+
* RPC null responses immediately after inclusion) and retries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Hex, PublicClient, TransactionReceipt } from 'viem'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 90_000
|
|
12
|
+
const DEFAULT_POLL_MS = 1_500
|
|
13
|
+
|
|
14
|
+
export async function waitForReceipt(
|
|
15
|
+
client: PublicClient,
|
|
16
|
+
hash: Hex,
|
|
17
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
18
|
+
): Promise<TransactionReceipt> {
|
|
19
|
+
const start = Date.now()
|
|
20
|
+
let lastErr: unknown
|
|
21
|
+
while (Date.now() - start < timeoutMs) {
|
|
22
|
+
try {
|
|
23
|
+
const r = await client.getTransactionReceipt({ hash })
|
|
24
|
+
return r
|
|
25
|
+
} catch (e) {
|
|
26
|
+
lastErr = e
|
|
27
|
+
// intermittent "not found" — keep polling
|
|
28
|
+
}
|
|
29
|
+
await new Promise(r => setTimeout(r, DEFAULT_POLL_MS))
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
`tx receipt timeout after ${timeoutMs}ms for ${hash}; lastErr=${(lastErr as Error)?.message ?? 'unknown'}`,
|
|
33
|
+
)
|
|
34
|
+
}
|