├── .changeset └── config.json ├── .cursor └── rules │ ├── documentation.mdc │ ├── project.mdc │ ├── react.mdc │ └── typescript.mdc ├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ ├── browser-sdk.yml │ ├── content-types.yml │ ├── lint.yml │ ├── node-sdk.yml │ ├── noop.yml │ ├── prettier.yml │ ├── release.yml │ ├── triage.yml │ └── xmtp-chat.yml ├── .gitignore ├── .node-version ├── .nvmrc ├── .prettierignore ├── .prettierrc.cjs ├── .vscode ├── extensions.json └── settings.json ├── .yarn └── releases │ └── yarn-4.9.1.cjs ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── apps └── xmtp.chat │ ├── .env.example │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ ├── favicon.ico │ └── xmtp-icon.png │ ├── src │ ├── assets │ │ └── xmtp-icon.png │ ├── components │ │ ├── AddressBadge.tsx │ │ ├── App │ │ │ ├── AccountCard.module.css │ │ │ ├── AccountCard.tsx │ │ │ ├── App.tsx │ │ │ ├── AppFooter.tsx │ │ │ ├── AppHeader.module.css │ │ │ ├── AppHeader.tsx │ │ │ ├── AppLayout.tsx │ │ │ ├── AppMenu.tsx │ │ │ ├── BlockchainSelect.tsx │ │ │ ├── Connect.module.css │ │ │ ├── Connect.tsx │ │ │ ├── ConnectModal.module.css │ │ │ ├── ConnectModal.tsx │ │ │ ├── DisableAnalytics.tsx │ │ │ ├── Disconnect.tsx │ │ │ ├── ErrorModal.tsx │ │ │ ├── LoggingSelect.tsx │ │ │ ├── NetworkSelect.tsx │ │ │ ├── SelectConversation.tsx │ │ │ ├── Settings.tsx │ │ │ ├── UseSCW.tsx │ │ │ ├── Welcome.tsx │ │ │ └── WelcomeLayout.tsx │ │ ├── BadgeWithCopy.module.css │ │ ├── BadgeWithCopy.tsx │ │ ├── CodeWithCopy.tsx │ │ ├── Conversation │ │ │ ├── Composer.module.css │ │ │ ├── Composer.tsx │ │ │ ├── Conversation.tsx │ │ │ ├── ConversationMenu.tsx │ │ │ ├── ConversationOutletContext.ts │ │ │ ├── LoadConversation.tsx │ │ │ ├── LoadDM.tsx │ │ │ ├── ManageConsentModal.tsx │ │ │ ├── ManageMembersModal.tsx │ │ │ ├── ManageMetadataModal.tsx │ │ │ ├── ManagePermissionsModal.tsx │ │ │ ├── Members.tsx │ │ │ ├── Metadata.tsx │ │ │ └── Permissions.tsx │ │ ├── Conversations │ │ │ ├── ConversationCard.module.css │ │ │ ├── ConversationCard.tsx │ │ │ ├── ConversationList.module.css │ │ │ ├── ConversationList.tsx │ │ │ ├── ConversationsMenu.tsx │ │ │ ├── ConversationsNavbar.tsx │ │ │ ├── CreateDmModal.tsx │ │ │ └── CreateGroupModal.tsx │ │ ├── CopyButton.module.css │ │ ├── CopyButton.tsx │ │ ├── DateLabel.tsx │ │ ├── Identity │ │ │ ├── IdentityModal.tsx │ │ │ └── InstallationTable.tsx │ │ ├── InboxId.tsx │ │ ├── LoadingMessage.tsx │ │ ├── Messages │ │ │ ├── FallbackContent.tsx │ │ │ ├── GroupUpdatedContent.tsx │ │ │ ├── Message.module.css │ │ │ ├── Message.tsx │ │ │ ├── MessageContent.tsx │ │ │ ├── MessageContentWrapper.tsx │ │ │ ├── MessageList.module.css │ │ │ ├── MessageList.tsx │ │ │ ├── MessageModal.tsx │ │ │ ├── MessageProperties.tsx │ │ │ ├── Messages.tsx │ │ │ ├── ReplyContent.tsx │ │ │ ├── TextContent.module.css │ │ │ ├── TextContent.tsx │ │ │ ├── TransactionReferenceContent.tsx │ │ │ └── WalletSendCallsContent.tsx │ │ └── Modal.tsx │ ├── contexts │ │ ├── ConversationContext.tsx │ │ └── XMTPContext.tsx │ ├── globals.css │ ├── globals.d.ts │ ├── helpers │ │ ├── createSigner.ts │ │ ├── date.ts │ │ ├── errors.ts │ │ └── strings.ts │ ├── hooks │ │ ├── useAnalytics.ts │ │ ├── useCollapsedMediaQuery.ts │ │ ├── useConversation.ts │ │ ├── useConversations.ts │ │ ├── useIdentity.ts │ │ ├── useRedirect.ts │ │ └── useSettings.ts │ ├── icons │ │ ├── CoinbaseWallet.tsx │ │ ├── EphemeralWallet.tsx │ │ ├── IconCopy.tsx │ │ ├── IconDots.tsx │ │ ├── InjectedWallet.tsx │ │ ├── MetamaskWallet.tsx │ │ └── WalletConnectWallet.tsx │ ├── layouts │ │ ├── CenteredLayout.module.css │ │ ├── CenteredLayout.tsx │ │ ├── ContentLayout.module.css │ │ ├── ContentLayout.tsx │ │ ├── MainLayout.module.css │ │ └── MainLayout.tsx │ ├── main.tsx │ └── types.ts │ ├── tsconfig.json │ ├── vercel.json │ └── vite.config.ts ├── content-types ├── content-type-group-updated │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── GroupUpdated.test.ts │ │ ├── GroupUpdated.ts │ │ └── index.ts │ └── tsconfig.json ├── content-type-primitives │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.json ├── content-type-reaction │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Reaction.test.ts │ │ ├── Reaction.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── content-type-read-receipt │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── ReadReceipt.test.ts │ │ ├── ReadReceipt.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── content-type-remote-attachment │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Attachment.test.ts │ │ ├── Attachment.ts │ │ ├── RemoteAttachment.test.ts │ │ ├── RemoteAttachment.ts │ │ ├── encryption │ │ │ ├── Ciphertext.ts │ │ │ ├── crypto.browser.ts │ │ │ ├── crypto.ts │ │ │ ├── encryption.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── content-type-reply │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Reply.test.ts │ │ ├── Reply.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── content-type-text │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Text.test.ts │ │ ├── Text.ts │ │ └── index.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── content-type-transaction-reference │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── TransactionReference.test.ts │ │ ├── TransactionReference.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts └── content-type-wallet-send-calls │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── WalletSendCalls.test.ts │ ├── WalletSendCalls.ts │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── dev ├── compose ├── docker-compose.yml ├── down ├── up └── uploadService │ ├── Dockerfile │ ├── index.js │ └── package.json ├── eslint.config.js ├── package.json ├── sdks ├── browser-sdk │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── AsyncStream.ts │ │ ├── Client.ts │ │ ├── ClientWorkerClass.ts │ │ ├── Conversation.ts │ │ ├── Conversations.ts │ │ ├── DecodedMessage.ts │ │ ├── Dm.ts │ │ ├── Group.ts │ │ ├── Preferences.ts │ │ ├── Utils.ts │ │ ├── UtilsWorkerClass.ts │ │ ├── WorkerClient.ts │ │ ├── WorkerConversation.ts │ │ ├── WorkerConversations.ts │ │ ├── WorkerPreferences.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── types │ │ │ ├── actions.ts │ │ │ ├── actions │ │ │ │ ├── client.ts │ │ │ │ ├── conversation.ts │ │ │ │ ├── conversations.ts │ │ │ │ ├── dm.ts │ │ │ │ ├── group.ts │ │ │ │ ├── preferences.ts │ │ │ │ ├── streams.ts │ │ │ │ └── utils.ts │ │ │ └── options.ts │ │ ├── utils │ │ │ ├── conversions.ts │ │ │ ├── createClient.ts │ │ │ ├── date.ts │ │ │ ├── errors.ts │ │ │ └── signer.ts │ │ └── workers │ │ │ ├── client.ts │ │ │ └── utils.ts │ ├── test │ │ ├── AsyncStream.test.ts │ │ ├── Client.test.ts │ │ ├── Conversation.test.ts │ │ ├── Conversations.test.ts │ │ ├── Preferences.test.ts │ │ ├── Utils.test.ts │ │ └── helpers.ts │ ├── tsconfig.json │ ├── typedoc.json │ └── vite.config.ts └── node-sdk │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── scripts │ ├── accounts.ts │ └── groups.ts │ ├── src │ ├── AsyncStream.ts │ ├── Client.ts │ ├── Conversation.ts │ ├── Conversations.ts │ ├── DecodedMessage.ts │ ├── Dm.ts │ ├── Group.ts │ ├── Preferences.ts │ ├── constants.ts │ ├── index.ts │ ├── types.ts │ └── utils │ │ ├── createClient.ts │ │ ├── date.ts │ │ ├── errors.ts │ │ ├── inboxId.ts │ │ ├── signer.ts │ │ └── version.ts │ ├── test │ ├── AsyncStream.test.ts │ ├── Client.test.ts │ ├── Conversation.test.ts │ ├── Conversations.test.ts │ ├── Preferences.test.ts │ ├── helpers.ts │ └── inboxId.test.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── tsconfig.json ├── turbo.json └── yarn.lock /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-git", 5 | { 6 | "repo": "xmtp/xmtp-js" 7 | } 8 | ], 9 | "commit": false, 10 | "fixed": [], 11 | "linked": [], 12 | "access": "public", 13 | "baseBranch": "main", 14 | "updateInternalDependencies": "patch", 15 | "ignore": ["@xmtp/xmtp.chat"] 16 | } 17 | -------------------------------------------------------------------------------- /.cursor/rules/documentation.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Documentation Rules 7 | 8 | ## File Patterns 9 | 10 | - **/*.md 11 | 12 | ## Documentation Guidelines 13 | 14 | - Use proper Markdown formatting 15 | - Include GitHub style markdown when appropriate 16 | - Include code examples where appropriate 17 | - Keep documentation up to date 18 | - Follow the project's documentation style 19 | - Use clear and concise explanations 20 | - Link to related documentation 21 | - Use sentence case in headings 22 | -------------------------------------------------------------------------------- /.cursor/rules/project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Project Rules 7 | 8 | ## Project Structure 9 | 10 | - SDKs are located in the `sdks/` directory 11 | - Content types are in `content-types/` directory 12 | - Example apps are in `apps/` directory 13 | - Use workspace dependencies for internal packages 14 | -------------------------------------------------------------------------------- /.cursor/rules/react.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # React Rules 7 | 8 | ## File Patterns 9 | 10 | - **/*.ts 11 | - **/*.tsx 12 | 13 | ## Code Style and Structure 14 | 15 | - Use named exports for components 16 | - Use `const` instead of `function` for components 17 | - Ensure all components have a TypeScript interface using `React.FC` 18 | - Prefix event handler functions with "handle" (e.g., handleClick, handleKeyDown) 19 | - Component props should have a defined type that matches the component name and ends in `Props` 20 | -------------------------------------------------------------------------------- /.cursor/rules/typescript.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # TypeScript Rules 7 | 8 | ## File Patterns 9 | 10 | - **/*.ts 11 | - **/*.tsx 12 | 13 | ## Code Style and Structure 14 | 15 | - Always use ES module syntax 16 | - Maintain consistent naming conventions 17 | - Focus on writing correct, best practice, DRY code 18 | - Use early returns to avoid nested conditions and improve readability 19 | - Use constants instead of functions where possible 20 | - Use descriptive names for variables and functions 21 | - Use custom error types when appropriate 22 | - Favor named exports over default exports 23 | - Use efficient data structures 24 | - Use efficient algorithms 25 | - Keep performance in mind but do not over-optimize at the cost of readability 26 | - Write code that is easy to test and maintain 27 | - Write comprehensive tests for all new features 28 | - Document code with JSDoc comments 29 | - Do not remove any existing code unless necessary 30 | - Do not remove my comments or commented-out code unless necessary 31 | - Use lowercase in single line comments 32 | 33 | ## TypeScript Guidelines 34 | 35 | - Use type inference where possible 36 | - Avoid enums, use const enums when necessary 37 | - Do not define function return types unless necessary 38 | - Prefer types over interfaces 39 | - Use proper type annotations 40 | - Prefer `unknown` type over `any` 41 | - Follow the project's TypeScript patterns 42 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.php] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.py] 16 | charset = utf-8 17 | indent_style = space 18 | indent_size = 4 19 | end_of_line = lf 20 | insert_final_newline = true 21 | trim_trailing_whitespace = true 22 | 23 | [Makefile] 24 | indent_style = tab 25 | indent_size = 4 26 | 27 | [*.sln] 28 | indent_style = tab 29 | 30 | [*.{md,mdx}] 31 | trim_trailing_whitespace = false 32 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Global rule: 2 | * @xmtp/protocol-sdk 3 | *.md @xmtp/documentation 4 | -------------------------------------------------------------------------------- /.github/workflows/browser-sdk.yml: -------------------------------------------------------------------------------- 1 | name: Browser SDK 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | paths: 10 | - "sdks/browser-sdk/**" 11 | - ".github/workflows/browser-sdk.yml" 12 | - "dev/**" 13 | - ".node-version" 14 | - ".nvmrc" 15 | - ".yarnrc.yml" 16 | - "turbo.json" 17 | 18 | jobs: 19 | typecheck: 20 | name: Typecheck 21 | runs-on: warp-ubuntu-latest-x64-8x 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version-file: ".nvmrc" 27 | cache: "yarn" 28 | env: 29 | SKIP_YARN_COREPACK_CHECK: "1" 30 | - name: Enable corepack 31 | run: corepack enable 32 | - name: Install dependencies 33 | run: yarn 34 | - name: Typecheck 35 | run: yarn turbo run typecheck --filter='./sdks/browser-sdk' 36 | 37 | test: 38 | name: Test 39 | runs-on: warp-ubuntu-latest-x64-8x 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version-file: ".nvmrc" 45 | cache: "yarn" 46 | env: 47 | SKIP_YARN_COREPACK_CHECK: "1" 48 | - name: Enable corepack 49 | run: corepack enable 50 | - name: Install dependencies 51 | run: yarn 52 | - name: Install playwright browsers 53 | working-directory: ./sdks/browser-sdk 54 | run: yarn playwright install 55 | - name: Start dev environment 56 | run: ./dev/up 57 | - name: Sleep for 5 seconds 58 | run: sleep 5s 59 | - name: Run tests 60 | run: yarn turbo run test --filter='./sdks/browser-sdk' 61 | 62 | build: 63 | name: Build 64 | runs-on: warp-ubuntu-latest-x64-8x 65 | steps: 66 | - uses: actions/checkout@v4 67 | - uses: actions/setup-node@v4 68 | with: 69 | node-version-file: ".nvmrc" 70 | cache: "yarn" 71 | env: 72 | SKIP_YARN_COREPACK_CHECK: "1" 73 | - name: Enable corepack 74 | run: corepack enable 75 | - name: Install dependencies 76 | run: yarn 77 | - name: Build 78 | run: yarn turbo run build --filter='./sdks/browser-sdk' 79 | -------------------------------------------------------------------------------- /.github/workflows/content-types.yml: -------------------------------------------------------------------------------- 1 | name: Content types 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | paths: 10 | - "content-types/**" 11 | - "sdks/node-sdk/src/**" 12 | - ".github/workflows/content-types.yml" 13 | - "dev/**" 14 | - ".node-version" 15 | - ".nvmrc" 16 | - ".yarnrc.yml" 17 | - "turbo.json" 18 | 19 | jobs: 20 | typecheck: 21 | name: Typecheck 22 | runs-on: warp-ubuntu-latest-x64-8x 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version-file: ".nvmrc" 28 | cache: "yarn" 29 | env: 30 | SKIP_YARN_COREPACK_CHECK: "1" 31 | - name: Enable corepack 32 | run: corepack enable 33 | - name: Install dependencies 34 | run: yarn 35 | - name: Typecheck 36 | run: yarn turbo run typecheck --filter='./content-types/*' 37 | 38 | test: 39 | name: Test 40 | runs-on: warp-ubuntu-latest-x64-8x 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version-file: ".nvmrc" 46 | cache: "yarn" 47 | env: 48 | SKIP_YARN_COREPACK_CHECK: "1" 49 | - name: Enable corepack 50 | run: corepack enable 51 | - name: Install dependencies 52 | run: yarn 53 | - name: Start dev environment 54 | run: ./dev/up 55 | - name: Sleep for 5 seconds 56 | run: sleep 5s 57 | - name: Run tests 58 | run: yarn turbo run test --filter='./content-types/*' 59 | 60 | build: 61 | name: Build 62 | runs-on: warp-ubuntu-latest-x64-8x 63 | steps: 64 | - uses: actions/checkout@v4 65 | - uses: actions/setup-node@v4 66 | with: 67 | node-version-file: ".nvmrc" 68 | cache: "yarn" 69 | env: 70 | SKIP_YARN_COREPACK_CHECK: "1" 71 | - name: Enable corepack 72 | run: corepack enable 73 | - name: Install dependencies 74 | run: yarn 75 | - name: Build 76 | run: yarn turbo run build --filter='./content-types/*' 77 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | name: Lint 13 | runs-on: warp-ubuntu-latest-x64-8x 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version-file: ".nvmrc" 19 | cache: "yarn" 20 | env: 21 | SKIP_YARN_COREPACK_CHECK: "1" 22 | - name: Enable corepack 23 | run: corepack enable 24 | - name: Install dependencies 25 | run: yarn 26 | - name: Lint 27 | run: yarn lint 28 | -------------------------------------------------------------------------------- /.github/workflows/node-sdk.yml: -------------------------------------------------------------------------------- 1 | name: Node SDK 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | paths: 10 | - "sdks/node-sdk/**" 11 | - ".github/workflows/node-sdk.yml" 12 | - "dev/**" 13 | - ".node-version" 14 | - ".nvmrc" 15 | - ".yarnrc.yml" 16 | - "turbo.json" 17 | 18 | jobs: 19 | typecheck: 20 | name: Typecheck 21 | runs-on: warp-ubuntu-latest-x64-8x 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version-file: ".nvmrc" 27 | cache: "yarn" 28 | env: 29 | SKIP_YARN_COREPACK_CHECK: "1" 30 | - name: Enable corepack 31 | run: corepack enable 32 | - name: Install dependencies 33 | run: yarn 34 | - name: Typecheck 35 | run: yarn turbo run typecheck --filter='./sdks/node-sdk' 36 | 37 | test: 38 | name: Test 39 | runs-on: warp-ubuntu-latest-x64-8x 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-node@v4 43 | with: 44 | node-version-file: ".nvmrc" 45 | cache: "yarn" 46 | env: 47 | SKIP_YARN_COREPACK_CHECK: "1" 48 | - name: Enable corepack 49 | run: corepack enable 50 | - name: Install dependencies 51 | run: yarn 52 | - name: Start dev environment 53 | run: ./dev/up 54 | - name: Sleep for 5 seconds 55 | run: sleep 5s 56 | - name: Run tests 57 | run: yarn turbo run test --filter='./sdks/node-sdk' 58 | 59 | build: 60 | name: Build 61 | runs-on: warp-ubuntu-latest-x64-8x 62 | steps: 63 | - uses: actions/checkout@v4 64 | - uses: actions/setup-node@v4 65 | with: 66 | node-version-file: ".nvmrc" 67 | cache: "yarn" 68 | env: 69 | SKIP_YARN_COREPACK_CHECK: "1" 70 | - name: Enable corepack 71 | run: corepack enable 72 | - name: Install dependencies 73 | run: yarn 74 | - name: Build 75 | run: yarn turbo run build --filter='./sdks/node-sdk' 76 | -------------------------------------------------------------------------------- /.github/workflows/noop.yml: -------------------------------------------------------------------------------- 1 | name: No code changes 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | paths: 10 | - ".changeset/config.json" 11 | - ".changeset/*.md" 12 | - ".cursor/**" 13 | - ".github/**" 14 | - "!.github/workflows/node-sdk.yml" 15 | - "!.github/workflows/content-types.yml" 16 | - ".vscode/**" 17 | - ".yarn/**" 18 | - "apps/xmtp.chat/**" 19 | - "*" 20 | - "!.node-version" 21 | - "!.nvmrc" 22 | - "!.yarnrc.yml" 23 | - "!turbo.json" 24 | - "yarn.lock" 25 | 26 | jobs: 27 | typecheck: 28 | name: Typecheck 29 | runs-on: ubuntu-latest 30 | steps: 31 | - run: echo "Nothing to typecheck" 32 | 33 | test: 34 | name: Test 35 | runs-on: ubuntu-latest 36 | steps: 37 | - run: echo "Nothing to test" 38 | 39 | build: 40 | name: Build 41 | runs-on: ubuntu-latest 42 | steps: 43 | - run: echo "Nothing to build" 44 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Format check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | prettier: 12 | name: Prettier 13 | runs-on: warp-ubuntu-latest-x64-8x 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version-file: ".nvmrc" 19 | cache: "yarn" 20 | env: 21 | SKIP_YARN_COREPACK_CHECK: "1" 22 | - name: Enable corepack 23 | run: corepack enable 24 | - name: Install dependencies 25 | run: yarn 26 | - name: Format check 27 | run: yarn format:check 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - releases/** 8 | 9 | workflow_dispatch: 10 | inputs: 11 | disable_audit: 12 | type: boolean 13 | description: "Disable audit in the release" 14 | default: false 15 | 16 | concurrency: ${{ github.workflow }}-${{ github.ref }} 17 | 18 | jobs: 19 | release: 20 | name: Release 21 | runs-on: ubuntu-latest 22 | permissions: 23 | id-token: write 24 | contents: write 25 | pull-requests: write 26 | issues: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | persist-credentials: false 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version-file: ".nvmrc" 37 | cache: "yarn" 38 | env: 39 | SKIP_YARN_COREPACK_CHECK: "1" 40 | - name: Enable corepack 41 | run: corepack enable 42 | - name: Update npm to latest 43 | run: npm install -g npm@latest 44 | - name: Install dependencies 45 | run: yarn 46 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies 47 | if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && !inputs.disable_audit) }} 48 | run: npm audit signatures 49 | - name: Publish 50 | uses: changesets/action@v1 51 | with: 52 | title: "release: version packages" 53 | commit: "release: version packages" 54 | publish: yarn publish 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | -------------------------------------------------------------------------------- /.github/workflows/triage.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | call-add-issues-to-project: 10 | uses: xmtp/workflow_calls/.github/workflows/add-issues-to-project.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/xmtp-chat.yml: -------------------------------------------------------------------------------- 1 | name: xmtp.chat 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | paths: 10 | - "apps/xmtp.chat/**" 11 | - "sdks/browser-sdk/**" 12 | - ".github/workflows/xmtp-chat.yml" 13 | - "dev/**" 14 | - ".node-version" 15 | - ".nvmrc" 16 | - ".yarnrc.yml" 17 | - "turbo.json" 18 | 19 | jobs: 20 | typecheck: 21 | name: Typecheck 22 | runs-on: warp-ubuntu-latest-x64-8x 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version-file: ".nvmrc" 28 | cache: "yarn" 29 | env: 30 | SKIP_YARN_COREPACK_CHECK: "1" 31 | - name: Enable corepack 32 | run: corepack enable 33 | - name: Install dependencies 34 | run: yarn 35 | - name: Typecheck 36 | run: yarn turbo run typecheck --filter='./apps/xmtp.chat' 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # misc 10 | .DS_Store 11 | *.pem 12 | 13 | # debug 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # local env files 18 | .env* 19 | !.env.example 20 | 21 | # turbo 22 | .turbo 23 | 24 | # logs 25 | logs 26 | *.log 27 | 28 | # editor directories and files 29 | .vscode/* 30 | !.vscode/extensions.json 31 | !.vscode/settings.json 32 | .idea 33 | .DS_Store 34 | *.suo 35 | *.ntvs* 36 | *.njsproj 37 | *.sln 38 | *.sw? 39 | 40 | # build artifacts 41 | lib 42 | dist 43 | build 44 | 45 | # yarn 46 | .pnp.* 47 | .yarn/* 48 | !.yarn/patches 49 | !.yarn/plugins 50 | !.yarn/releases 51 | !.yarn/sdks 52 | !.yarn/versions 53 | 54 | # typedocs 55 | docs 56 | tmp 57 | 58 | # next.js 59 | .next 60 | out 61 | next-env.d.ts 62 | 63 | # vercel 64 | .vercel 65 | 66 | # typescript 67 | *.tsbuildinfo 68 | 69 | # remix 70 | .cache 71 | 72 | # Benchmark results 73 | sdks/js-sdk/bench/results 74 | 75 | # local database files 76 | **/*.db3* 77 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22.13.0 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.13.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CODEOWNERS 2 | .yarn 3 | .changeset/**/*.md 4 | sdks/node-sdk/scripts/**/*.json 5 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: "always", 3 | bracketSameLine: true, 4 | bracketSpacing: true, 5 | embeddedLanguageFormatting: "auto", 6 | endOfLine: "lf", 7 | htmlWhitespaceSensitivity: "css", 8 | jsxSingleQuote: false, 9 | printWidth: 80, 10 | proseWrap: "preserve", 11 | quoteProps: "as-needed", 12 | semi: true, 13 | singleAttributePerLine: false, 14 | singleQuote: false, 15 | tabWidth: 2, 16 | trailingComma: "all", 17 | useTabs: false, 18 | plugins: [ 19 | "prettier-plugin-packagejson", 20 | "@ianvs/prettier-plugin-sort-imports", 21 | ], 22 | importOrder: [ 23 | "", 24 | "", 25 | "^@(/.*)$", 26 | "^@test(/.*)$", 27 | "^@bench(/.*)$", 28 | "^[.]", 29 | ], 30 | importOrderTypeScriptVersion: "5.6.3", 31 | }; 32 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "clinyong.vscode-css-modules", 6 | "vunguyentuan.vscode-css-variables", 7 | "vunguyentuan.vscode-postcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[javascript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[json]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "editor.tabSize": 2, 16 | "editor.detectIndentation": false, 17 | "files.insertFinalNewline": true, 18 | "eslint.workingDirectories": [ 19 | { 20 | "mode": "auto" 21 | } 22 | ], 23 | "cssVariables.lookupFiles": [ 24 | "**/*.css", 25 | "**/*.scss", 26 | "**/*.sass", 27 | "**/*.less", 28 | "node_modules/@mantine/core/styles.css" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | enableTelemetry: false 6 | 7 | nodeLinker: node-modules 8 | 9 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the official repository for XMTP client SDKs and content types for browsers and Node, written in TypeScript. 2 | 3 | To learn more about the contents of this repository, see this README and the READMEs provided in each workspace directory. 4 | 5 | ## What's inside? 6 | 7 | ### SDKs 8 | 9 | - [`browser-sdk`](sdks/browser-sdk): XMTP client SDK for browsers 10 | - [`node-sdk`](sdks/node-sdk): XMTP client SDK for Node 11 | 12 | ### Content types 13 | 14 | - [`content-type-primitives`](content-types/content-type-primitives): Primitives for building custom XMTP content types 15 | - [`content-type-group-updated`](content-types/content-type-group-updated): Content type for group update messages 16 | - [`content-type-reaction`](content-types/content-type-reaction): Content type for reactions to messages 17 | - [`content-type-read-receipt`](content-types/content-type-read-receipt): Content type for read receipts for messages 18 | - [`content-type-remote-attachment`](content-types/content-type-remote-attachment): Content type for sending file attachments that are stored off-network 19 | - [`content-type-reply`](content-types/content-type-reply): Content type for direct replies to messages 20 | - [`content-type-text`](content-types/content-type-text): Content type for plain text messages 21 | - [`content-type-transaction-reference`](content-types/content-type-transaction-reference): Content type for on-chain transaction references 22 | 23 | ## Contributing 24 | 25 | See our [contribution guide](./CONTRIBUTING.md) to learn more about contributing to this project. 26 | -------------------------------------------------------------------------------- /apps/xmtp.chat/.env.example: -------------------------------------------------------------------------------- 1 | # WalletConnect Project ID 2 | VITE_PROJECT_ID= 3 | -------------------------------------------------------------------------------- /apps/xmtp.chat/README.md: -------------------------------------------------------------------------------- 1 | # xmtp.chat app 2 | 3 | Use this React app as a tool to start building an app with XMTP. 4 | 5 | The app is built using the [XMTP client browser SDK](/sdks/browser-sdk/README.md), [React](https://react.dev/), and [RainbowKit](https://www.rainbowkit.com/). 6 | 7 | To keep up with the latest React app developments, see the [Issues tab](https://github.com/xmtp/xmtp-js/issues) in this repo. 8 | 9 | To learn more about XMTP and get answers to frequently asked questions, see the [XMTP documentation](https://xmtp.org/docs). 10 | 11 | ### Limitations 12 | 13 | This React app isn't a complete solution. For example, the list of conversations doesn't update when new messages arrive in existing conversations. 14 | 15 | ## Useful commands 16 | 17 | - `yarn clean`: Removes `node_modules` and `.turbo` folders 18 | - `yarn dev`: Runs the app in development mode 19 | - `yarn typecheck`: Runs `tsc` 20 | -------------------------------------------------------------------------------- /apps/xmtp.chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | XMTP Browser Developer Tools 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/xmtp.chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xmtp/xmtp.chat", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": "./src/index.ts" 8 | }, 9 | "main": "src/index.ts", 10 | "scripts": { 11 | "build": "vite build", 12 | "clean": "rm -rf .turbo && rm -rf node_modules && yarn clean:dist", 13 | "clean:dist": "rm -rf dist", 14 | "dev": "vite", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@mantine/core": "^8.0.2", 19 | "@mantine/form": "^8.0.2", 20 | "@mantine/hooks": "^8.0.2", 21 | "@mantine/modals": "^8.0.2", 22 | "@mantine/notifications": "^8.0.2", 23 | "@tanstack/react-query": "^5.77.2", 24 | "@xmtp/browser-sdk": "workspace:^", 25 | "@xmtp/content-type-group-updated": "workspace:^", 26 | "@xmtp/content-type-primitives": "workspace:^", 27 | "@xmtp/content-type-reaction": "workspace:^", 28 | "@xmtp/content-type-remote-attachment": "workspace:^", 29 | "@xmtp/content-type-reply": "workspace:^", 30 | "@xmtp/content-type-text": "workspace:^", 31 | "@xmtp/content-type-transaction-reference": "workspace:^", 32 | "@xmtp/content-type-wallet-send-calls": "workspace:^", 33 | "date-fns": "^4.1.0", 34 | "plausible-tracker": "^0.3.9", 35 | "react": "^19.1.0", 36 | "react-18-blockies": "^1.0.6", 37 | "react-dom": "^19.1.0", 38 | "react-router": "^7.6.1", 39 | "react-virtuoso": "^4.12.7", 40 | "uint8array-extras": "^1.4.0", 41 | "viem": "^2.30.5", 42 | "wagmi": "^2.15.4" 43 | }, 44 | "devDependencies": { 45 | "@types/react": "^19.1.6", 46 | "@types/react-dom": "^19.1.5", 47 | "@vitejs/plugin-react": "^4.5.0", 48 | "postcss": "^8.5.3", 49 | "postcss-preset-mantine": "^1.17.0", 50 | "postcss-simple-vars": "^7.0.1", 51 | "typescript": "^5.8.3", 52 | "vite": "^6.3.5", 53 | "vite-tsconfig-paths": "^5.1.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/xmtp.chat/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-preset-mantine": {}, 4 | "postcss-simple-vars": { 5 | variables: { 6 | "mantine-breakpoint-xs": "36em", 7 | "mantine-breakpoint-sm": "48em", 8 | "mantine-breakpoint-md": "62em", 9 | "mantine-breakpoint-lg": "75em", 10 | "mantine-breakpoint-xl": "88em", 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /apps/xmtp.chat/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmtp/xmtp-js/2748fad564993a9dbd7f1d2a58b79e0a8f1febbc/apps/xmtp.chat/public/favicon.ico -------------------------------------------------------------------------------- /apps/xmtp.chat/public/xmtp-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmtp/xmtp-js/2748fad564993a9dbd7f1d2a58b79e0a8f1febbc/apps/xmtp.chat/public/xmtp-icon.png -------------------------------------------------------------------------------- /apps/xmtp.chat/src/assets/xmtp-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmtp/xmtp-js/2748fad564993a9dbd7f1d2a58b79e0a8f1febbc/apps/xmtp.chat/src/assets/xmtp-icon.png -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/AddressBadge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Flex, Text, Tooltip } from "@mantine/core"; 2 | import { useClipboard } from "@mantine/hooks"; 3 | import { useCallback } from "react"; 4 | import { shortAddress } from "@/helpers/strings"; 5 | 6 | export type AddressTooltipLabelProps = { 7 | address: string; 8 | }; 9 | 10 | export const AddressTooltipLabel: React.FC = ({ 11 | address, 12 | }) => { 13 | return ( 14 | 15 | {address} 16 | 17 | click to copy 18 | 19 | 20 | ); 21 | }; 22 | 23 | export type AddressBadgeProps = { 24 | address: string; 25 | size?: "xs" | "sm" | "md" | "lg" | "xl"; 26 | }; 27 | 28 | export const AddressBadge: React.FC = ({ 29 | address, 30 | size = "lg", 31 | }) => { 32 | const clipboard = useClipboard({ timeout: 1000 }); 33 | 34 | const handleCopy = useCallback( 35 | ( 36 | event: 37 | | React.MouseEvent 38 | | React.KeyboardEvent, 39 | ) => { 40 | event.stopPropagation(); 41 | clipboard.copy(address); 42 | }, 43 | [clipboard, address], 44 | ); 45 | 46 | const handleKeyboardCopy = useCallback( 47 | (event: React.KeyboardEvent) => { 48 | if (event.key === "Enter" || event.key === " ") { 49 | handleCopy(event); 50 | } 51 | }, 52 | [handleCopy], 53 | ); 54 | 55 | return ( 56 | Copied! 60 | ) : ( 61 | 62 | ) 63 | } 64 | withArrow 65 | events={{ hover: true, focus: true, touch: true }}> 66 | 79 | {shortAddress(address)} 80 | 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/AccountCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | cursor: pointer; 3 | } 4 | 5 | .root:hover { 6 | background-color: var(--mantine-primary-color-light-hover); 7 | } 8 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/AccountCard.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Group, Text } from "@mantine/core"; 2 | import classes from "./AccountCard.module.css"; 3 | 4 | export type AccountCardProps = { 5 | icon: React.ReactNode; 6 | right?: React.ReactNode; 7 | label: string; 8 | onClick?: () => void; 9 | }; 10 | 11 | export const AccountCard: React.FC = ({ 12 | icon, 13 | label, 14 | onClick, 15 | right, 16 | }) => { 17 | return ( 18 | 25 | {icon} 26 | 27 | {label} 28 | 29 | {right && {right}} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/AppFooter.tsx: -------------------------------------------------------------------------------- 1 | import { Anchor, Box, Flex, Group, Image, Text } from "@mantine/core"; 2 | import logo from "@/assets/xmtp-icon.png"; 3 | 4 | export const AppFooter: React.FC = () => { 5 | return ( 6 | 7 | 8 | 14 | 15 | XMTP 16 | 17 | XMTP 18 | 19 | 20 | 21 | 22 | 23 | 29 | Contribute 30 | 31 | 32 | • 33 | 34 | 40 | Report an issue 41 | 42 | 43 | • 44 | 45 | 51 | Documentation 52 | 53 | 54 | • 55 | 56 | 62 | Forums 63 | 64 | 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/AppHeader.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background-color: light-dark( 3 | var(--mantine-color-gray-0), 4 | var(--mantine-color-dark-8) 5 | ); 6 | } 7 | 8 | .burger { 9 | display: none; 10 | } 11 | 12 | @media screen and (max-width: 1080px) { 13 | .burger { 14 | display: block; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingOverlay } from "@mantine/core"; 2 | import { useDisclosure } from "@mantine/hooks"; 3 | import { useEffect } from "react"; 4 | import { Outlet, useLocation, useNavigate } from "react-router"; 5 | import { AppFooter } from "@/components/App/AppFooter"; 6 | import { AppHeader } from "@/components/App/AppHeader"; 7 | import { ConversationsNavbar } from "@/components/Conversations/ConversationsNavbar"; 8 | import { useXMTP } from "@/contexts/XMTPContext"; 9 | import { useRedirect } from "@/hooks/useRedirect"; 10 | import { CenteredLayout } from "@/layouts/CenteredLayout"; 11 | import { 12 | MainLayout, 13 | MainLayoutContent, 14 | MainLayoutFooter, 15 | MainLayoutHeader, 16 | MainLayoutNav, 17 | } from "@/layouts/MainLayout"; 18 | 19 | export const AppLayout: React.FC = () => { 20 | const navigate = useNavigate(); 21 | const location = useLocation(); 22 | const { client } = useXMTP(); 23 | const { setRedirectUrl } = useRedirect(); 24 | const [opened, { toggle }] = useDisclosure(); 25 | 26 | useEffect(() => { 27 | if (!client) { 28 | // save the current path to redirect to it after the client is initialized 29 | if ( 30 | location.pathname !== "/welcome" && 31 | location.pathname !== "/disconnect" 32 | ) { 33 | setRedirectUrl(location.pathname); 34 | } 35 | void navigate("/welcome"); 36 | } 37 | }, [client]); 38 | 39 | return !client ? ( 40 | 41 | 42 | 43 | ) : ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/AppMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Menu } from "@mantine/core"; 2 | import { useCallback } from "react"; 3 | import { useNavigate } from "react-router"; 4 | import { useRedirect } from "@/hooks/useRedirect"; 5 | import { IconDots } from "@/icons/IconDots"; 6 | 7 | export const AppMenu: React.FC = () => { 8 | const navigate = useNavigate(); 9 | const { setRedirectUrl } = useRedirect(); 10 | 11 | const handleDisconnect = useCallback(() => { 12 | setRedirectUrl(location.pathname); 13 | void navigate("/disconnect"); 14 | }, [navigate, setRedirectUrl]); 15 | 16 | return ( 17 | 18 | 19 | 26 | 27 | 28 | Actions 29 | void navigate("new-dm")}> 30 | New direct message 31 | 32 | void navigate("new-group")}> 33 | New group 34 | 35 | Disconnect 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/BlockchainSelect.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Group, 3 | LoadingOverlay, 4 | NativeSelect, 5 | Stack, 6 | Text, 7 | } from "@mantine/core"; 8 | import { useMemo, useState } from "react"; 9 | import { useSwitchChain } from "wagmi"; 10 | import { 11 | arbitrum, 12 | base, 13 | linea, 14 | mainnet, 15 | optimism, 16 | polygon, 17 | worldchain, 18 | zksync, 19 | } from "wagmi/chains"; 20 | import { useSettings } from "@/hooks/useSettings"; 21 | 22 | const ALLOWED_CHAINS: number[] = [ 23 | arbitrum.id, 24 | base.id, 25 | linea.id, 26 | mainnet.id, 27 | optimism.id, 28 | polygon.id, 29 | worldchain.id, 30 | zksync.id, 31 | ]; 32 | 33 | export const BlockchainSelect: React.FC = () => { 34 | const { blockchain, setBlockchain } = useSettings(); 35 | const { chains, switchChain } = useSwitchChain(); 36 | const [loading, setLoading] = useState(false); 37 | 38 | const handleChange = (event: React.ChangeEvent) => { 39 | setLoading(true); 40 | switchChain( 41 | { 42 | chainId: parseInt(event.currentTarget.value), 43 | }, 44 | { 45 | onSuccess(data) { 46 | setBlockchain(data.id); 47 | }, 48 | onError(error) { 49 | console.error("An error occurred while switching chain", error); 50 | }, 51 | onSettled() { 52 | setLoading(false); 53 | }, 54 | }, 55 | ); 56 | }; 57 | 58 | const options = useMemo( 59 | () => 60 | chains 61 | .filter((chain) => ALLOWED_CHAINS.includes(chain.id)) 62 | .map((chain) => ({ 63 | value: chain.id.toString(), 64 | label: chain.name, 65 | })), 66 | [chains], 67 | ); 68 | 69 | return ( 70 | <> 71 | {loading && } 72 | 73 | 74 | Blockchain 75 | 80 | 81 | 82 | Select the blockchain to use for signing messages. 83 | 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/Connect.module.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --connect-border-radius: var(--mantine-radius-md); 3 | --connect-border-color: var(--mantine-color-default-border); 4 | } 5 | 6 | .root { 7 | position: relative; 8 | overflow: hidden; 9 | border-radius: var(--connect-border-radius); 10 | } 11 | 12 | .root > *:not(:first-child):not(:last-child) { 13 | border-left: 1px solid var(--connect-border-color); 14 | border-right: 1px solid var(--connect-border-color); 15 | border-bottom: 1px solid var(--connect-border-color); 16 | } 17 | 18 | .root > *:first-child { 19 | border-radius: var(--connect-border-radius) var(--connect-border-radius) 0 0; 20 | border: 1px solid var(--connect-border-color); 21 | } 22 | 23 | .root > *:last-child { 24 | border-radius: 0 0 var(--connect-border-radius) var(--connect-border-radius); 25 | border: 1px solid var(--connect-border-color); 26 | border-top: none; 27 | } 28 | 29 | @media screen and (max-width: 500px) { 30 | .options { 31 | flex-direction: column; 32 | align-items: flex-start; 33 | } 34 | 35 | .options > * { 36 | width: 100%; 37 | justify-content: space-between; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/Connect.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Group } from "@mantine/core"; 2 | import { useCallback } from "react"; 3 | import { useNavigate } from "react-router"; 4 | 5 | export const Connect = () => { 6 | const navigate = useNavigate(); 7 | 8 | const handleClick = useCallback(() => { 9 | void navigate("/welcome/connect"); 10 | }, [navigate]); 11 | 12 | return ( 13 | 14 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/ConnectModal.module.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --connect-border-radius: var(--mantine-radius-md); 3 | --connect-border-color: var(--mantine-color-default-border); 4 | } 5 | 6 | .root { 7 | position: relative; 8 | overflow: hidden; 9 | border-radius: 0 0 var(--connect-border-radius) var(--connect-border-radius); 10 | } 11 | 12 | .root > *:not(:last-child) { 13 | border-bottom: 1px solid var(--connect-border-color); 14 | } 15 | 16 | .root > *:last-child { 17 | border-radius: 0 0 var(--connect-border-radius) var(--connect-border-radius); 18 | } 19 | 20 | @media screen and (max-width: 1080px) { 21 | .root { 22 | border-radius: 0; 23 | } 24 | .root > *:last-child { 25 | border-bottom: 1px solid var(--connect-border-color); 26 | border-radius: 0; 27 | } 28 | } 29 | 30 | @media screen and (max-width: 500px) { 31 | .options { 32 | flex-direction: column; 33 | align-items: flex-start; 34 | } 35 | 36 | .options > * { 37 | width: 100%; 38 | justify-content: space-between; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/DisableAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import { Anchor, Group, Stack, Switch, Text } from "@mantine/core"; 2 | import { useLocalStorage } from "@mantine/hooks"; 3 | import React from "react"; 4 | 5 | export const DisableAnalytics: React.FC = () => { 6 | const [checked, setChecked] = useLocalStorage({ 7 | key: "plausible_ignore", 8 | defaultValue: false, 9 | }); 10 | 11 | const handleChange = (event: React.ChangeEvent) => { 12 | setChecked(event.currentTarget.checked); 13 | }; 14 | 15 | return ( 16 | 17 | 18 | Disable analytics 19 | 25 | 26 | 27 | We use{" "} 28 | 31 | Plausible Analytics 32 | {" "} 33 | to track usage and improve the app. 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/Disconnect.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingOverlay } from "@mantine/core"; 2 | import { useEffect } from "react"; 3 | import { useNavigate } from "react-router"; 4 | import { useDisconnect } from "wagmi"; 5 | import { useXMTP } from "@/contexts/XMTPContext"; 6 | import { useSettings } from "@/hooks/useSettings"; 7 | import { CenteredLayout } from "@/layouts/CenteredLayout"; 8 | 9 | export const Disconnect: React.FC = () => { 10 | const navigate = useNavigate(); 11 | const { disconnect } = useDisconnect(); 12 | const { setEphemeralAccountEnabled, ephemeralAccountEnabled } = useSettings(); 13 | const { disconnect: disconnectClient } = useXMTP(); 14 | 15 | useEffect(() => { 16 | if (ephemeralAccountEnabled) { 17 | setEphemeralAccountEnabled(false); 18 | } 19 | disconnect(undefined, { 20 | onSuccess: () => { 21 | disconnectClient(); 22 | void navigate("/"); 23 | }, 24 | }); 25 | }, []); 26 | 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/LoggingSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Group, NativeSelect, Stack, Text } from "@mantine/core"; 2 | import { type ClientOptions } from "@xmtp/browser-sdk"; 3 | import { useSettings } from "@/hooks/useSettings"; 4 | 5 | export const LoggingSelect: React.FC = () => { 6 | const { loggingLevel, setLoggingLevel } = useSettings(); 7 | 8 | const handleChange = (event: React.ChangeEvent) => { 9 | setLoggingLevel(event.currentTarget.value as ClientOptions["loggingLevel"]); 10 | }; 11 | 12 | return ( 13 | 14 | 15 | Logging level 16 | 21 | 22 | Enable logging to help debug issues. 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/NetworkSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Group, NativeSelect, Stack, Text, Tooltip } from "@mantine/core"; 2 | import { ApiUrls, type XmtpEnv } from "@xmtp/browser-sdk"; 3 | import { useSettings } from "@/hooks/useSettings"; 4 | 5 | export const NetworkSelect: React.FC = () => { 6 | const { environment, setEnvironment } = useSettings(); 7 | 8 | const handleChange = (event: React.ChangeEvent) => { 9 | setEnvironment(event.currentTarget.value as XmtpEnv); 10 | }; 11 | 12 | return ( 13 | 14 | 15 | XMTP network 16 | 20 | 25 | 26 | 27 | Select the network you want to connect to. 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/SelectConversation.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Stack, Text, Title } from "@mantine/core"; 2 | import { useNavigate } from "react-router"; 3 | 4 | export const SelectConversation = () => { 5 | const navigate = useNavigate(); 6 | return ( 7 | 8 | 9 | No conversation selected 10 | 11 | Select a conversation in the left sidebar to display its messages, 12 | or... 13 | 14 | 15 | 22 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "@mantine/core"; 2 | import { DisableAnalytics } from "@/components/App/DisableAnalytics"; 3 | import { LoggingSelect } from "@/components/App/LoggingSelect"; 4 | import { NetworkSelect } from "@/components/App/NetworkSelect"; 5 | import { UseSCW } from "@/components/App/UseSCW"; 6 | import classes from "./Connect.module.css"; 7 | 8 | export const Settings = () => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/UseSCW.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Stack, Switch, Text } from "@mantine/core"; 2 | import React from "react"; 3 | import { useSettings } from "@/hooks/useSettings"; 4 | 5 | export const UseSCW: React.FC = () => { 6 | const { useSCW, setUseSCW } = useSettings(); 7 | 8 | const handleChange = (event: React.ChangeEvent) => { 9 | setUseSCW(event.currentTarget.checked); 10 | }; 11 | 12 | return ( 13 | 14 | 15 | Use smart contract wallet 16 | 22 | 23 | 24 | Enable this option if you're connecting with a smart contract wallet. 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/App/WelcomeLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | import { CenteredLayout } from "@/layouts/CenteredLayout"; 3 | 4 | export const WelcomeLayout: React.FC = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/BadgeWithCopy.module.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | background-color: light-dark( 3 | var(--mantine-color-gray-1), 4 | var(--mantine-color-dark-5) 5 | ); 6 | color: light-dark(var(--mantine-color-light-4), var(--mantine-color-dark-1)); 7 | } 8 | 9 | .button { 10 | color: light-dark(var(--mantine-color-light-4), var(--mantine-color-dark-1)); 11 | } 12 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/BadgeWithCopy.tsx: -------------------------------------------------------------------------------- 1 | import { Badge, Button, Text, Tooltip } from "@mantine/core"; 2 | import { useClipboard } from "@mantine/hooks"; 3 | import { IconCopy } from "@/icons/IconCopy"; 4 | import classes from "./BadgeWithCopy.module.css"; 5 | 6 | type CopyIconProps = { 7 | value: string; 8 | }; 9 | 10 | const CopyIcon: React.FC = ({ value }) => { 11 | const clipboard = useClipboard({ timeout: 1000 }); 12 | 13 | const handleCopy = () => { 14 | clipboard.copy(value); 15 | }; 16 | 17 | const handleKeyboardCopy = ( 18 | event: React.KeyboardEvent, 19 | ) => { 20 | if (event.key === "Enter" || event.key === " ") { 21 | handleCopy(); 22 | } 23 | }; 24 | 25 | return ( 26 | Copied! 30 | ) : ( 31 | {value} 32 | ) 33 | } 34 | withArrow 35 | events={{ hover: true, focus: true, touch: true }}> 36 | 45 | 46 | ); 47 | }; 48 | 49 | export type BadgeWithCopyProps = { 50 | value: string; 51 | }; 52 | 53 | export const BadgeWithCopy: React.FC = ({ value }) => { 54 | return ( 55 | }> 68 | {value} 69 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/CodeWithCopy.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Code } from "@mantine/core"; 2 | import { CopyButton } from "./CopyButton"; 3 | 4 | type CodeWithCopyProps = { 5 | code: string; 6 | }; 7 | 8 | export const CodeWithCopy: React.FC = ({ code }) => { 9 | return ( 10 | 11 | 21 | {code} 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversation/Composer.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | border-top: 1px solid var(--mantine-color-default-border); 3 | } 4 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversation/Composer.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Group, TextInput } from "@mantine/core"; 2 | import type { Conversation } from "@xmtp/browser-sdk"; 3 | import { useRef, useState } from "react"; 4 | import type { ContentTypes } from "@/contexts/XMTPContext"; 5 | import { useConversation } from "@/hooks/useConversation"; 6 | import classes from "./Composer.module.css"; 7 | 8 | export type ComposerProps = { 9 | conversation: Conversation; 10 | }; 11 | 12 | export const Composer: React.FC = ({ conversation }) => { 13 | const { send, sending } = useConversation(conversation); 14 | const [message, setMessage] = useState(""); 15 | const inputRef = useRef(null); 16 | 17 | const handleSend = async () => { 18 | if (message.length === 0 || sending) { 19 | return; 20 | } 21 | 22 | await send(message); 23 | setMessage(""); 24 | setTimeout(() => { 25 | inputRef.current?.focus(); 26 | }, 50); 27 | }; 28 | 29 | return ( 30 | 37 | { 45 | if (event.key === "Enter") { 46 | void handleSend(); 47 | } 48 | }} 49 | onChange={(e) => { 50 | setMessage(e.target.value); 51 | }} 52 | /> 53 | 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversation/ConversationMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Menu } from "@mantine/core"; 2 | import { useNavigate } from "react-router"; 3 | import { IconDots } from "@/icons/IconDots"; 4 | 5 | export type ConversationMenuProps = { 6 | type: "group" | "dm"; 7 | onSync: () => void; 8 | disabled?: boolean; 9 | }; 10 | 11 | export const ConversationMenu: React.FC = ({ 12 | type, 13 | onSync, 14 | disabled, 15 | }) => { 16 | const navigate = useNavigate(); 17 | 18 | return ( 19 | 20 | 21 | 28 | 29 | 30 | Manage 31 | void navigate("manage/consent")}> 32 | Consent 33 | 34 | {type === "group" && ( 35 | <> 36 | void navigate("manage/members")}> 37 | Members 38 | 39 | void navigate("manage/metadata")}> 40 | Metadata 41 | 42 | void navigate("manage/permissions")}> 43 | Permissions 44 | 45 | 46 | )} 47 | Actions 48 | Sync 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversation/ConversationOutletContext.ts: -------------------------------------------------------------------------------- 1 | import type { Client, Conversation } from "@xmtp/browser-sdk"; 2 | 3 | export type ConversationOutletContext = { 4 | conversation: Conversation; 5 | client: Client; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversation/LoadConversation.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingOverlay } from "@mantine/core"; 2 | import type { Dm, Group } from "@xmtp/browser-sdk"; 3 | import { useEffect, useState } from "react"; 4 | import { useNavigate, useParams } from "react-router"; 5 | import type { ContentTypes } from "@/contexts/XMTPContext"; 6 | import { useConversations } from "@/hooks/useConversations"; 7 | import { CenteredLayout } from "@/layouts/CenteredLayout"; 8 | import { Conversation } from "./Conversation"; 9 | 10 | export const LoadConversation: React.FC = () => { 11 | const navigate = useNavigate(); 12 | const { conversationId } = useParams(); 13 | const { getConversationById } = useConversations(); 14 | const [conversation, setConversation] = useState< 15 | Group | Dm | undefined 16 | >(undefined); 17 | 18 | useEffect(() => { 19 | const loadConversation = async () => { 20 | if (conversationId) { 21 | const conversation = await getConversationById(conversationId); 22 | if (conversation) { 23 | setConversation(conversation); 24 | } else { 25 | void navigate("/conversations"); 26 | } 27 | } 28 | }; 29 | void loadConversation(); 30 | }, [conversationId]); 31 | 32 | return conversation ? ( 33 | 34 | ) : ( 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversations/ConversationCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | cursor: pointer; 3 | } 4 | 5 | .root:hover { 6 | background-color: var(--mantine-color-default-hover); 7 | } 8 | 9 | .root:focus { 10 | border-color: var(--mantine-primary-color-filled); 11 | } 12 | 13 | .selected { 14 | background-color: var(--mantine-color-default-hover); 15 | border-color: var(--mantine-primary-color-filled); 16 | } 17 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversations/ConversationList.module.css: -------------------------------------------------------------------------------- 1 | .root > * { 2 | padding-top: var(--mantine-spacing-sm); 3 | } 4 | 5 | .root > *:last-child { 6 | padding-bottom: var(--mantine-spacing-sm); 7 | } 8 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversations/ConversationList.tsx: -------------------------------------------------------------------------------- 1 | import type { Conversation } from "@xmtp/browser-sdk"; 2 | import { useMemo, type ComponentProps } from "react"; 3 | import { useParams } from "react-router"; 4 | import { Virtuoso } from "react-virtuoso"; 5 | import type { ContentTypes } from "@/contexts/XMTPContext"; 6 | import { ConversationCard } from "./ConversationCard"; 7 | import classes from "./ConversationList.module.css"; 8 | 9 | const List = (props: ComponentProps<"div">) => { 10 | return
; 11 | }; 12 | 13 | export type ConversationsListProps = { 14 | conversations: Conversation[]; 15 | }; 16 | 17 | export const ConversationsList: React.FC = ({ 18 | conversations, 19 | }) => { 20 | const { conversationId } = useParams(); 21 | const selectedConversationIndex = useMemo( 22 | () => 23 | conversations.findIndex( 24 | (conversation) => conversation.id === conversationId, 25 | ), 26 | [conversations, conversationId], 27 | ); 28 | return ( 29 | ( 37 | 38 | )} 39 | /> 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Conversations/ConversationsMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Menu } from "@mantine/core"; 2 | import { IconDots } from "@/icons/IconDots"; 3 | 4 | export type ConversationsMenuProps = { 5 | onSync: () => void; 6 | onSyncAll: () => void; 7 | disabled?: boolean; 8 | }; 9 | 10 | export const ConversationsMenu: React.FC = ({ 11 | onSync, 12 | onSyncAll, 13 | disabled, 14 | }) => { 15 | return ( 16 | 17 | 18 | 25 | 26 | 27 | Actions 28 | Sync 29 | Sync All 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/CopyButton.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | color: light-dark(var(--mantine-color-light-4), var(--mantine-color-dark-1)); 3 | } 4 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Text, Tooltip } from "@mantine/core"; 2 | import { useClipboard } from "@mantine/hooks"; 3 | import { IconCopy } from "@/icons/IconCopy"; 4 | import classes from "./CopyButton.module.css"; 5 | 6 | type CopyButtonProps = { 7 | value: string; 8 | }; 9 | 10 | export const CopyButton: React.FC = ({ value }) => { 11 | const clipboard = useClipboard({ timeout: 1000 }); 12 | 13 | const handleCopy = () => { 14 | clipboard.copy(value); 15 | }; 16 | 17 | const handleKeyboardCopy = ( 18 | event: React.KeyboardEvent, 19 | ) => { 20 | if (event.key === "Enter" || event.key === " ") { 21 | handleCopy(); 22 | } 23 | }; 24 | 25 | return ( 26 | Copied! 30 | ) : ( 31 | Copy 32 | ) 33 | } 34 | withArrow 35 | events={{ hover: true, focus: true, touch: true }}> 36 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/DateLabel.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Text, Tooltip } from "@mantine/core"; 2 | import { useClipboard } from "@mantine/hooks"; 3 | import { formatRFC3339, intlFormat } from "date-fns"; 4 | 5 | export type DateLabelTooltipProps = { 6 | date: Date; 7 | }; 8 | 9 | export const DateLabelTooltip: React.FC = ({ date }) => { 10 | return ( 11 | 12 | {formatRFC3339(date, { fractionDigits: 3 })} 13 | 14 | click to copy 15 | 16 | 17 | ); 18 | }; 19 | 20 | export type DateLabelProps = { 21 | date: Date; 22 | size?: "sm" | "xs" | "md" | "lg" | "xl"; 23 | align?: "left" | "right" | "center"; 24 | padding?: "xs" | "sm" | "md" | "lg" | "xl"; 25 | }; 26 | 27 | export const DateLabel: React.FC = ({ 28 | date, 29 | size = "sm", 30 | align = "left", 31 | padding, 32 | }) => { 33 | const clipboard = useClipboard({ timeout: 1000 }); 34 | 35 | const handleCopy = ( 36 | event: 37 | | React.MouseEvent 38 | | React.KeyboardEvent, 39 | ) => { 40 | event.stopPropagation(); 41 | clipboard.copy(formatRFC3339(date, { fractionDigits: 3 })); 42 | }; 43 | 44 | const handleKeyboardCopy = (event: React.KeyboardEvent) => { 45 | if (event.key === "Enter" || event.key === " ") { 46 | handleCopy(event); 47 | } 48 | }; 49 | 50 | return ( 51 | Copied! 55 | ) : ( 56 | 57 | ) 58 | } 59 | withArrow 60 | events={{ hover: true, focus: true, touch: true }}> 61 | 69 | {intlFormat(date, { 70 | year: "numeric", 71 | month: "2-digit", 72 | day: "2-digit", 73 | hour: "2-digit", 74 | minute: "2-digit", 75 | second: "2-digit", 76 | })} 77 | 78 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/InboxId.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Popover, Stack, Text } from "@mantine/core"; 2 | import { useState } from "react"; 3 | import { BadgeWithCopy } from "@/components/BadgeWithCopy"; 4 | import { shortAddress } from "@/helpers/strings"; 5 | 6 | export type InboxIdBadgeProps = { 7 | inboxId: string; 8 | address: string; 9 | size?: "xs" | "sm" | "md" | "lg" | "xl"; 10 | }; 11 | 12 | export const InboxIdBadge: React.FC = ({ 13 | inboxId, 14 | address, 15 | size = "lg", 16 | }) => { 17 | const [opened, setOpened] = useState(false); 18 | return ( 19 | 27 | 28 | 39 | 40 | { 43 | e.stopPropagation(); 44 | }}> 45 | 46 | 47 | Inbox ID 48 | 49 | 50 | 51 | Address 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/LoadingMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Loader, LoadingOverlay, Stack, Text } from "@mantine/core"; 2 | 3 | export type LoadingMessageProps = { 4 | message: string; 5 | }; 6 | 7 | export const LoadingMessage: React.FC = ({ message }) => { 8 | return ( 9 | 14 | 15 | {message} 16 | 17 | ), 18 | }} 19 | /> 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/FallbackContent.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, Text } from "@mantine/core"; 2 | import classes from "./TextContent.module.css"; 3 | 4 | export type FallbackContentProps = { 5 | text: string; 6 | }; 7 | 8 | export const FallbackContent: React.FC = ({ text }) => { 9 | return ( 10 | 11 | 18 | {text} 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/Message.module.css: -------------------------------------------------------------------------------- 1 | .root:hover { 2 | background-color: light-dark( 3 | var(--mantine-color-dark-light-hover), 4 | var(--mantine-color-dark-6) 5 | ); 6 | cursor: pointer; 7 | } 8 | 9 | .root:focus { 10 | border-color: var(--mantine-color-blue-outline); 11 | } 12 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/Message.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@mantine/core"; 2 | import type { Client, DecodedMessage } from "@xmtp/browser-sdk"; 3 | import { useNavigate, useOutletContext } from "react-router"; 4 | import classes from "./Message.module.css"; 5 | import { MessageContent } from "./MessageContent"; 6 | 7 | export type MessageProps = { 8 | message: DecodedMessage; 9 | scrollToMessage: (id: string) => void; 10 | }; 11 | 12 | export const Message: React.FC = ({ 13 | message, 14 | scrollToMessage, 15 | }) => { 16 | const { client } = useOutletContext<{ client: Client }>(); 17 | const isSender = client.inboxId === message.senderInboxId; 18 | const align = isSender ? "right" : "left"; 19 | const navigate = useNavigate(); 20 | return ( 21 | { 26 | if (e.key === "Enter") { 27 | void navigate( 28 | `/conversations/${message.conversationId}/message/${message.id}`, 29 | ); 30 | } 31 | }} 32 | onClick={() => 33 | void navigate( 34 | `/conversations/${message.conversationId}/message/${message.id}`, 35 | ) 36 | }> 37 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/MessageContentWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Group, Stack } from "@mantine/core"; 2 | import { DateLabel } from "@/components/DateLabel"; 3 | import { InboxIdBadge } from "@/components/InboxId"; 4 | import { useConversationContext } from "@/contexts/ConversationContext"; 5 | import { nsToDate } from "@/helpers/date"; 6 | 7 | export type MessageContentAlign = "left" | "right"; 8 | 9 | export type MessageContentWrapperProps = React.PropsWithChildren<{ 10 | align: MessageContentAlign; 11 | senderInboxId: string; 12 | sentAtNs: bigint; 13 | stopClickPropagation?: boolean; 14 | }>; 15 | 16 | export const MessageContentWrapper: React.FC = ({ 17 | align, 18 | senderInboxId, 19 | children, 20 | sentAtNs, 21 | stopClickPropagation = true, 22 | }) => { 23 | const { members } = useConversationContext(); 24 | return ( 25 | 26 | 27 | 31 | 32 | 37 | 38 | { 40 | if (stopClickPropagation) { 41 | event.stopPropagation(); 42 | } 43 | }}> 44 | {children} 45 | 46 | 47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/MessageList.module.css: -------------------------------------------------------------------------------- 1 | .root > * { 2 | padding-top: var(--mantine-spacing-sm); 3 | } 4 | 5 | .root > *:last-child { 6 | padding-bottom: var(--mantine-spacing-sm); 7 | } 8 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import type { DecodedMessage } from "@xmtp/browser-sdk"; 2 | import { useCallback, useMemo, useRef, type ComponentProps } from "react"; 3 | import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; 4 | import { Message } from "./Message"; 5 | import classes from "./MessageList.module.css"; 6 | 7 | const List = (props: ComponentProps<"div">) => { 8 | return
; 9 | }; 10 | 11 | export type MessageListProps = { 12 | messages: DecodedMessage[]; 13 | }; 14 | 15 | export const MessageList: React.FC = ({ messages }) => { 16 | const virtuoso = useRef(null); 17 | const messageMap = useMemo(() => { 18 | const map = new Map(); 19 | messages.forEach((message, index) => { 20 | map.set(message.id, index); 21 | }); 22 | return map; 23 | }, [messages]); 24 | const scrollToMessage = useCallback( 25 | (id: string) => { 26 | const index = messageMap.get(id); 27 | if (index !== undefined) { 28 | virtuoso.current?.scrollToIndex(index); 29 | } 30 | }, 31 | [messageMap], 32 | ); 33 | return ( 34 | ( 45 | 50 | )} 51 | /> 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/Messages.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from "@mantine/core"; 2 | import type { DecodedMessage } from "@xmtp/browser-sdk"; 3 | import { MessageList } from "./MessageList"; 4 | 5 | export type ConversationProps = { 6 | messages: DecodedMessage[]; 7 | }; 8 | 9 | export const Messages: React.FC = ({ messages }) => { 10 | return messages.length === 0 ? ( 11 | 18 | No messages 19 | 20 | ) : ( 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/TextContent.module.css: -------------------------------------------------------------------------------- 1 | .text { 2 | cursor: text; 3 | } 4 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/TextContent.tsx: -------------------------------------------------------------------------------- 1 | import { Paper, Text } from "@mantine/core"; 2 | import classes from "./TextContent.module.css"; 3 | 4 | export type TextContentProps = { 5 | text: string; 6 | }; 7 | 8 | export const TextContent: React.FC = ({ text }) => { 9 | return ( 10 | { 13 | event.stopPropagation(); 14 | }} 15 | bg="var(--mantine-color-blue-filled)" 16 | c="white" 17 | py="xs" 18 | px="sm" 19 | radius="md"> 20 | 27 | {text} 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Messages/TransactionReferenceContent.tsx: -------------------------------------------------------------------------------- 1 | import { Anchor, Box, Text } from "@mantine/core"; 2 | import type { TransactionReference } from "@xmtp/content-type-transaction-reference"; 3 | import { useMemo } from "react"; 4 | import * as viemChains from "viem/chains"; 5 | 6 | export type TransactionReferenceContentProps = { 7 | content: TransactionReference; 8 | }; 9 | 10 | export const TransactionReferenceContent: React.FC< 11 | TransactionReferenceContentProps 12 | > = ({ content }) => { 13 | const chain = useMemo(() => { 14 | const chains = Object.values(viemChains); 15 | const chainId = 16 | typeof content.networkId === "string" 17 | ? parseInt(content.networkId, 16) 18 | : content.networkId; 19 | return chains.find((chain) => chain.id === chainId); 20 | }, [content.networkId]); 21 | if (!chain) { 22 | return ( 23 | 24 | Chain Id: {content.networkId} 25 | Transaction Hash: {content.reference} 26 | 27 | ); 28 | } 29 | return ( 30 | { 35 | event.stopPropagation(); 36 | }}> 37 | View in explorer 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { Modal as MantineModal, type ModalProps } from "@mantine/core"; 2 | 3 | export const Modal: React.FC = ({ children, ...props }) => { 4 | return ( 5 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/contexts/ConversationContext.tsx: -------------------------------------------------------------------------------- 1 | import { Group, type Conversation } from "@xmtp/browser-sdk"; 2 | import { createContext, useContext, useEffect, useMemo, useState } from "react"; 3 | import type { ContentTypes } from "@/contexts/XMTPContext"; 4 | 5 | type ConversationContextType = { 6 | conversation?: Conversation; 7 | members: Map; 8 | }; 9 | 10 | const ConversationContext = createContext({ 11 | members: new Map(), 12 | }); 13 | 14 | export type ConversationProviderProps = React.PropsWithChildren<{ 15 | conversation: Conversation; 16 | }>; 17 | 18 | export const ConversationProvider: React.FC = ({ 19 | children, 20 | conversation, 21 | }) => { 22 | const [members, setMembers] = useState>(new Map()); 23 | 24 | useEffect(() => { 25 | if (!(conversation instanceof Group)) { 26 | return; 27 | } 28 | 29 | const loadMembers = async () => { 30 | const members = await conversation.members(); 31 | setMembers( 32 | new Map( 33 | members.map((m) => [m.inboxId, m.accountIdentifiers[0].identifier]), 34 | ), 35 | ); 36 | }; 37 | 38 | void loadMembers(); 39 | }, [conversation.id]); 40 | 41 | const value = useMemo( 42 | () => ({ conversation, members }), 43 | [conversation, members], 44 | ); 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | export const useConversationContext = () => { 54 | const context = useContext(ConversationContext); 55 | if (!context.conversation) { 56 | throw new Error( 57 | "useConversationContext must be used within a ConversationProvider", 58 | ); 59 | } 60 | return context as Required; 61 | }; 62 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --mantine-spacing-xxs: calc(0.5rem * var(--mantine-scale)); 3 | --mantine-spacing-xxxs: calc(0.25rem * var(--mantine-scale)); 4 | } 5 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.module.css" { 4 | const classes: { [key: string]: string }; 5 | export default classes; 6 | } 7 | 8 | declare module "*.png" { 9 | const src: string; 10 | export default src; 11 | } 12 | 13 | interface ImportMetaEnv { 14 | readonly VITE_PROJECT_ID: string; 15 | } 16 | 17 | interface ImportMeta { 18 | readonly env: ImportMetaEnv; 19 | } 20 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/helpers/createSigner.ts: -------------------------------------------------------------------------------- 1 | import type { Signer } from "@xmtp/browser-sdk"; 2 | import { toBytes, type Hex } from "viem"; 3 | import { privateKeyToAccount } from "viem/accounts"; 4 | 5 | export const createEphemeralSigner = (privateKey: Hex): Signer => { 6 | const account = privateKeyToAccount(privateKey); 7 | return { 8 | type: "EOA", 9 | getIdentifier: () => ({ 10 | identifier: account.address.toLowerCase(), 11 | identifierKind: "Ethereum", 12 | }), 13 | signMessage: async (message: string) => { 14 | const signature = await account.signMessage({ 15 | message, 16 | }); 17 | return toBytes(signature); 18 | }, 19 | }; 20 | }; 21 | 22 | export const createEOASigner = ( 23 | address: `0x${string}`, 24 | signMessage: (message: string) => Promise | string, 25 | ): Signer => { 26 | return { 27 | type: "EOA", 28 | getIdentifier: () => ({ 29 | identifier: address.toLowerCase(), 30 | identifierKind: "Ethereum", 31 | }), 32 | signMessage: async (message: string) => { 33 | const signature = await signMessage(message); 34 | return toBytes(signature); 35 | }, 36 | }; 37 | }; 38 | 39 | export const createSCWSigner = ( 40 | address: `0x${string}`, 41 | signMessage: (message: string) => Promise | string, 42 | chainId: number = 1, 43 | ): Signer => { 44 | console.log("Creating SCW signer with chain ID:", chainId); 45 | return { 46 | type: "SCW", 47 | getIdentifier: () => ({ 48 | identifier: address.toLowerCase(), 49 | identifierKind: "Ethereum", 50 | }), 51 | signMessage: async (message: string) => { 52 | const signature = await signMessage(message); 53 | const signatureBytes = toBytes(signature); 54 | return signatureBytes; 55 | }, 56 | getChainId: () => BigInt(chainId), 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/helpers/date.ts: -------------------------------------------------------------------------------- 1 | export function nsToDate(ns: bigint): Date { 2 | return new Date(Number(ns / 1_000_000n)); 3 | } 4 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/helpers/errors.ts: -------------------------------------------------------------------------------- 1 | export class ClientNotFoundError extends Error { 2 | constructor(context: string) { 3 | super(`XMTP client is required when ${context}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/helpers/strings.ts: -------------------------------------------------------------------------------- 1 | export const isValidEthereumAddress = ( 2 | address: string, 3 | ): address is `0x${string}` => /^0x[a-fA-F0-9]{40}$/.test(address); 4 | 5 | export const isValidInboxId = (inboxId: string): inboxId is string => 6 | /^[a-z0-9]{64}$/.test(inboxId); 7 | 8 | export const shortAddress = (address: string): string => 9 | `${address.substring(0, 6)}...${address.substring(address.length - 4)}`; 10 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/hooks/useAnalytics.ts: -------------------------------------------------------------------------------- 1 | import Plausible from "plausible-tracker"; 2 | import { useEffect } from "react"; 3 | 4 | export const useAnalytics = () => { 5 | useEffect(() => { 6 | const plausible = Plausible({ 7 | domain: "xmtp.chat", 8 | }); 9 | const cleanupAutoPageviews = plausible.enableAutoPageviews(); 10 | return () => { 11 | cleanupAutoPageviews(); 12 | }; 13 | }, []); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/hooks/useCollapsedMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from "@mantine/hooks"; 2 | 3 | export const useCollapsedMediaQuery = () => { 4 | return useMediaQuery("(max-width: 1080px)"); 5 | }; 6 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/hooks/useRedirect.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from "@mantine/hooks"; 2 | 3 | export const useRedirect = () => { 4 | const [redirectUrl, setRedirectUrl] = useLocalStorage({ 5 | key: "XMTP_REDIRECT_URL", 6 | defaultValue: "", 7 | getInitialValueInEffect: false, 8 | }); 9 | 10 | return { 11 | redirectUrl, 12 | setRedirectUrl, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/hooks/useSettings.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from "@mantine/hooks"; 2 | import { type ClientOptions, type XmtpEnv } from "@xmtp/browser-sdk"; 3 | import type { Hex } from "viem"; 4 | 5 | export const useSettings = () => { 6 | const [environment, setEnvironment] = useLocalStorage({ 7 | key: "XMTP_NETWORK", 8 | defaultValue: "dev", 9 | getInitialValueInEffect: false, 10 | }); 11 | const [ephemeralAccountKey, setEphemeralAccountKey] = 12 | useLocalStorage({ 13 | key: "XMTP_EPHEMERAL_ACCOUNT_KEY", 14 | defaultValue: null, 15 | getInitialValueInEffect: false, 16 | }); 17 | const [encryptionKey, setEncryptionKey] = useLocalStorage({ 18 | key: "XMTP_ENCRYPTION_KEY", 19 | defaultValue: "", 20 | getInitialValueInEffect: false, 21 | }); 22 | const [ephemeralAccountEnabled, setEphemeralAccountEnabled] = useLocalStorage( 23 | { 24 | key: "XMTP_USE_EPHEMERAL_ACCOUNT", 25 | defaultValue: false, 26 | getInitialValueInEffect: false, 27 | }, 28 | ); 29 | const [loggingLevel, setLoggingLevel] = useLocalStorage< 30 | ClientOptions["loggingLevel"] 31 | >({ 32 | key: "XMTP_LOGGING_LEVEL", 33 | defaultValue: "off", 34 | getInitialValueInEffect: false, 35 | }); 36 | const [forceSCW, setForceSCW] = useLocalStorage({ 37 | key: "XMTP_FORCE_SCW", 38 | defaultValue: false, 39 | getInitialValueInEffect: false, 40 | }); 41 | const [useSCW, setUseSCW] = useLocalStorage({ 42 | key: "XMTP_USE_SCW", 43 | defaultValue: false, 44 | getInitialValueInEffect: false, 45 | }); 46 | const [blockchain, setBlockchain] = useLocalStorage({ 47 | key: "XMTP_BLOCKCHAIN", 48 | defaultValue: 1, 49 | getInitialValueInEffect: false, 50 | }); 51 | 52 | return { 53 | blockchain, 54 | encryptionKey, 55 | environment, 56 | ephemeralAccountEnabled, 57 | ephemeralAccountKey, 58 | forceSCW, 59 | loggingLevel, 60 | useSCW, 61 | setBlockchain, 62 | setEncryptionKey, 63 | setEnvironment, 64 | setEphemeralAccountEnabled, 65 | setEphemeralAccountKey, 66 | setForceSCW, 67 | setLoggingLevel, 68 | setUseSCW, 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/icons/CoinbaseWallet.tsx: -------------------------------------------------------------------------------- 1 | export const CoinbaseWallet = () => { 2 | return ( 3 | 9 | 10 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/icons/EphemeralWallet.tsx: -------------------------------------------------------------------------------- 1 | export const EphemeralWallet = () => { 2 | return ( 3 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/icons/IconCopy.tsx: -------------------------------------------------------------------------------- 1 | export type IconCopyProps = { 2 | size?: number; 3 | }; 4 | 5 | export const IconCopy: React.FC = ({ size = 16 }) => { 6 | return ( 7 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/icons/IconDots.tsx: -------------------------------------------------------------------------------- 1 | export const IconDots = () => { 2 | return ( 3 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/icons/InjectedWallet.tsx: -------------------------------------------------------------------------------- 1 | export const InjectedWallet = () => { 2 | return ( 3 | 4 | 5 | 6 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/icons/WalletConnectWallet.tsx: -------------------------------------------------------------------------------- 1 | export const WalletConnectWallet = () => { 2 | return ( 3 | 9 | 10 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/layouts/CenteredLayout.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | height: 100dvh; 6 | width: 100dvw; 7 | position: relative; 8 | } 9 | 10 | .content { 11 | width: 70ch; 12 | } 13 | 14 | @media screen and (max-width: 1080px) { 15 | .content { 16 | width: auto; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/layouts/CenteredLayout.tsx: -------------------------------------------------------------------------------- 1 | import classes from "./CenteredLayout.module.css"; 2 | 3 | export const CenteredLayout: React.FC = ({ 4 | children, 5 | }) => { 6 | return ( 7 |
8 |
{children}
9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/layouts/ContentLayout.module.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --header-padding: var(--mantine-spacing-md); 3 | --header-button-height: rem(36px); 4 | --header-border-width: 1px; 5 | --header-height: calc( 6 | var(--header-padding) * 2 + var(--header-button-height) + 7 | var(--header-border-width) 8 | ); 9 | --header-border-color: var(--mantine-color-default-border); 10 | --content-gradient-color-light: var(--mantine-color-gray-0); 11 | --content-gradient-color-dark: var(--mantine-color-dark-7); 12 | } 13 | 14 | .root { 15 | position: relative; 16 | flex-grow: 1; 17 | } 18 | 19 | .header { 20 | flex-shrink: 0; 21 | position: relative; 22 | padding: var(--header-padding); 23 | border-bottom: var(--header-border-width) solid var(--header-border-color); 24 | height: var(--header-height); 25 | } 26 | 27 | .headerActions { 28 | flex-shrink: 0; 29 | margin-left: auto; 30 | } 31 | 32 | .content { 33 | position: relative; 34 | min-height: 0; 35 | flex-grow: 1; 36 | display: flex; 37 | flex-direction: column; 38 | --gradient-color: light-dark( 39 | var(--content-gradient-color-light), 40 | var(--content-gradient-color-dark) 41 | ); 42 | } 43 | 44 | .contentScrollFade::before { 45 | content: ""; 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | right: 0; 50 | height: 20px; 51 | background: linear-gradient(to top, transparent, var(--gradient-color) 20px); 52 | pointer-events: none; 53 | z-index: 1; 54 | border-radius: var(--mantine-radius-md) var(--mantine-radius-md) 0 0; 55 | } 56 | 57 | .contentScrollFade::after { 58 | content: ""; 59 | position: absolute; 60 | bottom: 0; 61 | left: 0; 62 | right: 0; 63 | height: 20px; 64 | background: linear-gradient( 65 | to bottom, 66 | transparent, 67 | var(--gradient-color) 20px 68 | ); 69 | pointer-events: none; 70 | z-index: 1; 71 | border-radius: 0 0 var(--mantine-radius-md) var(--mantine-radius-md); 72 | } 73 | 74 | .scrollArea { 75 | flex-grow: 1; 76 | } 77 | 78 | .footer { 79 | position: relative; 80 | flex-shrink: 0; 81 | } 82 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useClickOutside } from "@mantine/hooks"; 2 | import classes from "./MainLayout.module.css"; 3 | 4 | export const MainLayout: React.FC = ({ children }) => { 5 | return
{children}
; 6 | }; 7 | 8 | export const MainLayoutHeader: React.FC = ({ 9 | children, 10 | }) => { 11 | return ( 12 |
13 |
{children}
14 |
15 | ); 16 | }; 17 | 18 | export const MainLayoutContent: React.FC = ({ 19 | children, 20 | }) => { 21 | return ( 22 |
23 |
{children}
24 |
25 | ); 26 | }; 27 | 28 | export type MainLayoutNavProps = React.PropsWithChildren<{ 29 | opened?: boolean; 30 | toggle?: () => void; 31 | }>; 32 | 33 | export const MainLayoutNav: React.FC = ({ 34 | children, 35 | opened, 36 | toggle, 37 | }) => { 38 | const ref = useClickOutside(() => { 39 | if (opened) { 40 | toggle?.(); 41 | } 42 | }); 43 | const classNames = [classes.aside, opened && classes.showNavbar]; 44 | return ( 45 | 48 | ); 49 | }; 50 | 51 | export const MainLayoutFooter: React.FC = ({ 52 | children, 53 | }) => { 54 | return ( 55 |
56 |
{children}
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /apps/xmtp.chat/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { PermissionPolicySet } from "@xmtp/browser-sdk"; 2 | 3 | type AnyFn = (...args: unknown[]) => unknown; 4 | type ClassProperties = { 5 | [K in keyof C as C[K] extends AnyFn ? never : K]: C[K]; 6 | }; 7 | 8 | export type PolicySet = ClassProperties; 9 | -------------------------------------------------------------------------------- /apps/xmtp.chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "jsx": "react-jsx", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "noEmit": true, 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | }, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "ESNext" 18 | }, 19 | "include": ["src", "vite.config.ts", "postcss.config.cjs"], 20 | "exclude": ["dist", "node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /apps/xmtp.chat/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/xmtp.chat/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [tsconfigPaths(), react()], 8 | optimizeDeps: { 9 | exclude: ["@xmtp/wasm-bindings"], 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @xmtp/content-type-group-updated 2 | 3 | ## 2.0.2 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [779fd0c] 8 | - @xmtp/content-type-primitives@2.0.2 9 | 10 | ## 2.0.1 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [340fcf4] 15 | - @xmtp/content-type-primitives@2.0.1 16 | - @xmtp/proto@3.78.0 17 | 18 | ## 2.0.0 19 | 20 | ### Major Changes 21 | 22 | - 1777a23: Dropped support for CommonJS 23 | 24 | ### Patch Changes 25 | 26 | - Updated dependencies [1777a23] 27 | - @xmtp/content-type-primitives@2.0.0 28 | 29 | ## 1.0.1 30 | 31 | ### Patch Changes 32 | 33 | - Updated dependencies [63e5276] 34 | - @xmtp/content-type-primitives@1.0.3 35 | 36 | ## 1.0.0 37 | 38 | Initial release 39 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/README.md: -------------------------------------------------------------------------------- 1 | # Group updated content type 2 | 3 | This package provides an XMTP content type to support group updated messages. 4 | 5 | > [!NOTE] 6 | > This content type is included by default in official XMTP SDKs. 7 | 8 | ## Install the package 9 | 10 | ```bash 11 | # npm 12 | npm i @xmtp/content-type-group-updated 13 | 14 | # yarn 15 | yarn add @xmtp/content-type-group-updated 16 | 17 | # pnpm 18 | pnpm i @xmtp/content-type-group-updated 19 | ``` 20 | 21 | ## Developing 22 | 23 | Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. 24 | 25 | For more information on contributing to this repository, see our [contributing guidelines](../../CONTRIBUTING.md). 26 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/content-type-primitives", "@xmtp/proto"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/src/GroupUpdated.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { 3 | ContentTypeGroupUpdated, 4 | GroupUpdatedCodec, 5 | type GroupUpdated, 6 | } from "./GroupUpdated"; 7 | 8 | describe("ContentTypeGroupUpdated", () => { 9 | it("can encode/decode group updated data", () => { 10 | const groupUpdated: GroupUpdated = { 11 | initiatedByInboxId: "inbox-id", 12 | addedInboxes: [], 13 | removedInboxes: [], 14 | metadataFieldChanges: [], 15 | }; 16 | const codec = new GroupUpdatedCodec(); 17 | const ec = codec.encode(groupUpdated); 18 | expect(ec.type.sameAs(ContentTypeGroupUpdated)).toBe(true); 19 | const groupUpdated2 = codec.decode(ec); 20 | expect(groupUpdated2).toEqual(groupUpdated); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/src/GroupUpdated.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentTypeId, 3 | type ContentCodec, 4 | type EncodedContent, 5 | } from "@xmtp/content-type-primitives"; 6 | import { mlsTranscriptMessages } from "@xmtp/proto"; 7 | 8 | export const ContentTypeGroupUpdated = new ContentTypeId({ 9 | authorityId: "xmtp.org", 10 | typeId: "group_updated", 11 | versionMajor: 1, 12 | versionMinor: 0, 13 | }); 14 | 15 | export type GroupUpdated = mlsTranscriptMessages.GroupUpdated; 16 | 17 | export class GroupUpdatedCodec 18 | implements ContentCodec> 19 | { 20 | get contentType(): ContentTypeId { 21 | return ContentTypeGroupUpdated; 22 | } 23 | 24 | encode(content: GroupUpdated) { 25 | return { 26 | type: this.contentType, 27 | parameters: {}, 28 | content: mlsTranscriptMessages.GroupUpdated.encode(content).finish(), 29 | }; 30 | } 31 | 32 | decode(content: EncodedContent) { 33 | return mlsTranscriptMessages.GroupUpdated.decode(content.content); 34 | } 35 | 36 | fallback(): undefined { 37 | return undefined; 38 | } 39 | 40 | shouldPush() { 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./GroupUpdated"; 2 | -------------------------------------------------------------------------------- /content-types/content-type-group-updated/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "ESNext", 12 | "types": ["vitest/globals"] 13 | }, 14 | "include": ["src", "rollup.config.js"] 15 | } 16 | -------------------------------------------------------------------------------- /content-types/content-type-primitives/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @xmtp/content-type-primitives 2 | 3 | ## 2.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 592b5ff: 8 | - Converted `any` types to `unknown` 9 | - Removed `@deprecated` tag from `CodecRegistry` type 10 | - Refactored `registry` type of the `encode` and `decode` codec methods 11 | 12 | ## 2.0.1 13 | 14 | ### Patch Changes 15 | 16 | - 340fcf4: Upgraded `@xmtp/proto` dependency 17 | 18 | ## 2.0.0 19 | 20 | ### Major Changes 21 | 22 | - 1777a23: Dropped support for CommonJS 23 | 24 | ## 1.0.3 25 | 26 | ### Patch Changes 27 | 28 | - 63e5276: Upgraded `@xmtp/proto` dependency 29 | 30 | ## 1.0.2 31 | 32 | ### Patch Changes 33 | 34 | - 9addb1c: Added new parameter to `ContentCodec` generic type to allow typing of content parameters 35 | 36 | ## 1.0.1 37 | 38 | ### Patch Changes 39 | 40 | - [#71](https://github.com/xmtp/xmtp-js-content-types/pull/71) [`52bf31e`](https://github.com/xmtp/xmtp-js-content-types/commit/52bf31ec9d9b78da321727745d0a37bfa617362a) - Add more primitive types 41 | 42 | ## 1.0.0 43 | 44 | Initial release 45 | -------------------------------------------------------------------------------- /content-types/content-type-primitives/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-primitives/README.md: -------------------------------------------------------------------------------- 1 | # Content type primitives 2 | 3 | This package provides primitives for building custom XMTP content types. 4 | 5 | ## Install the package 6 | 7 | ```bash 8 | # npm 9 | npm i @xmtp/content-type-primitives 10 | 11 | # yarn 12 | yarn add @xmtp/content-type-primitives 13 | 14 | # pnpm 15 | pnpm i @xmtp/content-type-primitives 16 | ``` 17 | 18 | ## Developing 19 | 20 | Run `yarn dev` to build the content type primitives and watch for changes, which will trigger a rebuild. 21 | 22 | For more information on contributing to this repository, see our [contributing guidelines](../../CONTRIBUTING.md). 23 | -------------------------------------------------------------------------------- /content-types/content-type-primitives/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/proto"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-primitives/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { ContentTypeId } from "."; 3 | 4 | describe("ContentTypeId", () => { 5 | it("creates a new content type", () => { 6 | const contentType = new ContentTypeId({ 7 | authorityId: "foo", 8 | typeId: "bar", 9 | versionMajor: 1, 10 | versionMinor: 0, 11 | }); 12 | expect(contentType.authorityId).toEqual("foo"); 13 | expect(contentType.typeId).toEqual("bar"); 14 | expect(contentType.versionMajor).toEqual(1); 15 | expect(contentType.versionMinor).toEqual(0); 16 | }); 17 | 18 | it("creates a string from a content type", () => { 19 | const contentType = new ContentTypeId({ 20 | authorityId: "foo", 21 | typeId: "bar", 22 | versionMajor: 1, 23 | versionMinor: 0, 24 | }); 25 | expect(contentType.toString()).toEqual("foo/bar:1.0"); 26 | }); 27 | 28 | it("creates a content type from a string", () => { 29 | const contentType = ContentTypeId.fromString("foo/bar:1.0"); 30 | expect(contentType.authorityId).toEqual("foo"); 31 | expect(contentType.typeId).toEqual("bar"); 32 | expect(contentType.versionMajor).toEqual(1); 33 | expect(contentType.versionMinor).toEqual(0); 34 | }); 35 | 36 | it("compares two content types", () => { 37 | const contentType1 = new ContentTypeId({ 38 | authorityId: "foo", 39 | typeId: "bar", 40 | versionMajor: 1, 41 | versionMinor: 0, 42 | }); 43 | const contentType2 = new ContentTypeId({ 44 | authorityId: "baz", 45 | typeId: "qux", 46 | versionMajor: 1, 47 | versionMinor: 0, 48 | }); 49 | expect(contentType1.sameAs(contentType2)).toBe(false); 50 | expect(contentType1.sameAs(contentType1)).toBe(true); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /content-types/content-type-primitives/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { content } from "@xmtp/proto"; 2 | 3 | export class ContentTypeId { 4 | authorityId: string; 5 | 6 | typeId: string; 7 | 8 | versionMajor: number; 9 | 10 | versionMinor: number; 11 | 12 | constructor(obj: content.ContentTypeId) { 13 | this.authorityId = obj.authorityId; 14 | this.typeId = obj.typeId; 15 | this.versionMajor = obj.versionMajor; 16 | this.versionMinor = obj.versionMinor; 17 | } 18 | 19 | toString(): string { 20 | return `${this.authorityId}/${this.typeId}:${this.versionMajor}.${this.versionMinor}`; 21 | } 22 | 23 | static fromString(contentTypeString: string): ContentTypeId { 24 | const [idString, versionString] = contentTypeString.split(":"); 25 | const [authorityId, typeId] = idString.split("/"); 26 | const [major, minor] = versionString.split("."); 27 | return new ContentTypeId({ 28 | authorityId, 29 | typeId, 30 | versionMajor: Number(major), 31 | versionMinor: Number(minor), 32 | }); 33 | } 34 | 35 | sameAs(id: ContentTypeId): boolean { 36 | return this.authorityId === id.authorityId && this.typeId === id.typeId; 37 | } 38 | } 39 | 40 | export type EncodedContent> = { 41 | type: ContentTypeId; 42 | parameters: Parameters; 43 | fallback?: string; 44 | compression?: number; 45 | content: Uint8Array; 46 | }; 47 | 48 | export type ContentCodec< 49 | ContentType = unknown, 50 | Parameters = Record, 51 | > = { 52 | contentType: ContentTypeId; 53 | encode( 54 | content: ContentType, 55 | registry: CodecRegistry, 56 | ): EncodedContent; 57 | decode( 58 | content: EncodedContent, 59 | registry: CodecRegistry, 60 | ): ContentType; 61 | fallback(content: ContentType): string | undefined; 62 | shouldPush: (content: ContentType) => boolean; 63 | }; 64 | 65 | /** 66 | * An interface implemented for accessing codecs by content type. 67 | */ 68 | export interface CodecRegistry { 69 | codecFor(contentType: ContentTypeId): ContentCodec | undefined; 70 | } 71 | 72 | export type CodecMap = Map>; 73 | -------------------------------------------------------------------------------- /content-types/content-type-primitives/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "noEmit": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "target": "ESNext", 12 | "types": ["vitest/globals"] 13 | }, 14 | "include": ["src", "rollup.config.js"] 15 | } 16 | -------------------------------------------------------------------------------- /content-types/content-type-reaction/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-reaction/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/content-type-primitives"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-reaction/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Reaction"; 2 | -------------------------------------------------------------------------------- /content-types/content-type-reaction/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext", 13 | "types": ["vitest/globals"] 14 | }, 15 | "include": ["src", "rollup.config.js", "vitest.config.ts", "vitest.setup.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /content-types/content-type-reaction/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | globalSetup: ["./vitest.setup.ts"], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /content-types/content-type-reaction/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { glob } from "fast-glob"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export const teardown = async () => { 9 | const files = await glob("**/*.db3*", { cwd: __dirname }); 10 | await Promise.all(files.map((file) => unlink(join(__dirname, file)))); 11 | }; 12 | -------------------------------------------------------------------------------- /content-types/content-type-read-receipt/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-read-receipt/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/content-type-primitives"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-read-receipt/src/ReadReceipt.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentTypeId, 3 | type ContentCodec, 4 | } from "@xmtp/content-type-primitives"; 5 | 6 | export const ContentTypeReadReceipt = new ContentTypeId({ 7 | authorityId: "xmtp.org", 8 | typeId: "readReceipt", 9 | versionMajor: 1, 10 | versionMinor: 0, 11 | }); 12 | 13 | export type ReadReceipt = Record; 14 | 15 | export type ReadReceiptParameters = Record; 16 | 17 | export class ReadReceiptCodec 18 | implements ContentCodec 19 | { 20 | get contentType(): ContentTypeId { 21 | return ContentTypeReadReceipt; 22 | } 23 | 24 | encode() { 25 | return { 26 | type: ContentTypeReadReceipt, 27 | parameters: {}, 28 | content: new Uint8Array(), 29 | }; 30 | } 31 | 32 | decode(): ReadReceipt { 33 | return {}; 34 | } 35 | 36 | fallback(): string | undefined { 37 | return undefined; 38 | } 39 | 40 | shouldPush() { 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /content-types/content-type-read-receipt/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ReadReceipt"; 2 | -------------------------------------------------------------------------------- /content-types/content-type-read-receipt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext", 13 | "types": ["vitest/globals"] 14 | }, 15 | "include": ["src", "rollup.config.js", "vitest.config.ts", "vitest.setup.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /content-types/content-type-read-receipt/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | globalSetup: ["./vitest.setup.ts"], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /content-types/content-type-read-receipt/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { glob } from "fast-glob"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export const teardown = async () => { 9 | const files = await glob("**/*.db3*", { cwd: __dirname }); 10 | await Promise.all(files.map((file) => unlink(join(__dirname, file)))); 11 | }; 12 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/README.md: -------------------------------------------------------------------------------- 1 | # Remote attachment content type 2 | 3 | This package provides an XMTP content type to support sending file attachments that are stored off-network. Use it to enable your app to send and receive message attachments. 4 | 5 | ## What’s an attachment? 6 | 7 | Attachments are files. More specifically, attachments are objects that have: 8 | 9 | - `filename` Most files have names, at least the most common file types. 10 | - `mimeType` What kind of file is it? You can often assume this from the file extension, but it's nice to have a specific field for it. [Here's a list of common mime types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types). 11 | - `data` What is this file's data? Most files have data. If the file doesn't have data, then it's probably not the most interesting thing to send. 12 | 13 | ## Why remote attachments? 14 | 15 | Because XMTP messages can only be up to 1MB in size, we need to store the attachment somewhere other than the XMTP network. In other words, we need to store it in a remote location. 16 | 17 | ## What about encryption? 18 | 19 | End-to-end encryption must apply not only to XMTP messages, but to message attachments as well. For this reason, we need to encrypt the attachment before we store it. 20 | 21 | ## Install the package 22 | 23 | ```bash 24 | # npm 25 | npm i @xmtp/content-type-remote-attachment 26 | 27 | # yarn 28 | yarn add @xmtp/content-type-remote-attachment 29 | 30 | # pnpm 31 | pnpm i @xmtp/content-type-remote-attachment 32 | ``` 33 | 34 | ## Developing 35 | 36 | Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. 37 | 38 | For more information on contributing to this repository, see our [contributing guidelines](../../CONTRIBUTING.md). 39 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { resolveExtensions } from "@xmtp/rollup-plugin-resolve-extensions"; 4 | import { defineConfig } from "rollup"; 5 | import { dts } from "rollup-plugin-dts"; 6 | 7 | const plugins = [ 8 | typescript({ 9 | declaration: false, 10 | declarationMap: false, 11 | }), 12 | ]; 13 | 14 | const external = [ 15 | "@noble/secp256k1", 16 | "@xmtp/content-type-primitives", 17 | "@xmtp/proto", 18 | "node:crypto", 19 | ]; 20 | 21 | export default defineConfig([ 22 | { 23 | input: "src/index.ts", 24 | output: { 25 | file: "dist/index.js", 26 | format: "es", 27 | sourcemap: true, 28 | }, 29 | plugins, 30 | external, 31 | }, 32 | { 33 | input: "src/index.ts", 34 | output: { 35 | file: "dist/browser/index.js", 36 | format: "es", 37 | sourcemap: true, 38 | }, 39 | plugins: [ 40 | resolveExtensions({ extensions: [".browser"] }), 41 | terser(), 42 | ...plugins, 43 | ], 44 | external, 45 | }, 46 | { 47 | input: "src/index.ts", 48 | output: { 49 | file: "dist/index.d.ts", 50 | format: "es", 51 | }, 52 | plugins: [dts()], 53 | }, 54 | ]); 55 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/src/Attachment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentTypeId, 3 | type ContentCodec, 4 | type EncodedContent, 5 | } from "@xmtp/content-type-primitives"; 6 | 7 | export const ContentTypeAttachment = new ContentTypeId({ 8 | authorityId: "xmtp.org", 9 | typeId: "attachment", 10 | versionMajor: 1, 11 | versionMinor: 0, 12 | }); 13 | 14 | export type Attachment = { 15 | filename: string; 16 | mimeType: string; 17 | data: Uint8Array; 18 | }; 19 | 20 | export type AttachmentParameters = { 21 | filename: string; 22 | mimeType: string; 23 | }; 24 | 25 | export class AttachmentCodec 26 | implements ContentCodec 27 | { 28 | get contentType(): ContentTypeId { 29 | return ContentTypeAttachment; 30 | } 31 | 32 | encode(content: Attachment) { 33 | return { 34 | type: ContentTypeAttachment, 35 | parameters: { 36 | filename: content.filename, 37 | mimeType: content.mimeType, 38 | }, 39 | content: content.data, 40 | }; 41 | } 42 | 43 | decode(content: EncodedContent): Attachment { 44 | return { 45 | filename: content.parameters.filename, 46 | mimeType: content.parameters.mimeType, 47 | data: content.content, 48 | }; 49 | } 50 | 51 | fallback(content: Attachment): string | undefined { 52 | return `Can’t display "${content.filename}". This app doesn’t support attachments.`; 53 | } 54 | 55 | shouldPush() { 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/src/encryption/Ciphertext.ts: -------------------------------------------------------------------------------- 1 | import { ciphertext } from "@xmtp/proto"; 2 | 3 | export const AESKeySize = 32; // bytes 4 | export const KDFSaltSize = 32; // bytes 5 | // AES-GCM defaults from https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams 6 | export const AESGCMNonceSize = 12; // property iv 7 | export const AESGCMTagLength = 16; // property tagLength 8 | 9 | // Ciphertext packages the encrypted ciphertext with the salt and nonce used to produce it. 10 | // salt and nonce are not secret, and should be transmitted/stored along with the encrypted ciphertext. 11 | export default class Ciphertext implements ciphertext.Ciphertext { 12 | aes256GcmHkdfSha256: ciphertext.Ciphertext_Aes256gcmHkdfsha256 | undefined; 13 | 14 | constructor(obj: ciphertext.Ciphertext) { 15 | if (!obj.aes256GcmHkdfSha256) { 16 | throw new Error("invalid ciphertext"); 17 | } 18 | if (obj.aes256GcmHkdfSha256.payload.length < AESGCMTagLength) { 19 | throw new Error( 20 | `invalid ciphertext ciphertext length: ${obj.aes256GcmHkdfSha256.payload.length}`, 21 | ); 22 | } 23 | if (obj.aes256GcmHkdfSha256.hkdfSalt.length !== KDFSaltSize) { 24 | throw new Error( 25 | `invalid ciphertext salt length: ${obj.aes256GcmHkdfSha256.hkdfSalt.length}`, 26 | ); 27 | } 28 | if (obj.aes256GcmHkdfSha256.gcmNonce.length !== AESGCMNonceSize) { 29 | throw new Error( 30 | `invalid ciphertext nonce length: ${obj.aes256GcmHkdfSha256.gcmNonce.length}`, 31 | ); 32 | } 33 | this.aes256GcmHkdfSha256 = obj.aes256GcmHkdfSha256; 34 | } 35 | 36 | toBytes(): Uint8Array { 37 | return ciphertext.Ciphertext.encode(this).finish(); 38 | } 39 | 40 | static fromBytes(bytes: Uint8Array): Ciphertext { 41 | return new Ciphertext(ciphertext.Ciphertext.decode(bytes)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/src/encryption/crypto.browser.ts: -------------------------------------------------------------------------------- 1 | /*********************************************************************************************** 2 | * DO NOT IMPORT THIS FILE DIRECTLY 3 | ***********************************************************************************************/ 4 | const crypto = window.crypto; 5 | export default crypto; 6 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/src/encryption/crypto.ts: -------------------------------------------------------------------------------- 1 | import { webcrypto } from "node:crypto"; 2 | 3 | const crypto = webcrypto; 4 | export default crypto; 5 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/src/encryption/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Ciphertext } from "./Ciphertext"; 2 | export { default as crypto } from "./crypto"; 3 | export * from "./encryption"; 4 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Attachment"; 2 | export * from "./RemoteAttachment"; 3 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext", 13 | "types": ["vitest/globals"] 14 | }, 15 | "include": ["src", "rollup.config.js", "vitest.config.ts", "vitest.setup.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | globalSetup: ["./vitest.setup.ts"], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /content-types/content-type-remote-attachment/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { glob } from "fast-glob"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export const teardown = async () => { 9 | const files = await glob("**/*.db3*", { cwd: __dirname }); 10 | await Promise.all(files.map((file) => unlink(join(__dirname, file)))); 11 | }; 12 | -------------------------------------------------------------------------------- /content-types/content-type-reply/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-reply/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/proto", "@xmtp/content-type-primitives"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-reply/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Reply"; 2 | -------------------------------------------------------------------------------- /content-types/content-type-reply/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext", 13 | "types": ["vitest/globals"] 14 | }, 15 | "include": ["src", "rollup.config.js", "vitest.config.ts", "vitest.setup.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /content-types/content-type-reply/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | globalSetup: ["./vitest.setup.ts"], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /content-types/content-type-reply/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { glob } from "fast-glob"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export const teardown = async () => { 9 | const files = await glob("**/*.db3*", { cwd: __dirname }); 10 | await Promise.all(files.map((file) => unlink(join(__dirname, file)))); 11 | }; 12 | -------------------------------------------------------------------------------- /content-types/content-type-text/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @xmtp/content-type-text 2 | 3 | ## 2.0.2 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [779fd0c] 8 | - @xmtp/content-type-primitives@2.0.2 9 | 10 | ## 2.0.1 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [340fcf4] 15 | - @xmtp/content-type-primitives@2.0.1 16 | 17 | ## 2.0.0 18 | 19 | ### Major Changes 20 | 21 | - 1777a23: Dropped support for CommonJS 22 | 23 | ### Patch Changes 24 | 25 | - Updated dependencies [1777a23] 26 | - @xmtp/content-type-primitives@2.0.0 27 | 28 | ## 1.0.1 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies [63e5276] 33 | - @xmtp/content-type-primitives@1.0.3 34 | 35 | ## 1.0.0 36 | 37 | Initial release 38 | -------------------------------------------------------------------------------- /content-types/content-type-text/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-text/README.md: -------------------------------------------------------------------------------- 1 | # Text content type 2 | 3 | This package provides an XMTP content type to support text messages. 4 | 5 | > [!NOTE] 6 | > This content type is included by default in official XMTP SDKs. 7 | 8 | ## Install the package 9 | 10 | ```bash 11 | # npm 12 | npm i @xmtp/content-type-text 13 | 14 | # yarn 15 | yarn add @xmtp/content-type-text 16 | 17 | # pnpm 18 | pnpm i @xmtp/content-type-text 19 | ``` 20 | 21 | ## Send a text message 22 | 23 | Use a string to send a text message. It's not required to specify a content type in the send options for text messages. 24 | 25 | ```tsx 26 | await conversation.send("gm"); 27 | ``` 28 | 29 | ## Developing 30 | 31 | Run `yarn dev` to build the content type and watch for changes, which will trigger a rebuild. 32 | 33 | For more information on contributing to this repository, see our [contributing guidelines](../../CONTRIBUTING.md). 34 | -------------------------------------------------------------------------------- /content-types/content-type-text/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/content-type-primitives"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-text/src/Text.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { ContentTypeText, Encoding, TextCodec } from "./Text"; 3 | 4 | describe("ContentTypeText", () => { 5 | it("can encode/decode text", () => { 6 | const text = "Hey"; 7 | const codec = new TextCodec(); 8 | const ec = codec.encode(text); 9 | expect(ec.type.sameAs(ContentTypeText)).toBe(true); 10 | expect(ec.parameters.encoding).toEqual(Encoding.utf8); 11 | const text2 = codec.decode(ec); 12 | expect(text2).toEqual(text); 13 | }); 14 | 15 | it("defaults to utf-8", () => { 16 | const text = "Hey"; 17 | const codec = new TextCodec(); 18 | const ec = codec.encode(text); 19 | expect(ec.type.sameAs(ContentTypeText)).toBe(true); 20 | expect(ec.parameters.encoding).toEqual(Encoding.utf8); 21 | const text2 = codec.decode(ec); 22 | expect(text2).toEqual(text); 23 | }); 24 | 25 | it("throws on invalid input", () => { 26 | const codec = new TextCodec(); 27 | const ec = { 28 | type: ContentTypeText, 29 | parameters: { 30 | encoding: Encoding.utf8, 31 | }, 32 | content: {} as Uint8Array, 33 | }; 34 | expect(() => codec.decode(ec)).toThrow(); 35 | }); 36 | 37 | it("throws on unknown encoding", () => { 38 | const codec = new TextCodec(); 39 | const ec = { 40 | type: ContentTypeText, 41 | parameters: { encoding: "UTF-16" } as unknown as { encoding: Encoding }, 42 | content: new Uint8Array(0), 43 | }; 44 | expect(() => codec.decode(ec)).toThrow("unrecognized encoding UTF-16"); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /content-types/content-type-text/src/Text.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentTypeId, 3 | type ContentCodec, 4 | type EncodedContent, 5 | } from "@xmtp/content-type-primitives"; 6 | 7 | export const ContentTypeText = new ContentTypeId({ 8 | authorityId: "xmtp.org", 9 | typeId: "text", 10 | versionMajor: 1, 11 | versionMinor: 0, 12 | }); 13 | 14 | export enum Encoding { 15 | utf8 = "UTF-8", 16 | unknown = "unknown", 17 | } 18 | 19 | export type TextParameters = { 20 | encoding: Encoding; 21 | }; 22 | 23 | export class TextCodec implements ContentCodec { 24 | get contentType(): ContentTypeId { 25 | return ContentTypeText; 26 | } 27 | 28 | encode(content: string) { 29 | return { 30 | type: ContentTypeText, 31 | parameters: { encoding: Encoding.utf8 }, 32 | content: new TextEncoder().encode(content), 33 | }; 34 | } 35 | 36 | decode(content: EncodedContent) { 37 | if (content.parameters.encoding !== Encoding.utf8) { 38 | throw new Error(`unrecognized encoding ${content.parameters.encoding}`); 39 | } 40 | return new TextDecoder().decode(content.content); 41 | } 42 | 43 | fallback() { 44 | return undefined; 45 | } 46 | 47 | shouldPush() { 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /content-types/content-type-text/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Text"; 2 | -------------------------------------------------------------------------------- /content-types/content-type-text/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext", 13 | "types": ["vitest/globals"] 14 | }, 15 | "include": ["src", "rollup.config.js", "vitest.config.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /content-types/content-type-text/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @xmtp/content-type-transaction-reference 2 | 3 | ## 2.0.2 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [779fd0c] 8 | - @xmtp/content-type-primitives@2.0.2 9 | 10 | ## 2.0.1 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [340fcf4] 15 | - @xmtp/content-type-primitives@2.0.1 16 | 17 | ## 2.0.0 18 | 19 | ### Major Changes 20 | 21 | - 1777a23: Dropped support for CommonJS 22 | 23 | ### Patch Changes 24 | 25 | - Updated dependencies [1777a23] 26 | - @xmtp/content-type-primitives@2.0.0 27 | 28 | ## 1.0.5 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies [63e5276] 33 | - @xmtp/content-type-primitives@1.0.3 34 | 35 | ## 1.0.4 36 | 37 | ### Patch Changes 38 | 39 | - [#75](https://github.com/xmtp/xmtp-js-content-types/pull/75) [`da0bd85`](https://github.com/xmtp/xmtp-js-content-types/commit/da0bd8578d5f5032b221e25f02e8492b27929d6c) 40 | - Use primitives package for types 41 | 42 | ## 1.0.3 43 | 44 | ### Patch Changes 45 | 46 | - [#65](https://github.com/xmtp/xmtp-js-content-types/pull/65) [`c4d43dc`](https://github.com/xmtp/xmtp-js-content-types/commit/c4d43dc948231de5c7f730e06f0931076de0673b) 47 | - Add `shouldPush` to all content codecs 48 | 49 | ## 1.0.2 50 | 51 | ### Patch Changes 52 | 53 | - [#60](https://github.com/xmtp/xmtp-js-content-types/pull/60) [`5b9310a`](https://github.com/xmtp/xmtp-js-content-types/commit/5b9310ac89fd23e5cfd74903894073b6ef8af7c3) 54 | - Upgraded JS SDK to `11.3.12` 55 | 56 | ## 1.0.1 57 | 58 | ### Patch Changes 59 | 60 | - [#51](https://github.com/xmtp/xmtp-js-content-types/pull/51) [`aeb6db7`](https://github.com/xmtp/xmtp-js-content-types/commit/aeb6db73a63409a33c7d3d3431e33682b0ce4c4d) 61 | - Only publish files in the `/dist` directory 62 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/content-type-primitives"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/src/TransactionReference.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentTypeId, 3 | type ContentCodec, 4 | type EncodedContent, 5 | } from "@xmtp/content-type-primitives"; 6 | 7 | export const ContentTypeTransactionReference = new ContentTypeId({ 8 | authorityId: "xmtp.org", 9 | typeId: "transactionReference", 10 | versionMajor: 1, 11 | versionMinor: 0, 12 | }); 13 | 14 | export type TransactionReference = { 15 | /** 16 | * The namespace for the networkId 17 | */ 18 | namespace?: string; 19 | /** 20 | * The networkId for the transaction, in decimal or hexidecimal format 21 | */ 22 | networkId: number | string; 23 | /** 24 | * The transaction hash 25 | */ 26 | reference: string; 27 | /** 28 | * Optional metadata object 29 | */ 30 | metadata?: { 31 | transactionType: string; 32 | currency: string; 33 | amount: number; 34 | decimals: number; 35 | fromAddress: string; 36 | toAddress: string; 37 | }; 38 | }; 39 | 40 | export class TransactionReferenceCodec 41 | implements ContentCodec 42 | { 43 | get contentType(): ContentTypeId { 44 | return ContentTypeTransactionReference; 45 | } 46 | 47 | encode(content: TransactionReference): EncodedContent { 48 | const encoded = { 49 | type: ContentTypeTransactionReference, 50 | parameters: {}, 51 | content: new TextEncoder().encode(JSON.stringify(content)), 52 | }; 53 | return encoded; 54 | } 55 | 56 | decode(encodedContent: EncodedContent): TransactionReference { 57 | const uint8Array = encodedContent.content; 58 | const contentReceived = JSON.parse( 59 | new TextDecoder().decode(uint8Array), 60 | ) as TransactionReference; 61 | return contentReceived; 62 | } 63 | 64 | fallback(content: TransactionReference): string | undefined { 65 | if (content.reference) { 66 | return `[Crypto transaction] Use a blockchain explorer to learn more using the transaction hash: ${content.reference}`; 67 | } 68 | return `Crypto transaction`; 69 | } 70 | 71 | shouldPush() { 72 | return true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | TransactionReferenceCodec, 3 | ContentTypeTransactionReference, 4 | } from "./TransactionReference"; 5 | export type { TransactionReference } from "./TransactionReference"; 6 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext", 13 | "types": ["vitest/globals"] 14 | }, 15 | "include": ["src", "rollup.config.js", "vitest.config.ts", "vitest.setup.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | globalSetup: ["./vitest.setup.ts"], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /content-types/content-type-transaction-reference/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { glob } from "fast-glob"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export const teardown = async () => { 9 | const files = await glob("**/*.db3*", { cwd: __dirname }); 10 | await Promise.all(files.map((file) => unlink(join(__dirname, file)))); 11 | }; 12 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @xmtp/content-type-wallet-send-calls 2 | 3 | ## 2.0.0 4 | 5 | ### BREAKING CHANGES 6 | 7 | - Replaced `Record` types with `Record` 8 | 9 | ### Minor Changes 10 | 11 | - Removed redundant `| undefined` types 12 | 13 | ## 1.0.1 14 | 15 | ### Patch Changes 16 | 17 | - Updated dependencies [779fd0c] 18 | - @xmtp/content-type-primitives@2.0.2 19 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | 6 | const plugins = [ 7 | typescript({ 8 | declaration: false, 9 | declarationMap: false, 10 | }), 11 | ]; 12 | 13 | const external = ["@xmtp/content-type-primitives"]; 14 | 15 | export default defineConfig([ 16 | { 17 | input: "src/index.ts", 18 | output: { 19 | file: "dist/index.js", 20 | format: "es", 21 | sourcemap: true, 22 | }, 23 | plugins, 24 | external, 25 | }, 26 | { 27 | input: "src/index.ts", 28 | output: { 29 | file: "dist/browser/index.js", 30 | format: "es", 31 | sourcemap: true, 32 | }, 33 | plugins: [...plugins, terser()], 34 | external, 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.d.ts", 40 | format: "es", 41 | }, 42 | plugins: [dts()], 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/src/WalletSendCalls.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentTypeId, 3 | type ContentCodec, 4 | type EncodedContent, 5 | } from "@xmtp/content-type-primitives"; 6 | 7 | export const ContentTypeWalletSendCalls = new ContentTypeId({ 8 | authorityId: "xmtp.org", 9 | typeId: "walletSendCalls", 10 | versionMajor: 1, 11 | versionMinor: 0, 12 | }); 13 | 14 | export type WalletSendCallsParams = { 15 | version: string; 16 | chainId: `0x${string}`; // Hex chain id 17 | from: `0x${string}`; 18 | calls: { 19 | to?: `0x${string}`; 20 | data?: `0x${string}`; 21 | value?: `0x${string}`; // Hex value 22 | gas?: `0x${string}`; 23 | metadata?: { 24 | description: string; 25 | transactionType: string; 26 | } & Record; 27 | }[]; 28 | capabilities?: Record; 29 | }; 30 | 31 | export class WalletSendCallsCodec 32 | implements ContentCodec 33 | { 34 | get contentType(): ContentTypeId { 35 | return ContentTypeWalletSendCalls; 36 | } 37 | 38 | encode(content: WalletSendCallsParams): EncodedContent { 39 | const encoded = { 40 | type: ContentTypeWalletSendCalls, 41 | parameters: {}, 42 | content: new TextEncoder().encode(JSON.stringify(content)), 43 | }; 44 | return encoded; 45 | } 46 | 47 | decode(encodedContent: EncodedContent): WalletSendCallsParams { 48 | const uint8Array = encodedContent.content; 49 | const contentReceived = JSON.parse( 50 | new TextDecoder().decode(uint8Array), 51 | ) as WalletSendCallsParams; 52 | return contentReceived; 53 | } 54 | 55 | fallback(content: WalletSendCallsParams): string | undefined { 56 | return `[Transaction request generated]: ${JSON.stringify(content)}`; 57 | } 58 | 59 | shouldPush() { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | WalletSendCallsCodec, 3 | ContentTypeWalletSendCalls, 4 | } from "./WalletSendCalls"; 5 | export type { WalletSendCallsParams } from "./WalletSendCalls"; 6 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext", 13 | "types": ["vitest/globals"] 14 | }, 15 | "include": ["src", "rollup.config.js", "vitest.config.ts", "vitest.setup.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | globalSetup: ["./vitest.setup.ts"], 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /content-types/content-type-wallet-send-calls/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { glob } from "fast-glob"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export const teardown = async () => { 9 | const files = await glob("**/*.db3*", { cwd: __dirname }); 10 | await Promise.all(files.map((file) => unlink(join(__dirname, file)))); 11 | }; 12 | -------------------------------------------------------------------------------- /dev/compose: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | 4 | docker compose -f dev/docker-compose.yml -p "xmtp-js" "$@" 5 | -------------------------------------------------------------------------------- /dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | node: 3 | image: xmtp/node-go:latest 4 | platform: linux/amd64 5 | environment: 6 | - GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67 7 | command: 8 | - --store.enable 9 | - --store.db-connection-string=postgres://postgres:xmtp@db:5432/postgres?sslmode=disable 10 | - --store.reader-db-connection-string=postgres://postgres:xmtp@db:5432/postgres?sslmode=disable 11 | - --mls-store.db-connection-string=postgres://postgres:xmtp@mlsdb:5432/postgres?sslmode=disable 12 | - --mls-validation.grpc-address=validation:50051 13 | - --api.enable-mls 14 | - --wait-for-db=30s 15 | - --api.authn.enable 16 | ports: 17 | - 5555:5555 18 | - 5556:5556 19 | depends_on: 20 | - db 21 | - mlsdb 22 | - validation 23 | 24 | validation: 25 | image: ghcr.io/xmtp/mls-validation-service:main 26 | platform: linux/amd64 27 | 28 | db: 29 | image: postgres:13 30 | environment: 31 | POSTGRES_PASSWORD: xmtp 32 | 33 | mlsdb: 34 | image: postgres:13 35 | environment: 36 | POSTGRES_PASSWORD: xmtp 37 | 38 | upload-service: 39 | build: ./uploadService 40 | ports: 41 | - 3000:3000 42 | 43 | history-server: 44 | image: ghcr.io/xmtp/message-history-server:main 45 | platform: linux/amd64 46 | ports: 47 | - 5558:5558 48 | -------------------------------------------------------------------------------- /dev/down: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | script_dir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | 5 | "${script_dir}"/compose down 6 | -------------------------------------------------------------------------------- /dev/up: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eou pipefail 3 | script_dir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 4 | 5 | "${script_dir}"/compose pull 6 | "${script_dir}"/compose up -d --wait 7 | -------------------------------------------------------------------------------- /dev/uploadService/Dockerfile: -------------------------------------------------------------------------------- 1 | # Fetching the minified node image on apline linux 2 | FROM node:18.19-slim 3 | 4 | WORKDIR /uploadService 5 | COPY . . 6 | 7 | RUN apt-get update && \ 8 | apt-get install -y openssl && \ 9 | openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -sha256 -days 3650 -subj /CN=localhost -out cert.pem 10 | 11 | RUN npm install 12 | 13 | CMD ["node", "index.js"] 14 | 15 | EXPOSE 3000 16 | -------------------------------------------------------------------------------- /dev/uploadService/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const https = require("https"); 3 | const express = require("express"); 4 | const bodyParser = require("body-parser"); 5 | const app = express(); 6 | const port = 3000; 7 | 8 | const key = fs.readFileSync("key.pem", "utf-8"); 9 | const cert = fs.readFileSync("cert.pem", "utf-8"); 10 | 11 | const UPLOADS = {}; 12 | 13 | app.use(bodyParser.raw({ type: "application/octet-stream" })); 14 | 15 | app.get("/:path", (req, res) => { 16 | const path = req.params.path; 17 | console.log(`GET /${path}`); 18 | const file = UPLOADS[path]; 19 | if (file) { 20 | res.header("Content-Type", "application/octet-stream"); 21 | res.send(file); 22 | } else { 23 | console.log(`Upload path found: ${path}`); 24 | } 25 | }); 26 | 27 | app.post("/:path", (req, res) => { 28 | const path = req.params.path; 29 | console.log(`POST /${path}`); 30 | UPLOADS[path] = req.body; 31 | res.sendStatus(200); 32 | }); 33 | 34 | https.createServer({ key, cert }, app).listen(port, () => { 35 | console.log(`Upload service listening on port ${port}`); 36 | }); 37 | -------------------------------------------------------------------------------- /dev/uploadService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uploadservice", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "body-parser": "^1.20.2", 11 | "express": "^4.18.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmtp-js", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "workspaces": [ 7 | "apps/*", 8 | "content-types/*", 9 | "examples/*", 10 | "packages/*", 11 | "sdks/*" 12 | ], 13 | "scripts": { 14 | "build": "turbo run build", 15 | "clean": "turbo run clean && rimraf .turbo node_modules && yarn cache clean", 16 | "format": "prettier -w .", 17 | "format:check": "prettier -c .", 18 | "lint": "yarn build && eslint .", 19 | "publish": "yarn build && changeset publish", 20 | "reset": "yarn clean && yarn && yarn build", 21 | "test": "FORCE_COLOR=1 turbo run test", 22 | "test:setup": "./dev/up", 23 | "test:teardown": "./dev/down", 24 | "typecheck": "FORCE_COLOR=1 turbo run typecheck" 25 | }, 26 | "dependencies": { 27 | "@changesets/changelog-git": "^0.2.1", 28 | "@changesets/cli": "^2.29.4" 29 | }, 30 | "devDependencies": { 31 | "@eslint/compat": "^1.2.9", 32 | "@eslint/js": "^9.27.0", 33 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1", 34 | "@types/node": "^22.15.23", 35 | "eslint": "^9.27.0", 36 | "eslint-config-prettier": "^10.1.5", 37 | "eslint-plugin-prettier": "^5.4.0", 38 | "globals": "^16.2.0", 39 | "prettier": "^3.5.3", 40 | "prettier-plugin-packagejson": "^2.5.14", 41 | "rimraf": "^6.0.1", 42 | "turbo": "^2.5.3", 43 | "typescript": "^5.8.3", 44 | "typescript-eslint": "^8.33.0" 45 | }, 46 | "packageManager": "yarn@4.9.1", 47 | "engines": { 48 | "node": ">=22" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sdks/browser-sdk/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sdks/browser-sdk/rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from "@rollup/plugin-terser"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | import tsConfigPaths from "rollup-plugin-tsconfig-paths"; 6 | 7 | const plugins = [ 8 | tsConfigPaths(), 9 | typescript({ 10 | declaration: false, 11 | declarationMap: false, 12 | }), 13 | terser(), 14 | ]; 15 | 16 | const external = [ 17 | "@xmtp/content-type-text", 18 | "@xmtp/wasm-bindings", 19 | "@xmtp/content-type-primitives", 20 | "@xmtp/content-type-group-updated", 21 | "uuid", 22 | ]; 23 | 24 | export default defineConfig([ 25 | { 26 | input: "src/index.ts", 27 | output: { 28 | file: "dist/index.js", 29 | format: "es", 30 | sourcemap: true, 31 | }, 32 | plugins, 33 | external, 34 | }, 35 | { 36 | input: "src/workers/client.ts", 37 | output: { 38 | file: "dist/workers/client.js", 39 | format: "es", 40 | sourcemap: true, 41 | }, 42 | plugins, 43 | external, 44 | }, 45 | { 46 | input: "src/workers/utils.ts", 47 | output: { 48 | file: "dist/workers/utils.js", 49 | format: "es", 50 | sourcemap: true, 51 | }, 52 | plugins, 53 | external, 54 | }, 55 | { 56 | input: "src/index.ts", 57 | output: { 58 | file: "dist/index.d.ts", 59 | format: "es", 60 | }, 61 | plugins: [tsConfigPaths(), dts()], 62 | }, 63 | ]); 64 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/Dm.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from "@/Client"; 2 | import { Conversation } from "@/Conversation"; 3 | import type { SafeConversation } from "@/utils/conversions"; 4 | 5 | /** 6 | * Represents a direct message conversation between two inboxes 7 | * 8 | * This class is not intended to be initialized directly. 9 | */ 10 | export class Dm extends Conversation { 11 | #client: Client; 12 | #id: string; 13 | 14 | /** 15 | * Creates a new direct message conversation instance 16 | * 17 | * @param client - The client instance managing this direct message conversation 18 | * @param id - Identifier for the direct message conversation 19 | * @param data - Optional conversation data to initialize with 20 | */ 21 | constructor( 22 | client: Client, 23 | id: string, 24 | data?: SafeConversation, 25 | ) { 26 | super(client, id, data); 27 | this.#client = client; 28 | this.#id = id; 29 | } 30 | 31 | /** 32 | * Retrieves the inbox ID of the other participant in the DM 33 | * 34 | * @returns Promise that resolves with the peer's inbox ID 35 | */ 36 | async peerInboxId() { 37 | return this.#client.sendMessage("dm.peerInboxId", { 38 | id: this.#id, 39 | }); 40 | } 41 | 42 | async getDuplicateDms() { 43 | return this.#client.sendMessage("dm.getDuplicateDms", { 44 | id: this.#id, 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/Utils.ts: -------------------------------------------------------------------------------- 1 | import type { Identifier } from "@xmtp/wasm-bindings"; 2 | import type { XmtpEnv } from "@/types/options"; 3 | import { UtilsWorkerClass } from "@/UtilsWorkerClass"; 4 | 5 | /** 6 | * Utility class that provides helper functions for XMTP inbox IDs 7 | */ 8 | export class Utils extends UtilsWorkerClass { 9 | /** 10 | * Creates a new Utils instance 11 | * 12 | * @param enableLogging - Optional flag to enable logging 13 | */ 14 | constructor(enableLogging?: boolean) { 15 | const worker = new Worker(new URL("./workers/utils", import.meta.url), { 16 | type: "module", 17 | }); 18 | super(worker, enableLogging ?? false); 19 | } 20 | 21 | /** 22 | * Generates an inbox ID for a given identifier 23 | * 24 | * @param identifier - The identifier to generate an inbox ID for 25 | * @returns Promise that resolves with the generated inbox ID 26 | */ 27 | async generateInboxId(identifier: Identifier) { 28 | return this.sendMessage("utils.generateInboxId", { 29 | identifier, 30 | }); 31 | } 32 | 33 | /** 34 | * Gets the inbox ID for a specific identifier and optional environment 35 | * 36 | * @param identifier - The identifier to get the inbox ID for 37 | * @param env - Optional XMTP environment configuration (default: "dev") 38 | * @returns Promise that resolves with the inbox ID for the identifier 39 | */ 40 | async getInboxIdForIdentifier(identifier: Identifier, env?: XmtpEnv) { 41 | return this.sendMessage("utils.getInboxIdForIdentifier", { 42 | identifier, 43 | env, 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/WorkerPreferences.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Client, 3 | type Consent, 4 | type ConsentEntityType, 5 | type Conversations, 6 | type UserPreference, 7 | } from "@xmtp/wasm-bindings"; 8 | import type { StreamCallback } from "@/AsyncStream"; 9 | import { fromSafeConsent, type SafeConsent } from "@/utils/conversions"; 10 | 11 | export class WorkerPreferences { 12 | #client: Client; 13 | #conversations: Conversations; 14 | 15 | constructor(client: Client, conversations: Conversations) { 16 | this.#client = client; 17 | this.#conversations = conversations; 18 | } 19 | 20 | sync() { 21 | return this.#client.syncPreferences(); 22 | } 23 | 24 | async inboxState(refreshFromNetwork: boolean) { 25 | return this.#client.inboxState(refreshFromNetwork); 26 | } 27 | 28 | async inboxStateFromInboxIds( 29 | inboxIds: string[], 30 | refreshFromNetwork?: boolean, 31 | ) { 32 | return this.#client.inboxStateFromInboxIds( 33 | inboxIds, 34 | refreshFromNetwork ?? false, 35 | ); 36 | } 37 | 38 | async getLatestInboxState(inboxId: string) { 39 | return this.#client.getLatestInboxState(inboxId); 40 | } 41 | 42 | async setConsentStates(records: SafeConsent[]) { 43 | return this.#client.setConsentStates(records.map(fromSafeConsent)); 44 | } 45 | 46 | async getConsentState(entityType: ConsentEntityType, entity: string) { 47 | return this.#client.getConsentState(entityType, entity); 48 | } 49 | 50 | streamConsent(callback?: StreamCallback) { 51 | const on_consent_update = (consent: Consent[]) => { 52 | void callback?.(null, consent); 53 | }; 54 | const on_error = (error: Error | null) => { 55 | void callback?.(error, undefined); 56 | }; 57 | return this.#conversations.streamConsent({ on_consent_update, on_error }); 58 | } 59 | 60 | streamPreferences(callback?: StreamCallback) { 61 | const on_user_preference_update = (preferences: UserPreference[]) => { 62 | void callback?.(null, preferences); 63 | }; 64 | const on_error = (error: Error | null) => { 65 | void callback?.(error, undefined); 66 | }; 67 | return this.#conversations.streamPreferences({ 68 | on_user_preference_update, 69 | on_error, 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pre-configured URLs for the XMTP network based on the environment 3 | * 4 | * @constant 5 | * @property {string} local - The local URL for the XMTP network 6 | * @property {string} dev - The development URL for the XMTP network 7 | * @property {string} production - The production URL for the XMTP network 8 | */ 9 | export const ApiUrls = { 10 | local: "http://localhost:5555", 11 | dev: "https://dev.xmtp.network", 12 | production: "https://production.xmtp.network", 13 | } as const; 14 | 15 | /** 16 | * Pre-configured URLs for the XMTP history sync service based on the environment 17 | * 18 | * @constant 19 | * @property {string} local - The local URL for the XMTP history sync service 20 | * @property {string} dev - The development URL for the XMTP history sync service 21 | * @property {string} production - The production URL for the XMTP history sync service 22 | */ 23 | export const HistorySyncUrls = { 24 | local: "http://localhost:5558", 25 | dev: "https://message-history.dev.ephemera.network", 26 | production: "https://message-history.production.ephemera.network", 27 | } as const; 28 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Client } from "./Client"; 2 | export type { ExtractCodecContentTypes } from "./Client"; 3 | export { Conversations } from "./Conversations"; 4 | export { Conversation } from "./Conversation"; 5 | export { Dm } from "./Dm"; 6 | export { Group } from "./Group"; 7 | export type { MessageDeliveryStatus, MessageKind } from "./DecodedMessage"; 8 | export { DecodedMessage } from "./DecodedMessage"; 9 | export { Utils } from "./Utils"; 10 | export { ApiUrls, HistorySyncUrls } from "./constants"; 11 | export type * from "./types/options"; 12 | export * from "./utils/conversions"; 13 | export type { AsyncStream, StreamCallback } from "./AsyncStream"; 14 | export type { 15 | Identifier, 16 | IdentifierKind, 17 | UserPreference, 18 | } from "@xmtp/wasm-bindings"; 19 | export { 20 | Consent, 21 | ConsentEntityType, 22 | ConsentState, 23 | ContentType, 24 | ContentTypeId, 25 | ConversationListItem, 26 | ConversationType, 27 | CreateDMOptions, 28 | CreateGroupOptions, 29 | DeliveryStatus, 30 | EncodedContent, 31 | GroupMember, 32 | GroupMembershipState, 33 | GroupMessageKind, 34 | GroupMetadata, 35 | GroupPermissions, 36 | GroupPermissionsOptions, 37 | HmacKey, 38 | InboxState, 39 | Installation, 40 | ListConversationsOptions, 41 | ListMessagesOptions, 42 | LogOptions, 43 | Message, 44 | MessageDisappearingSettings, 45 | MetadataField, 46 | PermissionLevel, 47 | PermissionPolicy, 48 | PermissionPolicySet, 49 | PermissionUpdateType, 50 | SignatureRequestType, 51 | SortDirection, 52 | } from "@xmtp/wasm-bindings"; 53 | export type { Signer } from "./utils/signer"; 54 | export * from "./utils/errors"; 55 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/types/actions.ts: -------------------------------------------------------------------------------- 1 | import type { ClientAction } from "@/types/actions/client"; 2 | import type { ConversationAction } from "@/types/actions/conversation"; 3 | import type { ConversationsAction } from "@/types/actions/conversations"; 4 | import type { DmAction } from "@/types/actions/dm"; 5 | import type { GroupAction } from "@/types/actions/group"; 6 | import type { PreferencesAction } from "@/types/actions/preferences"; 7 | 8 | type UnknownAction = { 9 | action: string; 10 | id: string; 11 | result: unknown; 12 | data: unknown; 13 | }; 14 | 15 | export type ClientWorkerAction = 16 | | { 17 | action: "endStream"; 18 | id: string; 19 | result: undefined; 20 | data: { 21 | streamId: string; 22 | }; 23 | } 24 | | ClientAction 25 | | ConversationAction 26 | | ConversationsAction 27 | | DmAction 28 | | GroupAction 29 | | PreferencesAction; 30 | 31 | export type ActionName = T["action"]; 32 | 33 | export type ExtractAction< 34 | T extends UnknownAction, 35 | A extends ActionName, 36 | > = Extract; 37 | 38 | export type ExtractActionWithoutData< 39 | T extends UnknownAction, 40 | A extends ActionName, 41 | > = Omit, "data">; 42 | 43 | export type ExtractActionWithoutResult< 44 | T extends UnknownAction, 45 | A extends ActionName, 46 | > = Omit, "result">; 47 | 48 | export type ExtractActionData< 49 | T extends UnknownAction, 50 | A extends ActionName, 51 | > = ExtractAction["data"]; 52 | 53 | export type ExtractActionResult< 54 | T extends UnknownAction, 55 | A extends ActionName, 56 | > = ExtractAction["result"]; 57 | 58 | export type ActionWithoutData = { 59 | [A in T["action"]]: Omit, "data">; 60 | }[T["action"]]; 61 | 62 | export type ActionWithoutResult = { 63 | [A in T["action"]]: Omit, "result">; 64 | }[T["action"]]; 65 | 66 | export type ActionErrorData = { 67 | id: string; 68 | action: ActionName; 69 | error: Error; 70 | }; 71 | 72 | export type ExtractActionGroup< 73 | T extends UnknownAction, 74 | U extends string, 75 | > = Extract; 76 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/types/actions/dm.ts: -------------------------------------------------------------------------------- 1 | import type { SafeConversation } from "@/utils/conversions"; 2 | 3 | export type DmAction = 4 | | { 5 | action: "dm.peerInboxId"; 6 | id: string; 7 | result: string; 8 | data: { 9 | id: string; 10 | }; 11 | } 12 | | { 13 | action: "dm.getDuplicateDms"; 14 | id: string; 15 | result: SafeConversation[]; 16 | data: { 17 | id: string; 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/types/actions/preferences.ts: -------------------------------------------------------------------------------- 1 | import type { ConsentEntityType, ConsentState } from "@xmtp/wasm-bindings"; 2 | import type { SafeConsent, SafeInboxState } from "@/utils/conversions"; 3 | 4 | export type PreferencesAction = 5 | | { 6 | action: "preferences.inboxState"; 7 | id: string; 8 | result: SafeInboxState; 9 | data: { 10 | refreshFromNetwork: boolean; 11 | }; 12 | } 13 | | { 14 | action: "preferences.inboxStateFromInboxIds"; 15 | id: string; 16 | result: SafeInboxState[]; 17 | data: { 18 | inboxIds: string[]; 19 | refreshFromNetwork: boolean; 20 | }; 21 | } 22 | | { 23 | action: "preferences.getLatestInboxState"; 24 | id: string; 25 | result: SafeInboxState; 26 | data: { 27 | inboxId: string; 28 | }; 29 | } 30 | | { 31 | action: "preferences.setConsentStates"; 32 | id: string; 33 | result: undefined; 34 | data: { 35 | records: SafeConsent[]; 36 | }; 37 | } 38 | | { 39 | action: "preferences.getConsentState"; 40 | id: string; 41 | result: ConsentState; 42 | data: { 43 | entityType: ConsentEntityType; 44 | entity: string; 45 | }; 46 | } 47 | | { 48 | action: "preferences.sync"; 49 | id: string; 50 | result: number; 51 | data: undefined; 52 | } 53 | | { 54 | action: "preferences.streamConsent"; 55 | id: string; 56 | result: undefined; 57 | data: { 58 | streamId: string; 59 | }; 60 | } 61 | | { 62 | action: "preferences.streamPreferences"; 63 | id: string; 64 | result: undefined; 65 | data: { 66 | streamId: string; 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/types/actions/streams.ts: -------------------------------------------------------------------------------- 1 | import type { UserPreference } from "@xmtp/wasm-bindings"; 2 | import type { 3 | SafeConsent, 4 | SafeConversation, 5 | SafeMessage, 6 | } from "@/utils/conversions"; 7 | 8 | export type StreamAction = 9 | | { 10 | action: "stream.message"; 11 | streamId: string; 12 | result: SafeMessage | undefined; 13 | } 14 | | { 15 | action: "stream.conversation"; 16 | streamId: string; 17 | result: SafeConversation | undefined; 18 | } 19 | | { 20 | action: "stream.consent"; 21 | streamId: string; 22 | result: SafeConsent[] | undefined; 23 | } 24 | | { 25 | action: "stream.preferences"; 26 | streamId: string; 27 | result: UserPreference[] | undefined; 28 | }; 29 | 30 | export type StreamActionName = StreamAction["action"]; 31 | 32 | export type ExtractStreamAction = Extract< 33 | StreamAction, 34 | { action: A } 35 | >; 36 | 37 | export type StreamActionResult = 38 | ExtractStreamAction["result"]; 39 | 40 | export type StreamActionErrorData = { 41 | action: StreamActionName; 42 | error: Error; 43 | streamId: string; 44 | }; 45 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/types/actions/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Identifier } from "@xmtp/wasm-bindings"; 2 | import type { XmtpEnv } from "@/types/options"; 3 | 4 | export type UtilsWorkerAction = 5 | | { 6 | action: "utils.init"; 7 | id: string; 8 | result: undefined; 9 | data: { 10 | enableLogging: boolean; 11 | }; 12 | } 13 | | { 14 | action: "utils.generateInboxId"; 15 | id: string; 16 | result: string; 17 | data: { 18 | identifier: Identifier; 19 | }; 20 | } 21 | | { 22 | action: "utils.getInboxIdForIdentifier"; 23 | id: string; 24 | result: string | undefined; 25 | data: { 26 | identifier: Identifier; 27 | env?: XmtpEnv; 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/utils/createClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createClient as createWasmClient, 3 | generateInboxId, 4 | getInboxIdForIdentifier, 5 | LogOptions, 6 | type Identifier, 7 | } from "@xmtp/wasm-bindings"; 8 | import { ApiUrls, HistorySyncUrls } from "@/constants"; 9 | import type { ClientOptions } from "@/types/options"; 10 | 11 | export const createClient = async ( 12 | identifier: Identifier, 13 | options?: Omit, 14 | ) => { 15 | const env = options?.env || "dev"; 16 | const host = options?.apiUrl || ApiUrls[env]; 17 | const inboxId = 18 | (await getInboxIdForIdentifier(host, identifier)) || 19 | generateInboxId(identifier); 20 | const dbPath = 21 | options?.dbPath === undefined 22 | ? `xmtp-${env}-${inboxId}.db3` 23 | : options.dbPath; 24 | const isLogging = 25 | options && 26 | (options.loggingLevel !== undefined || 27 | options.structuredLogging || 28 | options.performanceLogging); 29 | 30 | const historySyncUrl = 31 | options?.historySyncUrl === undefined 32 | ? HistorySyncUrls[env] 33 | : options.historySyncUrl; 34 | 35 | const deviceSyncWorkerMode = options?.disableDeviceSync 36 | ? "disabled" 37 | : "enabled"; 38 | 39 | return createWasmClient( 40 | host, 41 | inboxId, 42 | identifier, 43 | dbPath, 44 | options?.dbEncryptionKey, 45 | historySyncUrl, 46 | deviceSyncWorkerMode, 47 | isLogging 48 | ? new LogOptions( 49 | options.structuredLogging ?? false, 50 | options.performanceLogging ?? false, 51 | options.loggingLevel, 52 | ) 53 | : undefined, 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export function nsToDate(ns: bigint): Date { 2 | return new Date(Number(ns / 1_000_000n)); 3 | } 4 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/utils/signer.ts: -------------------------------------------------------------------------------- 1 | import type { Identifier } from "@xmtp/wasm-bindings"; 2 | 3 | export type SignMessage = (message: string) => Promise | Uint8Array; 4 | export type GetIdentifier = () => Promise | Identifier; 5 | export type GetChainId = () => bigint; 6 | export type GetBlockNumber = () => bigint; 7 | 8 | export type Signer = 9 | | { 10 | type: "EOA"; 11 | getIdentifier: GetIdentifier; 12 | signMessage: SignMessage; 13 | } 14 | | { 15 | type: "SCW"; 16 | getIdentifier: GetIdentifier; 17 | signMessage: SignMessage; 18 | getBlockNumber?: GetBlockNumber; 19 | getChainId: GetChainId; 20 | }; 21 | 22 | export type EOASigner = Extract; 23 | export type SCWSigner = Extract; 24 | -------------------------------------------------------------------------------- /sdks/browser-sdk/src/workers/utils.ts: -------------------------------------------------------------------------------- 1 | import init, { 2 | generateInboxId, 3 | getInboxIdForIdentifier as get_inbox_id_for_identifier, 4 | type Identifier, 5 | } from "@xmtp/wasm-bindings"; 6 | import { ApiUrls } from "@/constants"; 7 | import type { 8 | ActionErrorData, 9 | ActionName, 10 | ActionWithoutResult, 11 | ExtractActionWithoutData, 12 | } from "@/types/actions"; 13 | import type { UtilsWorkerAction } from "@/types/actions/utils"; 14 | import type { XmtpEnv } from "@/types/options"; 15 | 16 | /** 17 | * Type-safe postMessage 18 | */ 19 | const postMessage = >( 20 | data: ExtractActionWithoutData, 21 | ) => { 22 | self.postMessage(data); 23 | }; 24 | 25 | /** 26 | * Type-safe postMessage for errors 27 | */ 28 | const postMessageError = (data: ActionErrorData) => { 29 | self.postMessage(data); 30 | }; 31 | 32 | const getInboxIdForIdentifier = async ( 33 | identifier: Identifier, 34 | env?: XmtpEnv, 35 | ) => { 36 | const host = env ? ApiUrls[env] : ApiUrls.dev; 37 | return get_inbox_id_for_identifier(host, identifier); 38 | }; 39 | 40 | let enableLogging = false; 41 | 42 | self.onmessage = async ( 43 | event: MessageEvent>, 44 | ) => { 45 | const { action, id, data } = event.data; 46 | 47 | if (enableLogging) { 48 | console.log("utils worker received event data", event.data); 49 | } 50 | 51 | // initialize WASM module 52 | await init(); 53 | 54 | try { 55 | switch (action) { 56 | case "utils.init": { 57 | enableLogging = data.enableLogging; 58 | postMessage({ id, action, result: undefined }); 59 | break; 60 | } 61 | case "utils.generateInboxId": { 62 | const result = generateInboxId(data.identifier); 63 | postMessage({ 64 | id, 65 | action, 66 | result, 67 | }); 68 | break; 69 | } 70 | case "utils.getInboxIdForIdentifier": { 71 | const result = await getInboxIdForIdentifier(data.identifier, data.env); 72 | postMessage({ id, action, result }); 73 | break; 74 | } 75 | } 76 | } catch (e) { 77 | postMessageError({ id, action, error: e as Error }); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /sdks/browser-sdk/test/Utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { Utils } from "@/Utils"; 3 | import { 4 | createRegisteredClient, 5 | createSigner, 6 | createUser, 7 | } from "@test/helpers"; 8 | 9 | describe.concurrent("Utils", () => { 10 | it("should generate inbox id", async () => { 11 | const user = createUser(); 12 | const signer = createSigner(user); 13 | const utils = new Utils(); 14 | const inboxId = await utils.generateInboxId(await signer.getIdentifier()); 15 | expect(inboxId).toBeDefined(); 16 | }); 17 | 18 | it("should get inbox id for address", async () => { 19 | const user = createUser(); 20 | const signer = createSigner(user); 21 | const client = await createRegisteredClient(signer); 22 | const utils = new Utils(); 23 | const inboxId = await utils.getInboxIdForIdentifier( 24 | await signer.getIdentifier(), 25 | "local", 26 | ); 27 | expect(inboxId).toBe(client.inboxId); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /sdks/browser-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "paths": { 10 | "@/*": ["./src/*"], 11 | "@test/*": ["./test/*"] 12 | }, 13 | "preserveConstEnums": true, 14 | "skipLibCheck": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "ESNext", 18 | "types": ["vitest/globals", "@vitest/browser/providers/playwright"] 19 | }, 20 | "include": ["src", "test", "vite.config.ts", "rollup.config.js"] 21 | } 22 | -------------------------------------------------------------------------------- /sdks/browser-sdk/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["src/index.ts"], 4 | "out": "docs", 5 | "excludeExternals": true, 6 | "excludePrivate": true, 7 | "excludeProtected": true 8 | } 9 | -------------------------------------------------------------------------------- /sdks/browser-sdk/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from "vite"; 2 | import tsconfigPaths from "vite-tsconfig-paths"; 3 | import { defineConfig as defineVitestConfig } from "vitest/config"; 4 | 5 | // https://vitejs.dev/config/ 6 | const viteConfig = defineConfig({ 7 | plugins: [tsconfigPaths()], 8 | }); 9 | 10 | const vitestConfig = defineVitestConfig({ 11 | optimizeDeps: { 12 | exclude: ["@xmtp/wasm-bindings"], 13 | }, 14 | test: { 15 | browser: { 16 | provider: "playwright", 17 | enabled: true, 18 | headless: true, 19 | screenshotFailures: false, 20 | instances: [ 21 | { 22 | browser: "chromium", 23 | }, 24 | ], 25 | }, 26 | testTimeout: 120000, 27 | }, 28 | }); 29 | 30 | export default mergeConfig(viteConfig, vitestConfig); 31 | -------------------------------------------------------------------------------- /sdks/node-sdk/.gitignore: -------------------------------------------------------------------------------- 1 | scripts/accounts.json 2 | -------------------------------------------------------------------------------- /sdks/node-sdk/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XMTP (xmtp.org) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sdks/node-sdk/rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from "@rollup/plugin-json"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import { defineConfig } from "rollup"; 4 | import { dts } from "rollup-plugin-dts"; 5 | import tsConfigPaths from "rollup-plugin-tsconfig-paths"; 6 | 7 | const external = [ 8 | "node:path", 9 | "node:process", 10 | "@xmtp/content-type-group-updated", 11 | "@xmtp/content-type-primitives", 12 | "@xmtp/content-type-text", 13 | "@xmtp/node-bindings", 14 | "@xmtp/node-bindings/version.json", 15 | ]; 16 | 17 | const plugins = [ 18 | tsConfigPaths(), 19 | typescript({ 20 | declaration: false, 21 | declarationMap: false, 22 | }), 23 | json({ 24 | preferConst: true, 25 | }), 26 | ]; 27 | 28 | export default defineConfig([ 29 | { 30 | input: "src/index.ts", 31 | output: { 32 | file: "dist/index.js", 33 | format: "es", 34 | sourcemap: true, 35 | importAttributesKey: "with", 36 | }, 37 | plugins, 38 | external, 39 | }, 40 | { 41 | input: "src/index.ts", 42 | output: { 43 | file: "dist/index.d.ts", 44 | format: "es", 45 | }, 46 | plugins: [tsConfigPaths(), dts()], 47 | }, 48 | ]); 49 | -------------------------------------------------------------------------------- /sdks/node-sdk/scripts/accounts.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "node:fs/promises"; 2 | import path from "node:path"; 3 | import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; 4 | 5 | type Account = [key: string, address: string]; 6 | type AccountsMap = Record; 7 | 8 | const createRandomAccount = (): Account => { 9 | const privateKey = generatePrivateKey(); 10 | const account = privateKeyToAccount(privateKey); 11 | return [privateKey, account.address]; 12 | }; 13 | 14 | const writeAccountsJson = async (accounts: AccountsMap) => { 15 | console.log("Writing accounts to file..."); 16 | const accountsJsonPath = path.join(import.meta.dirname, "accounts.json"); 17 | await writeFile(accountsJsonPath, JSON.stringify(accounts, null, 2)); 18 | console.log(`Accounts data written to '${accountsJsonPath}'`); 19 | }; 20 | 21 | const main = async () => { 22 | console.log("Creating 1000 accounts..."); 23 | const accounts = Object.fromEntries( 24 | Array.from({ length: 1000 }, () => createRandomAccount()), 25 | ); 26 | await writeAccountsJson(accounts); 27 | }; 28 | 29 | await main(); 30 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/Dm.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Message, 3 | Conversation as XmtpConversation, 4 | } from "@xmtp/node-bindings"; 5 | import type { Client } from "@/Client"; 6 | import { Conversation } from "@/Conversation"; 7 | 8 | /** 9 | * Represents a direct message conversation between two inboxes 10 | * 11 | * This class is not intended to be initialized directly. 12 | */ 13 | export class Dm extends Conversation { 14 | #client: Client; 15 | #conversation: XmtpConversation; 16 | 17 | /** 18 | * Creates a new direct message conversation instance 19 | * 20 | * @param client - The client instance managing this direct message conversation 21 | * @param conversation - The underlying conversation instance 22 | * @param lastMessage - Optional last message in the conversation 23 | */ 24 | constructor( 25 | client: Client, 26 | conversation: XmtpConversation, 27 | lastMessage?: Message | null, 28 | ) { 29 | super(client, conversation, lastMessage); 30 | this.#client = client; 31 | this.#conversation = conversation; 32 | } 33 | 34 | /** 35 | * Retrieves the inbox ID of the other participant in the DM 36 | * 37 | * @returns Promise that resolves with the peer's inbox ID 38 | */ 39 | get peerInboxId() { 40 | return this.#conversation.dmPeerInboxId(); 41 | } 42 | 43 | async getDuplicateDms() { 44 | const duplicateDms = await this.#conversation.findDuplicateDms(); 45 | return duplicateDms.map((dm) => new Dm(this.#client, dm)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pre-configured URLs for the XMTP network based on the environment 3 | * 4 | * @constant 5 | * @property {string} local - The local URL for the XMTP network 6 | * @property {string} dev - The development URL for the XMTP network 7 | * @property {string} production - The production URL for the XMTP network 8 | */ 9 | export const ApiUrls = { 10 | local: "http://localhost:5556", 11 | dev: "https://grpc.dev.xmtp.network:443", 12 | production: "https://grpc.production.xmtp.network:443", 13 | } as const; 14 | 15 | /** 16 | * Pre-configured URLs for the XMTP history sync service based on the environment 17 | * 18 | * @constant 19 | * @property {string} local - The local URL for the XMTP history sync service 20 | * @property {string} dev - The development URL for the XMTP history sync service 21 | * @property {string} production - The production URL for the XMTP history sync service 22 | */ 23 | export const HistorySyncUrls = { 24 | local: "http://localhost:5558", 25 | dev: "https://message-history.dev.ephemera.network", 26 | production: "https://message-history.production.ephemera.network", 27 | } as const; 28 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | ClientOptions, 3 | OtherOptions, 4 | NetworkOptions, 5 | StorageOptions, 6 | XmtpEnv, 7 | } from "./types"; 8 | export { ApiUrls, HistorySyncUrls } from "./constants"; 9 | export { Client } from "./Client"; 10 | export type { ExtractCodecContentTypes } from "./Client"; 11 | export { Conversation } from "./Conversation"; 12 | export { Conversations } from "./Conversations"; 13 | export { Dm } from "./Dm"; 14 | export { Group } from "./Group"; 15 | export type { PreferenceUpdate } from "./Preferences"; 16 | export { DecodedMessage } from "./DecodedMessage"; 17 | export type { AsyncStream, StreamCallback } from "./AsyncStream"; 18 | export type { 19 | Consent, 20 | ContentType, 21 | ContentTypeId, 22 | ConversationListItem, 23 | CreateDmOptions, 24 | CreateGroupOptions, 25 | EncodedContent, 26 | HmacKey, 27 | Identifier, 28 | InboxState, 29 | Installation, 30 | KeyPackageStatus, 31 | Lifetime, 32 | ListConversationsOptions, 33 | ListMessagesOptions, 34 | LogOptions, 35 | Message, 36 | MessageDisappearingSettings, 37 | PermissionPolicySet, 38 | } from "@xmtp/node-bindings"; 39 | export { 40 | ConsentEntityType, 41 | ConsentState, 42 | ConversationType, 43 | DeliveryStatus, 44 | GroupMember, 45 | GroupMembershipState, 46 | GroupMessageKind, 47 | GroupMetadata, 48 | GroupPermissions, 49 | GroupPermissionsOptions, 50 | IdentifierKind, 51 | LogLevel, 52 | MetadataField, 53 | PermissionLevel, 54 | PermissionPolicy, 55 | PermissionUpdateType, 56 | SignatureRequestType, 57 | SortDirection, 58 | } from "@xmtp/node-bindings"; 59 | export { generateInboxId, getInboxIdForIdentifier } from "./utils/inboxId"; 60 | export type { Signer } from "./utils/signer"; 61 | export * from "./utils/errors"; 62 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ContentCodec } from "@xmtp/content-type-primitives"; 2 | import type { LogLevel } from "@xmtp/node-bindings"; 3 | import type { ApiUrls } from "@/constants"; 4 | 5 | /** 6 | * XMTP environment 7 | */ 8 | export type XmtpEnv = keyof typeof ApiUrls; 9 | 10 | /** 11 | * Network options 12 | */ 13 | export type NetworkOptions = { 14 | /** 15 | * Specify which XMTP environment to connect to. (default: `dev`) 16 | */ 17 | env?: XmtpEnv; 18 | /** 19 | * apiUrl can be used to override the `env` flag and connect to a 20 | * specific endpoint 21 | */ 22 | apiUrl?: string; 23 | /** 24 | * historySyncUrl can be used to override the `env` flag and connect to a 25 | * specific endpoint for syncing history 26 | */ 27 | historySyncUrl?: string | null; 28 | }; 29 | 30 | /** 31 | * Storage options 32 | */ 33 | export type StorageOptions = { 34 | /** 35 | * Path to the local DB 36 | * 37 | * There are 3 value types that can be used to specify the database path: 38 | * 39 | * - `undefined` (or excluded from the client options) 40 | * The database will be created in the current working directory and is based on 41 | * the XMTP environment and client inbox ID. 42 | * Example: `xmtp-dev-.db3` 43 | * 44 | * - `null` 45 | * No database will be created and all data will be lost once the client disconnects. 46 | * 47 | * - `string` 48 | * The given path will be used to create the database. 49 | * Example: `./my-db.db3` 50 | */ 51 | dbPath?: string | null; 52 | /** 53 | * Encryption key for the local DB 54 | */ 55 | dbEncryptionKey?: Uint8Array; 56 | }; 57 | 58 | export type ContentOptions = { 59 | /** 60 | * Allow configuring codecs for additional content types 61 | */ 62 | codecs?: ContentCodec[]; 63 | }; 64 | 65 | export type OtherOptions = { 66 | /** 67 | * Enable structured JSON logging 68 | */ 69 | structuredLogging?: boolean; 70 | /** 71 | * Logging level 72 | */ 73 | loggingLevel?: LogLevel; 74 | /** 75 | * Disable automatic registration when creating a client 76 | */ 77 | disableAutoRegister?: boolean; 78 | /** 79 | * Disable device sync 80 | */ 81 | disableDeviceSync?: boolean; 82 | }; 83 | 84 | export type ClientOptions = NetworkOptions & 85 | StorageOptions & 86 | ContentOptions & 87 | OtherOptions; 88 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/utils/createClient.ts: -------------------------------------------------------------------------------- 1 | import { join } from "node:path"; 2 | import process from "node:process"; 3 | import { 4 | createClient as createNodeClient, 5 | LogLevel, 6 | SyncWorkerMode, 7 | type Identifier, 8 | type LogOptions, 9 | } from "@xmtp/node-bindings"; 10 | import { ApiUrls, HistorySyncUrls } from "@/constants"; 11 | import type { ClientOptions } from "@/types"; 12 | import { generateInboxId, getInboxIdForIdentifier } from "@/utils/inboxId"; 13 | 14 | export const createClient = async ( 15 | identifier: Identifier, 16 | options?: ClientOptions, 17 | ) => { 18 | const env = options?.env || "dev"; 19 | const host = options?.apiUrl || ApiUrls[env]; 20 | const isSecure = host.startsWith("https"); 21 | const inboxId = 22 | (await getInboxIdForIdentifier(identifier, env)) || 23 | generateInboxId(identifier); 24 | const dbPath = 25 | options?.dbPath === undefined 26 | ? join(process.cwd(), `xmtp-${env}-${inboxId}.db3`) 27 | : options.dbPath; 28 | 29 | const logOptions: LogOptions = { 30 | structured: options?.structuredLogging ?? false, 31 | level: options?.loggingLevel ?? LogLevel.off, 32 | }; 33 | const historySyncUrl = 34 | options?.historySyncUrl === undefined 35 | ? HistorySyncUrls[env] 36 | : options.historySyncUrl; 37 | 38 | const deviceSyncWorkerMode = options?.disableDeviceSync 39 | ? SyncWorkerMode.disabled 40 | : SyncWorkerMode.enabled; 41 | 42 | return createNodeClient( 43 | host, 44 | isSecure, 45 | dbPath, 46 | inboxId, 47 | identifier, 48 | options?.dbEncryptionKey, 49 | historySyncUrl, 50 | deviceSyncWorkerMode, 51 | logOptions, 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export function nsToDate(ns: number): Date { 2 | return new Date(ns / 1_000_000); 3 | } 4 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | import type { ContentTypeId } from "@xmtp/content-type-primitives"; 2 | import { SignatureRequestType } from "@xmtp/node-bindings"; 3 | 4 | export class CodecNotFoundError extends Error { 5 | constructor(contentType: ContentTypeId) { 6 | super(`Codec not found for "${contentType.toString()}" content type`); 7 | } 8 | } 9 | 10 | export class InboxReassignError extends Error { 11 | constructor() { 12 | super( 13 | "Unable to create add account signature text, `allowInboxReassign` must be true", 14 | ); 15 | } 16 | } 17 | 18 | export class AccountAlreadyAssociatedError extends Error { 19 | constructor(inboxId: string) { 20 | super(`Account already associated with inbox ${inboxId}`); 21 | } 22 | } 23 | 24 | export class GenerateSignatureError extends Error { 25 | constructor(signatureType: SignatureRequestType) { 26 | let type = ""; 27 | 28 | switch (signatureType) { 29 | case SignatureRequestType.AddWallet: 30 | type = "add account"; 31 | break; 32 | case SignatureRequestType.CreateInbox: 33 | type = "create inbox"; 34 | break; 35 | case SignatureRequestType.RevokeWallet: 36 | type = "remove account"; 37 | break; 38 | case SignatureRequestType.RevokeInstallations: 39 | type = "revoke installations"; 40 | break; 41 | case SignatureRequestType.ChangeRecoveryIdentifier: 42 | type = "change recovery identifier"; 43 | break; 44 | } 45 | 46 | super(`Failed to generate ${type} signature text`); 47 | } 48 | } 49 | 50 | export class InvalidGroupMembershipChangeError extends Error { 51 | constructor(messageId: string) { 52 | super(`Invalid group membership change for message ${messageId}`); 53 | } 54 | } 55 | 56 | export class MissingContentTypeError extends Error { 57 | constructor() { 58 | super("Content type is required when sending content other than text"); 59 | } 60 | } 61 | 62 | export class SignerUnavailableError extends Error { 63 | constructor() { 64 | super( 65 | "Signer unavailable, use Client.create to create a client with a signer", 66 | ); 67 | } 68 | } 69 | 70 | export class ClientNotInitializedError extends Error { 71 | constructor() { 72 | super( 73 | "Client not initialized, use Client.create or Client.build to create a client", 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/utils/inboxId.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateInboxId as generateInboxIdBinding, 3 | getInboxIdForIdentifier as getInboxIdForIdentifierBinding, 4 | type Identifier, 5 | } from "@xmtp/node-bindings"; 6 | import { ApiUrls } from "@/constants"; 7 | import type { XmtpEnv } from "@/types"; 8 | 9 | export const generateInboxId = (identifier: Identifier): string => { 10 | return generateInboxIdBinding(identifier); 11 | }; 12 | 13 | export const getInboxIdForIdentifier = async ( 14 | identifier: Identifier, 15 | env: XmtpEnv = "dev", 16 | ) => { 17 | const host = ApiUrls[env]; 18 | const isSecure = host.startsWith("https"); 19 | return getInboxIdForIdentifierBinding(host, isSecure, identifier); 20 | }; 21 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/utils/signer.ts: -------------------------------------------------------------------------------- 1 | import type { Identifier } from "@xmtp/node-bindings"; 2 | 3 | export type SignMessage = (message: string) => Promise | Uint8Array; 4 | export type GetIdentifier = () => Promise | Identifier; 5 | export type GetChainId = () => bigint; 6 | export type GetBlockNumber = () => bigint; 7 | 8 | export type Signer = 9 | | { 10 | type: "EOA"; 11 | signMessage: SignMessage; 12 | getIdentifier: GetIdentifier; 13 | } 14 | | { 15 | type: "SCW"; 16 | signMessage: SignMessage; 17 | getIdentifier: GetIdentifier; 18 | getBlockNumber?: GetBlockNumber; 19 | getChainId: GetChainId; 20 | }; 21 | 22 | export type EOASigner = Extract; 23 | export type SCWSigner = Extract; 24 | -------------------------------------------------------------------------------- /sdks/node-sdk/src/utils/version.ts: -------------------------------------------------------------------------------- 1 | import bindingsVersion from "@xmtp/node-bindings/version.json" with { type: "json" }; 2 | 3 | export const version = `${bindingsVersion.branch}@${bindingsVersion.version} (${bindingsVersion.date})`; 4 | -------------------------------------------------------------------------------- /sdks/node-sdk/test/inboxId.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { generateInboxId, getInboxIdForIdentifier } from "@/utils/inboxId"; 3 | import { 4 | createRegisteredClient, 5 | createSigner, 6 | createUser, 7 | } from "@test/helpers"; 8 | 9 | describe("generateInboxId", () => { 10 | it("should generate an inbox id", async () => { 11 | const user = createUser(); 12 | const signer = createSigner(user); 13 | const inboxId = generateInboxId(await signer.getIdentifier()); 14 | expect(inboxId).toBeDefined(); 15 | }); 16 | }); 17 | 18 | describe("getInboxIdForAddress", () => { 19 | it("should return `null` inbox ID for unregistered address", async () => { 20 | const user = createUser(); 21 | const signer = createSigner(user); 22 | const inboxId = await getInboxIdForIdentifier( 23 | await signer.getIdentifier(), 24 | "local", 25 | ); 26 | expect(inboxId).toBe(null); 27 | }); 28 | 29 | it("should return inbox ID for registered address", async () => { 30 | const user = createUser(); 31 | const signer = createSigner(user); 32 | const client = await createRegisteredClient(signer); 33 | const inboxId = await getInboxIdForIdentifier( 34 | await signer.getIdentifier(), 35 | "local", 36 | ); 37 | expect(inboxId).toBe(client.inboxId); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /sdks/node-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "noEmit": true, 8 | "paths": { 9 | "@/*": ["./src/*"], 10 | "@test/*": ["./test/*"] 11 | }, 12 | "preserveConstEnums": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "target": "ESNext", 17 | "types": ["vitest/globals"] 18 | }, 19 | "include": [ 20 | "rollup.config.js", 21 | "scripts/**/*", 22 | "src/**/*", 23 | "test/**/*", 24 | "vitest.config.ts", 25 | "vitest.setup.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /sdks/node-sdk/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig, mergeConfig } from "vite"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | import { defineConfig as defineVitestConfig } from "vitest/config"; 5 | 6 | // https://vitejs.dev/config/ 7 | const viteConfig = defineConfig({ 8 | plugins: [tsconfigPaths()], 9 | }); 10 | 11 | const vitestConfig = defineVitestConfig({ 12 | test: { 13 | globals: true, 14 | testTimeout: 120000, 15 | hookTimeout: 60000, 16 | globalSetup: ["./vitest.setup.ts"], 17 | }, 18 | }); 19 | 20 | export default mergeConfig(viteConfig, vitestConfig); 21 | -------------------------------------------------------------------------------- /sdks/node-sdk/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { unlink } from "node:fs/promises"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { glob } from "fast-glob"; 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | 8 | export const teardown = async () => { 9 | const files = await glob("**/*.db3*", { cwd: __dirname }); 10 | await Promise.all(files.map((file) => unlink(join(__dirname, file)))); 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ESNext" 13 | }, 14 | "include": [".prettierrc.cjs", "eslint.config.js", "dev/uploadService/*"] 15 | } 16 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "clean": { 10 | "outputs": [] 11 | }, 12 | "format": { 13 | "outputs": [] 14 | }, 15 | "format:check": { 16 | "outputs": [] 17 | }, 18 | "lint": { 19 | "dependsOn": ["^build"], 20 | "outputs": [] 21 | }, 22 | "test": { 23 | "dependsOn": ["build"], 24 | "outputs": [] 25 | }, 26 | "typecheck": { 27 | "dependsOn": ["^build"], 28 | "outputs": [] 29 | } 30 | } 31 | } 32 | --------------------------------------------------------------------------------