diff --git a/packages/xmtp-cli/src/commands/client/change-recovery-identifier.ts b/packages/xmtp-cli/src/commands/client/change-recovery-identifier.ts index 0c654698f..ff52eb1b3 100644 --- a/packages/xmtp-cli/src/commands/client/change-recovery-identifier.ts +++ b/packages/xmtp-cli/src/commands/client/change-recovery-identifier.ts @@ -1,5 +1,6 @@ import { Flags } from "@oclif/core"; import { BaseCommand } from "../../baseCommand.js"; +import { validateIdentifier } from "../../utils/client.js"; import { identifierKindMap } from "../../utils/enums.js"; export default class ClientChangeRecoveryIdentifier extends BaseCommand { @@ -55,9 +56,12 @@ The recovery identifier must be an Ethereum address.`; const { flags } = await this.parse(ClientChangeRecoveryIdentifier); const client = await this.initClient(); + const identifierKind = identifierKindMap[flags.kind]; + validateIdentifier(flags.identifier, identifierKind); + // Build identifier before confirming so invalid input fails fast const identifier = { - identifierKind: identifierKindMap[flags.kind], + identifierKind, identifier: flags.identifier.toLowerCase(), }; diff --git a/packages/xmtp-cli/src/commands/client/inbox-id.ts b/packages/xmtp-cli/src/commands/client/inbox-id.ts index ee161ec88..2ff7ddc38 100644 --- a/packages/xmtp-cli/src/commands/client/inbox-id.ts +++ b/packages/xmtp-cli/src/commands/client/inbox-id.ts @@ -1,5 +1,6 @@ import { Flags } from "@oclif/core"; import { BaseCommand } from "../../baseCommand.js"; +import { validateIdentifier } from "../../utils/client.js"; import { identifierKindMap } from "../../utils/enums.js"; export default class ClientInboxId extends BaseCommand { @@ -51,8 +52,11 @@ Returns null if the identifier has no associated inbox ID (not registered).`; const { flags } = await this.parse(ClientInboxId); const client = await this.initClient(); + const identifierKind = identifierKindMap[flags.kind]; + validateIdentifier(flags.identifier, identifierKind); + const identifier = { - identifierKind: identifierKindMap[flags.kind], + identifierKind, identifier: flags.identifier.toLowerCase(), }; diff --git a/packages/xmtp-cli/src/commands/client/remove-account.ts b/packages/xmtp-cli/src/commands/client/remove-account.ts index fa6679b0d..d885dbdb3 100644 --- a/packages/xmtp-cli/src/commands/client/remove-account.ts +++ b/packages/xmtp-cli/src/commands/client/remove-account.ts @@ -1,5 +1,6 @@ import { Flags } from "@oclif/core"; import { BaseCommand } from "../../baseCommand.js"; +import { validateIdentifier } from "../../utils/client.js"; import { identifierKindMap } from "../../utils/enums.js"; export default class ClientRemoveAccount extends BaseCommand { @@ -56,9 +57,12 @@ this client's inbox.`; const { flags } = await this.parse(ClientRemoveAccount); const client = await this.initClient(); + const identifierKind = identifierKindMap[flags.kind]; + validateIdentifier(flags.identifier, identifierKind); + // Build identifier before confirming so invalid input fails fast const identifier = { - identifierKind: identifierKindMap[flags.kind], + identifierKind, identifier: flags.identifier.toLowerCase(), }; diff --git a/packages/xmtp-cli/src/commands/conversation/add-members.ts b/packages/xmtp-cli/src/commands/conversation/add-members.ts index 26b58e7f3..30cc32ff8 100644 --- a/packages/xmtp-cli/src/commands/conversation/add-members.ts +++ b/packages/xmtp-cli/src/commands/conversation/add-members.ts @@ -1,6 +1,7 @@ import { Args } from "@oclif/core"; import { IdentifierKind } from "@xmtp/node-sdk"; import { BaseCommand } from "../../baseCommand.js"; +import { validateIdentifier } from "../../utils/client.js"; import { requireGroup } from "../../utils/conversation.js"; export default class ConversationAddMembers extends BaseCommand { @@ -63,10 +64,13 @@ Requires appropriate permissions to add members (based on group settings).`; this.error(`Conversation not found: ${args.id}`); } - const identifiers = addresses.map((address) => ({ - identifier: address.toLowerCase(), - identifierKind: IdentifierKind.Ethereum, - })); + const identifiers = addresses.map((address) => { + validateIdentifier(address, IdentifierKind.Ethereum); + return { + identifier: address.toLowerCase(), + identifierKind: IdentifierKind.Ethereum, + }; + }); const group = requireGroup(conversation); await group.addMembersByIdentifiers(identifiers); diff --git a/packages/xmtp-cli/src/commands/conversation/remove-members.ts b/packages/xmtp-cli/src/commands/conversation/remove-members.ts index d6ff8ce17..9a223cfce 100644 --- a/packages/xmtp-cli/src/commands/conversation/remove-members.ts +++ b/packages/xmtp-cli/src/commands/conversation/remove-members.ts @@ -1,6 +1,7 @@ import { Args } from "@oclif/core"; import { IdentifierKind } from "@xmtp/node-sdk"; import { BaseCommand } from "../../baseCommand.js"; +import { validateIdentifier } from "../../utils/client.js"; import { requireGroup } from "../../utils/conversation.js"; export default class ConversationRemoveMembers extends BaseCommand { @@ -64,10 +65,13 @@ Requires appropriate permissions to remove members (based on group settings).`; this.error(`Conversation not found: ${args.id}`); } - const identifiers = addresses.map((address) => ({ - identifier: address.toLowerCase(), - identifierKind: IdentifierKind.Ethereum, - })); + const identifiers = addresses.map((address) => { + validateIdentifier(address, IdentifierKind.Ethereum); + return { + identifier: address.toLowerCase(), + identifierKind: IdentifierKind.Ethereum, + }; + }); const group = requireGroup(conversation); await group.removeMembersByIdentifiers(identifiers); diff --git a/packages/xmtp-cli/src/commands/conversations/create-dm.ts b/packages/xmtp-cli/src/commands/conversations/create-dm.ts index 5aca313a1..4fd0b6c2d 100644 --- a/packages/xmtp-cli/src/commands/conversations/create-dm.ts +++ b/packages/xmtp-cli/src/commands/conversations/create-dm.ts @@ -1,5 +1,6 @@ import { Args, Flags } from "@oclif/core"; import { BaseCommand } from "../../baseCommand.js"; +import { validateIdentifier } from "../../utils/client.js"; import { identifierKindMap } from "../../utils/enums.js"; export default class ConversationsCreateDm extends BaseCommand { @@ -50,9 +51,12 @@ Returns the DM's ID and details.`; const { args, flags } = await this.parse(ConversationsCreateDm); const client = await this.initClient(); + const identifierKind = identifierKindMap[flags["identifier-kind"]]; + validateIdentifier(args.identifier, identifierKind); + const identifier = { identifier: args.identifier.toLowerCase(), - identifierKind: identifierKindMap[flags["identifier-kind"]], + identifierKind, }; const dm = await client.conversations.createDmWithIdentifier(identifier); diff --git a/packages/xmtp-cli/src/commands/conversations/create-group.ts b/packages/xmtp-cli/src/commands/conversations/create-group.ts index e081c35a6..3b9c8a8db 100644 --- a/packages/xmtp-cli/src/commands/conversations/create-group.ts +++ b/packages/xmtp-cli/src/commands/conversations/create-group.ts @@ -5,6 +5,7 @@ import { type CreateGroupOptions, } from "@xmtp/node-sdk"; import { BaseCommand } from "../../baseCommand.js"; +import { validateIdentifier } from "../../utils/client.js"; export default class ConversationsCreateGroup extends BaseCommand { static description = `Create a new group conversation. @@ -88,10 +89,13 @@ Returns the new group's ID and details.`; const client = await this.initClient(); - const identifierObjects = identifiers.map((id) => ({ - identifier: id.toLowerCase(), - identifierKind: IdentifierKind.Ethereum, - })); + const identifierObjects = identifiers.map((id) => { + validateIdentifier(id, IdentifierKind.Ethereum); + return { + identifier: id.toLowerCase(), + identifierKind: IdentifierKind.Ethereum, + }; + }); const permissionsMap: Record = { "all-members": GroupPermissionsOptions.Default, diff --git a/packages/xmtp-cli/src/utils/client.ts b/packages/xmtp-cli/src/utils/client.ts index 1fd86130f..4e75b931f 100644 --- a/packages/xmtp-cli/src/utils/client.ts +++ b/packages/xmtp-cli/src/utils/client.ts @@ -7,7 +7,7 @@ import { type NetworkOptions, type Signer, } from "@xmtp/node-sdk"; -import { isHex, toBytes } from "viem"; +import { isAddress, isHex, toBytes } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import type { XmtpConfig } from "./config.js"; @@ -66,6 +66,15 @@ export function hexToBytes(value: string): Uint8Array { return toBytes(hex); } +export function validateIdentifier( + identifier: string, + kind: IdentifierKind, +): void { + if (kind === IdentifierKind.Ethereum && !isAddress(identifier)) { + throw new Error(`Invalid Ethereum address: ${identifier}`); + } +} + export async function createClient(config: XmtpConfig): Promise { if (!config.walletKey) { throw new Error( diff --git a/packages/xmtp-cli/test/commands/conversations/create-dm.test.ts b/packages/xmtp-cli/test/commands/conversations/create-dm.test.ts index 3a90145d8..4d9412e57 100644 --- a/packages/xmtp-cli/test/commands/conversations/create-dm.test.ts +++ b/packages/xmtp-cli/test/commands/conversations/create-dm.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { createRegisteredIdentity, + createTestIdentity, parseJsonOutput, runWithIdentity, } from "../../helpers.js"; @@ -128,6 +129,22 @@ describe("conversations create-dm", () => { expect(result.exitCode).not.toBe(0); }); + it("fails with an invalid ethereum address", async () => { + // Note: Use createTestIdentity so it validates fast locally and does not + // hit the docker network limit when running isolated testing + const sender = createTestIdentity(); + + const result = await runWithIdentity(sender, [ + "conversations", + "create-dm", + "invalid-address", + "--json", + ]); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("Invalid Ethereum address"); + }); + it("both parties can see the DM", async () => { const sender = await createRegisteredIdentity(); const recipient = await createRegisteredIdentity();