diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 8f54c0b..18a472b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import type { RiskAssessment } from "@custos/core"; -import { requiresFriction, requiresHardBlock } from "@custos/core"; +import { requiresFriction, requiresHardBlock, detectChainNetwork } from "@custos/core"; import { analyzeSendIntent } from "./compositionRoot.js"; export function App() { @@ -9,12 +9,21 @@ export function App() { const [context, setContext] = useState(""); const [assessment, setAssessment] = useState(null); const [busy, setBusy] = useState(false); + const [unrecognizedNetwork, setUnrecognizedNetwork] = useState(false); async function handleCheck() { + const network = detectChainNetwork(address); + if (network === "other") { + setUnrecognizedNetwork(true); + setAssessment(null); + return; + } + + setUnrecognizedNetwork(false); setBusy(true); try { const result = await analyzeSendIntent.execute({ - destination: { value: address, network: "tron" }, + destination: { value: address, network }, amountUsdt: Number(amount) || 0, context: context ? { text: context } : undefined, }); @@ -62,6 +71,12 @@ export function App() { {busy ? "Analyzing on-device..." : "Check before sending"} + {unrecognizedNetwork && ( +

+ Unrecognized address format — doesn't match Tron or Ethereum. Double-check it before sending. +

+ )} + {assessment && (
{ + it("detects a Tron base58 address", () => { + expect(detectChainNetwork("TAbc123456789abcdefghjkmnpqrstuvwx")).toBe("tron"); + }); + + it("detects an Ethereum hex address", () => { + expect(detectChainNetwork("0x1234567890abcdef1234567890abcdef12345678")).toBe("ethereum"); + }); + + it("detects an Ethereum hex address with uppercase digits", () => { + expect(detectChainNetwork("0xABCDEF1234567890ABCDEF1234567890ABCDEF12")).toBe("ethereum"); + }); + + it("returns other for an address matching neither format", () => { + expect(detectChainNetwork("not-a-real-address")).toBe("other"); + }); + + it("returns other for a Tron-like address of the wrong length", () => { + expect(detectChainNetwork("TAbc123")).toBe("other"); + }); + + it("returns other for an Ethereum-like address missing the 0x prefix", () => { + expect(detectChainNetwork("1234567890abcdef1234567890abcdef12345678")).toBe("other"); + }); + + it("returns other for an empty string", () => { + expect(detectChainNetwork("")).toBe("other"); + }); +}); diff --git a/packages/core/src/domain/services/detectChainNetwork.ts b/packages/core/src/domain/services/detectChainNetwork.ts new file mode 100644 index 0000000..3f6f78e --- /dev/null +++ b/packages/core/src/domain/services/detectChainNetwork.ts @@ -0,0 +1,16 @@ +import type { ChainNetwork } from "../entities/Address.js"; + +const TRON_ADDRESS = /^T[1-9A-HJ-NP-Za-km-z]{33}$/; +const ETHEREUM_ADDRESS = /^0x[0-9a-fA-F]{40}$/; + +/** + * Infers the chain network from an address's format alone. Returns "other" + * for anything matching neither Tron base58 nor Ethereum hex — callers must + * not treat "other" as a safe default, since `RiskListPort.lookup` keys on + * `network:value` and a wrong network silently skips the real risk list. + */ +export function detectChainNetwork(value: string): ChainNetwork { + if (TRON_ADDRESS.test(value)) return "tron"; + if (ETHEREUM_ADDRESS.test(value)) return "ethereum"; + return "other"; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 626d9b0..a95c01e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,6 +13,7 @@ export * from "./domain/ports/WalletPort.js"; export * from "./domain/ports/RiskListPort.js"; export * from "./domain/ports/AuditLogPort.js"; export * from "./domain/services/detectAddressPoisoning.js"; +export * from "./domain/services/detectChainNetwork.js"; // Application export * from "./application/use-cases/AnalyzeSendIntent.js";