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,127 @@
1
+ /**
2
+ * DeFiLlama yield discovery for Mantle.
3
+ *
4
+ * Per the project rule (CLAUDE.md), DeFiLlama is used for ANALYTICS and
5
+ * DISCOVERY only — never for execution. This module fetches the public yields
6
+ * feed, filters to Mantle, and annotates each pool with the risk signals a
7
+ * treasury assistant needs: stablecoin / impermanent-loss / single-vs-multi
8
+ * exposure, and whether the asset is a RESTRICTED product (MI4, USDY, mUSD)
9
+ * that must not be entered without explicit eligibility confirmation.
10
+ *
11
+ * No API key. The endpoint is GET-only and unauthenticated.
12
+ */
13
+
14
+ export const DEFILLAMA_YIELDS_URL = 'https://yields.llama.fi/pools'
15
+
16
+ /** Assets the project treats as restricted (CLAUDE.md). Discovery surfaces them but flags them. */
17
+ const RESTRICTED_PATTERNS: ReadonlyArray<RegExp> = [/\bUSDY\b/i, /\bMI4\b/i, /\bmUSD\b/i]
18
+
19
+ export function isRestrictedAsset(symbol: string, project: string): boolean {
20
+ const hay = `${symbol} ${project}`
21
+ return RESTRICTED_PATTERNS.some(re => re.test(hay))
22
+ }
23
+
24
+ export interface YieldPool {
25
+ /** DeFiLlama protocol slug, e.g. "aave-v3", "agni", "merchant-moe". */
26
+ project: string
27
+ /** Pool asset symbol, e.g. "USDC", "WMNT-USDC". */
28
+ symbol: string
29
+ /** DeFiLlama pool id (opaque). */
30
+ pool: string
31
+ tvlUsd: number
32
+ /** Total APY (base + reward), percent. */
33
+ apy: number
34
+ apyBase: number | null
35
+ apyReward: number | null
36
+ /** 7-day APY change in percentage points (momentum signal). */
37
+ apyPct7D: number | null
38
+ /** True when both legs are stablecoins. */
39
+ stablecoin: boolean
40
+ /** "no" | "yes" — impermanent-loss risk per DeFiLlama. */
41
+ ilRisk: string
42
+ /** "single" | "multi" — token exposure. */
43
+ exposure: string
44
+ poolMeta: string | null
45
+ /** CLAUDE.md restricted product (USDY/MI4/mUSD) — needs eligibility confirmation. */
46
+ restricted: boolean
47
+ }
48
+
49
+ interface RawPool {
50
+ chain?: string
51
+ project?: string
52
+ symbol?: string
53
+ pool?: string
54
+ tvlUsd?: number
55
+ apy?: number
56
+ apyBase?: number | null
57
+ apyReward?: number | null
58
+ apyPct7D?: number | null
59
+ stablecoin?: boolean
60
+ ilRisk?: string
61
+ exposure?: string
62
+ poolMeta?: string | null
63
+ }
64
+
65
+ function toYieldPool(p: RawPool): YieldPool {
66
+ const symbol = p.symbol ?? '?'
67
+ const project = p.project ?? '?'
68
+ return {
69
+ project,
70
+ symbol,
71
+ pool: p.pool ?? '',
72
+ tvlUsd: p.tvlUsd ?? 0,
73
+ apy: p.apy ?? 0,
74
+ apyBase: p.apyBase ?? null,
75
+ apyReward: p.apyReward ?? null,
76
+ apyPct7D: p.apyPct7D ?? null,
77
+ stablecoin: p.stablecoin ?? false,
78
+ ilRisk: p.ilRisk ?? 'unknown',
79
+ exposure: p.exposure ?? 'unknown',
80
+ poolMeta: p.poolMeta ?? null,
81
+ restricted: isRestrictedAsset(symbol, project),
82
+ }
83
+ }
84
+
85
+ export interface FetchYieldsOpts {
86
+ /** Minimum pool TVL in USD (filters dust). Default 50_000. */
87
+ minTvlUsd?: number
88
+ /** Only stablecoin pools (lower-risk treasury parking). */
89
+ stableOnly?: boolean
90
+ /** Exclude pools with impermanent-loss risk. */
91
+ noIlRisk?: boolean
92
+ /** Filter to a single protocol slug substring (e.g. "aave"). */
93
+ project?: string
94
+ /** Sort key. Default "apy". */
95
+ sortBy?: 'apy' | 'tvl'
96
+ /** Max rows. Default 10, hard-capped at 50. */
97
+ limit?: number
98
+ /** Injected fetch for tests. */
99
+ fetchImpl?: typeof fetch
100
+ }
101
+
102
+ /**
103
+ * Fetch + filter + rank Mantle yield pools. Pure aside from the single GET.
104
+ */
105
+ export async function fetchMantleYields(opts: FetchYieldsOpts = {}): Promise<YieldPool[]> {
106
+ const f = opts.fetchImpl ?? fetch
107
+ const res = await f(DEFILLAMA_YIELDS_URL)
108
+ if (!res.ok) throw new Error(`DeFiLlama yields API ${res.status}`)
109
+ const json = (await res.json()) as { status?: string; data?: RawPool[] }
110
+ const all = json.data ?? []
111
+ let pools = all.filter(p => p.chain === 'Mantle').map(toYieldPool)
112
+
113
+ const minTvl = opts.minTvlUsd ?? 50_000
114
+ pools = pools.filter(p => p.tvlUsd >= minTvl)
115
+ if (opts.stableOnly) pools = pools.filter(p => p.stablecoin)
116
+ if (opts.noIlRisk) pools = pools.filter(p => p.ilRisk !== 'yes')
117
+ if (opts.project) {
118
+ const needle = opts.project.toLowerCase()
119
+ pools = pools.filter(p => p.project.toLowerCase().includes(needle))
120
+ }
121
+
122
+ const key = opts.sortBy === 'tvl' ? (p: YieldPool) => p.tvlUsd : (p: YieldPool) => p.apy
123
+ pools.sort((a, b) => key(b) - key(a))
124
+
125
+ const limit = Math.min(Math.max(opts.limit ?? 10, 1), 50)
126
+ return pools.slice(0, limit)
127
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Always-on guidance contributed to the frozen prefix when plugin-onchain is
3
+ * loaded.
4
+ */
5
+
6
+ export const ONCHAIN_GUIDANCE = `On-chain wallet + chain ops (Mantle; gas token MNT):
7
+
8
+ - Limits + guardrails: call \`policy.show\` to report the active fund-control policy (caps, allowlists, slippage cap, autonomy tier, approval threshold) for "what are my limits" / "what can you spend" / "show the policy", and before explaining why an action was blocked or needs approval.
9
+ - Your agent EOA pays gas; the operator funds it. Value-moving tools (\`chain.send\`, \`swap.execute\`, \`aave.supply\`/\`aave.withdraw\`, \`chain.wrap\`/\`chain.unwrap\`, \`chain.write\` w/ value) run through a deterministic policy: hard caps and allowlists can BLOCK an action outright, and material-risk actions REQUIRE operator approval before broadcast even in \`yolo\` mode. Every write is also dry-run simulated first; a failing simulation aborts before any gas is spent. The AI is advisory — the controls are enforced in code, not by you. Do not try to bypass a block or approval gate.
10
+ - Balance + identity: \`chain.balance\` with no args returns native MNT + every ERC-20 the agent has ever held (Transfer-event discovery, no curated list). Pass \`token\` for a single asset or \`address\` to inspect another wallet. \`account.info\` bundles wallet + (optional) iNFT + brain provider + recent activity. Call it before answering identity / "who are you" questions instead of guessing.
11
+ - Tokens: \`tokens.info\` resolves a symbol or address to {address, decimals, symbol}. The Agni pool list is bundled; unknown tokens fall back to on-chain reads (cached after).
12
+ - Transfers: \`chain.send\` auto-detects native vs ERC-20 by token symbol; recipients are 0x addresses. \`chain.wrap\`/\`chain.unwrap\` move between native MNT and WMNT.
13
+ - Trading: two DEX venues, Agni Finance (Uniswap-V3-style) and Merchant Moe (Liquidity Book). EASIEST: \`swap.best\` quotes both and executes on the better venue in one call; \`swap.compare\` does the same read-only. For a specific venue use \`swap.quote\`/\`swap.execute\` (Agni) or \`moe.quote\`/\`moe.swap\` (Moe). Slippage default 0.5%. ERC-20 input swaps auto-approve the venue's router on first use. A token with no pool on a venue can't trade there (chain.balance still shows it).
14
+ - Lending: \`aave.markets\` lists every Aave V3 reserve with its live supply + variable-borrow APR (compare rates before supplying/borrowing). \`aave.position\` reads the agent's Aave V3 supplied/borrowed/health-factor; \`aave.supply\`/\`aave.withdraw\` move collateral in/out; \`aave.borrow\`/\`aave.repay\` open/close variable-rate debt against that collateral. Borrowing is leverage: the borrow/repay receipts report the resulting health factor (lower = closer to liquidation), and the pre-flight simulation rejects an over-borrow. Surface the health factor when proposing a borrow.
15
+ - CEX balance: \`cex.balance\` reads the Bybit Unified account balance (read-only) so you can show a combined CEX + on-chain treasury picture. Needs BYBIT_API_KEY/SECRET in the env (read-only key). You CANNOT trade or transfer on the CEX — those bypass the on-chain safety pipeline and are intentionally not exposed.
16
+ - Counterparty intel: \`nansen.labels\` returns Nansen entity labels for an address (exchange / fund / smart-money / contract / red-flags like scam, hack, sanctioned, mixer). Call it to vet a transfer recipient or a contract before interacting; if it returns \`flagged: true\`, do NOT transact without operator confirmation. Needs NANSEN_API_KEY in the env — if unset it reports so and you proceed without the intel.
17
+ - Token risk: \`risk.token\` vets a token before you hold or buy it — can-you-exit (live quote on Agni/Moe), liquidity depth, restricted-RWA flag, real-contract check — returning a low/elevated/high verdict. Call it before proposing a buy/supply into an unfamiliar token; if it returns high (untradeable / no contract) do NOT trade, and treat restricted/elevated as needing operator confirmation.
18
+ - Yield discovery: \`defi.yields\` lists Mantle lending/LP opportunities (via DeFiLlama) ranked by APY or TVL, with risk signals (stablecoin, IL risk, exposure, 7d trend). Use it for "best yield on Mantle" / "where to park USDC". It is ANALYTICS ONLY — it never moves funds; to act on a result, use \`aave.supply\` or \`swap.execute\`. Rows flagged \`restricted: true\` (USDY/MI4/mUSD) need eligibility confirmation before you propose entering them.
19
+ - Analysis: \`chain.tx\` decodes any tx hash. \`chain.contract\` introspects code/proxy/ERC standards. \`chain.activity\` shows recent transfers.
20
+ - Generic: \`chain.read\`/\`chain.write\` for any contract not covered above; takes \`signature\` + \`args\` like cast.
21
+ - Dry-run: \`tx.simulate\` previews ANY call (\`to\` + \`signature\`/\`args\` or raw \`data\`, + optional \`value\`/\`from\`) without broadcasting — returns would-succeed + gas, or the decoded revert reason. Use it to preview an action before \`chain.write\`, debug a revert, or sanity-check a call you're unsure about.
22
+ - Blockchain: \`chain.block\` for current head/timestamp/gasUsed. \`chain.gas\` for current gas price.
23
+ `
package/src/index.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * nebula-ai-plugin-onchain
3
+ *
4
+ * Brain limbs for on-chain operations on Mantle:
5
+ *
6
+ * Wallet/account: account.info, account.balance
7
+ * Balance: chain.balance
8
+ * Tokens: tokens.info
9
+ * Transfers: chain.send, chain.wrap, chain.unwrap
10
+ * Trading: swap.quote, swap.execute (Agni V3, 3-tier scan)
11
+ * moe.quote, moe.swap (Merchant Moe Liquidity Book)
12
+ * swap.compare, swap.best (multi-venue best execution)
13
+ * Lending: aave.position, aave.markets, aave.supply, aave.withdraw,
14
+ * aave.borrow, aave.repay (Aave V3)
15
+ * Discovery: defi.yields (DeFiLlama, read-only analytics)
16
+ * Risk: risk.token (pre-trade token risk assessment, read-only)
17
+ * nansen.labels (Nansen counterparty intel, env NANSEN_API_KEY)
18
+ * Controls: policy.show (active fund-control policy, read-only)
19
+ * Blockchain: chain.block, chain.gas
20
+ * Analysis: chain.tx, chain.contract, chain.activity
21
+ * Generic: chain.read, chain.write, tx.simulate
22
+ *
23
+ * Value-moving tools run through policy -> simulate -> (approval) -> execute.
24
+ *
25
+ * Side-band runtime ctx attached to PluginContext under `.onchain` (see
26
+ * `OnchainRuntimeContext` in `./types.ts`). Without it, the plugin registers
27
+ * nothing — graceful no-op for unit-test loaders.
28
+ */
29
+
30
+ import type { NativePlugin, ToolDef } from 'nebula-ai-core'
31
+ export {
32
+ simulateNativeSend,
33
+ simulateContractWrite,
34
+ simulateRawTx,
35
+ type SimResult,
36
+ } from './simulate'
37
+ export {
38
+ evaluatePolicy,
39
+ policyFromEnv,
40
+ type OnchainPolicy,
41
+ type PolicyAction,
42
+ type PolicyVerdict,
43
+ } from './policy'
44
+ export { policyRequiresApprovalForCall } from './approval'
45
+ import {
46
+ makeAaveBorrow,
47
+ makeAaveMarkets,
48
+ makeAavePosition,
49
+ makeAaveRepay,
50
+ makeAaveSupply,
51
+ makeAaveWithdraw,
52
+ } from './tools/aave'
53
+ import { makeAccountInfo } from './tools/account'
54
+ import { makeAccountBalance } from './tools/account-balance'
55
+ import { makeChainActivity, makeChainContract, makeChainTx } from './tools/analysis'
56
+ import { makeChainBalance } from './tools/balance'
57
+ import { makeChainBlock, makeChainGas } from './tools/blockchain'
58
+ import { makeCexBalance } from './tools/cex'
59
+ import { makeDefiYields } from './tools/defillama'
60
+ import { makeChainRead, makeChainWrite } from './tools/generic'
61
+ import { makeIdentityRegister, makeIdentityResolve } from './tools/identity'
62
+ import { makeMoeQuote, makeMoeSwap } from './tools/moe'
63
+ import { makeNansenLabels } from './tools/nansen'
64
+ import { makePolicyShow } from './tools/policy-show'
65
+ import { makeRiskToken } from './tools/risk'
66
+ import { makeTxSimulate } from './tools/simulate-tx'
67
+ import { makeSwapExecute, makeSwapQuote } from './tools/swap'
68
+ import { makeSwapBest, makeSwapCompare } from './tools/swap-best'
69
+ import { makeTokensInfo } from './tools/tokens-info'
70
+ import { makeChainSend } from './tools/transfer'
71
+ import { makeChainUnwrap, makeChainWrap } from './tools/wrap'
72
+ import type { OnchainRuntimeContext } from './types'
73
+
74
+ export { ONCHAIN_GUIDANCE } from './guidance'
75
+ export { discoverMintBlock } from './mint-block'
76
+ export type { OnchainRuntimeContext } from './types'
77
+ export {
78
+ AGNI_BY_NETWORK,
79
+ AAVE_POOL_BY_NETWORK,
80
+ MULTICALL3,
81
+ FEE_TIERS,
82
+ DEFAULT_DEADLINE_SECS,
83
+ DEFAULT_SLIPPAGE_BPS,
84
+ } from './constants'
85
+
86
+ const plugin: NativePlugin = {
87
+ name: 'onchain',
88
+ register: ctx => {
89
+ const onchain = (ctx as unknown as { onchain?: OnchainRuntimeContext }).onchain
90
+ if (!onchain) return // soft-init for tests/non-onchain contexts
91
+
92
+ ctx.registerTool(makeAccountInfo(onchain) as ToolDef)
93
+ ctx.registerTool(makeAccountBalance(onchain) as ToolDef)
94
+ ctx.registerTool(makeChainBalance(onchain) as ToolDef)
95
+ ctx.registerTool(makeTokensInfo(onchain) as ToolDef)
96
+
97
+ ctx.registerTool(makeChainSend(onchain) as ToolDef)
98
+ ctx.registerTool(makeChainWrap(onchain) as ToolDef)
99
+ ctx.registerTool(makeChainUnwrap(onchain) as ToolDef)
100
+
101
+ ctx.registerTool(makeSwapQuote(onchain) as ToolDef)
102
+ ctx.registerTool(makeSwapExecute(onchain) as ToolDef)
103
+
104
+ ctx.registerTool(makeMoeQuote(onchain) as ToolDef)
105
+ ctx.registerTool(makeMoeSwap(onchain) as ToolDef)
106
+
107
+ ctx.registerTool(makeSwapCompare(onchain) as ToolDef)
108
+ ctx.registerTool(makeSwapBest(onchain) as ToolDef)
109
+
110
+ ctx.registerTool(makeAavePosition(onchain) as ToolDef)
111
+ ctx.registerTool(makeAaveMarkets(onchain) as ToolDef)
112
+ ctx.registerTool(makeAaveSupply(onchain) as ToolDef)
113
+ ctx.registerTool(makeAaveWithdraw(onchain) as ToolDef)
114
+ ctx.registerTool(makeAaveBorrow(onchain) as ToolDef)
115
+ ctx.registerTool(makeAaveRepay(onchain) as ToolDef)
116
+
117
+ ctx.registerTool(makeDefiYields(onchain) as ToolDef)
118
+ ctx.registerTool(makeRiskToken(onchain) as ToolDef)
119
+ ctx.registerTool(makeNansenLabels(onchain) as ToolDef)
120
+ ctx.registerTool(makeCexBalance(onchain) as ToolDef)
121
+ ctx.registerTool(makePolicyShow(onchain) as ToolDef)
122
+
123
+ ctx.registerTool(makeIdentityResolve(onchain) as ToolDef)
124
+ ctx.registerTool(makeIdentityRegister(onchain) as ToolDef)
125
+
126
+ ctx.registerTool(makeChainBlock(onchain) as ToolDef)
127
+ ctx.registerTool(makeChainGas(onchain) as ToolDef)
128
+
129
+ ctx.registerTool(makeChainTx(onchain) as ToolDef)
130
+ ctx.registerTool(makeChainContract(onchain) as ToolDef)
131
+ ctx.registerTool(makeChainActivity(onchain) as ToolDef)
132
+
133
+ ctx.registerTool(makeChainRead(onchain) as ToolDef)
134
+ ctx.registerTool(makeChainWrite(onchain) as ToolDef)
135
+ ctx.registerTool(makeTxSimulate(onchain) as ToolDef)
136
+ },
137
+ }
138
+
139
+ export default plugin
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One-shot iNFT mint block discovery for the Transfer-event scan floor.
3
+ *
4
+ * Uses `rawGetLogs` to bypass viem's `getLogs` topic-stripping (verified May
5
+ * 1 2026; without raw, the call falls through to "topics:[]" and the Mantle RPC
6
+ * rejects with "result set exceeds 10000 logs"). Walks recent → old in
7
+ * 50k-block chunks, capped at LOG_SCAN_MAX_CHUNKS, and returns the first
8
+ * (newest) match. iNFT mint Transfers have `from = 0x0` and `tokenId` in
9
+ * topic3, so the filter is precise.
10
+ */
11
+
12
+ import type { Address, PublicClient } from 'viem'
13
+ import { LOG_SCAN_CHUNK_BLOCKS, LOG_SCAN_MAX_CHUNKS, TRANSFER_TOPIC0 } from './constants'
14
+ import { rawGetLogs } from './raw-logs'
15
+
16
+ function pad32(hex: string): `0x${string}` {
17
+ const stripped = hex.startsWith('0x') ? hex.slice(2) : hex
18
+ return `0x${stripped.padStart(64, '0')}` as `0x${string}`
19
+ }
20
+
21
+ export async function discoverMintBlock(
22
+ client: PublicClient,
23
+ contract: Address,
24
+ tokenId: bigint,
25
+ ): Promise<bigint | null> {
26
+ const head = await client.getBlockNumber()
27
+ const tokenIdTopic = pad32(tokenId.toString(16))
28
+ const fromZeroTopic = pad32('0')
29
+ let cursor = head
30
+ for (let chunks = 0; chunks < LOG_SCAN_MAX_CHUNKS && cursor > 0n; chunks++) {
31
+ const start = cursor - LOG_SCAN_CHUNK_BLOCKS + 1n
32
+ const from = start > 0n ? start : 0n
33
+ try {
34
+ const logs = await rawGetLogs({
35
+ client,
36
+ address: contract,
37
+ topics: [TRANSFER_TOPIC0, fromZeroTopic, null, tokenIdTopic],
38
+ fromBlock: from,
39
+ toBlock: cursor,
40
+ })
41
+ if (logs.length > 0) {
42
+ const earliest = logs.reduce((acc, l) =>
43
+ BigInt(l.blockNumber) < BigInt(acc.blockNumber) ? l : acc,
44
+ )
45
+ return BigInt(earliest.blockNumber)
46
+ }
47
+ } catch {
48
+ // RPC chunk failure; keep walking.
49
+ }
50
+ cursor = from - 1n
51
+ }
52
+ return null
53
+ }
package/src/moe.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Merchant Moe Liquidity Book quote + swap-calldata builder.
3
+ *
4
+ * The LBQuoter's `findBestPathFromAmountIn(route, amountIn)` returns the best
5
+ * route across LB pairs as parallel arrays (route, binSteps, versions, amounts).
6
+ * The last `amounts` element is the output. Those arrays feed directly into the
7
+ * router's `Path` struct, so the executed swap uses exactly the quoted route.
8
+ *
9
+ * Native handling: the quoter/router treat native MNT via WNATIVE (WMNT), so a
10
+ * native leg uses the WMNT address in the token path plus the native-specific
11
+ * router entrypoint (swapExactNATIVEForTokens / swapExactTokensForNATIVE).
12
+ */
13
+
14
+ import { type Address, type PublicClient, encodeFunctionData } from 'viem'
15
+ import { LB_QUOTER_ABI, LB_ROUTER_ABI } from './abis'
16
+
17
+ export interface MoeQuote {
18
+ /** Token path the router will use (== quoter route). */
19
+ route: readonly Address[]
20
+ /** Per-hop bin steps for the Path struct. */
21
+ binSteps: readonly bigint[]
22
+ /** Per-hop pair versions (0=V1, 1=V2, 2=V2_1, 3=V2_2). */
23
+ versions: readonly number[]
24
+ /** Quoted output amount (raw units of the last route token). */
25
+ amountOut: bigint
26
+ }
27
+
28
+ /**
29
+ * Quote `amountIn` of `route[0]` to `route[route.length-1]` via Merchant Moe LB.
30
+ * Returns null when no route has liquidity (amountOut == 0).
31
+ */
32
+ export async function quoteMoe(opts: {
33
+ client: PublicClient
34
+ quoter: Address
35
+ route: readonly Address[]
36
+ amountIn: bigint
37
+ }): Promise<MoeQuote | null> {
38
+ const { client, quoter, route, amountIn } = opts
39
+ const q = (await client.readContract({
40
+ address: quoter,
41
+ abi: LB_QUOTER_ABI,
42
+ functionName: 'findBestPathFromAmountIn',
43
+ args: [route as readonly Address[], amountIn],
44
+ })) as {
45
+ route: readonly Address[]
46
+ binSteps: readonly bigint[]
47
+ versions: readonly number[]
48
+ amounts: readonly bigint[]
49
+ }
50
+ const amounts = q.amounts
51
+ const amountOut = amounts.length > 0 ? amounts[amounts.length - 1]! : 0n
52
+ if (amountOut === 0n) return null
53
+ return { route: q.route, binSteps: q.binSteps, versions: q.versions, amountOut }
54
+ }
55
+
56
+ export interface MoeSwapCalldata {
57
+ data: `0x${string}`
58
+ /** msg.value (native input only). */
59
+ value: bigint
60
+ }
61
+
62
+ /**
63
+ * Encode the LB router swap call. Picks the entrypoint by native in/out:
64
+ * - native IN -> swapExactNATIVEForTokens (msg.value = amountIn)
65
+ * - native OUT -> swapExactTokensForNATIVE
66
+ * - ERC-20 ↔ ERC-20 -> swapExactTokensForTokens
67
+ */
68
+ export function encodeMoeSwap(opts: {
69
+ quote: MoeQuote
70
+ amountIn: bigint
71
+ amountOutMin: bigint
72
+ to: Address
73
+ deadline: bigint
74
+ nativeIn: boolean
75
+ nativeOut: boolean
76
+ }): MoeSwapCalldata {
77
+ const { quote, amountIn, amountOutMin, to, deadline, nativeIn, nativeOut } = opts
78
+ const path = {
79
+ pairBinSteps: quote.binSteps as readonly bigint[],
80
+ versions: quote.versions as readonly number[],
81
+ tokenPath: quote.route as readonly Address[],
82
+ }
83
+ if (nativeIn) {
84
+ return {
85
+ data: encodeFunctionData({
86
+ abi: LB_ROUTER_ABI,
87
+ functionName: 'swapExactNATIVEForTokens',
88
+ args: [amountOutMin, path, to, deadline],
89
+ }),
90
+ value: amountIn,
91
+ }
92
+ }
93
+ if (nativeOut) {
94
+ return {
95
+ data: encodeFunctionData({
96
+ abi: LB_ROUTER_ABI,
97
+ functionName: 'swapExactTokensForNATIVE',
98
+ args: [amountIn, amountOutMin, path, to, deadline],
99
+ }),
100
+ value: 0n,
101
+ }
102
+ }
103
+ return {
104
+ data: encodeFunctionData({
105
+ abi: LB_ROUTER_ABI,
106
+ functionName: 'swapExactTokensForTokens',
107
+ args: [amountIn, amountOutMin, path, to, deadline],
108
+ }),
109
+ value: 0n,
110
+ }
111
+ }
package/src/nansen.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Nansen address-intelligence (read-only). Returns the entity labels Nansen
3
+ * has for an address — exchange / fund / smart-money / contract / and red-flag
4
+ * categories (scam, hack, sanctioned, mixer) — so the agent can vet a
5
+ * counterparty before transacting. Fits the "unified risk analysis" thesis.
6
+ *
7
+ * Auth: `apiKey` header (NANSEN_API_KEY, env only — never committed). Endpoints
8
+ * are credit-metered on Nansen's side; the tool surfaces a clear message when
9
+ * the key is unset or out of credits rather than failing hard.
10
+ */
11
+
12
+ export const NANSEN_BASE = 'https://api.nansen.ai/api/v1'
13
+
14
+ /** Label categories that should raise a counterparty flag. */
15
+ const RED_FLAG_CATEGORIES = ['scam', 'hack', 'exploit', 'sanctioned', 'mixer', 'phish', 'fraud']
16
+
17
+ export interface NansenLabel {
18
+ label: string
19
+ category: string
20
+ }
21
+
22
+ export interface NansenLabelsResult {
23
+ ok: boolean
24
+ /** Set when the call failed (missing key, no credits, HTTP error). */
25
+ error?: string
26
+ labels: NansenLabel[]
27
+ }
28
+
29
+ export async function fetchNansenLabels(opts: {
30
+ address: string
31
+ chain: string
32
+ apiKey: string
33
+ fetchImpl?: typeof fetch
34
+ }): Promise<NansenLabelsResult> {
35
+ const { address, chain, apiKey, fetchImpl } = opts
36
+ const f = fetchImpl ?? fetch
37
+ let res: Response
38
+ try {
39
+ res = await f(`${NANSEN_BASE}/profiler/address/labels`, {
40
+ method: 'POST',
41
+ headers: { apiKey, 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ address, chain }),
43
+ })
44
+ } catch (e) {
45
+ return {
46
+ ok: false,
47
+ error: `Nansen request failed: ${(e as Error).message.slice(0, 120)}`,
48
+ labels: [],
49
+ }
50
+ }
51
+ if (res.status === 403) {
52
+ const j = (await res.json().catch(() => ({}))) as { error?: string }
53
+ return {
54
+ ok: false,
55
+ error: j.error ?? 'Nansen 403 (out of credits or unauthorized)',
56
+ labels: [],
57
+ }
58
+ }
59
+ if (!res.ok) return { ok: false, error: `Nansen API ${res.status}`, labels: [] }
60
+ const j = (await res.json().catch(() => ({}))) as {
61
+ data?: Array<{ label?: string; category?: string }>
62
+ }
63
+ const labels = (j.data ?? []).map(l => ({
64
+ label: l.label ?? '?',
65
+ category: l.category ?? 'unknown',
66
+ }))
67
+ return { ok: true, labels }
68
+ }
69
+
70
+ /** Red-flag category names present in a label set (for a counterparty warning). */
71
+ export function redFlags(labels: NansenLabel[]): string[] {
72
+ const cats = new Set<string>()
73
+ for (const l of labels) {
74
+ const c = l.category.toLowerCase()
75
+ if (RED_FLAG_CATEGORIES.some(rf => c.includes(rf))) cats.add(l.category)
76
+ }
77
+ return [...cats]
78
+ }
79
+
80
+ /** Distinct label categories, with counts, for a compact summary. */
81
+ export function categorySummary(labels: NansenLabel[]): Record<string, number> {
82
+ const out: Record<string, number> = {}
83
+ for (const l of labels) out[l.category] = (out[l.category] ?? 0) + 1
84
+ return out
85
+ }