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.
Files changed (54) hide show
  1. package/README.md +26 -0
  2. package/abis/erc20.json +78 -0
  3. package/abis/factory.json +236 -0
  4. package/abis/gimo-pool.json +53 -0
  5. package/abis/multicall3.json +76 -0
  6. package/abis/quoter.json +193 -0
  7. package/abis/stog.json +58 -0
  8. package/abis/swap-router.json +565 -0
  9. package/abis/weth9.json +65 -0
  10. package/data/tokens.json +94 -0
  11. package/package.json +52 -0
  12. package/src/aave.ts +193 -0
  13. package/src/abis.ts +84 -0
  14. package/src/allowance.ts +77 -0
  15. package/src/analysis.ts +195 -0
  16. package/src/approval.ts +99 -0
  17. package/src/balances.ts +262 -0
  18. package/src/bybit.ts +118 -0
  19. package/src/constants.ts +102 -0
  20. package/src/defillama.ts +127 -0
  21. package/src/guidance.ts +23 -0
  22. package/src/index.ts +139 -0
  23. package/src/mint-block.ts +53 -0
  24. package/src/moe.ts +111 -0
  25. package/src/nansen.ts +85 -0
  26. package/src/policy.ts +213 -0
  27. package/src/quoter.ts +87 -0
  28. package/src/raw-logs.ts +49 -0
  29. package/src/risk.ts +79 -0
  30. package/src/simulate.ts +121 -0
  31. package/src/swap.ts +108 -0
  32. package/src/tokens.ts +232 -0
  33. package/src/tools/aave.ts +425 -0
  34. package/src/tools/account-balance.ts +67 -0
  35. package/src/tools/account.ts +111 -0
  36. package/src/tools/analysis.ts +371 -0
  37. package/src/tools/balance.ts +119 -0
  38. package/src/tools/blockchain.ts +95 -0
  39. package/src/tools/cex.ts +54 -0
  40. package/src/tools/defillama.ts +83 -0
  41. package/src/tools/generic.ts +213 -0
  42. package/src/tools/identity.ts +139 -0
  43. package/src/tools/moe.ts +245 -0
  44. package/src/tools/nansen.ts +71 -0
  45. package/src/tools/policy-show.ts +74 -0
  46. package/src/tools/risk.ts +134 -0
  47. package/src/tools/simulate-tx.ts +98 -0
  48. package/src/tools/swap-best.ts +218 -0
  49. package/src/tools/swap.ts +253 -0
  50. package/src/tools/tokens-info.ts +49 -0
  51. package/src/tools/transfer.ts +164 -0
  52. package/src/tools/wrap.ts +183 -0
  53. package/src/types.ts +53 -0
  54. 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
+ }