@@ -43,7 +43,7 @@ export function TokenBalanceCard({ data }: { data: Out<"getTokenBalance"> }) {
diff --git a/src/components/cards/transaction-preview-card.tsx b/src/components/cards/transaction-preview-card.tsx
index 98d57a2..dff5d97 100644
--- a/src/components/cards/transaction-preview-card.tsx
+++ b/src/components/cards/transaction-preview-card.tsx
@@ -91,7 +91,7 @@ export function TransactionPreviewCard({ data, onAskAgent }: Props) {
diff --git a/src/lib/agent/prompt.ts b/src/lib/agent/prompt.ts
index f7a90c8..893cdd3 100644
--- a/src/lib/agent/prompt.ts
+++ b/src/lib/agent/prompt.ts
@@ -11,6 +11,7 @@ ${connectedAddress ? `The user's connected wallet is ${connectedAddress}. "My wa
- After tools return, give a short, clear answer. The UI already renders a rich card for every tool result, so summarize the key numbers and explain what they mean — don't repeat every field.
- If a tool fails, explain the error in plain language and suggest a fix (e.g. check the address, retry, use a faucet).
- Format amounts with units (ETH, gwei). Shorten addresses like 0x1234…abcd in prose.
+- Tools accept ENS names (e.g. alice.eth) wherever they take an address. Pass the name as-is; the tool resolves it on ${NETWORK_NAME}'s ENS. When a result includes ensName, mention both the name and the resolved address.
## Wallet analysis
- Use analyzeWallet, then interpret the stats: overall behaviour, incoming vs outgoing, spending pattern, gas costs, main counterparties, contract usage, notable or risky signals. Base insights only on the returned data.
@@ -19,6 +20,7 @@ ${connectedAddress ? `The user's connected wallet is ${connectedAddress}. "My wa
- You can NEVER sign, send or broadcast a transaction, and you never ask for or handle private keys or seed phrases. If someone shares one, tell them to treat it as compromised.
- To send ETH, call prepareTransaction. It validates the address, amount, balance and network and returns a preview. The user then reviews it and approves it in their own wallet by clicking the button on the preview card.
- Never claim a transaction was sent. Say it is prepared and waiting for their approval.
+- When the recipient is an ENS name, tell the user which address it resolved to so they can confirm it before approving.
- If the user gives no recipient or amount, ask for it. If a check fails (e.g. insufficient balance), explain it.
- Only native ETH transfers on ${NETWORK_NAME} are supported for now.`;
}
diff --git a/src/lib/agent/tools.ts b/src/lib/agent/tools.ts
index d0a98da..70ba16c 100644
--- a/src/lib/agent/tools.ts
+++ b/src/lib/agent/tools.ts
@@ -14,7 +14,7 @@ import {
KNOWN_TOKENS,
} from "../blockchain/reader";
import { prepareEthTransfer } from "../blockchain/transfer";
-import { parseAddress } from "../blockchain/validation";
+import { resolveAddress } from "../blockchain/ens";
/**
* The agent's toolbox. Each tool is a thin, typed wrapper around a reusable
@@ -36,7 +36,7 @@ export type ToolsContext = {
const addressSchema = z
.string()
- .describe("A 0x-prefixed EVM address. Use the connected wallet address when the user says 'my wallet'.");
+ .describe("A 0x-prefixed EVM address or an ENS name (e.g. alice.eth). Use the connected wallet address when the user says 'my wallet'.");
export function createChainlyTools(ctx: ToolsContext) {
const requireAddress = (address: string | undefined) => {
@@ -89,7 +89,7 @@ export function createChainlyTools(ctx: ToolsContext) {
limit: z.number().int().min(5).max(200).default(50).describe("How many recent transactions to analyze"),
}),
execute: async ({ address, limit }) => {
- const target = parseAddress(requireAddress(address));
+ const { address: target, ensName } = await resolveAddress(ctx.client, requireAddress(address));
const [txs, tokenTransfers] = await Promise.all([
fetchTransactions(target, limit, ctx.explorer),
// Token history is a nice-to-have: don't fail the whole analysis on it.
@@ -97,6 +97,7 @@ export function createChainlyTools(ctx: ToolsContext) {
]);
return {
...analyzeWalletActivity(target, txs, tokenTransfers),
+ ensName,
analyzedLimit: limit,
source: explorerName(ctx.explorer),
};
@@ -107,7 +108,7 @@ export function createChainlyTools(ctx: ToolsContext) {
description:
"Prepare a native ETH transfer from the user's connected wallet on Ethereum Sepolia. Validates the recipient and amount, checks the balance, estimates gas and returns a preview. This does NOT send anything: the user must review and approve it in their wallet. Also use this to estimate the gas fee of a transfer.",
inputSchema: z.object({
- to: z.string().describe("Recipient 0x address"),
+ to: z.string().describe("Recipient 0x address or ENS name (e.g. alice.eth)"),
amountEth: z.string().describe("Amount of ETH as a decimal string, e.g. '0.001'"),
}),
execute: async ({ to, amountEth }) =>
diff --git a/src/lib/blockchain/ens.ts b/src/lib/blockchain/ens.ts
new file mode 100644
index 0000000..23a758d
--- /dev/null
+++ b/src/lib/blockchain/ens.ts
@@ -0,0 +1,54 @@
+import type { Address } from "viem";
+import { normalize } from "viem/ens";
+import type { ChainClient } from "./types";
+import { NETWORK_NAME } from "./chain";
+import { BlockchainError, toBlockchainError } from "./errors";
+import { parseAddress } from "./validation";
+
+/**
+ * ENS support. Names are resolved on Ethereum Sepolia's own ENS deployment,
+ * which is separate from mainnet: a name registered on mainnet does not
+ * automatically exist (or point to the same address) on Sepolia.
+ */
+
+export type ResolvedAddress = { address: Address; ensName: string | null };
+
+/** Anything that isn't 0x-prefixed but contains a dot, e.g. "alice.eth". */
+export function looksLikeEnsName(input: string) {
+ const value = input.trim();
+ return !/^0x/i.test(value) && value.includes(".");
+}
+
+/**
+ * Accept either a 0x address or an ENS name and return a checksummed address.
+ * The resolved address is what gets used (and shown), never the name alone.
+ */
+export async function resolveAddress(client: ChainClient, input: string, label = "address"): Promise