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,95 @@
1
+ /**
2
+ * `chain.block` + `chain.gas` — passive RPC introspection.
3
+ */
4
+
5
+ import type { ToolDef } from 'nebula-ai-core'
6
+ import { getGasPriceWithFloor } from 'nebula-ai-core'
7
+ import { formatEther, formatGwei } from 'viem'
8
+ import { z } from 'zod'
9
+ import type { OnchainRuntimeContext } from '../types'
10
+
11
+ /** Representative gas units per operation (Mantle), for cost estimates in MNT. */
12
+ const TYPICAL_GAS: ReadonlyArray<readonly [string, bigint]> = [
13
+ ['nativeTransfer', 21_000n],
14
+ ['erc20Transfer', 52_000n],
15
+ ['swap', 180_000n],
16
+ ['aaveSupply', 250_000n],
17
+ ]
18
+
19
+ const BlockSchema = z.object({
20
+ tag: z
21
+ .union([
22
+ z.enum(['latest', 'finalized', 'safe', 'earliest', 'pending']),
23
+ z.number().int().nonnegative(),
24
+ ])
25
+ .optional()
26
+ .describe('Block tag or number (default: "latest").'),
27
+ })
28
+ type BlockArgs = z.infer<typeof BlockSchema>
29
+
30
+ export function makeChainBlock(ctx: OnchainRuntimeContext): ToolDef<BlockArgs> {
31
+ return {
32
+ name: 'chain.block',
33
+ description:
34
+ 'Read a Mantle block summary (number, hash, timestamp, txCount, gasUsed). Default: latest.',
35
+ searchHint: 'block number height timestamp head',
36
+ schema: BlockSchema,
37
+ handler: async args => {
38
+ try {
39
+ const tag = args.tag ?? 'latest'
40
+ const block =
41
+ typeof tag === 'number'
42
+ ? await ctx.publicClient.getBlock({ blockNumber: BigInt(tag) })
43
+ : await ctx.publicClient.getBlock({ blockTag: tag })
44
+ return {
45
+ ok: true,
46
+ data: {
47
+ number: Number(block.number ?? 0n),
48
+ hash: block.hash,
49
+ parentHash: block.parentHash,
50
+ timestamp: Number(block.timestamp),
51
+ txCount: block.transactions.length,
52
+ gasUsed: block.gasUsed.toString(),
53
+ gasLimit: block.gasLimit.toString(),
54
+ },
55
+ }
56
+ } catch (e) {
57
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
58
+ }
59
+ },
60
+ }
61
+ }
62
+
63
+ const GasSchema = z.object({})
64
+ type GasArgs = z.infer<typeof GasSchema>
65
+
66
+ export function makeChainGas(ctx: OnchainRuntimeContext): ToolDef<GasArgs> {
67
+ return {
68
+ name: 'chain.gas',
69
+ description:
70
+ 'Current Mantle gas price (network 4 gwei floor applied) plus estimated MNT cost of common operations (native/ERC-20 transfer, swap, Aave supply). Use to estimate cost or detect spikes. Costs are in MNT, not USD.',
71
+ searchHint: 'gas price gwei fee estimate cost mnt how much transfer swap',
72
+ schema: GasSchema,
73
+ handler: async () => {
74
+ try {
75
+ const wei = await getGasPriceWithFloor(ctx.publicClient)
76
+ const estimatedCostMnt = Object.fromEntries(
77
+ TYPICAL_GAS.map(([op, units]) => [
78
+ op,
79
+ { gasUnits: Number(units), costMnt: formatEther(wei * units) },
80
+ ]),
81
+ )
82
+ return {
83
+ ok: true,
84
+ data: {
85
+ gasPriceWei: wei.toString(),
86
+ gasPriceGwei: formatGwei(wei),
87
+ estimatedCostMnt,
88
+ },
89
+ }
90
+ } catch (e) {
91
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
92
+ }
93
+ },
94
+ }
95
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `cex.balance` — read-only Bybit Unified account balance (portfolio view).
3
+ *
4
+ * READ-ONLY by design: no CEX trading/transfers (they'd bypass the on-chain
5
+ * safety pipeline). Keys come from BYBIT_API_KEY / BYBIT_API_SECRET in the env,
6
+ * never committed. Degrades gracefully when keys are unset.
7
+ */
8
+
9
+ import type { ToolDef } from 'nebula-ai-core'
10
+ import { z } from 'zod'
11
+ import { fetchBybitBalance } from '../bybit'
12
+ import type { OnchainRuntimeContext } from '../types'
13
+
14
+ const Schema = z.object({})
15
+ type Args = z.infer<typeof Schema>
16
+
17
+ export function makeCexBalance(_ctx: OnchainRuntimeContext): ToolDef<Args> {
18
+ return {
19
+ name: 'cex.balance',
20
+ description:
21
+ 'Read-only Bybit Unified account balance (CEX portfolio: per-coin balances + exchange-reported total equity). Lets you show a combined CEX + on-chain treasury picture. Needs BYBIT_API_KEY + BYBIT_API_SECRET in the env. Read-only — this agent never trades or transfers on the CEX (that would bypass the on-chain safety controls).',
22
+ searchHint: 'cex bybit exchange balance portfolio account holdings off-chain unified',
23
+ schema: Schema,
24
+ handler: async () => {
25
+ try {
26
+ const apiKey = process.env.BYBIT_API_KEY
27
+ const apiSecret = process.env.BYBIT_API_SECRET
28
+ if (!apiKey || !apiSecret) {
29
+ return {
30
+ ok: true,
31
+ data: {
32
+ configured: false,
33
+ note: 'BYBIT_API_KEY / BYBIT_API_SECRET not set — CEX balance unavailable. Set them in the env (never commit them); use a READ-ONLY key.',
34
+ },
35
+ }
36
+ }
37
+ const r = await fetchBybitBalance({ apiKey, apiSecret })
38
+ if (!r.ok) return { ok: true, data: { venue: 'Bybit', available: false, note: r.error } }
39
+ return {
40
+ ok: true,
41
+ data: {
42
+ venue: 'Bybit (Unified, read-only)',
43
+ accountType: r.accountType,
44
+ totalEquityUsd: r.totalEquityUsd,
45
+ coins: r.coins,
46
+ note: 'Exchange-reported figures; this agent does not trade or transfer on the CEX.',
47
+ },
48
+ }
49
+ } catch (e) {
50
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
51
+ }
52
+ },
53
+ }
54
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `defi.yields` — discover Mantle yield opportunities via DeFiLlama (analytics
3
+ * + discovery only; never execution, per CLAUDE.md). Read-only: no signer, no
4
+ * policy/simulation gate. Restricted products (USDY/MI4/mUSD) are surfaced but
5
+ * flagged so the brain proposes them only with eligibility confirmation.
6
+ */
7
+
8
+ import type { ToolDef } from 'nebula-ai-core'
9
+ import { z } from 'zod'
10
+ import { fetchMantleYields } from '../defillama'
11
+ import type { OnchainRuntimeContext } from '../types'
12
+
13
+ const Schema = z.object({
14
+ minTvlUsd: z
15
+ .number()
16
+ .optional()
17
+ .describe('Minimum pool TVL in USD (filters illiquid pools). Default 50000.'),
18
+ stableOnly: z
19
+ .boolean()
20
+ .optional()
21
+ .describe('Only stablecoin pools — lower-risk treasury parking. Default false.'),
22
+ noIlRisk: z
23
+ .boolean()
24
+ .optional()
25
+ .describe('Exclude pools flagged with impermanent-loss risk. Default false.'),
26
+ project: z
27
+ .string()
28
+ .optional()
29
+ .describe('Filter to a protocol slug substring, e.g. "aave", "agni", "merchant-moe".'),
30
+ sortBy: z.enum(['apy', 'tvl']).optional().describe('Rank by APY (default) or TVL.'),
31
+ limit: z.number().optional().describe('Max rows (1-50). Default 10.'),
32
+ })
33
+ type Args = z.infer<typeof Schema>
34
+
35
+ export function makeDefiYields(_ctx: OnchainRuntimeContext): ToolDef<Args> {
36
+ return {
37
+ name: 'defi.yields',
38
+ description:
39
+ 'Discover Mantle yield opportunities (lending + LP pools) ranked by APY or TVL, via DeFiLlama. Each row carries risk signals: stablecoin, impermanent-loss risk, single/multi exposure, 7d APY trend, and a `restricted` flag for products that need eligibility confirmation (USDY/MI4/mUSD). Analytics + discovery ONLY — does not execute anything. Use for "best yields on Mantle", "where can I park USDC", "safe stablecoin yield".',
40
+ searchHint:
41
+ 'defi yield apy pool discover farm lend stablecoin tvl mantle defillama best return',
42
+ schema: Schema,
43
+ handler: async (args: Args) => {
44
+ try {
45
+ const pools = await fetchMantleYields({
46
+ minTvlUsd: args.minTvlUsd,
47
+ stableOnly: args.stableOnly,
48
+ noIlRisk: args.noIlRisk,
49
+ project: args.project,
50
+ sortBy: args.sortBy,
51
+ limit: args.limit,
52
+ })
53
+ return {
54
+ ok: true,
55
+ data: {
56
+ chain: 'Mantle',
57
+ source: 'DeFiLlama (analytics/discovery only — not executable)',
58
+ count: pools.length,
59
+ restrictedNote:
60
+ pools.some(p => p.restricted) === true
61
+ ? 'Some results are RESTRICTED products (USDY/MI4/mUSD); confirm eligibility before proposing entry.'
62
+ : undefined,
63
+ pools: pools.map(p => ({
64
+ project: p.project,
65
+ symbol: p.symbol,
66
+ apy: Number(p.apy.toFixed(2)),
67
+ apyBase: p.apyBase === null ? null : Number(p.apyBase.toFixed(2)),
68
+ apyReward: p.apyReward === null ? null : Number(p.apyReward.toFixed(2)),
69
+ apy7dTrend: p.apyPct7D === null ? null : Number(p.apyPct7D.toFixed(2)),
70
+ tvlUsd: Math.round(p.tvlUsd),
71
+ stablecoin: p.stablecoin,
72
+ ilRisk: p.ilRisk,
73
+ exposure: p.exposure,
74
+ restricted: p.restricted,
75
+ })),
76
+ },
77
+ }
78
+ } catch (e) {
79
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
80
+ }
81
+ },
82
+ }
83
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `chain.read` + `chain.write` — generic ABI-call escape hatch for contracts
3
+ * not covered by the curated tools.
4
+ *
5
+ * Argument format mirrors `cast`:
6
+ * - `signature: 'balanceOf(address)'`
7
+ * - `args: ['0xabc...']`
8
+ * Decimal numbers are auto-converted to bigint (zod number → BigInt) to keep
9
+ * the interface friendly for the LLM. Hex strings stay as-is.
10
+ */
11
+
12
+ import type { ToolDef } from 'nebula-ai-core'
13
+ import { getGasPriceWithFloor } from 'nebula-ai-core'
14
+ import {
15
+ type Address,
16
+ decodeAbiParameters,
17
+ encodeFunctionData,
18
+ getAddress,
19
+ parseAbiItem,
20
+ parseAbiParameters,
21
+ parseEther,
22
+ } from 'viem'
23
+ import { z } from 'zod'
24
+ import { evaluatePolicy } from '../policy'
25
+ import { simulateRawTx } from '../simulate'
26
+ import type { OnchainRuntimeContext } from '../types'
27
+ import { waitForReceipt } from '../wait-receipt'
28
+
29
+ const ReadSchema = z.object({
30
+ to: z.string().min(42).describe('0x contract address.'),
31
+ signature: z
32
+ .string()
33
+ .min(1)
34
+ .describe('Function signature, e.g. "balanceOf(address)" or "totalSupply()".'),
35
+ args: z.array(z.unknown()).optional().describe('Encoded args matching signature.'),
36
+ returnTypes: z
37
+ .array(z.string())
38
+ .optional()
39
+ .describe('Optional explicit return types for decoding (e.g. ["uint256"]).'),
40
+ })
41
+ type ReadArgs = z.infer<typeof ReadSchema>
42
+
43
+ export function coerceArg(raw: unknown): unknown {
44
+ if (typeof raw === 'string') {
45
+ if (/^-?\d+$/.test(raw) && !raw.startsWith('0x')) {
46
+ try {
47
+ return BigInt(raw)
48
+ } catch {
49
+ return raw
50
+ }
51
+ }
52
+ return raw
53
+ }
54
+ if (typeof raw === 'number') {
55
+ return BigInt(raw)
56
+ }
57
+ if (Array.isArray(raw)) return raw.map(coerceArg)
58
+ return raw
59
+ }
60
+
61
+ export function buildAbiFunction(signature: string): import('viem').AbiFunction {
62
+ const trimmed = signature.trim()
63
+ const text = trimmed.startsWith('function ') ? trimmed : `function ${trimmed}`
64
+ const item = parseAbiItem(text)
65
+ if (typeof item !== 'object' || item.type !== 'function') {
66
+ throw new Error(`could not parse function signature: ${signature}`)
67
+ }
68
+ return item as import('viem').AbiFunction
69
+ }
70
+
71
+ export function makeChainRead(ctx: OnchainRuntimeContext): ToolDef<ReadArgs> {
72
+ return {
73
+ name: 'chain.read',
74
+ description:
75
+ 'Generic eth_call. Pass `signature` (e.g. "balanceOf(address)") + `args`. Returns hex `data` plus a decoded version when `returnTypes` provided OR the signature itself includes returns.',
76
+ searchHint: 'read view eth_call generic abi cast',
77
+ schema: ReadSchema,
78
+ handler: async args => {
79
+ try {
80
+ const fn = buildAbiFunction(args.signature)
81
+ const coerced = (args.args ?? []).map(coerceArg)
82
+ const data = encodeFunctionData({
83
+ abi: [fn] as readonly [import('viem').AbiFunction],
84
+ args: coerced,
85
+ })
86
+ const result = await ctx.publicClient.call({
87
+ to: getAddress(args.to) as Address,
88
+ data,
89
+ })
90
+ const raw = result.data ?? '0x'
91
+ let decoded: unknown[] | null = null
92
+ if (args.returnTypes && args.returnTypes.length > 0) {
93
+ try {
94
+ const params = parseAbiParameters(args.returnTypes.join(', '))
95
+ decoded = [...decodeAbiParameters(params, raw)]
96
+ } catch {
97
+ decoded = null
98
+ }
99
+ } else if (fn.outputs && fn.outputs.length > 0) {
100
+ try {
101
+ decoded = [...decodeAbiParameters(fn.outputs as never, raw)]
102
+ } catch {
103
+ decoded = null
104
+ }
105
+ }
106
+ return {
107
+ ok: true,
108
+ data: {
109
+ to: getAddress(args.to),
110
+ signature: args.signature,
111
+ raw,
112
+ decoded:
113
+ decoded === null
114
+ ? null
115
+ : decoded.map(d => (typeof d === 'bigint' ? d.toString() : d)),
116
+ },
117
+ }
118
+ } catch (e) {
119
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
120
+ }
121
+ },
122
+ }
123
+ }
124
+
125
+ const WriteSchema = z.object({
126
+ to: z.string().min(42),
127
+ signature: z.string().min(1),
128
+ args: z.array(z.unknown()).optional(),
129
+ value: z
130
+ .string()
131
+ .optional()
132
+ .describe(
133
+ 'Native value to send. Accepts decimal Mantle ("0.0001") OR wei integer ("100000000000000").',
134
+ ),
135
+ })
136
+ type WriteArgs = z.infer<typeof WriteSchema>
137
+
138
+ export function parseChainWriteValue(raw: string): bigint {
139
+ const trimmed = raw.trim()
140
+ if (trimmed.includes('.')) {
141
+ return parseEther(trimmed as `${number}`)
142
+ }
143
+ return BigInt(trimmed)
144
+ }
145
+
146
+ export function makeChainWrite(ctx: OnchainRuntimeContext): ToolDef<WriteArgs> {
147
+ return {
148
+ name: 'chain.write',
149
+ description:
150
+ 'Generic state-changing call. Pass `signature` + `args` (+ optional `value`). Policy-checked (native value), dry-run simulated, and approval-gated before broadcast like every other write.',
151
+ searchHint: 'write contract send call generic state-change',
152
+ schema: WriteSchema,
153
+ handler: async args => {
154
+ try {
155
+ const account = ctx.walletClient.account
156
+ if (!account) return { ok: false, error: 'walletClient has no account; cannot write' }
157
+ const fn = buildAbiFunction(args.signature)
158
+ const coerced = (args.args ?? []).map(coerceArg)
159
+ const data = encodeFunctionData({
160
+ abi: [fn] as readonly [import('viem').AbiFunction],
161
+ args: coerced,
162
+ })
163
+ const value = args.value ? parseChainWriteValue(args.value) : 0n
164
+ const to = getAddress(args.to) as Address
165
+ // Policy caps the NATIVE value moved by an arbitrary call (it can't see
166
+ // token semantics — the harness approval gate covers the rest).
167
+ if (ctx.policy && value > 0n) {
168
+ const verdict = evaluatePolicy(
169
+ { kind: 'transfer', asset: 'native', amountRaw: value, to },
170
+ ctx.policy,
171
+ )
172
+ if (!verdict.allowed) {
173
+ return { ok: false, error: `policy blocked: ${verdict.violations.join('; ')}` }
174
+ }
175
+ }
176
+ // Simulate before broadcasting: a doomed arbitrary call aborts here.
177
+ const sim = await simulateRawTx(ctx.publicClient, {
178
+ account: account.address,
179
+ to,
180
+ data,
181
+ value,
182
+ })
183
+ if (!sim.ok) return { ok: false, error: `pre-flight simulation reverted: ${sim.reason}` }
184
+ const gasPrice = await getGasPriceWithFloor(ctx.publicClient)
185
+ const txHash = await ctx.walletClient.sendTransaction({
186
+ to,
187
+ data,
188
+ value,
189
+ chain: ctx.walletClient.chain,
190
+ account,
191
+ gasPrice,
192
+ })
193
+ const receipt = await waitForReceipt(ctx.publicClient, txHash)
194
+ return {
195
+ ok: true,
196
+ data: {
197
+ txHash,
198
+ blockNumber: Number(receipt.blockNumber),
199
+ gasUsed: receipt.gasUsed.toString(),
200
+ status: receipt.status === 'success' ? 'success' : 'reverted',
201
+ to: getAddress(args.to),
202
+ value: value.toString(),
203
+ signature: args.signature,
204
+ simGasEstimate: sim.gas.toString(),
205
+ policyEnforced: ctx.policy != null,
206
+ },
207
+ }
208
+ } catch (e) {
209
+ return { ok: false, error: (e as Error).message.slice(0, 240) }
210
+ }
211
+ },
212
+ }
213
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `identity.resolve` / `identity.register` — ERC-8004 ("Trustless Agents")
3
+ * on-chain agent identity on Mantle. Lets the agent discover any agent's
4
+ * identity card and register/publish its own.
5
+ */
6
+ import {
7
+ type ToolDef,
8
+ agentIdByAddress,
9
+ buildAgentCard,
10
+ cardToDataUri,
11
+ registerAgent,
12
+ resolveAgentById,
13
+ resolveRegistryAddress,
14
+ } from 'nebula-ai-core'
15
+ import { z } from 'zod'
16
+ import type { OnchainRuntimeContext } from '../types'
17
+
18
+ const ResolveSchema = z.object({
19
+ agentId: z
20
+ .string()
21
+ .optional()
22
+ .describe('Agent id to resolve. Omit (and omit address) to resolve THIS agent by its EOA.'),
23
+ address: z.string().optional().describe('Resolve the agent registered to this EOA (0x...).'),
24
+ })
25
+ type ResolveArgs = z.infer<typeof ResolveSchema>
26
+
27
+ export function makeIdentityResolve(ctx: OnchainRuntimeContext): ToolDef<ResolveArgs> {
28
+ return {
29
+ name: 'identity.resolve',
30
+ description:
31
+ 'Resolve an ERC-8004 (Trustless Agents) on-chain agent identity on Mantle to its owner, operational address, and agent-card URI. Resolve by agentId, by an EOA via `address`, or (no args) this agent itself. Read-only — call it to discover who/what an agent is before trusting it.',
32
+ searchHint:
33
+ 'erc-8004 erc8004 identity registry agent card resolve trustless agent reputation discover who is',
34
+ schema: ResolveSchema,
35
+ handler: async (args: ResolveArgs) => {
36
+ const registry = resolveRegistryAddress(ctx.network)
37
+ if (!registry) {
38
+ return {
39
+ ok: false as const,
40
+ error: `No ERC-8004 Identity Registry deployed for ${ctx.network}. Set NEBULA_IDENTITY_REGISTRY.`,
41
+ }
42
+ }
43
+ try {
44
+ let id: bigint
45
+ if (args.agentId) {
46
+ id = BigInt(args.agentId)
47
+ } else {
48
+ const addr = (args.address ?? ctx.agentEoa) as `0x${string}`
49
+ id = await agentIdByAddress({
50
+ publicClient: ctx.publicClient,
51
+ registry,
52
+ agentAddress: addr,
53
+ })
54
+ if (id === 0n) {
55
+ return { ok: true as const, data: { registered: false, address: addr, registry } }
56
+ }
57
+ }
58
+ const r = await resolveAgentById({ publicClient: ctx.publicClient, registry, agentId: id })
59
+ return {
60
+ ok: true as const,
61
+ data: {
62
+ registered: true,
63
+ agentId: r.agentId.toString(),
64
+ owner: r.owner,
65
+ agentAddress: r.agentAddress,
66
+ cardURI: r.cardURI,
67
+ registry,
68
+ network: ctx.network,
69
+ },
70
+ }
71
+ } catch (e) {
72
+ return { ok: false as const, error: (e as Error).message }
73
+ }
74
+ },
75
+ }
76
+ }
77
+
78
+ const RegisterSchema = z.object({
79
+ name: z
80
+ .string()
81
+ .optional()
82
+ .describe('Display name for the agent card. Defaults to a nebula-<hex> slug.'),
83
+ })
84
+ type RegisterArgs = z.infer<typeof RegisterSchema>
85
+
86
+ export function makeIdentityRegister(ctx: OnchainRuntimeContext): ToolDef<RegisterArgs> {
87
+ return {
88
+ name: 'identity.register',
89
+ description:
90
+ "Register THIS agent's own ERC-8004 (Trustless Agents) identity on Mantle: mints a transferable identity NFT to the agent and publishes its agent card (name, endpoints, agent address, skills) as the on-chain tokenURI. One-time; idempotent (no-op if already registered). Costs a little gas. Resolve afterwards with identity.resolve.",
91
+ searchHint:
92
+ 'erc-8004 erc8004 register identity mint agent card publish trustless on-chain identity self',
93
+ schema: RegisterSchema,
94
+ handler: async (args: RegisterArgs) => {
95
+ const registry = resolveRegistryAddress(ctx.network)
96
+ if (!registry) {
97
+ return {
98
+ ok: false as const,
99
+ error: `No ERC-8004 Identity Registry deployed for ${ctx.network}. Set NEBULA_IDENTITY_REGISTRY.`,
100
+ }
101
+ }
102
+ if (!ctx.walletClient?.account) {
103
+ return { ok: false as const, error: 'no signer available to register identity' }
104
+ }
105
+ try {
106
+ const agentAddress = ctx.agentEoa
107
+ const existing = await agentIdByAddress({
108
+ publicClient: ctx.publicClient,
109
+ registry,
110
+ agentAddress,
111
+ })
112
+ if (existing !== 0n) {
113
+ return {
114
+ ok: true as const,
115
+ data: { alreadyRegistered: true, agentId: existing.toString(), registry },
116
+ }
117
+ }
118
+ const card = buildAgentCard({
119
+ name: args.name ?? `nebula-${agentAddress.slice(2, 8)}`,
120
+ agentAddress,
121
+ network: ctx.network,
122
+ })
123
+ const { agentId, txHash } = await registerAgent({
124
+ walletClient: ctx.walletClient,
125
+ publicClient: ctx.publicClient,
126
+ registry,
127
+ cardURI: cardToDataUri(card),
128
+ agentAddress,
129
+ })
130
+ return {
131
+ ok: true as const,
132
+ data: { agentId: agentId.toString(), txHash, registry, network: ctx.network },
133
+ }
134
+ } catch (e) {
135
+ return { ok: false as const, error: (e as Error).message }
136
+ }
137
+ },
138
+ }
139
+ }