diff --git a/llms/llms-full.txt b/llms/llms-full.txt index 8ca25bb0..3d5dbbdf 100644 --- a/llms/llms-full.txt +++ b/llms/llms-full.txt @@ -1,6 +1,6 @@ # XMTP Full Documentation -Generated at 08:06 PM UTC / September 16, 2025 +Generated at 07:38 PM UTC / September 17, 2025 ## Instructions for AI Tools @@ -21,7 +21,7 @@ layout: landing showLogo: false --- -import { CustomHomePage } from '../components/CustomHomePage' +import { CustomHomePage } from '../components/CustomHomePage'; @@ -38,8 +38,18 @@ The largest and most secure decentralized messaging network
- Build an agent - Build a chat app + + Build an agent + + + Build a chat app +
@@ -145,7 +155,7 @@ If you have any questions about this privacy policy, post to the [XMTP Community ## pages/terms.md --- -description: "We are pleased to license much of the documentation on this site under terms that explicitly encourage people to take, modify, reuse, re-purpose, and remix this content as they see fit." +description: 'We are pleased to license much of the documentation on this site under terms that explicitly encourage people to take, modify, reuse, re-purpose, and remix this content as they see fit.' --- # Terms of service @@ -184,7 +194,7 @@ There are several typical ways in which this might apply: If your online work _exactly reproduces_ text or images from this site, in whole or in part, please include a paragraph at the bottom of your page that reads: ->Portions of this page are reproduced from work created and shared by XMTP and used according to terms described in the [Creative Commons 4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). +> Portions of this page are reproduced from work created and shared by XMTP and used according to terms described in the [Creative Commons 4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). Also, please link back to the original source page so that readers can refer to it for more information. @@ -192,7 +202,7 @@ Also, please link back to the original source page so that readers can refer to If your online work shows _modified_ text or images based on the content from this site, please include a paragraph at the bottom of your page that reads: ->Portions of this page are modifications based on work created and shared by XMTP and used according to terms described in the [Creative Commons 4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). +> Portions of this page are modifications based on work created and shared by XMTP and used according to terms described in the [Creative Commons 4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). Again, please link back to the original source page so that readers can refer to it for more information. This is even more important when the content has been modified. @@ -220,12 +230,12 @@ Here are some security best practices: This section covers how to deploy an agent using Railway—a platform many developers prefer for quickly and easily deploying agents. While this tutorial focuses on Railway, you can use any hosting provider that supports Node.js and environment variables. -Alternative platforms include: +Alternative platforms include: - [Fly.io](https://fly.io/) - [Heroku](https://www.heroku.com/) - [Render](https://render.com/) -- [Vercel](https://vercel.com) +- [Vercel](https://vercel.com) Want to contribute a deployment guide for another platform? We welcome [pull requests](https://github.com/xmtp/docs-xmtp-org/blob/main/README.md)! @@ -264,9 +274,9 @@ Your XMTP agent will need persistent storage. Add a volume to your container: Use this code in your agent to properly connect to the Railway volume: ```tsx [Node] -export const getDbPath = (env: string, suffix: string = "xmtp") => { +export const getDbPath = (env: string, suffix: string = 'xmtp') => { //Checks if the environment is a Railway deployment - const volumePath = process.env.RAILWAY_VOLUME_MOUNT_PATH ?? ".data/xmtp"; + const volumePath = process.env.RAILWAY_VOLUME_MOUNT_PATH ?? '.data/xmtp'; // Create database directory if it doesn't exist if (!fs.existsSync(volumePath)) { fs.mkdirSync(volumePath, { recursive: true }); @@ -281,10 +291,10 @@ Then, specify `dbPath` in your client options: ```tsx [Node] const receiverClient = await Client.create(signer, { - dbEncryptionKey, - env: XMTP_ENV as XmtpEnv, - dbPath: getDbPath(XMTP_ENV), - }); + dbEncryptionKey, + env: XMTP_ENV as XmtpEnv, + dbPath: getDbPath(XMTP_ENV), +}); ``` ## 5. Configure environment variables @@ -323,26 +333,27 @@ You can use this code to get your agent's client information, which provides use const clientsByAddress = client.accountIdentifier?.identifier; // Get XMTP SDK version from package.json const require = createRequire(import.meta.url); -const packageJson = require("../package.json") as { +const packageJson = require('../package.json') as { dependencies: Record; }; -const xmtpSdkVersion = packageJson.dependencies["@xmtp/node-sdk"]; +const xmtpSdkVersion = packageJson.dependencies['@xmtp/node-sdk']; const bindingVersion = ( - require("../node_modules/@xmtp/node-bindings/package.json") as { + require('../node_modules/@xmtp/node-bindings/package.json') as { version: string; } ).version; const inboxId = client.inboxId; const installationId = client.installationId; -const environments = client.options?.env ?? "dev"; +const environments = client.options?.env ?? 'dev'; const urls = [`http://xmtp.chat/dm/${clientsByAddress}`]; const conversations = await client.conversations.list(); const inboxState = await client.preferences.inboxState(); -const keyPackageStatuses = - await client.getKeyPackageStatusesForInstallationIds([installationId]); +const keyPackageStatuses = await client.getKeyPackageStatusesForInstallationIds( + [installationId] +); let createdDate = new Date(); let expiryDate = new Date(); @@ -366,7 +377,7 @@ console.log(` • Key Package created: ${createdDate.toLocaleString()} • Key Package valid until: ${expiryDate.toLocaleString()} • Networks: ${environments} - ${urls.map((url) => `• URL: ${url}`).join("\n")}`); + ${urls.map((url) => `• URL: ${url}`).join('\n')}`); ``` @@ -383,23 +394,23 @@ Build with XMTP to: - **Deliver secure and private messaging** - Using the [Messaging Layer Security](/protocol/security) (MLS) standard, a ratified [IETF](https://www.ietf.org/about/introduction/) standard, XMTP provides end-to-end encrypted messaging with forward secrecy and post-compromise security. + Using the [Messaging Layer Security](/protocol/security) (MLS) standard, a ratified [IETF](https://www.ietf.org/about/introduction/) standard, XMTP provides end-to-end encrypted messaging with forward secrecy and post-compromise security. - **Provide spam-free chats** - In any open and permissionless messaging ecosystem, spam is an inevitable reality, and XMTP is no exception. However, with XMTP user consent preferences, developers can give their users spam-free chats displaying conversations with chosen contacts only. + In any open and permissionless messaging ecosystem, spam is an inevitable reality, and XMTP is no exception. However, with XMTP user consent preferences, developers can give their users spam-free chats displaying conversations with chosen contacts only. - **Build on native crypto rails** - Build with XMTP to tap into the capabilities of crypto and web3. Support decentralized identities, crypto transactions, and more, directly in a messaging experience. + Build with XMTP to tap into the capabilities of crypto and web3. Support decentralized identities, crypto transactions, and more, directly in a messaging experience. - **Empower users to own and control their communications** - With apps built with XMTP, users own their conversations, data, and identity. Combined with the interoperability that comes with protocols, this means users can access their end-to-end encrypted communications using any app built with XMTP. + With apps built with XMTP, users own their conversations, data, and identity. Combined with the interoperability that comes with protocols, this means users can access their end-to-end encrypted communications using any app built with XMTP. - **Create with confidence** - Developers are free to create the messaging experiences their users want—on a censorship-resistant protocol architected to last forever. Because XMTP isn't a closed proprietary platform, developers can build confidently, knowing their access and functionality can't be revoked by a central authority. + Developers are free to create the messaging experiences their users want—on a censorship-resistant protocol architected to last forever. Because XMTP isn't a closed proprietary platform, developers can build confidently, knowing their access and functionality can't be revoked by a central authority. ## Try an app built with XMTP @@ -410,13 +421,11 @@ Try [xmtp.chat](https://xmtp.chat/), an app made for devs to learn to build with ## Join the XMTP community - **XMTP builds in the open** - - Explore the documentation on this site - Explore the open [XMTP GitHub org](https://github.com/xmtp), which contains code for LibXMTP, XMTP SDKs, and xmtpd, node software powering the XMTP testnet. - Explore the open source code for [xmtp.chat](https://github.com/xmtp/xmtp-js/tree/main/apps/xmtp.chat), an app made for devs to learn to build with XMTP—using an app built with XMTP. - **XMTP is for everyone** - - [Join the conversation](https://community.xmtp.org/) and become part of the movement to redefine digital communications. @@ -435,7 +444,16 @@ Building with XMTP gives your agent access to: ### Quickstart video guide - + ### Agent examples @@ -623,7 +641,16 @@ The XMTP SDK currently requires you to use [ethers](https://ethers.org/) or anot ### What is the invalid key package error? - + ### Where can I get official XMTP brand assets? @@ -672,7 +699,7 @@ This retention policy would represent a minimum retention period, not a maximum. For example, a retention policy may look something like the following, though specifics are subject to change: - One year for messages -- Indefinite storage for account information and personal preferences +- Indefinite storage for account information and personal preferences The team is researching a way to provide this indefinite storage and have it scale forever. @@ -742,15 +769,15 @@ XMTP SDKs support message signing with 2 different types of Ethereum accounts: E The EOA signer must have 3 properties: the account type, a function that returns the account identifier, and a function that signs messages. ```tsx [Node] -import type { Signer, Identifier, IdentifierKind } from "@xmtp/node-sdk"; +import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const accountIdentifier: Identifier = { - identifier: "0x...", // Ethereum address as the identifier + identifier: '0x...', // Ethereum address as the identifier identifierKind: IdentifierKind.Ethereum, // Specifies the identity type }; const signer: Signer = { - type: "EOA", + type: 'EOA', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string @@ -765,14 +792,14 @@ The SCW signer has the same 3 required properties as the EOA signer, but also re Here is a list of supported chain IDs: -- chain_rpc_1 = string -- chain_rpc_8453 = string +- chain_rpc_1 = string +- chain_rpc_8453 = string - chain_rpc_42161 = string -- chain_rpc_10 = string -- chain_rpc_137 = string -- chain_rpc_324 = string +- chain_rpc_10 = string +- chain_rpc_137 = string +- chain_rpc_324 = string - chain_rpc_59144 = string -- chain_rpc_480 = string +- chain_rpc_480 = string Need support for a different chain ID? Please post your request to the [XMTP Community Forums](https://community.xmtp.org/c/general/ideas/54). @@ -793,15 +820,15 @@ The details of creating an SCW signer are highly dependent on the wallet provide The code snippets below are examples only and will need to be adapted based on your specific wallet provider and library. ```tsx [Node] -import type { Signer, Identifier, IdentifierKind } from "@xmtp/node-sdk"; +import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const accountIdentifier: Identifier = { - identifier: "0x...", // Ethereum address as the identifier + identifier: '0x...', // Ethereum address as the identifier identifierKind: IdentifierKind.Ethereum, // Specifies the identity type }; const signer: Signer = { - type: "SCW", + type: 'SCW', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string @@ -908,11 +935,13 @@ If you want to inspect the database visually, you can use [DB Browser for SQLite To call `Client.create()`, you must pass in a required `signer` and can also pass in any of the optional parameters covered in [Configure an XMTP client](#configure-an-xmtp-client). ```tsx [Node] -import { Client, type Signer } from "@xmtp/node-sdk"; -import { getRandomValues } from "node:crypto"; +import { Client, type Signer } from '@xmtp/node-sdk'; +import { getRandomValues } from 'node:crypto'; // create a signer -const signer: Signer = { /* ... */ }; +const signer: Signer = { + /* ... */ +}; /** * The database encryption key is optional but strongly recommended for @@ -930,7 +959,7 @@ const client = await Client.create( dbEncryptionKey, // Optional: Use a function to dynamically set the database path based on inbox ID // dbPath: (inboxId) => `./agent-databases/xmtp-${inboxId}.db3`, - }, + } ); ``` @@ -939,14 +968,14 @@ const client = await Client.create( You can configure an XMTP client with these options passed to `Client.create`: ```tsx [Node] -import type { ContentCodec } from "@xmtp/content-type-primitives"; -import type { LogLevel } from "@xmtp/node-bindings"; +import type { ContentCodec } from '@xmtp/content-type-primitives'; +import type { LogLevel } from '@xmtp/node-bindings'; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ - env?: "local" | "dev" | "production"; + env?: 'local' | 'dev' | 'production'; /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. @@ -954,11 +983,11 @@ type ClientOptions = { * You can use the following format: `appVersion: 'AGENT_NAME/AGENT_VERSION'`. * For example, `appVersion: 'alix/2.x'` * - * If you have an agent and an app, it's best to distinguish them from each other by + * If you have an agent and an app, it's best to distinguish them from each other by * adding `-agent` and `-app` to the names. For example: * - Agent: `appVersion: 'alix-agent/2.x'` * - App: `appVersion: 'alix-app/3.x'` - * + * * Setting this value provides telemetry that shows which agents are using the * XMTP client SDK. This information can help XMTP core developers provide you with agent * support, especially around communicating important SDK updates, including @@ -1114,19 +1143,23 @@ The app's developer can provide a UI that enables group participants to make fur - Update group metadata
- UI screenshot showing group permission toggle options including Add members and Edit group info settings + UI screenshot showing group permission toggle options including Add members and Edit group info settings
You can use member statuses, options, and permissions to create a custom policy set. The following table represents the valid policy options for each of the permissions: -| Permission | Allow all | Deny all | Admin only | Super admin only | -| --- | --- | --- | --- | --- | -| Add member | ✅ | ✅ | ✅ | ✅ | -| Remove member | ✅ | ✅ | ✅ | ✅ | -| Add admin | ❌ | ✅ | ✅ | ✅ | -| Remove admin | ❌ | ✅ | ✅ | ✅ | -| Update group permissions | ❌ | ❌ | ❌ | ✅ | -| Update group metadata | ✅ | ✅ | ✅ | ✅ | +| Permission | Allow all | Deny all | Admin only | Super admin only | +| ------------------------ | --------- | -------- | ---------- | ---------------- | +| Add member | ✅ | ✅ | ✅ | ✅ | +| Remove member | ✅ | ✅ | ✅ | ✅ | +| Add admin | ❌ | ✅ | ✅ | ✅ | +| Remove admin | ❌ | ✅ | ✅ | ✅ | +| Update group permissions | ❌ | ❌ | ❌ | ✅ | +| Update group metadata | ✅ | ✅ | ✅ | ✅ | If you aren't opinionated and don't set any permissions and options, groups will default to using the delivered `All_Members` policy set, which applies the following permissions and options: @@ -1208,7 +1241,10 @@ await group.removeMembers([inboxId]); ### Get inbox IDs for members ```js [Node] -const inboxId = await client.getInboxIdByIdentities([bo.identity, caro.identity]); +const inboxId = await client.getInboxIdByIdentities([ + bo.identity, + caro.identity, +]); ``` ### Get identities for members @@ -1240,10 +1276,10 @@ Once you have the group chat or DM conversation, you can send messages in the co ```tsx [Node] // For a DM conversation -await dm.send("Hello world"); +await dm.send('Hello world'); // OR for a group chat -await group.send("Hello everyone"); +await group.send('Hello everyone'); ``` @@ -1258,21 +1294,21 @@ Listens to the network for new group chats and DMs. Whenever a new conversation const stream = await client.conversations.stream({ onValue: (conversation) => { // Received a conversation - console.log("New conversation:", conversation); + console.log('New conversation:', conversation); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Stream failed"); - } + console.log('Stream failed'); + }, }); // Or use for-await loop for await (const conversation of stream) { // Received a conversation - console.log("New conversation:", conversation); + console.log('New conversation:', conversation); } ``` @@ -1293,35 +1329,35 @@ The stream is infinite. Therefore, any looping construct used with the stream wo const stream = await client.conversations.streamAllMessages({ onValue: (message) => { // Received a message - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Stream failed"); - } + console.log('Stream failed'); + }, }); - + // stream only group messages const groupMessageStream = await client.conversations.streamAllGroupMessages({ onValue: (message) => { - console.log("New group message:", message); - } + console.log('New group message:', message); + }, }); - + // stream only dm messages const dmMessageStream = await client.conversations.streamAllDmMessages({ onValue: (message) => { - console.log("New DM message:", message); - } + console.log('New DM message:', message); + }, }); - + // Or use for-await loop for await (const message of stream) { // Received a message - console.log("New message:", message); + console.log('New message:', message); } ``` @@ -1337,8 +1373,8 @@ Streams will automatically attempt to reconnect if they fail. By default, a stre const stream = await client.conversations.streamAllMessages({ retryOnFail: false, onValue: (message) => { - console.log("New message:", message); - } + console.log('New message:', message); + }, }); // use stream options with retry configuration @@ -1346,16 +1382,16 @@ const stream = await client.conversations.streamAllMessages({ retryAttempts: 10, retryDelay: 20000, // 20 seconds onValue: (message) => { - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { - console.error("Stream error:", error); + console.error('Stream error:', error); }, onFail: () => { - console.log("Stream failed after retries"); + console.log('Stream failed after retries'); }, onRestart: () => { - console.log("Stream restarted"); + console.log('Stream restarted'); }, onRetry: (attempt, maxAttempts) => { console.log(`Stream retry attempt ${attempt} of ${maxAttempts}`); @@ -1382,11 +1418,15 @@ Your agent can have **up to 10 active installations** before you need to revoke For example, if you deploy your agent across these network environments, you will have 3 inboxes, each with 1 installation: - Local development: `local` network -- Railway: `dev` network +- Railway: `dev` network - Production server: `production` network
- Diagram showing agent deployments across different network environments (local, dev, production) creating separate inboxes with one installation each + Diagram showing agent deployments across different network environments (local, dev, production) creating separate inboxes with one installation each
If you deploy your agent to this same network environment, you have 1 inbox with 3 installations: @@ -1396,7 +1436,11 @@ If you deploy your agent to this same network environment, you have 1 inbox with - Production server: `production` network
- Diagram showing agent deployments to the same production network environment creating one inbox with three installations + Diagram showing agent deployments to the same production network environment creating one inbox with three installations
Here are some best practices for agent installation management: @@ -1467,7 +1511,7 @@ const groupName = group.name; ## Update a group chat name ```js [Node] -await group.updateName("New Group Name"); +await group.updateName('New Group Name'); ``` ## Get a group chat description @@ -1479,7 +1523,7 @@ const groupDescription = group.description; ## Update a group chat description ```js [Node] -await group.updateDescription("New Group Description"); +await group.updateDescription('New Group Description'); ``` ## Get a group chat image URL @@ -1491,7 +1535,7 @@ const groupImageUrl = group.imageUrl; ## Update a group chat image URL ```js [Node] -await group.updateImageUrl("newurl.com"); +await group.updateImageUrl('newurl.com'); ``` @@ -1541,9 +1585,8 @@ Use these helper methods to quickly locate and access specific conversations—w ```js [Node] // get a conversation by its ID -const conversationById = await client.conversations.getConversationById( - conversationId -); +const conversationById = + await client.conversations.getConversationById(conversationId); // get a message by its ID const messageById = await client.conversations.getMessageById(messageId); @@ -1587,12 +1630,10 @@ pnpm add @xmtp/content-type-reaction After importing the package, you can register the codec. ```jsx [Node] -import { - ReactionCodec, -} from "@xmtp/content-type-reaction"; +import { ReactionCodec } from '@xmtp/content-type-reaction'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new ReactionCodec()], }); ``` @@ -1612,8 +1653,8 @@ With XMTP, reactions are represented as objects with the following keys: ```tsx [Node] const reaction = { reference: someMessageID, - action: "added", - content: "smile", + action: 'added', + content: 'smile', }; await conversation.send(reaction, { @@ -1675,12 +1716,10 @@ pnpm i @xmtp/content-type-wallet-send-calls After importing the package, you can register the codec. ```js [Node] -import { - WalletSendCallsCodec, -} from "@xmtp/content-type-wallet-send-calls"; +import { WalletSendCallsCodec } from '@xmtp/content-type-wallet-send-calls'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new WalletSendCallsCodec()], }); ``` @@ -1691,33 +1730,33 @@ With XMTP, a transaction request is represented using `wallet_sendCalls` with ad ```ts [Node] const walletSendCalls: WalletSendCallsParams = { - version: "1.0", - from: "0x123...abc", - chainId: "0x2105", + version: '1.0', + from: '0x123...abc', + chainId: '0x2105', calls: [ { - to: "0x456...def", - value: "0x5AF3107A4000", + to: '0x456...def', + value: '0x5AF3107A4000', metadata: { - description: "Send 0.0001 ETH on base to 0x456...def", - transactionType: "transfer", - currency: "ETH", + description: 'Send 0.0001 ETH on base to 0x456...def', + transactionType: 'transfer', + currency: 'ETH', amount: 100000000000000, decimals: 18, - toAddress: "0x456...def", + toAddress: '0x456...def', }, }, { - to: "0x789...cba", - data: "0xdead...beef", + to: '0x789...cba', + data: '0xdead...beef', metadata: { - description: "Lend 10 USDC on base with Morpho @ 8.5% APY", - transactionType: "lend", - currency: "USDC", + description: 'Lend 10 USDC on base with Morpho @ 8.5% APY', + transactionType: 'lend', + currency: 'USDC', amount: 10000000, decimals: 6, - platform: "morpho", - apy: "8.5", + platform: 'morpho', + apy: '8.5', }, }, ], @@ -1791,10 +1830,10 @@ After importing the package, you can register the codec. import { ContentTypeTransactionReference, TransactionReferenceCodec, -} from "@xmtp/content-type-transaction-reference"; +} from '@xmtp/content-type-transaction-reference'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new TransactionReferenceCodec()], }); ``` @@ -1894,10 +1933,10 @@ After importing the package, you can register the codec. import { AttachmentCodec, RemoteAttachmentCodec, -} from "@xmtp/content-type-remote-attachment"; +} from '@xmtp/content-type-remote-attachment'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], }); ``` @@ -1914,7 +1953,7 @@ const data = await new Promise((resolve, reject) => { if (reader.result instanceof ArrayBuffer) { resolve(reader.result); } else { - reject(new Error("Not an ArrayBuffer")); + reject(new Error('Not an ArrayBuffer')); } }; reader.readAsArrayBuffer(image); @@ -1952,7 +1991,7 @@ const remoteAttachment = { salt: encryptedEncoded.salt, nonce: encryptedEncoded.nonce, secret: encryptedEncoded.secret, - scheme: "https://", + scheme: 'https://', filename: attachment.filename, contentLength: attachment.data.byteLength, }; @@ -1971,7 +2010,7 @@ await conversation.send(remoteAttachment, { Now that you can send a remote attachment, you need a way to receive it. For example: ```tsx [Node] -import { ContentTypeRemoteAttachment } from "@xmtp/content-type-remote-attachment"; +import { ContentTypeRemoteAttachment } from '@xmtp/content-type-remote-attachment'; if (message.contentType.sameAs(RemoteAttachmentContentType)) { const attachment = await RemoteAttachmentCodec.load(message.content, client); @@ -1995,7 +2034,7 @@ const objectURL = URL.createObjectURL( }) ); -const img = document.createElement("img"); +const img = document.createElement('img'); img.src = objectURL; img.title = attachment.filename; ``` @@ -2027,7 +2066,7 @@ Here is the current standard content type: An agent built with XMTP uses the `TextCodec` (plain text) standard content type by default. This means that if your agent is sending plain text messages only, you don't need to perform any additional steps related to content types. ```jsx [Node] -await conversation.send("gm"); +await conversation.send('gm'); ``` ## Standards-track content types @@ -2077,10 +2116,10 @@ pnpm add @xmtp/content-type-reply After importing the package, you can register the codec. ```js [Node] -import { ReplyCodec } from "@xmtp/content-type-reply"; +import { ReplyCodec } from '@xmtp/content-type-reply'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new ReplyCodec()], }); ``` @@ -2094,14 +2133,14 @@ Once you've created a reply, you can send it. Replies are represented as objects - `content`: String representation of the reply ```ts [Node] -import { ContentTypeText } from "@xmtp/content-type-text"; -import { ContentTypeReply } from "@xmtp/content-type-reply"; -import type { Reply } from "@xmtp/content-type-reply"; +import { ContentTypeText } from '@xmtp/content-type-text'; +import { ContentTypeReply } from '@xmtp/content-type-reply'; +import type { Reply } from '@xmtp/content-type-reply'; const reply: Reply = { reference: someMessageID, contentType: ContentTypeText, - content: "I concur", + content: 'I concur', }; await conversation.send(reply, { @@ -2179,7 +2218,7 @@ Understanding commit messages is especially helpful when debugging and understan #### User-initiated commits - **Add member commits**: When a user explicitly adds someone to a group -- **Remove member commits**: When a user removes someone from a group +- **Remove member commits**: When a user removes someone from a group - **Update metadata commits**: When a user changes group name, description, or permissions - **Update permissions commits**: When a user modifies group permission settings @@ -2244,7 +2283,7 @@ Identity update messages are stored permanently to ensure continuity of trust an ## pages/protocol/signatures.mdx --- -description: "Learn about wallet signature types when using XMTP" +description: 'Learn about wallet signature types when using XMTP' --- # Wallet signatures with XMTP @@ -2280,7 +2319,11 @@ More specifically, the message will request that you sign: Sign the **XMTP : Authenticate to inbox** message with your wallet address to consent to the message requests. -MetaMask wallet browser extension Sign this message? window showing an XMTP: Authenticate to inbox message +MetaMask wallet browser extension Sign this message? window showing an XMTP: Authenticate to inbox message ## Sign to add another address to your inbox @@ -2405,8 +2448,8 @@ For most developers, the [Build chat apps](/chat-apps/intro/get-started) and [Bu The encryption elements are mainly defined by MLS, with some additions by XMTP. To learn more, see: - [Security](/protocol/security) - - XMTP and MLS prioritize security, privacy, and message integrity through advanced cryptographic techniques, delivering end-to-end + + XMTP and MLS prioritize security, privacy, and message integrity through advanced cryptographic techniques, delivering end-to-end encryption for both 1:1 and group conversations - [Epochs](/protocol/epochs) @@ -2435,7 +2478,7 @@ The delivery elements are mainly defined by XMTP. To learn more, see: - [Topics](/protocol/topics) - Messages are routed through topics, which are unique addresses that identify conversation channels. + Messages are routed through topics, which are unique addresses that identify conversation channels. - [Cursors](/protocol/cursors) @@ -2508,7 +2551,7 @@ The group message topic is used to send and receive messages within a specific c - **Purpose**: Carries all ongoing communication for a conversation, including [application messages](/protocol/envelope-types#application-messages) (text, reactions, etc.) and [commit messages](/protocol/envelope-types#commit-messages) that modify the group state. - **Usage**: When an app wants to receive messages for a conversation, it subscribes to this topic. The `conversation.topic` property in the SDKs provides this value. -> **Note on [DM stitching](/chat-apps/push-notifs/understand-push-notifs#understand-dm-stitching-and-push-notifications): For direct messages, multiple underlying conversations might be "stitched" together in the UI. For push notifications to be reliable, an app must subscribe to the group message topic for each of these underlying conversations. +> \*\*Note on [DM stitching](/chat-apps/push-notifs/understand-push-notifs#understand-dm-stitching-and-push-notifications): For direct messages, multiple underlying conversations might be "stitched" together in the UI. For push notifications to be reliable, an app must subscribe to the group message topic for each of these underlying conversations. ### Welcome message topic @@ -2543,7 +2586,16 @@ Specifically, XMTP messaging provides the comprehensive security properties cove This video provides a walkthrough of XMTP's implementation of MLS. - + To dive deeper into how XMTP implements MLS, see the [XMTP MLS protocol specification](https://github.com/xmtp/libxmtp/tree/main/xmtp_mls). @@ -2613,23 +2665,23 @@ Here is a summary of individual cryptographic tools used to collectively ensure - [HPKE](https://www.rfc-editor.org/rfc/rfc9180.html) - Used to encrypt Welcome messages, protect the identities of group invitees, and maintain the confidentiality of group membership. We use the ciphersuite HPKEX25519. + Used to encrypt Welcome messages, protect the identities of group invitees, and maintain the confidentiality of group membership. We use the ciphersuite HPKEX25519. - [AEAD](https://developers.google.com/tink/aead) - Used to ensure both confidentiality and integrity of messages. In particular, we use the ciphersuite CHACHA20POLY1305. + Used to ensure both confidentiality and integrity of messages. In particular, we use the ciphersuite CHACHA20POLY1305. - [SHA3_256 and SHA2_256](http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) - XMTP uses two cryptographic hash functions to ensure data integrity and provide strong cryptographic binding. SHA3_256 is used in the multi-wallet identity structure. SHA2_256 is used in MLS. The ciphersuite is SHA256. + XMTP uses two cryptographic hash functions to ensure data integrity and provide strong cryptographic binding. SHA3_256 is used in the multi-wallet identity structure. SHA2_256 is used in MLS. The ciphersuite is SHA256. - [Ed25519](https://ed25519.cr.yp.to/ed25519-20110926.pdf) - Used for digital signatures to provide secure, high-performance signing and verification of messages. The ciphersuite is Ed25519. + Used for digital signatures to provide secure, high-performance signing and verification of messages. The ciphersuite is Ed25519. - [XWING KEM](https://www.ietf.org/archive/id/draft-connolly-cfrg-xwing-kem-02.html) - Used for quantum-resistant key encapsulation in Welcome messages. XWING is a hybrid post-quantum KEM that combines conventional cryptography with [ML-KEM](https://csrc.nist.gov/pubs/fips/203/final) (the NIST-standardized post-quantum component), providing protection against future quantum computer attacks while maintaining current security standards. + Used for quantum-resistant key encapsulation in Welcome messages. XWING is a hybrid post-quantum KEM that combines conventional cryptography with [ML-KEM](https://csrc.nist.gov/pubs/fips/203/final) (the NIST-standardized post-quantum component), providing protection against future quantum computer attacks while maintaining current security standards. ## FAQ about messaging security @@ -2675,7 +2727,7 @@ The identity model also allows XMTP to support any identity type, as long as it ## Inbox ID -An **inbox ID** is a user's stable destination for their messages. Their inbox ID remains constant even as they add or remove [identities](#identity) and [installations](#installations). +An **inbox ID** is a user's stable destination for their messages. Their inbox ID remains constant even as they add or remove [identities](#identity) and [installations](#installation). The inbox ID is derived from the hash of the first associated wallet address and a nonce and acts as an opaque identifier that apps use for messaging. @@ -2700,9 +2752,9 @@ An **installation** represents a specific app installation that can access an in **One inbox ID** → **multiple identities**: Users can receive messages as any of their identities, all flowing to the same inbox -``` +```text Inbox ID (stable destination for messages) -├── Identity 1 (recovery identity, first identity added to an inbox) +├── Identity 1 (recovery identity, first identity added to an inbox) ├── Identity 2 (EOA wallet) ├── Identity 3 (SCW wallet) └── Any identity that can produce a verifiable cryptographic signature @@ -2710,11 +2762,11 @@ Inbox ID (stable destination for messages) **One identity** → **multiple installations**: Users can access their messages from different apps on the same or different devices -``` +```text Each identity can authenticate new installations: ├── Installation A (phone app) -├── Installation B (web app) -├── Installation C (desktop app) +├── Installation B (web app) +├── Installation C (desktop app) └── Up to 10 installations ``` @@ -2774,7 +2826,7 @@ The primary role of a cursor becomes evident when you use the `sync()` functions 1. **Initial sync**: The first time an app installation calls `sync()` for a specific conversation, it fetches all available messages and events from the network for that conversation's topic. 2. **Cursor placement**: Once the sync is complete, the SDK places a cursor at the end of that batch of fetched messages. -3. **Subsequent syncs**: On the next `sync()` call for that same conversation, the client sends its current cursor position to the network. The network then returns only the messages and events that have occurred *after* that cursor. +3. **Subsequent syncs**: On the next `sync()` call for that same conversation, the client sends its current cursor position to the network. The network then returns only the messages and events that have occurred _after_ that cursor. 4. **Cursor advancement**: After the new messages are successfully fetched, the SDK advances the cursor to the new latest point. This process ensures that each `sync()` call only retrieves what's new, making synchronization efficient by avoiding the re-downloading of messages the client already has. @@ -2796,8 +2848,8 @@ The XMTP SDKs use cursors to make message synchronization highly efficient. The Each `sync()` function corresponds to a different type of cursor: - `conversation.sync()`: This operates on the **group message topic** for a single conversation. It moves the cursor for that specific conversation, fetching new messages or group updates (like name changes). -- `conversations.sync()`: This operates on the **welcome message topic**. It moves the cursor for welcome messages, fetching any new conversations the user has been invited to. It does *not* fetch the contents of those new conversations. -- `conversations.syncAll()`: This is the most comprehensive sync. It effectively performs the actions of the other two syncs for all of the user's conversations. It moves the cursors for the welcome topic *and* for every individual group message topic, ensuring the client has fetched all new conversations and all new messages in existing conversations. +- `conversations.sync()`: This operates on the **welcome message topic**. It moves the cursor for welcome messages, fetching any new conversations the user has been invited to. It does _not_ fetch the contents of those new conversations. +- `conversations.syncAll()`: This is the most comprehensive sync. It effectively performs the actions of the other two syncs for all of the user's conversations. It moves the cursors for the welcome topic _and_ for every individual group message topic, ensuring the client has fetched all new conversations and all new messages in existing conversations. For example, here is a sequence diagram illustrating how cursors operate with `conversation.sync()`: @@ -2868,25 +2920,39 @@ XMTP does not currently have a token. Disregard any information regarding airdro For real-time statuses of nodes in the XMTP testnet, see [XMTP Node Status](https://status.testnet.xmtp-partners.xyz/). The following table lists the nodes currently registered to power the XMTP testnet. -| | Node operator | Node address | -|-----|---------------|--------------| -| 1 | Artifact Capital | xmtp.artifact.systems:443 | -| 2 | Crystal One | xmtp.node-op.com:443 | -| 3 | Emerald Onion | xmtp.disobey.net:443 | -| 4 | Encapsulate | lb.validator.xmtp.testnet.encapsulate.xyz:443 | -| 5 | Ethereum Name Service (ENS) | grpc.ens-xmtp.com:443 | -| 6 | Laminated Labs | xmtp.validators.laminatedlabs.net:443 | -| 7 | Next.id | xmtp.nextnext.id:443 | -| 8 | Nodle | xmtpd.nodleprotocol.io:443 | -| 9 | Ephemera | grpc.testnet.xmtp.network:443 | -| 10 | Ephemera | grpc2.testnet.xmtp.network:443 | +| | Node operator | Node address | +| --- | --------------------------- | --------------------------------------------- | +| 1 | Artifact Capital | xmtp.artifact.systems:443 | +| 2 | Crystal One | xmtp.node-op.com:443 | +| 3 | Emerald Onion | xmtp.disobey.net:443 | +| 4 | Encapsulate | lb.validator.xmtp.testnet.encapsulate.xyz:443 | +| 5 | Ethereum Name Service (ENS) | grpc.ens-xmtp.com:443 | +| 6 | Laminated Labs | xmtp.validators.laminatedlabs.net:443 | +| 7 | Next.id | xmtp.nextnext.id:443 | +| 8 | Nodle | xmtpd.nodleprotocol.io:443 | +| 9 | Ephemera | grpc.testnet.xmtp.network:443 | +| 10 | Ephemera | grpc2.testnet.xmtp.network:443 | Here is a map of node locations: -
+
@@ -2959,7 +3025,7 @@ You can also check fork status directly from conversation lists: // Check fork status when listing conversations const conversations = await client.conversations.list(); -conversations.forEach(conversation => { +conversations.forEach((conversation) => { if (conversation.isCommitLogForked) { console.log(`Conversation ${conversation.id} is forked`); } @@ -2970,7 +3036,7 @@ conversations.forEach(conversation => { // Check fork status when listing conversations const conversations = await client.conversations.list(); -conversations.forEach(conversation => { +conversations.forEach((conversation) => { if (conversation.commitLogForkStatus === 'forked') { console.log(`Conversation ${conversation.id} is forked`); } @@ -3069,7 +3135,7 @@ const result1 = await client.debugInformation.uploadDebugArchive(); // Upload to custom server (serverUrl provided) const result2 = await client.debugInformation.uploadDebugArchive( - "https://my-debug-server.com/api/upload" + 'https://my-debug-server.com/api/upload' ); ``` @@ -3077,30 +3143,30 @@ const result2 = await client.debugInformation.uploadDebugArchive( #### API statistics -| Statistic | Description | -|-----------|-------------| -| UploadKeyPackage | Number of times key packages have been uploaded. | -| FetchKeyPackage | Number of times key packages have been fetched. | -| SendGroupMessages | Number of times messages have been sent to group chat and DM conversations. | -| SendWelcomeMessages | Number of times welcome messages have been sent. | -| QueryGroupMessages | Number of times queries have been made to fetch messages being sent to group chat and DM conversations. | -| QueryWelcomeMessages | Number of times queries have been made to fetch welcome messages. | +| Statistic | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------- | +| UploadKeyPackage | Number of times key packages have been uploaded. | +| FetchKeyPackage | Number of times key packages have been fetched. | +| SendGroupMessages | Number of times messages have been sent to group chat and DM conversations. | +| SendWelcomeMessages | Number of times welcome messages have been sent. | +| QueryGroupMessages | Number of times queries have been made to fetch messages being sent to group chat and DM conversations. | +| QueryWelcomeMessages | Number of times queries have been made to fetch welcome messages. | #### Identity statistics -| Statistic | Description | -|-----------|-------------| -| PublishIdentityUpdate | Number of times identity updates have been published. | -| GetIdentityUpdatesV2 | Number of times identity updates have been fetched. | -| GetInboxIds | Number of times inbox ID queries have been made. | -| VerifySCWSignatures | Number of times smart contract wallet signature verifications have been performed. | +| Statistic | Description | +| --------------------- | ---------------------------------------------------------------------------------- | +| PublishIdentityUpdate | Number of times identity updates have been published. | +| GetIdentityUpdatesV2 | Number of times identity updates have been fetched. | +| GetInboxIds | Number of times inbox ID queries have been made. | +| VerifySCWSignatures | Number of times smart contract wallet signature verifications have been performed. | #### Stream statistics -| Statistic | Description | -|-----------|-------------| +| Statistic | Description | +| ----------------- | ----------------------------------------------------------------------------------------------------------- | | SubscribeMessages | Number of times message subscription requests have been made. This is streaming messages in a conversation. | -| SubscribeWelcomes | Number of times welcome message subscription requests have been made. This is streaming conversations. | +| SubscribeWelcomes | Number of times welcome message subscription requests have been made. This is streaming conversations. | ## pages/chat-apps/use-signatures.mdx @@ -3123,7 +3189,7 @@ const signature = client.signWithInstallationKey(signatureText); ``` ```jsx [React Native] -const signature = await client.signWithInstallationKey(signatureText) +const signature = await client.signWithInstallationKey(signatureText); ``` ```kotlin [Kotlin] @@ -3138,16 +3204,19 @@ let signature = try client.signWithInstallationKey(message: signatureText) ## Verify with the same installation that signed - You can also sign with XMTP keys and verify that a payload was sent by the same client. +You can also sign with XMTP keys and verify that a payload was sent by the same client. :::code-group ```js [Node] -const isValidSignature = client.verifySignedWithInstallationKey(signatureText, signature); +const isValidSignature = client.verifySignedWithInstallationKey( + signatureText, + signature +); ``` ```jsx [React Native] -const isVerified = await client.verifySignature(signatureText, signature) +const isVerified = await client.verifySignature(signatureText, signature); ``` ```kotlin [Kotlin] @@ -3156,7 +3225,7 @@ val isVerified = client.verifySignature(signatureText, signature) ```swift [Swift] let isVerified = try client.verifySignature( - message: signatureText, + message: signatureText, signature: signature ) @@ -3171,13 +3240,17 @@ You can use an XMTP key's `installationId` to create a signature, then pass both :::code-group ```js [Node] -const isValidSignature = client.verifySignedWithPrivateKey(signatureText, signature, installationId); +const isValidSignature = client.verifySignedWithPrivateKey( + signatureText, + signature, + installationId +); ``` ```kotlin [Kotlin] val isVerified = client.verifySignatureWithInstallationId( - signatureText, - signature, + signatureText, + signature, installationId ) ``` @@ -3217,7 +3290,16 @@ History sync enables your users pick up conversations where they left off, regar This video provides a walkthrough of history sync, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. - + ## Enable history sync @@ -3237,13 +3319,21 @@ If needed, you can turn off history sync by setting the `historySyncUrl` client When your app initializes an XMTP client and a `historySyncUrl` client option is present, history sync automatically triggers an initial sync request and creates an encrypted payload.
- Diagram showing step 1 of history sync: client initialization triggers sync request and creates encrypted payload + Diagram showing step 1 of history sync: client initialization triggers sync request and creates encrypted payload
History sync then uploads the payload, sends a sync reply, and pulls all conversation state history into the new app installation, merging it with the existing app installations in the sync group.
- Diagram showing step 2 of history sync: payload upload, sync reply, and merging conversation state history across app installations + Diagram showing step 2 of history sync: payload upload, sync reply, and merging conversation state history across app installations
Ongoing updates to history are streamed automatically. Updates, whether for user consent preferences or messages, are sent across the sync group, ensuring all app installations have up-to-date information. @@ -3298,7 +3388,16 @@ Syncing does not refetch existing conversations and messages. It also does not f This video provides a walkthrough of key concepts required to implement syncing correctly. - + ## Sync a specific conversation @@ -3371,15 +3470,15 @@ To sync preferences only, you can call [`preferences.sync`](/chat-apps/list-stre :::code-group ```js [Browser] -await client.conversations.syncAll(["allowed"]); +await client.conversations.syncAll(['allowed']); ``` ```js [Node] -await client.conversations.syncAll(["allowed"]); +await client.conversations.syncAll(['allowed']); ``` ```tsx [React Native] -await client.conversations.syncAllConversations(["allowed"]); +await client.conversations.syncAllConversations(['allowed']); ``` ```kotlin [Kotlin] @@ -3417,7 +3516,6 @@ To enable a user to create an archive: 1. Specify the archive file path (e.g., iCloud, Google Cloud, or your server). Ensure the parent folder already exists. 2. Generate a 32-byte array encryption key to protect the archive contents. This ensures that other apps and devices cannot access the contents without the key. Securely store the key in a location that is secure, independent of the archive file, and will persist after your app has been uninstalled. A common place to store this encryption key is the iCloud Keychain. 3. Call `createArchive(path, encryptionKey, options?)` with the archive file path and the encryption key. Optionally, you can pass in the following: - - Archive start and end time. If left blank, the archive will include all time. - Archive contents, which can be `Consent` or `Messages`. If left blank, the archive will include both. @@ -3673,34 +3771,44 @@ Conversations are listed in descending order by their `lastMessage` created at v :::code-group ```js [Browser] -const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed] }); -const allGroups = await client.conversations.listGroups({ consentStates: [ConsentState.Allowed] }); -const allDms = await client.conversations.listDms({ consentStates: [ConsentState.Allowed] }); +const allConversations = await client.conversations.list({ + consentStates: [ConsentState.Allowed], +}); +const allGroups = await client.conversations.listGroups({ + consentStates: [ConsentState.Allowed], +}); +const allDms = await client.conversations.listDms({ + consentStates: [ConsentState.Allowed], +}); ``` ```js [Node] -const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed] }); -const allGroups = await client.conversations.listGroups({ consentStates: [ConsentState.Allowed] }); -const allDms = await client.conversations.listDms({ consentStates: [ConsentState.Allowed] }); +const allConversations = await client.conversations.list({ + consentStates: [ConsentState.Allowed], +}); +const allGroups = await client.conversations.listGroups({ + consentStates: [ConsentState.Allowed], +}); +const allDms = await client.conversations.listDms({ + consentStates: [ConsentState.Allowed], +}); ``` ```tsx [React Native] // List Conversation items -await alix.conversations.list(["allowed"]); +await alix.conversations.list(['allowed']); // List Conversation items and return only the fields set to true. Optimize data transfer // by requesting only the fields the app needs. -await alix.conversations.list( - { - members: false, - consentState: false, - description: false, - creatorInboxId: false, - addedByInboxId: false, - isActive: false, - lastMessage: true, - }, -); +await alix.conversations.list({ + members: false, + consentState: false, + description: false, + creatorInboxId: false, + addedByInboxId: false, + isActive: false, + lastMessage: true, +}); ``` ```kotlin [Kotlin] @@ -3760,7 +3868,11 @@ Each XMTP node contains: - A **group messages database**: Keeps a ledger of all messages in the groups an inbox ID is a part of.
- databases in each xmtpd node + databases in each xmtpd node
In the welcomes database, the groups are of these types: @@ -3784,7 +3896,11 @@ One-to-one chat, group chat, and preferences groups in the welcome database are - `syncAll`
- groups in the welcome database + groups in the welcome database
## Preferences group @@ -3796,19 +3912,31 @@ To describe preference sync, let's first focus on how the preferences group work 2. Let's say Inbox ID Alix has an Installation A of App A on their phone. At this time, Inbox ID Alix has a preferences group that looks like this:
- Diagram showing preferences group with one member (Inbox ID Alix) and Installation A of App A + Diagram showing preferences group with one member (Inbox ID Alix) and Installation A of App A
3. Inbox ID Alix then logs in to an Installation B of App B on their phone. The next time Installation A runs `preferences.sync` or `syncAll`, it updates the preferences group as follows:
- Diagram showing preferences group updated to include Installation B of App B after Alix logs in to a second app + Diagram showing preferences group updated to include Installation B of App B after Alix logs in to a second app
4. Then let's say Inbox ID Alix logs in to an Installation C of App A on their tablet. The next time Installation A or B runs `preferences.sync` or `syncAll`, it updates the preferences group as follows:
- Diagram showing preferences group updated to include Installation C of App A on tablet after Alix logs in to a third installation + Diagram showing preferences group updated to include Installation C of App A on tablet after Alix logs in to a third installation
## Preferences sync worker @@ -3818,25 +3946,41 @@ Now, let's describe how the preferences sync worker helps keep user consent pref 1. Let's say Inbox ID Alix uses Installation A to block Inbox ID Bo.
- Diagram showing Installation A blocking Inbox ID Bo + Diagram showing Installation A blocking Inbox ID Bo
2. This sends a message to the preferences group in the group message database. This is not an actual chat message, but a serialized proto message that is not shown to app users.
- Diagram showing serialized proto message sent to preferences group in the group message database + Diagram showing serialized proto message sent to preferences group in the group message database
3. When Installation B calls `preferences.sync` or `syncAll`, it gets the message from the preferences group. The sync worker listens for these preferences group messages and processes the message to block Inbox ID Bo in Installation B.
- Diagram showing Installation B processing the sync message to block Inbox ID Bo + Diagram showing Installation B processing the sync message to block Inbox ID Bo
4. Likewise, when Installation C calls `preferences.sync` or `syncAll`, it gets the message from the preferences group, and the sync worker ensures Inbox ID Bo is blocked there as well.
- Diagram showing Installation C processing the sync message to block Inbox ID Bo + Diagram showing Installation C processing the sync message to block Inbox ID Bo
Preferences sync handles HMAC keys in the same way. @@ -3860,7 +4004,7 @@ await client.preferences.sync(); ``` ```tsx [React Native] -await client.preferences.sync() +await client.preferences.sync(); ``` ```kotlin [Kotlin] @@ -3887,21 +4031,21 @@ Listens to the network for new group chats and DMs. Whenever a new conversation const stream = await client.conversations.stream({ onValue: (conversation) => { // Received a conversation - console.log("New conversation:", conversation); + console.log('New conversation:', conversation); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Stream failed"); - } + console.log('Stream failed'); + }, }); // Or use for-await loop for await (const conversation of stream) { // Received a conversation - console.log("New conversation:", conversation); + console.log('New conversation:', conversation); } ``` @@ -3909,35 +4053,35 @@ for await (const conversation of stream) { const stream = await client.conversations.stream({ onValue: (conversation) => { // Received a conversation - console.log("New conversation:", conversation); + console.log('New conversation:', conversation); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Stream failed"); - } + console.log('Stream failed'); + }, }); // To stream only groups const groupStream = await client.conversations.streamGroups({ onValue: (conversation) => { - console.log("New group:", conversation); - } + console.log('New group:', conversation); + }, }); // To stream only DMs const dmStream = await client.conversations.streamDms({ onValue: (conversation) => { - console.log("New DM:", conversation); - } + console.log('New DM:', conversation); + }, }); // Or use for-await loop for await (const conversation of stream) { // Received a conversation - console.log("New conversation:", conversation); + console.log('New conversation:', conversation); } ``` @@ -3987,21 +4131,21 @@ const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { // Received a message - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Stream failed"); - } + console.log('Stream failed'); + }, }); - + // Or use for-await loop for await (const message of stream) { // Received a message - console.log("New message:", message); + console.log('New message:', message); } ``` @@ -4011,37 +4155,37 @@ const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { // Received a message - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Stream failed"); - } + console.log('Stream failed'); + }, }); - + // stream only group messages const groupMessageStream = await client.conversations.streamAllGroupMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { - console.log("New group message:", message); - } + console.log('New group message:', message); + }, }); - + // stream only dm messages const dmMessageStream = await client.conversations.streamAllDmMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { - console.log("New DM message:", message); - } + console.log('New DM message:', message); + }, }); - + // Or use for-await loop for await (const message of stream) { // Received a message - console.log("New message:", message); + console.log('New message:', message); } ``` @@ -4050,7 +4194,7 @@ await alix.conversations.streamAllMessages( async (message: DecodedMessage) => { // Received a message }, - { consentState: ["allowed"] } + { consentState: ['allowed'] } ); ``` @@ -4083,8 +4227,8 @@ Streams will automatically attempt to reconnect if they fail. By default, a stre const stream = await client.conversations.streamAllMessages({ retryOnFail: false, onValue: (message) => { - console.log("New message:", message); - } + console.log('New message:', message); + }, }); // use stream options with retry configuration @@ -4093,16 +4237,16 @@ const stream = await client.conversations.streamAllMessages({ retryAttempts: 10, retryDelay: 20000, // 20 seconds onValue: (message) => { - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { - console.error("Stream error:", error); + console.error('Stream error:', error); }, onFail: () => { - console.log("Stream failed after retries"); + console.log('Stream failed after retries'); }, onRestart: () => { - console.log("Stream restarted"); + console.log('Stream restarted'); }, onRetry: (attempt, maxAttempts) => { console.log(`Stream retry attempt ${attempt} of ${maxAttempts}`); @@ -4117,16 +4261,16 @@ const stream = await client.conversations.streamAllMessages({ retryAttempts: 5, retryDelay: 15000, // 15 seconds onValue: (message) => { - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { - console.error("Stream error:", error); + console.error('Stream error:', error); }, onFail: () => { - console.log("Stream failed after retries"); + console.log('Stream failed after retries'); }, onRestart: () => { - console.log("Stream restarted"); + console.log('Stream restarted'); }, onRetry: (attempt, maxAttempts) => { console.log(`Stream retry attempt ${attempt} of ${maxAttempts}`); @@ -4135,25 +4279,25 @@ const stream = await client.conversations.streamAllMessages({ ``` ```tsx [React Native] -const [messages, setMessages] = useState([]); - - const messageCallback = async (message: DecodedMessage) => { - setMessages(prev => [...prev, message]); - } - const conversationFilterType: ConversationFilterType = 'all' - const consentStates: ConsentState[] = ['allowed'] - const onCloseCallback = () => { - console.log("Message stream closed, handle retries here") - } +const [messages, setMessages] = useState([]); - const startMessageStream = async () => { - await alix.conversations.streamAllMessages( - messageCallback, - conversationFilterType, - consentStates, - onCloseCallback - ); - }; +const messageCallback = async (message: DecodedMessage) => { + setMessages((prev) => [...prev, message]); +}; +const conversationFilterType: ConversationFilterType = 'all'; +const consentStates: ConsentState[] = ['allowed']; +const onCloseCallback = () => { + console.log('Message stream closed, handle retries here'); +}; + +const startMessageStream = async () => { + await alix.conversations.streamAllMessages( + messageCallback, + conversationFilterType, + consentStates, + onCloseCallback + ); +}; ``` ```kotlin [Kotlin] @@ -4341,7 +4485,6 @@ This guide is for macOS users, but the steps should be similar for Linux users. ## Install Docker 1. Install Docker Desktop: - - [Mac](https://docs.docker.com/docker-for-mac/install/) - [Windows](https://docs.docker.com/docker-for-windows/install/) - [Linux](https://docs.docker.com/desktop/install/linux-install/) @@ -4444,55 +4587,55 @@ You can now send notifications to your device using an [XMTP push notification c - Here is a piece of code that points to the ports and network. Be sure to use TLS like this `./dev/run --xmtp-listener-tls --api`. - :::code-group + :::code-group - ```tsx [Browser] - export const ApiUrls = { - local: "http://localhost:5555", - dev: "https://dev.xmtp.network", - production: "https://production.xmtp.network", - } as const; - - export const HistorySyncUrls = { - local: "http://localhost:5558", - dev: "https://message-history.dev.ephemera.network", - production: "https://message-history.production.ephemera.network", - } as const; - ``` + ```tsx [Browser] + export const ApiUrls = { + local: 'http://localhost:5555', + dev: 'https://dev.xmtp.network', + production: 'https://production.xmtp.network', + } as const; + + export const HistorySyncUrls = { + local: 'http://localhost:5558', + dev: 'https://message-history.dev.ephemera.network', + production: 'https://message-history.production.ephemera.network', + } as const; + ``` - ```tsx [Node] - export const ApiUrls = { - local: "http://localhost:5556", - dev: "https://grpc.dev.xmtp.network:443", - production: "https://grpc.production.xmtp.network:443", - } as const; - ``` + ```tsx [Node] + export const ApiUrls = { + local: 'http://localhost:5556', + dev: 'https://grpc.dev.xmtp.network:443', + production: 'https://grpc.production.xmtp.network:443', + } as const; + ``` - ```tsx [React Native] - const ApiUrls = { - local: 'http://localhost:5556', - dev: 'https://grpc.dev.xmtp.network:443', - production: 'https://grpc.production.xmtp.network:443' - } - ``` + ```tsx [React Native] + const ApiUrls = { + local: 'http://localhost:5556', + dev: 'https://grpc.dev.xmtp.network:443', + production: 'https://grpc.production.xmtp.network:443', + }; + ``` - ```kotlin [Kotlin] - enum ApiUrls { - static let local = "http://localhost:5556" - static let dev = "https://grpc.dev.xmtp.network:443" - static let production = "https://grpc.production.xmtp.network:443" - } - ``` + ```kotlin [Kotlin] + enum ApiUrls { + static let local = "http://localhost:5556" + static let dev = "https://grpc.dev.xmtp.network:443" + static let production = "https://grpc.production.xmtp.network:443" + } + ``` - ```swift [Swift] - object ApiUrls { - const val local = "http://localhost:5556" - const val dev = "https://grpc.dev.xmtp.network:443" - const val production = "https://grpc.production.xmtp.network:443" - } - ``` + ```swift [Swift] + object ApiUrls { + const val local = "http://localhost:5556" + const val dev = "https://grpc.dev.xmtp.network:443" + const val production = "https://grpc.production.xmtp.network:443" + } + ``` - ::: + ::: ## pages/chat-apps/push-notifs/ios-pn.mdx @@ -4512,41 +4655,41 @@ Perform this setup to understand how you can enable push notifications for your For this tutorial, we'll use [Firebase Cloud Messaging](https://console.firebase.google.com/) (FCM) as a convenient way to set up a messaging server. 1. Create an FCM project -Go to the [Firebase Console](https://console.firebase.google.com/), create a new project, and follow the setup instructions. + Go to the [Firebase Console](https://console.firebase.google.com/), create a new project, and follow the setup instructions. 2. Add your app to the FCM project -Add your iOS app to the project by following the Firebase setup workflow. You'll need your app's bundle ID. + Add your iOS app to the project by following the Firebase setup workflow. You'll need your app's bundle ID. 3. Download `GoogleService-Info.plist` -At the end of the setup, download the `GoogleService-Info.plist` file and add it to your Xcode project. + At the end of the setup, download the `GoogleService-Info.plist` file and add it to your Xcode project. 4. Generate FCM credentials -In the Firebase console, navigate to your project settings, select the **Cloud Messaging** tab, and note your server key and sender ID. You'll need these for your notification server. + In the Firebase console, navigate to your project settings, select the **Cloud Messaging** tab, and note your server key and sender ID. You'll need these for your notification server. ## Configure the iOS example app for push notifications 1. Enable push notifications -In Xcode, go to your project's target capabilities and enable push notifications. + In Xcode, go to your project's target capabilities and enable push notifications. 2. Register for notifications -Modify the `AppDelegate` to register for remote notifications and handle the device token. + Modify the `AppDelegate` to register for remote notifications and handle the device token. 3. Handle incoming notifications -Implement the necessary delegate methods to handle incoming notifications and foreground notification display. + Implement the necessary delegate methods to handle incoming notifications and foreground notification display. ## Run the notification server 1. Clone and configure the notification server -If you're using the example notification server, clone the repository and follow the setup instructions. Make sure to configure it with your FCM server key. + If you're using the example notification server, clone the repository and follow the setup instructions. Make sure to configure it with your FCM server key. 2. Run the server -Start the server locally or deploy it to a hosting service. + Start the server locally or deploy it to a hosting service. - Subscribe to push notifications in the app When initializing the XMTP client in your app, subscribe to push notifications using the device token obtained during registration. - Decode a notification envelope -When you receive a push notification, you may want to decode the notification envelope to display a message preview or other information. + When you receive a push notification, you may want to decode the notification envelope to display a message preview or other information. ## pages/chat-apps/push-notifs/push-notifs.mdx @@ -4883,21 +5026,21 @@ Let's dive deeper into how the XMTP push notification server filters messages to ![XMTP push notification server filtering flow](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/pn-server-filtering.png) 1. **Check if the server is subscribed to the message's topic** - - A topic is a way to organize messages, and each message has a topic. To support push notifications, your app must [subscribe the server to the topics](https://docs.xmtp.org/chat-apps/push-notifs/push-notifs#subscribe-to-topics) that are relevant to your users. For example, for a user Alix, you must subscribe to all topics associated with Alix's conversations. The XMTP push notification server has a list of these subscriptions. Your push notification server should expose functions to post the subscriptions to. The SDKs use protobufs as a universal language that allows the creation of these functions in any language. For example, [here are bufs](https://github.com/xmtp/xmtp-android/tree/main/library/src/main/java/org/xmtp/android/library/push) generated from the [XMTP example push notification server](/chat-apps/push-notifs/pn-server). You can use these directly if you clone and use the example server. - - If the arriving message's topic is **not on the list**, the server ignores it. - - If the arriving message's topic is **on the list**, the server proceeds to check the message with the next filter. + - A topic is a way to organize messages, and each message has a topic. To support push notifications, your app must [subscribe the server to the topics](https://docs.xmtp.org/chat-apps/push-notifs/push-notifs#subscribe-to-topics) that are relevant to your users. For example, for a user Alix, you must subscribe to all topics associated with Alix's conversations. The XMTP push notification server has a list of these subscriptions. Your push notification server should expose functions to post the subscriptions to. The SDKs use protobufs as a universal language that allows the creation of these functions in any language. For example, [here are bufs](https://github.com/xmtp/xmtp-android/tree/main/library/src/main/java/org/xmtp/android/library/push) generated from the [XMTP example push notification server](/chat-apps/push-notifs/pn-server). You can use these directly if you clone and use the example server. + - If the arriving message's topic is **not on the list**, the server ignores it. + - If the arriving message's topic is **on the list**, the server proceeds to check the message with the next filter. 2. **Check the `shouldPush` field value** - - Each [content type](/chat-apps/content-types/content-types), such as text, attachment, or reply, can have - - A `shouldPush` boolean value is set at the content type level for each content type, such as text, attachment, or reply, so it can't be overwritten on send. By default, this value is set to ***true*** for all content types except read receipts and reactions. - - If the message's content type `shouldPush` value is ***false***, the server ignores the message. - - If the message's content type `shouldPush` value is ***true***, the server proceeds to check the message with the next filter. + - Each [content type](/chat-apps/content-types/content-types), such as text, attachment, or reply, can have + - A `shouldPush` boolean value is set at the content type level for each content type, such as text, attachment, or reply, so it can't be overwritten on send. By default, this value is set to **_true_** for all content types except read receipts and reactions. + - If the message's content type `shouldPush` value is **_false_**, the server ignores the message. + - If the message's content type `shouldPush` value is **_true_**, the server proceeds to check the message with the next filter. 3. **Check the message's HMAC key** - - Each message sent with XMTP carries a single HMAC key. This key is updated with the encrypted message payload before being sent out. - - If the message is signed by an HMAC key that **matches** the user's HMAC key, the push notification server ignores the message. This match means that the message was sent by the user themself, and they should not receive a push notification about a message they sent. - - If the message is signed by an HMAC key that **does not match** the user's HMAC key, this means someone else sent the message and the user should be notified about it. At this point, the push notification server will send a notification. + - Each message sent with XMTP carries a single HMAC key. This key is updated with the encrypted message payload before being sent out. + - If the message is signed by an HMAC key that **matches** the user's HMAC key, the push notification server ignores the message. This match means that the message was sent by the user themself, and they should not receive a push notification about a message they sent. + - If the message is signed by an HMAC key that **does not match** the user's HMAC key, this means someone else sent the message and the user should be notified about it. At this point, the push notification server will send a notification. 4. **Send to the push notification service** - - The server sends the message to the push notification service. - - Once the push notification service has the message, it can format it appropriately for the push notification. This includes extracting necessary information, such as the sender's identity and message content, to craft meaningful notifications. This is only possible with the push notifications service inside the app and not with the push notification server because the server doesn't have the notion of a client and, therefore, can't decrypt the message. + - The server sends the message to the push notification service. + - Once the push notification service has the message, it can format it appropriately for the push notification. This includes extracting necessary information, such as the sender's identity and message content, to craft meaningful notifications. This is only possible with the push notifications service inside the app and not with the push notification server because the server doesn't have the notion of a client and, therefore, can't decrypt the message. XMTP provides an example XMTP push notification server that implements the filtering described here. To learn more, see [Run a push notification server for an app built with XMTP](/chat-apps/push-notifs/pn-server). @@ -4942,11 +5085,11 @@ After the push notification service sends the notification, the app must [receiv There are some nuances to how push notifications can be handled once received by the app. While it is useful for all of these app types to use the XMTP push notification server filtering capabilities, it is especially important for an iOS app without the user notification entitlement. -| | Can decrypt before displaying? | -| --- | --- | -| Android and web apps | Yes | -| iOS app with user notification entitlement | Yes | -| iOS app without user notification entitlement | No | +| | Can decrypt before displaying? | +| --------------------------------------------- | ------------------------------ | +| Android and web apps | Yes | +| iOS app with user notification entitlement | Yes | +| iOS app without user notification entitlement | No | - If the app **can** **decrypt** the push notification before displaying it to the user, it can perform additional logic (should I display this?) before displaying the push notification. For example, the app can decrypt the push notification, see the topic type, and process it accordingly: - Is the message in a welcome topic? @@ -4955,7 +5098,7 @@ There are some nuances to how push notifications can be handled once received by - `conversation.processMessage` - If the app **cannot** **decrypt** the push notification before displaying it to the user, it can't perform additional logic (should I display this?) before displaying the push notification. - For example, without the user notification entitlement, the app cannot decrypt and modify the push notification before displaying it to the user. The notification arrives on the device, and iOS handles displaying it automatically. You can decrypt the content after the notification is shown, but you cannot intercept it before display and decide not to show it, for example. + For example, without the user notification entitlement, the app cannot decrypt and modify the push notification before displaying it to the user. The notification arrives on the device, and iOS handles displaying it automatically. You can decrypt the content after the notification is shown, but you cannot intercept it before display and decide not to show it, for example. ## Understand HMAC keys and push notifications @@ -4971,7 +5114,16 @@ This is one of the jobs of the [history sync](/chat-apps/list-stream-sync/histor This video provides a walkthrough of direct message (DM) stitching, covering the key ideas discussed in this section. After watching, feel free to continue reading for more details. - + Consider a scenario where `alix.id` using an existing installation #1 to create a conversation with `bo.id` and sends them a DM. And then `alix.id` creates a new installation #2, and instead of waiting for [history sync](https://docs.xmtp.org/chat-apps/list-stream-sync/history-sync) to bring in their existing conversations, `alix.id` creates a new conversation with `bo.id` and sends them a DM. Under the hood, this results in two DM conversations (or two MLS groups) with the same pair of identities, `alix.id` and `bo.id`, resulting in a confusing DM UX like this one: @@ -4986,17 +5138,17 @@ For example, with DM stitching, instead of seeing two separate DM conversations ### How DM stitching works 1. When fetching messages from any of the MLS groups associated with a DM conversation, the XMTP SDK responds with messages from all of the groups associated with the DM conversation. - - For example, let's say you have three MLS groups associated with a DM conversation: - - alix-bo-1 - - alix-bo-2 - - alix-bo-3 + - For example, let's say you have three MLS groups associated with a DM conversation: + - alix-bo-1 + - alix-bo-2 + - alix-bo-3 - Any messages sent in any of these DM conversations will display in all of these DM conversations. + Any messages sent in any of these DM conversations will display in all of these DM conversations. - For example, `alix.id` sends a message in alix-bo-1. `bo.id` is on alix-bo-3, but can still see messages sent in alix-bo-1. + For example, `alix.id` sends a message in alix-bo-1. `bo.id` is on alix-bo-3, but can still see messages sent in alix-bo-1. 2. When sending messages in a DM conversation, all installations in that DM will eventually converge to sending them to the same MLS group, even if they originally start off using different ones. - - For example, `bo.id` sends a message in alix-bo-3. `alix.id` is on alix-bo-1, but can still see messages from alix-bo-3. When `alix.id` sends a reply to `bo.id`, it uses the most recently used DM conversation, alix-bo-3. In this way, all messaging will eventually move to alix-bo-3, and 1 and 2 will slowly fade away due to non-use. + - For example, `bo.id` sends a message in alix-bo-3. `alix.id` is on alix-bo-1, but can still see messages from alix-bo-3. When `alix.id` sends a reply to `bo.id`, it uses the most recently used DM conversation, alix-bo-3. In this way, all messaging will eventually move to alix-bo-3, and 1 and 2 will slowly fade away due to non-use. ### DM stitching considerations for push notifications @@ -5033,13 +5185,13 @@ For all other server-side applications, including backends for chat apps, follow ```tsx [Node] // 1. Create an EOA or SCW signer. // Details depend on your app's wallet implementation. -import type { Signer, Identifier, IdentifierKind } from "@xmtp/node-sdk"; +import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const signer: Signer = { - type: "EOA", + type: 'EOA', getIdentifier: () => ({ - identifier: "0x...", // Ethereum address as the identifier - identifierKind: IdentifierKind.Ethereum + identifier: '0x...', // Ethereum address as the identifier + identifierKind: IdentifierKind.Ethereum, }), signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string @@ -5048,8 +5200,8 @@ const signer: Signer = { }; // 2. Create the XMTP client -import { Client } from "@xmtp/node-sdk"; -import { getRandomValues } from "node:crypto"; +import { Client } from '@xmtp/node-sdk'; +import { getRandomValues } from 'node:crypto'; const dbEncryptionKey = getRandomValues(new Uint8Array(32)); const client = await Client.create(signer, { dbEncryptionKey }); @@ -5061,30 +5213,32 @@ const group = await client.conversations.newGroup( ); // 4. Send messages -await group.send("Hello everyone"); +await group.send('Hello everyone'); // 5. List, stream, and sync // List existing conversations -const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed] }); +const allConversations = await client.conversations.list({ + consentStates: [ConsentState.Allowed], +}); // Stream new messages const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { console.error(error); - } + }, }); // Or use for-await loop for await (const message of stream) { // Received a message - console.log("New message:", message); + console.log('New message:', message); } // Sync all new welcomes, preference updates, conversations, -// and messages from allowed conversations -await client.conversations.syncAll(["allowed"]); +// and messages from allowed conversations +await client.conversations.syncAll(['allowed']); ``` @@ -5104,22 +5258,22 @@ The guide provides some quickstart code, as well as a map to building a [secure ```tsx [Browser] // 1. Create an EOA or SCW signer. // Details depend on your app's wallet implementation. -import type { Signer, Identifier } from "@xmtp/browser-sdk"; +import type { Signer, Identifier } from '@xmtp/browser-sdk'; const signer: Signer = { - type: "EOA", + type: 'EOA', getIdentifier: () => ({ - identifier: "0x...", // Ethereum address as the identifier - identifierKind: "Ethereum" + identifier: '0x...', // Ethereum address as the identifier + identifierKind: 'Ethereum', }), signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string // this string must be converted to bytes and returned in this function - } + }, }; // 2. Create the XMTP client -import { Client } from "@xmtp/browser-sdk"; +import { Client } from '@xmtp/browser-sdk'; const client = await Client.create(signer, { // Note: dbEncryptionKey is not used for encryption in browser environments }); @@ -5131,31 +5285,37 @@ const group = await client.conversations.newGroup( ); // 4. Send messages -await group.send("Hello everyone"); +await group.send('Hello everyone'); // 5. List, stream, and sync // List existing conversations -const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed] }); -const allGroups = await client.conversations.listGroups({ consentStates: [ConsentState.Allowed] }); -const allDms = await client.conversations.listDms({ consentStates: [ConsentState.Allowed] }); +const allConversations = await client.conversations.list({ + consentStates: [ConsentState.Allowed], +}); +const allGroups = await client.conversations.listGroups({ + consentStates: [ConsentState.Allowed], +}); +const allDms = await client.conversations.listDms({ + consentStates: [ConsentState.Allowed], +}); // Stream new messages const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { - console.log("New message:", message); + console.log('New message:', message); }, onError: (error) => { console.error(error); - } + }, }); // Or use for-await loop for await (const message of stream) { - console.log("New message:", message); + console.log('New message:', message); } // Sync all new welcomes, preference updates, conversations, -// and messages from allowed conversations -await client.conversations.syncAll(["allowed"]); +// and messages from allowed conversations +await client.conversations.syncAll(['allowed']); ``` @@ -5217,7 +5377,7 @@ for await message in try await client.conversations.streamAllMessages(type: /* O // Received a message } // Sync all new welcomes, preference updates, conversations, -// and messages from allowed conversations +// and messages from allowed conversations try await client.conversations.syncAllConversations(consentState: [.allowed]) ``` @@ -5282,7 +5442,7 @@ client.conversations.streamAllMessages(consentStates = listOf(ConsentState.ALLOW // Received a message } // Sync all new welcomes, preference updates, conversations, -// and messages from allowed conversations +// and messages from allowed conversations client.conversations.syncAll(consentStates = ConsentState.ALLOWED) ``` @@ -5305,47 +5465,53 @@ The guide provides some quickstart code, as well as a map to building a [secure // Details depend on your app's wallet implementation. export function convertEOAToSigner(eoaAccount: EOAAccount): Signer { return { - getIdentifier: async () => new PublicIdentity(eoaAccount.address, "ETHEREUM"), + getIdentifier: async () => + new PublicIdentity(eoaAccount.address, 'ETHEREUM'), getChainId: () => undefined, getBlockNumber: () => undefined, - signerType: () => "EOA", - signMessage: async (message: string) => ({ signature: await eoaAccount.signMessage(message) }), + signerType: () => 'EOA', + signMessage: async (message: string) => ({ + signature: await eoaAccount.signMessage(message), + }), }; } // 2. Create the XMTP client const client = Client.create(signer, { - env: "production", + env: 'production', dbEncryptionKey: keyBytes, // 32 bytes }); // 3. Start conversations const group = await client.conversations.newGroup([bo.inboxId, caro.inboxId]); -const groupWithMeta = await client.conversations.newGroup([bo.inboxId, caro.inboxId], { - name: "The Group Name", - imageUrl: "www.groupImage.com", - description: "The description of the group", - permissionLevel: "admin_only", -}); +const groupWithMeta = await client.conversations.newGroup( + [bo.inboxId, caro.inboxId], + { + name: 'The Group Name', + imageUrl: 'www.groupImage.com', + description: 'The description of the group', + permissionLevel: 'admin_only', + } +); // 4. Send messages const dm = await client.conversations.findOrCreateDm(recipientInboxId); -await dm.send("Hello world"); -await group.send("Hello everyone"); +await dm.send('Hello world'); +await group.send('Hello everyone'); // 5. List, stream, and sync // List existing conversations -await client.conversations.list(["allowed"]); +await client.conversations.list(['allowed']); // Stream new messages await client.conversations.streamAllMessages( async (message: DecodedMessage) => { // Received a message }, - { consentState: ["allowed"] } + { consentState: ['allowed'] } ); // Sync all new welcomes, preference updates, conversations, -// and messages from allowed conversations -await client.conversations.syncAllConversations(["allowed"]); +// and messages from allowed conversations +await client.conversations.syncAllConversations(['allowed']); ``` @@ -5361,15 +5527,15 @@ The EOA signer must have 3 properties: the account type, a function that returns :::code-group ```tsx [Browser] -import type { Signer, Identifier } from "@xmtp/browser-sdk"; +import type { Signer, Identifier } from '@xmtp/browser-sdk'; const accountIdentifier: Identifier = { - identifier: "0x...", // Ethereum address as the identifier - identifierKind: "Ethereum", // Specifies the identity type + identifier: '0x...', // Ethereum address as the identifier + identifierKind: 'Ethereum', // Specifies the identity type }; const signer: Signer = { - type: "EOA", + type: 'EOA', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string @@ -5379,15 +5545,15 @@ const signer: Signer = { ``` ```tsx [Node] -import type { Signer, Identifier, IdentifierKind } from "@xmtp/node-sdk"; +import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const accountIdentifier: Identifier = { - identifier: "0x...", // Ethereum address as the identifier + identifier: '0x...', // Ethereum address as the identifier identifierKind: IdentifierKind.Ethereum, // Specifies the identity type }; const signer: Signer = { - type: "EOA", + type: 'EOA', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string @@ -5401,10 +5567,10 @@ const signer: Signer = { export function convertEOAToSigner(eoaAccount: EOAAccount): Signer { return { getIdentifier: async () => - new PublicIdentity(eoaAccount.address, "ETHEREUM"), + new PublicIdentity(eoaAccount.address, 'ETHEREUM'), getChainId: () => undefined, // Provide a chain ID if available or return undefined getBlockNumber: () => undefined, // Block number is typically not available in Wallet, return undefined - signerType: () => "EOA", // "EOA" indicates an externally owned account + signerType: () => 'EOA', // "EOA" indicates an externally owned account signMessage: async (message: string) => { const signature = await eoaAccount.signMessage(message); @@ -5456,14 +5622,14 @@ The SCW signer has the same 3 required properties as the EOA signer, but also re Here is a list of supported chain IDs: -- chain_rpc_1 = string -- chain_rpc_8453 = string +- chain_rpc_1 = string +- chain_rpc_8453 = string - chain_rpc_42161 = string -- chain_rpc_10 = string -- chain_rpc_137 = string -- chain_rpc_324 = string +- chain_rpc_10 = string +- chain_rpc_137 = string +- chain_rpc_324 = string - chain_rpc_59144 = string -- chain_rpc_480 = string +- chain_rpc_480 = string Need support for a different chain ID? Please post your request to the [XMTP Community Forums](https://community.xmtp.org/c/general/ideas/54). @@ -5511,15 +5677,15 @@ export const createSCWSigner = ( ``` ```tsx [Node] -import type { Signer, Identifier, IdentifierKind } from "@xmtp/node-sdk"; +import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const accountIdentifier: Identifier = { - identifier: "0x...", // Ethereum address as the identifier + identifier: '0x...', // Ethereum address as the identifier identifierKind: IdentifierKind.Ethereum, // Specifies the identity type }; const signer: Signer = { - type: "SCW", + type: 'SCW', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string @@ -5534,10 +5700,10 @@ const signer: Signer = { export function convertSCWToSigner(scwAccount: SCWAccount): Signer { return { getIdentifier: async () => - new PublicIdentity(scwAccount.address, "ETHEREUM"), + new PublicIdentity(scwAccount.address, 'ETHEREUM'), getChainId: () => 8453, // https://chainlist.org/ getBlockNumber: () => undefined, // Optional: will be computed at runtime - signerType: () => "SCW", // "SCW" indicates smart contract wallet account + signerType: () => 'SCW', // "SCW" indicates smart contract wallet account signMessage: async (message: string) => { const byteArray = await scwAccount.signMessage(message); const signature = ethers.utils.hexlify(byteArray); // Convert to hex string @@ -5696,26 +5862,30 @@ To call `Client.create()`, you must pass in a required `signer` and can also pas :::code-group ```tsx [Browser] -import { Client, type Signer } from "@xmtp/browser-sdk"; +import { Client, type Signer } from '@xmtp/browser-sdk'; // create a signer -const signer: Signer = { /* ... */ }; +const signer: Signer = { + /* ... */ +}; const client = await Client.create( signer, // client options { - // Note: dbEncryptionKey is not used for encryption in browser environments - }, + // Note: dbEncryptionKey is not used for encryption in browser environments + } ); ``` ```tsx [Node] -import { Client, type Signer } from "@xmtp/node-sdk"; -import { getRandomValues } from "node:crypto"; +import { Client, type Signer } from '@xmtp/node-sdk'; +import { getRandomValues } from 'node:crypto'; // create a signer -const signer: Signer = { /* ... */ }; +const signer: Signer = { + /* ... */ +}; /** * The database encryption key is optional but strongly recommended for @@ -5733,13 +5903,13 @@ const client = await Client.create( dbEncryptionKey, // Optional: Use a function to dynamically set the database path based on inbox ID // dbPath: (inboxId) => `./databases/xmtp-${inboxId}.db3`, - }, + } ); ``` ```tsx [React Native] Client.create(signer, { - env: "production", // 'local' | 'dev' | 'production' + env: 'production', // 'local' | 'dev' | 'production' dbEncryptionKey: keyBytes, // 32 bytes }); ``` @@ -5776,13 +5946,13 @@ You can configure an XMTP client with these options passed to `Client.create`: :::code-group ```tsx [Browser] -import type { ContentCodec } from "@xmtp/content-type-primitives"; +import type { ContentCodec } from '@xmtp/content-type-primitives'; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ - env?: "local" | "dev" | "production"; + env?: 'local' | 'dev' | 'production'; /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. @@ -5790,13 +5960,13 @@ type ClientOptions = { * You can use the following format: `appVersion: 'APP_NAME/APP_VERSION'`. * * For example: `appVersion: 'alix/2.x'` - * - * If you have an app and an agent, it's best to distinguish them from each other by + * + * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: - * + * * - App: `appVersion: 'alix-app/3.x'` * - Agent: `appVersion: 'alix-agent/2.x'` - * + * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you with app * support, especially around communicating important SDK updates, deprecations, @@ -5854,7 +6024,7 @@ type ClientOptions = { /** * Logging level */ - loggingLevel?: "off" | "error" | "warn" | "info" | "debug" | "trace"; + loggingLevel?: 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; /** * Disable automatic registration when creating a client */ @@ -5864,18 +6034,17 @@ type ClientOptions = { */ disableDeviceSync?: boolean; }; - ``` ```tsx [Node] -import type { ContentCodec } from "@xmtp/content-type-primitives"; -import type { LogLevel } from "@xmtp/node-bindings"; +import type { ContentCodec } from '@xmtp/content-type-primitives'; +import type { LogLevel } from '@xmtp/node-bindings'; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ - env?: "local" | "dev" | "production"; + env?: 'local' | 'dev' | 'production'; /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. @@ -5883,13 +6052,13 @@ type ClientOptions = { * You can use the following format: `appVersion: 'APP_NAME/APP_VERSION'`. * * For example: `appVersion: 'alix/2.x'` - * - * If you have an app and an agent, it's best to distinguish them from each other by + * + * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: - * + * * - App: `appVersion: 'alix-app/3.x'` * - Agent: `appVersion: 'alix-agent/2.x'` - * + * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you with app * support, especially around communicating important SDK updates, deprecations, @@ -5956,13 +6125,13 @@ type ClientOptions = { ``` ```tsx [React Native] -import type { ContentCodec } from "@xmtp/react-native-sdk"; +import type { ContentCodec } from '@xmtp/react-native-sdk'; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ - env: 'local' | 'dev' | 'production' + env: 'local' | 'dev' | 'production'; /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. @@ -5970,13 +6139,13 @@ type ClientOptions = { * You can use the following format: `appVersion: 'APP_NAME/APP_VERSION'`. * * For example: `appVersion: 'alix/2.x'` - * - * If you have an app and an agent, it's best to distinguish them from each other by + * + * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: - * + * * - App: `appVersion: 'alix-app/3.x'` * - Agent: `appVersion: 'alix-agent/2.x'` - * + * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you with app * support, especially around communicating important SDK updates, deprecations, @@ -5986,28 +6155,28 @@ type ClientOptions = { /** * REQUIRED specify the encryption key for the database. The encryption key must be exactly 32 bytes. */ - dbEncryptionKey: Uint8Array + dbEncryptionKey: Uint8Array; /** * Set optional callbacks for handling identity setup */ - preAuthenticateToInboxCallback?: () => Promise | void + preAuthenticateToInboxCallback?: () => Promise | void; /** * OPTIONAL specify the XMTP managed database directory */ - dbDirectory?: string + dbDirectory?: string; /** * OPTIONAL specify a url to sync message history from */ - historySyncUrl?: string + historySyncUrl?: string; /** * OPTIONAL specify a custom local host for testing on physical devices for example `localhost` */ - customLocalHost?: string + customLocalHost?: string; /** * Allow configuring codecs for additional content types */ - codecs?: ContentCodec[] -} + codecs?: ContentCodec[]; +}; ``` ```kotlin [Kotlin] @@ -6038,7 +6207,7 @@ data class ClientOptions( * * For example: `appVersion: 'alix/2.x'` * - * If you have an app and an agent, it's best to distinguish them from each other by + * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: * * - App: `appVersion: 'alix-app/3.x'` @@ -6046,7 +6215,7 @@ data class ClientOptions( * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you - * with app support, especially around communicating important SDK updates, + * with app support, especially around communicating important SDK updates, * deprecations, and required upgrades. */ val appVersion: String? = null, @@ -6072,8 +6241,8 @@ public struct ClientOptions { /// You can use the following format: `appVersion: "APP_NAME/APP_VERSION"`. /// /// For example: `appVersion: 'alix/2.x'` - /// - /// If you have an app and an agent, it's best to distinguish them from each other by + /// + /// If you have an app and an agent, it's best to distinguish them from each other by /// adding `-app` and `-agent` to the names. For example: /// - App: `appVersion: 'alix-app/3.x'` /// - Agent: `appVersion: 'alix-agent/2.x'` @@ -6141,20 +6310,20 @@ For React Native, Android, and iOS SDKs, when building a client with an existing :::code-group ```tsx [Browser] -import { Client, type Identifier } from "@xmtp/browser-sdk"; +import { Client, type Identifier } from '@xmtp/browser-sdk'; const identifier: Identifier = { - identifier: "0x1234567890abcdef1234567890abcdef12345678", - identifierKind: "Ethereum", + identifier: '0x1234567890abcdef1234567890abcdef12345678', + identifierKind: 'Ethereum', }; const client = await Client.build(identifier, options); ``` ```tsx [Node] -import { Client, IdentifierKind, type Identifier } from "@xmtp/node-sdk"; +import { Client, IdentifierKind, type Identifier } from '@xmtp/node-sdk'; const identifier: Identifier = { - identifier: "0x1234567890abcdef1234567890abcdef12345678", + identifier: '0x1234567890abcdef1234567890abcdef12345678', identifierKind: IdentifierKind.Ethereum, }; const client = await Client.build(identifier, options); @@ -6162,7 +6331,7 @@ const client = await Client.build(identifier, options); ```tsx [React Native] Client.build(identity, { - env: "production", // 'local' | 'dev' | 'production' + env: 'production', // 'local' | 'dev' | 'production' dbEncryptionKey: keyBytes, // 32 bytes }); ``` @@ -6292,19 +6461,23 @@ As the app developer, you can provide a UI that enables group participants to ma - Update group metadata
- UI screenshot showing group permission toggle options including Add members and Edit group info settings + UI screenshot showing group permission toggle options including Add members and Edit group info settings
You can use member statuses, options, and permissions to create a custom policy set. The following table represents the valid policy options for each of the permissions: -| Permission | Allow all | Deny all | Admin only | Super admin only | -| --- | --- | --- | --- | --- | -| Add member | ✅ | ✅ | ✅ | ✅ | -| Remove member | ✅ | ✅ | ✅ | ✅ | -| Add admin | ❌ | ✅ | ✅ | ✅ | -| Remove admin | ❌ | ✅ | ✅ | ✅ | -| Update group permissions | ❌ | ❌ | ❌ | ✅ | -| Update group metadata | ✅ | ✅ | ✅ | ✅ | +| Permission | Allow all | Deny all | Admin only | Super admin only | +| ------------------------ | --------- | -------- | ---------- | ---------------- | +| Add member | ✅ | ✅ | ✅ | ✅ | +| Remove member | ✅ | ✅ | ✅ | ✅ | +| Add admin | ❌ | ✅ | ✅ | ✅ | +| Remove admin | ❌ | ✅ | ✅ | ✅ | +| Update group permissions | ❌ | ❌ | ❌ | ✅ | +| Update group metadata | ✅ | ✅ | ✅ | ✅ | If you aren't opinionated and don't set any permissions and options, groups will default to using the delivered `All_Members` policy set, which applies the following permissions and options: @@ -6595,11 +6768,17 @@ try await group.removeMembers(inboxIds: [inboxId]) :::code-group ```js [Browser] -const inboxId = await client.findInboxIdByIdentities([bo.identity, caro.identity]); +const inboxId = await client.findInboxIdByIdentities([ + bo.identity, + caro.identity, +]); ``` ```js [Node] -const inboxId = await client.getInboxIdByIdentities([bo.identity, caro.identity]); +const inboxId = await client.getInboxIdByIdentities([ + bo.identity, + caro.identity, +]); ``` ```tsx [React Native] @@ -6709,28 +6888,31 @@ Once you have the group chat or DM conversation, you can send messages in the co ```tsx [Browser] // For a DM conversation -await dm.send("Hello world"); +await dm.send('Hello world'); // OR for a group chat -await group.send("Hello everyone"); +await group.send('Hello everyone'); ``` ```tsx [Node] // For a DM conversation -await dm.send("Hello world"); +await dm.send('Hello world'); // OR for a group chat -await group.send("Hello everyone"); +await group.send('Hello everyone'); ``` ```tsx [React Native] // For a DM conversation const dm = await client.conversations.findOrCreateDm(recipientInboxId); -await dm.send("Hello world"); +await dm.send('Hello world'); // OR for a group chat -const group = await client.conversations.newGroup([recipientInboxId1, recipientInboxId2]); -await group.send("Hello everyone"); +const group = await client.conversations.newGroup([ + recipientInboxId1, + recipientInboxId2, +]); +await group.send('Hello everyone'); ``` ```kotlin [Kotlin] @@ -6790,35 +6972,45 @@ Send the message to the local database. This ensures that the message will be th ```tsx [Browser] // Optimistically send the message to the local database -conversation.sendOptimistic("Hello world"); +conversation.sendOptimistic('Hello world'); // For custom content types, specify the content type -const customContent = { foo: "bar" }; -const contentType = { authorityId: "example", typeId: "test", versionMajor: 1, versionMinor: 0 }; +const customContent = { foo: 'bar' }; +const contentType = { + authorityId: 'example', + typeId: 'test', + versionMajor: 1, + versionMinor: 0, +}; conversation.sendOptimistic(customContent, contentType); ``` ```tsx [Node] // Optimistically send the message to the local database -conversation.sendOptimistic("Hello world"); +conversation.sendOptimistic('Hello world'); // For custom content types, specify the content type -const customContent = { foo: "bar" }; -const contentType = { authorityId: "example", typeId: "test", versionMajor: 1, versionMinor: 0 }; +const customContent = { foo: 'bar' }; +const contentType = { + authorityId: 'example', + typeId: 'test', + versionMajor: 1, + versionMinor: 0, +}; conversation.sendOptimistic(customContent, contentType); ``` ```tsx [React Native] // Optimistically send the message to the local database -await conversation.prepareMessage("Hello world"); +await conversation.prepareMessage('Hello world'); // For custom content types, specify the content type -const customContent = { foo: "bar" }; +const customContent = { foo: 'bar' }; const contentType = new ContentTypeId({ - authorityId: "example", - typeId: "test", + authorityId: 'example', + typeId: 'test', versionMajor: 1, - versionMinor: 0 + versionMinor: 0, }); await conversation.prepareMessage(customContent, contentType); ``` @@ -6868,12 +7060,12 @@ async function sendMessageWithOptimisticUI(conversation, messageText) { try { // Add message to UI immediately conversation.sendOptimistic(messageText); - + // Actually send the message to the network await conversation.publishMessages(); return true; } catch (error) { - console.error("Failed to send message:", error); + console.error('Failed to send message:', error); return false; } } @@ -6886,12 +7078,12 @@ async function sendMessageWithOptimisticUI(conversation, messageText) { try { // Add message to UI immediately conversation.sendOptimistic(messageText); - + // Actually send the message to the network await conversation.publishMessages(); return true; } catch (error) { - console.error("Failed to send message:", error); + console.error('Failed to send message:', error); return false; } } @@ -6900,16 +7092,19 @@ async function sendMessageWithOptimisticUI(conversation, messageText) { ```tsx [React Native] // Publish all pending optimistically sent messages to the network // Call this only after using prepareMessage to send a message locally -async function sendMessageWithOptimisticUI(conversation: Conversation, messageText: string): Promise { +async function sendMessageWithOptimisticUI( + conversation: Conversation, + messageText: string +): Promise { try { // Add message to UI immediately await conversation.prepareMessage(messageText); - + // Actually send the message to the network await conversation.publishMessages(); return true; } catch (error) { - console.error("Failed to send message:", error); + console.error('Failed to send message:', error); return false; } } @@ -6922,7 +7117,7 @@ suspend fun sendMessageWithOptimisticUI(conversation: Conversation, messageText: return try { // Add message to UI immediately conversation.prepareMessage(messageText) - + // Actually send the message to the network conversation.publishMessages() true @@ -6940,7 +7135,7 @@ func sendMessageWithOptimisticUI(conversation: Conversation, messageText: String do { // Add message to UI immediately try await conversation.prepareMessage(messageText) - + // Actually send the message to the network try await conversation.publishMessages() return true @@ -7068,50 +7263,38 @@ For example: ```js [Browser] // DM -await client.conversations.newDm( - inboxId, - { - messageDisappearingSettings: { - fromNs: 1738620126404999936n, - inNs: 1800000000000000n - } - } -) +await client.conversations.newDm(inboxId, { + messageDisappearingSettings: { + fromNs: 1738620126404999936n, + inNs: 1800000000000000n, + }, +}); // Group -await client.conversations.newGroup( - [inboxId], - { - messageDisappearingSettings: { - fromNs: 1738620126404999936n, - inNs: 1800000000000000n - } - } -) +await client.conversations.newGroup([inboxId], { + messageDisappearingSettings: { + fromNs: 1738620126404999936n, + inNs: 1800000000000000n, + }, +}); ``` ```js [Node] // DM -await client.conversations.newDm( - inboxId, - { - messageDisappearingSettings: { - fromNs: 1738620126404999936, - inNs: 1800000000000000 - } - } -) +await client.conversations.newDm(inboxId, { + messageDisappearingSettings: { + fromNs: 1738620126404999936, + inNs: 1800000000000000, + }, +}); // Group -await client.conversations.newGroup( - [inboxId], - { - messageDisappearingSettings: { - fromNs: 1738620126404999936, - inNs: 1800000000000000 - } - } -) +await client.conversations.newGroup([inboxId], { + messageDisappearingSettings: { + fromNs: 1738620126404999936, + inNs: 1800000000000000, + }, +}); ``` ```tsx [React Native] @@ -7129,7 +7312,7 @@ await client.conversations.newConversation( // Group await client.conversations.newGroup( [inboxId], - { + { disappearingMessageSettings: DisappearingMessageSettings( disappearStartingAtNs: 1738620126404999936, retentionDurationInNs: 1800000000000000 @@ -7188,23 +7371,29 @@ For example: ```tsx [Browser] // Update disappearing message settings -await conversation.updateMessageDisappearingSettings(1738620126404999936n, 1800000000000000n) +await conversation.updateMessageDisappearingSettings( + 1738620126404999936n, + 1800000000000000n +); // Clear disappearing message settings -await conversation.removeMessageDisappearingSettings() +await conversation.removeMessageDisappearingSettings(); ``` ```tsx [Node] // Update disappearing message settings -await conversation.updateMessageDisappearingSettings(1738620126404999936, 1800000000000000) +await conversation.updateMessageDisappearingSettings( + 1738620126404999936, + 1800000000000000 +); // Clear disappearing message settings -await conversation.removeMessageDisappearingSettings() +await conversation.removeMessageDisappearingSettings(); ``` ```tsx [React Native] -await conversation.updateDisappearingMessageSettings(updatedSettings) -await conversation.clearDisappearingMessageSettings() +await conversation.updateDisappearingMessageSettings(updatedSettings); +await conversation.clearDisappearingMessageSettings(); ``` ```kotlin [Kotlin] @@ -7227,22 +7416,22 @@ For example: ```tsx [Browser] // Get the disappearing message settings -const settings = await conversation.messageDisappearingSettings() +const settings = await conversation.messageDisappearingSettings(); // Check if disappearing messages are enabled -const isEnabled = await conversation.isDisappearingMessagesEnabled() +const isEnabled = await conversation.isDisappearingMessagesEnabled(); ``` ```tsx [Node] // Get the disappearing message settings -const settings = conversation.messageDisappearingSettings() +const settings = conversation.messageDisappearingSettings(); -const isEnabled = conversation.isDisappearingMessagesEnabled() +const isEnabled = conversation.isDisappearingMessagesEnabled(); ``` ```tsx [React Native] -conversation.disappearingMessageSettings -conversation.isDisappearingMessagesEnabled() +conversation.disappearingMessageSettings; +conversation.isDisappearingMessagesEnabled(); ``` ```kotlin [Kotlin] @@ -7324,15 +7513,15 @@ try group.groupname() :::code-group ```js [Browser] -await group.updateName("New Group Name"); +await group.updateName('New Group Name'); ``` ```js [Node] -await group.updateName("New Group Name"); +await group.updateName('New Group Name'); ``` ```tsx [React Native] -await group.updateName("New Group Name"); +await group.updateName('New Group Name'); ``` ```kotlin [Kotlin] @@ -7376,15 +7565,15 @@ try group.groupDescription() :::code-group ```js [Browser] -await group.updateDescription("New Group Description"); +await group.updateDescription('New Group Description'); ``` ```js [Node] -await group.updateDescription("New Group Description"); +await group.updateDescription('New Group Description'); ``` ```tsx [React Native] -await group.updateDescription("New Group Description"); +await group.updateDescription('New Group Description'); ``` ```kotlin [Kotlin] @@ -7428,15 +7617,15 @@ try group.imageUrl() :::code-group ```js [Browser] -await group.updateImageUrl("newurl.com"); +await group.updateImageUrl('newurl.com'); ``` ```js [Node] -await group.updateImageUrl("newurl.com"); +await group.updateImageUrl('newurl.com'); ``` ```tsx [React Native] -await group.updateImageUrl("ImageURL"); +await group.updateImageUrl('ImageURL'); ``` ```kotlin [Kotlin] @@ -7460,9 +7649,9 @@ This document provides suggested paths you might take to provide XMTP group chat 1. A group member with permission to add group members [creates an invite link](#create-a-group-invite-link). 2. An non-member clicks the invite link to [request to join the group](#generate-an-invite-landing-page). 3. The non-member is granted access to the group in one of the following ways: - - [Automatic join via silent push notification to the link creator](#automatic-join-via-silent-push-notification-to-link-creator) - - [Automatic join via silent push notification to all group members](#automatic-join-via-silent-push-notification-to-all-group-members) - - [Manual join via push notification to the link creator](#manual-join-via-push-notification-to-link-creator) + - [Automatic join via silent push notification to the link creator](#automatic-join-via-silent-push-notification-to-link-creator) + - [Automatic join via silent push notification to all group members](#automatic-join-via-silent-push-notification-to-all-group-members) + - [Manual join via push notification to the link creator](#manual-join-via-push-notification-to-link-creator) 4. [Add the invitee to the group](#add-the-member-to-the-group) 5. [Check and manage the invite status](#check-the-invite-status) @@ -7472,15 +7661,24 @@ These diagrams help illustrate the sequence of interactions between users and pa #### Option 1: Automatic join via silent push notification to the link creator -Sequence diagram for Automatic join via silent push notification to the link creator +Sequence diagram for Automatic join via silent push notification to the link creator #### Option 2: Automatic join via silent push notification to all group members -Sequence diagram for Automatic join via silent push notification to all group members +Sequence diagram for Automatic join via silent push notification to all group members #### Option 3: Manual join via push notification to the link creator -Sequence diagram for Manual join via push notification to the link creator +Sequence diagram for Manual join via push notification to the link creator ## Create a group invite link @@ -7494,10 +7692,10 @@ Create the group invite by making a `POST` to a `/groupInvite` endpoint with any ```json { - groupName: "XMTP Builders", - groupImage: "https://...", -// Inviter can be inferred from the request auth token. -// Backend doesn't need the actual group ID, since the client can keep track in their DB + "groupName": "XMTP Builders", + "groupImage": "https://..." + // Inviter can be inferred from the request auth token. + // Backend doesn't need the actual group ID, since the client can keep track in their DB } ``` @@ -7509,8 +7707,8 @@ Save the `linkUrl` to the client's local database (alongside the XMTP `group_id` ```json { - id: "abcdefg", - linkUrl: "https://converse.xyz/invite/abcdefg" + "id": "abcdefg", + "linkUrl": "https://converse.xyz/invite/abcdefg" } ``` @@ -7555,7 +7753,7 @@ Can be used by the invitee to request to join the group. ```json { - inviteId: "abcdefg", // User info implied from auth token + "inviteId": "abcdefg" // User info implied from auth token } ``` @@ -7563,8 +7761,8 @@ Can be used by the invitee to request to join the group. ```json { - id: "hijklmn", // The id of the join request - status: "pending" + "id": "hijklmn", // The id of the join request + "status": "pending" } ``` @@ -7696,15 +7894,15 @@ Want XMTP to build this API? [Open an issue](https://github.com/xmtp/libxmtp/iss If the invite link creator approves the request, in the background, you can call LibXMTP to load the group and add the member. -#### Example request +### Example request ```json { - inviteId: "abcdefg", // User info implied from auth token + "inviteId": "abcdefg" // User info implied from auth token } ``` -#### Example response +### Example response ```json { @@ -7751,7 +7949,7 @@ You can have the invite link creator mark an invite as approved or rejected. For ```json { - status: "approved" + "status": "approved" } ``` @@ -7759,9 +7957,9 @@ You can have the invite link creator mark an invite as approved or rejected. For ```json { - id: "hijklmn", - status: "approved", - reason: null // Allow the system to pass a reason back to the client + "id": "hijklmn", + "status": "approved", + "reason": null // Allow the system to pass a reason back to the client } ``` @@ -7787,7 +7985,11 @@ For more agent-specific guidance, see [Manage agent installations](/agents/core- The client creates an inbox ID and installation ID associated with the identity. By default, this identity is designated as the recovery identity. A recovery identity will always have the same inbox ID and cannot be reassigned to a different inbox ID.
- Diagram showing creation of inbox ID and installation ID when a user first creates a client with their identity + Diagram showing creation of inbox ID and installation ID when a user first creates a client with their identity
When you make subsequent calls to create a client for the same identity and a local database is not present, the client uses the same inbox ID, but creates a new installation ID. @@ -7795,13 +7997,21 @@ When you make subsequent calls to create a client for the same identity and a lo An inbox ID can have up to 10 app installations before it needs to [revoke installations](#revoke-installations).
- Diagram showing how creating a client for the same identity without a local database creates a new installation ID with the same inbox ID + Diagram showing how creating a client for the same identity without a local database creates a new installation ID with the same inbox ID
You can enable a user to add multiple identities to their inbox. Added identities use the same inbox ID and the installation ID of the installation used to add the identity.
- Diagram showing how adding multiple identities to an inbox uses the same inbox ID and installation ID + Diagram showing how adding multiple identities to an inbox uses the same inbox ID and installation ID
You can enable a user to remove an identity from their inbox. You cannot remove the recovery identity. @@ -7812,7 +8022,16 @@ You can enable a user to remove an identity from their inbox. You cannot remove This video provides a walkthrough of the idea behind the XMTP installation limit (10) and how to use [static installation revocation](#revoke-installations-for-a-user-who-cant-log-in). After watching, feel free to continue reading for more details. - + An inbox ID is limited to 256 inbox updates. Inbox updates include actions like: @@ -7853,7 +8072,16 @@ Installation IDs that don't have this additional information can be treated as u This video provides a walkthrough of revoking installations, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. - + ### Revoke a specific installation @@ -7870,15 +8098,15 @@ If preferred, you can use the [revoke all other installations](#revoke-all-other :::code-group ```tsx [Browser] -await client.revokeInstallations([installationId1, installationId2]) +await client.revokeInstallations([installationId1, installationId2]); ``` ```tsx [Node] -await client.revokeInstallations([installationId1, installationId2]) +await client.revokeInstallations([installationId1, installationId2]); ``` ```jsx [React Native] -await client.revokeInstallations(signingKey, [installationIds]) +await client.revokeInstallations(signingKey, [installationIds]); ``` ```kotlin [Kotlin] @@ -7898,13 +8126,21 @@ You can revoke all installations other than the currently accessed installation. For example, consider a user using this current installation:
- Diagram showing a user's current installation among multiple installations for their inbox + Diagram showing a user's current installation among multiple installations for their inbox
When the user revokes all other installations, the action removes their identity's access to all installations other than the current installation:
- Diagram showing the result after revoking all other installations, leaving only the current installation active + Diagram showing the result after revoking all other installations, leaving only the current installation active
An inbox ID can have up to 10 app installations before it needs to revoke an installation. @@ -7914,15 +8150,15 @@ If preferred, you can use the [revoke a specific installation](#revoke-a-specifi :::code-group ```tsx [Browser] -await client.revokeAllOtherInstallations() +await client.revokeAllOtherInstallations(); ``` ```tsx [Node] -await client.revokeAllOtherInstallations() +await client.revokeAllOtherInstallations(); ``` ```jsx [React Native] -await client.revokeAllOtherInstallations(signingKey) +await client.revokeAllOtherInstallations(signingKey); ``` ```kotlin [Kotlin] @@ -7972,15 +8208,20 @@ await Client.revokeInstallations( ``` ```tsx [Node] -const inboxStates = await Client.inboxStateFromInboxIds([inboxId], "production"); +const inboxStates = await Client.inboxStateFromInboxIds( + [inboxId], + 'production' +); -const toRevokeInstallationBytes = inboxStates[0].installations.map((i) => i.bytes); +const toRevokeInstallationBytes = inboxStates[0].installations.map( + (i) => i.bytes +); await Client.revokeInstallations( signer, inboxId, toRevokeInstallationBytes, - "production", // optional, defaults to "dev" + 'production' // optional, defaults to "dev" ); ``` @@ -7999,7 +8240,7 @@ await Client.revokeInstallations( ```kotlin [Kotlin] val states = Client.inboxStatesForInboxIds( listOf(inboxId), api) - + val toRevokeIds = states.first().installations.map { it.id } Client.revokeInstallations( @@ -8012,7 +8253,7 @@ await Client.revokeInstallations( ```swift [Swift] let states = try await Client.inboxStatesForInboxIds(inboxIds: [inboxId], api) - + let toRevokeIds = states.first.installations.map { $0.id } try await Client.revokeInstallations( @@ -8032,15 +8273,15 @@ Find an `inboxId` for an identity: :::code-group ```tsx [Browser] -const inboxState = await client.preferences.inboxState() +const inboxState = await client.preferences.inboxState(); ``` ```tsx [Node] -const inboxState = await client.preferences.inboxState() +const inboxState = await client.preferences.inboxState(); ``` ```jsx [React Native] -const inboxId = await client.inboxIdFromIdentity(identity) +const inboxId = await client.inboxIdFromIdentity(identity); ``` ```kotlin [Kotlin] @@ -8061,21 +8302,27 @@ View the state of any inbox to see the identities, installations, and other info ```tsx [Browser] // the second argument is optional and refreshes the state from the network. -const states = await client.preferences.inboxStateFromInboxIds([inboxId, inboxId], true) +const states = await client.preferences.inboxStateFromInboxIds( + [inboxId, inboxId], + true +); ``` ```tsx [Node] // the second argument is optional and refreshes the state from the network. -const states = await client.preferences.inboxStateFromInboxIds([inboxId, inboxId], true) +const states = await client.preferences.inboxStateFromInboxIds( + [inboxId, inboxId], + true +); ``` ```jsx [React Native] -const state = await client.inboxState(true) -const states = await client.inboxStates(true, [inboxId, inboxId]) +const state = await client.inboxState(true); +const states = await client.inboxStates(true, [inboxId, inboxId]); ``` ```kotlin [Kotlin] -val state = client.inboxState(true) +val state = client.inboxState(true) val states = client.inboxStatesForInboxIds(true, listOf(inboxID, inboxID)) ``` @@ -8123,15 +8370,15 @@ This function is delicate and should be used with caution. Adding an identity to :::code-group ```tsx [Browser] -await client.unsafe_addAccount(signer, true) +await client.unsafe_addAccount(signer, true); ``` ```tsx [Node] -await client.unsafe_addAccount(signer, true) +await client.unsafe_addAccount(signer, true); ``` ```jsx [React Native] -await client.addAccount(identityToAdd) +await client.addAccount(identityToAdd); ``` ```kotlin [Kotlin] @@ -8147,21 +8394,21 @@ try await client.addAccount(newAccount: identityToAdd) ## Remove an identity from an inbox :::tip[Note] - A recovery identity cannot be removed. For example, if an inbox has only one associated identity, that identity serves as the recovery identity and cannot be removed. +A recovery identity cannot be removed. For example, if an inbox has only one associated identity, that identity serves as the recovery identity and cannot be removed. ::: :::code-group ```tsx [Browser] -await client.removeAccount(identifier) +await client.removeAccount(identifier); ``` ```tsx [Node] -await client.removeAccount(identifier) +await client.removeAccount(identifier); ``` ```jsx [React Native] -await client.removeAccount(recoveryIdentity, identityToRemove) +await client.removeAccount(recoveryIdentity, identityToRemove); ``` ```kotlin [Kotlin] @@ -8193,19 +8440,31 @@ For UI display purposes, you can use the following logic to select the most appr Consider an inbox with three associated identities:
- Diagram showing an inbox with three associated identities (wallet addresses) + Diagram showing an inbox with three associated identities (wallet addresses)
If the user removes an identity from the inbox, the identity no longer has access to the inbox it was removed from.
- Diagram showing the state after removing one identity from the inbox, leaving two remaining identities + Diagram showing the state after removing one identity from the inbox, leaving two remaining identities
The identity can no longer be added to or used to access conversations in that inbox. If someone sends a message to the identity, the message is not associated with the original inbox. If the user logs in to a new installation with the identity, this will create a new inbox ID.
- Diagram showing how a removed identity creates a new inbox ID when logging into a new installation + Diagram showing how a removed identity creates a new inbox ID when logging into a new installation
### How is the recovery identity used? @@ -8223,13 +8482,21 @@ However, while Bo has access to Alix's inbox, Bo cannot remove Alix from their o If a user logs in with an identity with address 0x62EE...309c and creates inbox 1 and then logs in with an identity with address 0xd0e4...DCe8 and creates inbox 2; there is no way to combine inbox 1 and 2.
- Diagram showing two separate inboxes created by logging in with two different identities + Diagram showing two separate inboxes created by logging in with two different identities
You can add an identity with address 0xd0e4...DCe8 to inbox 1, but both identities with addresses 0x62EE...309c and 0xd0e4...DCe8 would then have access to inbox 1 only. The identity with address 0xd0e4...DCe8 would no longer be able to access inbox 2.
- Diagram showing how adding an identity to inbox 1 removes its access to inbox 2 + Diagram showing how adding an identity to inbox 1 removes its access to inbox 2
To help users avoid this state, ensure that your UX surfaces their ability to add multiple identities to a single inbox. @@ -8251,7 +8518,11 @@ The identity accesses that inbox ID and does not create a new inbox ID. For example, let's say that you create a client with an identity with address 0x62EE...309c. Inbox ID 1 is generated from that identity.
-Simple diagram showing inbox ID 1 with one identity + Simple diagram showing inbox ID 1 with one identity
If you then add an identity with address 0xd0e4...DCe8 to inbox ID 1, the identity is also associated with inbox ID 1. @@ -8261,19 +8532,31 @@ If you then log into a new app installation with the identity with address 0xd0e Once the identity with address 0xd0e4...DCe8 has been associated with inbox ID 1, it can then be used to log into inbox ID 1 using a new app installation.
-Diagram showing inbox ID 1 with two identities after adding a second identity + Diagram showing inbox ID 1 with two identities after adding a second identity
The inverse is also true. Let's say an identity with address 0xd0e4...DCe8 was previously used to create and log into inbox ID 2.
-Diagram showing two separate inboxes, each with their own identity, before identity migration + Diagram showing two separate inboxes, each with their own identity, before identity migration
If the identity is then added as an associated identity to inbox ID 1, the identity will no longer be able to log into inbox ID 2.
-Diagram showing the result after moving an identity from inbox 2 to inbox 1, leaving inbox 2 empty + Diagram showing the result after moving an identity from inbox 2 to inbox 1, leaving inbox 2 empty
To enable the user of the identity with address 0xd0e4...DCe8 to log into inbox ID 2 again, you can use the recovery identity for inbox ID 2 to add a different identity to inbox ID 2 and have the user use that identity access it. @@ -8392,10 +8675,10 @@ const group = await alix.conversations.newGroup([bo.inboxId, caro.inboxId]); // New Group with Metadata const group = await alix.conversations.newGroup([bo.inboxId, caro.inboxId], { - name: "The Group Name", - imageUrl: "www.groupImage.com", - description: "The description of the group", - permissionLevel: "admin_only", // 'all_members' | 'admin_only' + name: 'The Group Name', + imageUrl: 'www.groupImage.com', + description: 'The description of the group', + permissionLevel: 'admin_only', // 'all_members' | 'admin_only' }); ``` @@ -8452,7 +8735,7 @@ To learn more about optimistically sending messages using `prepareMessage()` and const optimisticGroup = await alixClient.conversations.newGroupOptimistic(); // send optimistic message (stays local) -await optimisticGroup.sendOptimistic("gm"); +await optimisticGroup.sendOptimistic('gm'); // later, sync the group by adding members await optimisticGroup.addMembers([boClient.inboxId]); @@ -8465,7 +8748,7 @@ await optimisticGroup.publishMessages(); const optimisticGroup = client.conversations.newGroupOptimistic(); // send optimistic message (stays local) -optimisticGroup.sendOptimistic("gm"); +optimisticGroup.sendOptimistic('gm'); // later, sync the group by adding members await optimisticGroup.addMembers([boClient.inboxId]); @@ -8477,7 +8760,7 @@ await optimisticGroup.publishMessages(); const optimisticGroup = await boClient.conversations.newGroupOptimistic(); // Prepare a message (stays local) -await optimisticGroup.prepareMessage("Hello group!"); +await optimisticGroup.prepareMessage('Hello group!'); // Later, add members and sync await optimisticGroup.addMembers([alixClient.inboxId]); // also syncs group to the network @@ -8552,9 +8835,8 @@ Use these helper methods to quickly locate and access specific conversations—w ```js [Browser] // get a conversation by its ID -const conversationById = await client.conversations.getConversationById( - conversationId -); +const conversationById = + await client.conversations.getConversationById(conversationId); // get a message by its ID const messageById = await client.conversations.getMessageById(messageId); @@ -8565,9 +8847,8 @@ const dmByInboxId = await client.conversations.getDmByInboxId(peerInboxId); ```js [Node] // get a conversation by its ID -const conversationById = await client.conversations.getConversationById( - conversationId -); +const conversationById = + await client.conversations.getConversationById(conversationId); // get a message by its ID const messageById = await client.conversations.getMessageById(messageId); @@ -8634,23 +8915,23 @@ Build with XMTP to: - **Deliver secure and private messaging** - Using the [Messaging Layer Security](/protocol/security) (MLS) standard, a ratified [IETF](https://www.ietf.org/about/introduction/) standard, XMTP provides end-to-end encrypted messaging with forward secrecy and post-compromise security. + Using the [Messaging Layer Security](/protocol/security) (MLS) standard, a ratified [IETF](https://www.ietf.org/about/introduction/) standard, XMTP provides end-to-end encrypted messaging with forward secrecy and post-compromise security. - **Provide spam-free chats** - In any open and permissionless messaging ecosystem, spam is an inevitable reality, and XMTP is no exception. However, with XMTP [user consent preferences](/chat-apps/user-consent/user-consent), developers can give their users spam-free chats displaying conversations with chosen contacts only. + In any open and permissionless messaging ecosystem, spam is an inevitable reality, and XMTP is no exception. However, with XMTP [user consent preferences](/chat-apps/user-consent/user-consent), developers can give their users spam-free chats displaying conversations with chosen contacts only. - **Build on native crypto rails** - Build with XMTP to tap into the capabilities of crypto and web3. Support decentralized identities, crypto transactions, and more, directly in a messaging experience. + Build with XMTP to tap into the capabilities of crypto and web3. Support decentralized identities, crypto transactions, and more, directly in a messaging experience. - **Empower users to own and control their communications** - With apps built with XMTP, users own their conversations, data, and identity. Combined with the interoperability that comes with protocols, this means users can access their end-to-end encrypted communications using any app built with XMTP. + With apps built with XMTP, users own their conversations, data, and identity. Combined with the interoperability that comes with protocols, this means users can access their end-to-end encrypted communications using any app built with XMTP. - **Create with confidence** - Developers are free to create the messaging experiences their users want—on a censorship-resistant protocol architected to last forever. Because XMTP isn't a closed proprietary platform, developers can build confidently, knowing their access and functionality can't be revoked by a central authority. + Developers are free to create the messaging experiences their users want—on a censorship-resistant protocol architected to last forever. Because XMTP isn't a closed proprietary platform, developers can build confidently, knowing their access and functionality can't be revoked by a central authority. ## Try an app built with XMTP @@ -8661,13 +8942,11 @@ Try [xmtp.chat](https://xmtp.chat/), an app made for devs to learn to build with ## Join the XMTP community - **XMTP builds in the open** - - Explore the documentation on this site - Explore the open [XMTP GitHub org](https://github.com/xmtp), which contains code for LibXMTP, XMTP SDKs, and xmtpd, node software powering the XMTP testnet. - Explore the open source code for [xmtp.chat](https://github.com/xmtp/xmtp-js/tree/main/apps/xmtp.chat), an app made for devs to learn to build with XMTP—using an app built with XMTP. - **XMTP is for everyone** - - [Join the conversation](https://community.xmtp.org/) and become part of the movement to redefine digital communications. @@ -8756,7 +9035,16 @@ The XMTP SDK currently requires you to use [ethers](https://ethers.org/) or anot ### What is the invalid key package error? - + ### Where can I get official XMTP brand assets? @@ -8805,7 +9093,7 @@ This retention policy would represent a minimum retention period, not a maximum. For example, a retention policy may look something like the following, though specifics are subject to change: - One year for messages -- Indefinite storage for account information and personal preferences +- Indefinite storage for account information and personal preferences The team is researching a way to provide this indefinite storage and have it scale forever. @@ -9043,19 +9331,17 @@ After importing the package, you can register the codec. :::code-group ```jsx [Browser] -import { - ReactionCodec, -} from "@xmtp/content-type-reaction"; +import { ReactionCodec } from '@xmtp/content-type-reaction'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new ReactionCodec()], }); ``` ```jsx [React Native] const client = await Client.create(signer, { - env: "production", + env: 'production', codecs: [new ReactionCodec()], }); ``` @@ -9089,8 +9375,8 @@ With XMTP, reactions are represented as objects with the following keys: ```tsx [Browser] const reaction = { reference: someMessageID, - action: "added", - content: "smile", + action: 'added', + content: 'smile', }; await conversation.send(reaction, { @@ -9103,9 +9389,9 @@ await conversation.send(reaction, { const reactionContent = { reaction: { reference: messageId, // ID of the message you're reacting to - action: "added", // Action can be 'added' or 'removed' - schema: "unicode", // Schema can be 'unicode', 'shortcode', or 'custom' - content: "👍", // Content of the reaction + action: 'added', // Action can be 'added' or 'removed' + schema: 'unicode', // Schema can be 'unicode', 'shortcode', or 'custom' + content: '👍', // Content of the reaction }, }; @@ -9162,7 +9448,7 @@ if (message.contentType.sameAs(ContentTypeReaction)) { ``` ```jsx [React Native] -if (message.contentTypeId === "xmtp.org/reaction:1.0") { +if (message.contentTypeId === 'xmtp.org/reaction:1.0') { const reaction = message.content(); return reaction; //reaction.reference = id of the message being reacted to, @@ -9233,12 +9519,10 @@ pnpm i @xmtp/content-type-wallet-send-calls After importing the package, you can register the codec. ```js [Browser] -import { - WalletSendCallsCodec, -} from "@xmtp/content-type-wallet-send-calls"; +import { WalletSendCallsCodec } from '@xmtp/content-type-wallet-send-calls'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new WalletSendCallsCodec()], }); ``` @@ -9249,33 +9533,33 @@ With XMTP, a transaction request is represented using `wallet_sendCalls` with ad ```ts [TypeScript] const walletSendCalls: WalletSendCallsParams = { - version: "1.0", - from: "0x123...abc", - chainId: "0x2105", + version: '1.0', + from: '0x123...abc', + chainId: '0x2105', calls: [ { - to: "0x456...def", - value: "0x5AF3107A4000", + to: '0x456...def', + value: '0x5AF3107A4000', metadata: { - description: "Send 0.0001 ETH on base to 0x456...def", - transactionType: "transfer", - currency: "ETH", + description: 'Send 0.0001 ETH on base to 0x456...def', + transactionType: 'transfer', + currency: 'ETH', amount: 100000000000000, decimals: 18, - toAddress: "0x456...def", + toAddress: '0x456...def', }, }, { - to: "0x789...cba", - data: "0xdead...beef", + to: '0x789...cba', + data: '0xdead...beef', metadata: { - description: "Lend 10 USDC on base with Morpho @ 8.5% APY", - transactionType: "lend", - currency: "USDC", + description: 'Lend 10 USDC on base with Morpho @ 8.5% APY', + transactionType: 'lend', + currency: 'USDC', amount: 10000000, decimals: 6, - platform: "morpho", - apy: "8.5", + platform: 'morpho', + apy: '8.5', }, }, ], @@ -9314,6 +9598,7 @@ const walletSendCalls: WalletSendCallsParams = message.content; --- description: Learn how to build custom content types --- + # Build custom content types Any developer building with XMTP can create a custom content type and immediately start using it in their app. Unlike a standard content type, use of a custom content type doesn't require prerequisite formal adoption through the XRC and XIP processes. @@ -9373,19 +9658,17 @@ pnpm add @xmtp/content-type-read-receipt :::code-group ```js [Browser] -import { - ReadReceiptCodec, -} from "@xmtp/content-type-read-receipt"; +import { ReadReceiptCodec } from '@xmtp/content-type-read-receipt'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new ReadReceiptCodec()], }); ``` ```js [React Native] const client = await Client.create(signer, { - env: "production", + env: 'production', codecs: [new ReadReceiptCodec()], }); ``` @@ -9458,7 +9741,7 @@ if (message.contentType.sameAs(ContentTypeReadReceipt)) { ``` ```js [React Native] -if (message.contentTypeId === "xmtp.org/readReceipt:1.0") { +if (message.contentTypeId === 'xmtp.org/readReceipt:1.0') { return message.sent; //Date received } ``` @@ -9502,6 +9785,7 @@ You can use a read receipt timestamp to calculate the time since the last messag --- description: Learn how to implement an onchain transaction reference content type --- + # Support onchain transaction references in your app built with XMTP This package provides an XMTP content type to support onchain transaction references. It is a reference to an onchain transaction sent as a message. This content type facilitates sharing transaction hashes or IDs, thereby providing a direct link to onchain activities. Transaction references serve to display transaction details, facilitating the sharing of onchain activities, such as token transfers, between users. @@ -9540,10 +9824,10 @@ After importing the package, you can register the codec. import { ContentTypeTransactionReference, TransactionReferenceCodec, -} from "@xmtp/content-type-transaction-reference"; +} from '@xmtp/content-type-transaction-reference'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new TransactionReferenceCodec()], }); ``` @@ -9725,6 +10009,7 @@ Displaying a transaction reference typically involves rendering details such as --- description: Learn how to use the remote attachment, multiple remote attachment, or attachment content types to support attachments in your app built with XMTP --- + # Support attachments in your app built with XMTP Use the remote attachment, multiple remote attachments, or attachment content type to support attachments in your app. @@ -9771,17 +10056,17 @@ After importing the package, you can register the codec. import { AttachmentCodec, RemoteAttachmentCodec, -} from "@xmtp/content-type-remote-attachment"; +} from '@xmtp/content-type-remote-attachment'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], }); ``` ```jsx [React Native] const client = await Client.create(signer, { - env: "production", + env: 'production', codecs: [new RemoteAttachmentCodec(), new StaticAttachmentCodec()], }); ``` @@ -9817,7 +10102,7 @@ const data = await new Promise((resolve, reject) => { if (reader.result instanceof ArrayBuffer) { resolve(reader.result); } else { - reject(new Error("Not an ArrayBuffer")); + reject(new Error('Not an ArrayBuffer')); } }; reader.readAsArrayBuffer(image); @@ -9855,7 +10140,7 @@ const remoteAttachment = { salt: encryptedEncoded.salt, nonce: encryptedEncoded.nonce, secret: encryptedEncoded.secret, - scheme: "https://", + scheme: 'https://', filename: attachment.filename, contentLength: attachment.data.byteLength, }; @@ -9878,7 +10163,7 @@ This method takes a `DecryptedLocalAttachment` object as an argument: ```jsx const { encryptedLocalFileUri, metadata } = await alice.encryptAttachment({ fileUri: `file://${file}`, - mimeType: "text/plain", + mimeType: 'text/plain', }); ``` @@ -9894,7 +10179,7 @@ Send a remote attachment message: await convo.send({ remoteAttachment: { ...metadata, - scheme: "https://", + scheme: 'https://', url, }, }); @@ -10021,7 +10306,7 @@ Now that you can send a remote attachment, you need a way to receive it. For exa
```tsx -import { ContentTypeRemoteAttachment } from "@xmtp/content-type-remote-attachment"; +import { ContentTypeRemoteAttachment } from '@xmtp/content-type-remote-attachment'; if (message.contentType.sameAs(RemoteAttachmentContentType)) { const attachment = await RemoteAttachmentCodec.load(message.content, client); @@ -10045,7 +10330,7 @@ const objectURL = URL.createObjectURL( }) ); -const img = document.createElement("img"); +const img = document.createElement('img'); img.src = objectURL; img.title = attachment.filename; ``` @@ -10144,7 +10429,7 @@ Client.register(codec = MultiRemoteAttachmentCodec()) ```swift [Swift] Client.register(codec: AttachmentCodec()) -Client.register(codec: RemoteAttachmentCodec()) +Client.register(codec: RemoteAttachmentCodec()) Client.register(codec: MultiRemoteAttachmentCodec()) ``` @@ -10158,16 +10443,16 @@ Each attachment in the attachments array contains a URL that points to an encryp ```ts [React Native] const attachment1: DecryptedLocalAttachment = { - fileUri: "content://media/external/images/media/image-1.png", - mimeType: "image/png", - filename: "image-1.png" -} + fileUri: 'content://media/external/images/media/image-1.png', + mimeType: 'image/png', + filename: 'image-1.png', +}; const attachment2: DecryptedLocalAttachment = { - fileUri: "content://media/external/images/media/image-2.png", - mimeType: "image/png", - filename: "image-2.png" -} + fileUri: 'content://media/external/images/media/image-2.png', + mimeType: 'image/png', + filename: 'image-2.png', +}; ``` ```kotlin [Kotlin] @@ -10187,7 +10472,7 @@ val attachment2 = Attachment( ```swift [Swift] let attachment1 = Attachment( filename: "test1.txt", - mimeType: "text/plain", + mimeType: "text/plain", data: Data("hello world".utf8) ) @@ -10205,20 +10490,21 @@ let attachment2 = Attachment( :::code-group ```ts [React Native] -const remoteAttachments: RemoteAttachmentInfo[] = [] - for (const attachment of [attachment1, attachment2]) { - // Encrypt the attachment and receive the local URI of the encrypted file - const { encryptedLocalFileUri, metadata } = await alix.encryptAttachment(attachment) - - // Upload the attachment to a remote server and receive the URL - // (Integrator must supply upload from local uri and return url functionality!) - const url = uploadAttachmentForUrl(encryptedLocalFileUri) - - // Build the remote attachment info - const remoteAttachmentInfo = - MultiRemoteAttachmentCodec.buildMultiRemoteAttachmentInfo(url, metadata) - remoteAttachments.push(remoteAttachmentInfo) - } +const remoteAttachments: RemoteAttachmentInfo[] = []; +for (const attachment of [attachment1, attachment2]) { + // Encrypt the attachment and receive the local URI of the encrypted file + const { encryptedLocalFileUri, metadata } = + await alix.encryptAttachment(attachment); + + // Upload the attachment to a remote server and receive the URL + // (Integrator must supply upload from local uri and return url functionality!) + const url = uploadAttachmentForUrl(encryptedLocalFileUri); + + // Build the remote attachment info + const remoteAttachmentInfo = + MultiRemoteAttachmentCodec.buildMultiRemoteAttachmentInfo(url, metadata); + remoteAttachments.push(remoteAttachmentInfo); +} ``` ```kotlin [Kotlin] @@ -10267,10 +10553,10 @@ for att in [attachment1, attachment2] { ```ts [React Native] await convo.send({ - multiRemoteAttachment: { - attachments: remoteAttachments, - }, - }) + multiRemoteAttachment: { + attachments: remoteAttachments, + }, +}); ``` ```kotlin [Kotlin] @@ -10297,12 +10583,15 @@ try await alixConversation.send(encodedContent: encodedContent) :::code-group ```ts [React Native] -const messages = await conversation.messages() -if (messages.size > 0 && messages[0].contentTypeId == 'xmtp.org/multiRemoteStaticContent:1.0') { +const messages = await conversation.messages(); +if ( + messages.size > 0 && + messages[0].contentTypeId == 'xmtp.org/multiRemoteStaticContent:1.0' +) { // Decode the raw content back into a MultiRemoteAttachment - const multiRemoteAttachment: MultiRemoteAttachment = messages[0].content() + const multiRemoteAttachment: MultiRemoteAttachment = messages[0].content(); - // See next section for download, and decrypt the attachments + // See next section for download, and decrypt the attachments } ``` @@ -10333,27 +10622,27 @@ if messages.size > 0 && messages[0].encodedContent.type.id.equals(ContentTypeMul :::code-group ```ts [React Native] -const decryptedAttachments: DecryptedLocalAttachment[] = [] +const decryptedAttachments: DecryptedLocalAttachment[] = []; for (const attachment of multiRemoteAttachment.attachments) { - // Downloading the encrypted payload from the attachment URL and save the local file - // (Integrator must supply download from url and save to local Uri functionality!) - const encryptedFileLocalURIAfterDownload: string = downloadFromUrl( - attachment.url - ) - // Decrypt the local file - const decryptedLocalAttachment = await alix.decryptAttachment({ - encryptedLocalFileUri: encryptedFileLocalURIAfterDownload, - metadata: { - secret: attachment.secret, - salt: attachment.salt, - nonce: attachment.nonce, - contentDigest: attachment.contentDigest, - filename: attachment.filename, - } as RemoteAttachmentContent, - }) - decryptedAttachments.push(decryptedLocalAttachment) - } + // Downloading the encrypted payload from the attachment URL and save the local file + // (Integrator must supply download from url and save to local Uri functionality!) + const encryptedFileLocalURIAfterDownload: string = downloadFromUrl( + attachment.url + ); + // Decrypt the local file + const decryptedLocalAttachment = await alix.decryptAttachment({ + encryptedLocalFileUri: encryptedFileLocalURIAfterDownload, + metadata: { + secret: attachment.secret, + salt: attachment.salt, + nonce: attachment.nonce, + contentDigest: attachment.contentDigest, + filename: attachment.filename, + } as RemoteAttachmentContent, + }); + decryptedAttachments.push(decryptedLocalAttachment); +} ``` ```kotlin [Kotlin] @@ -10377,10 +10666,10 @@ for (remoteAttachmentInfo: FfiRemoteAttachmentInfo in multiRemoteAttachment.atta // Combine encrypted payload with RemoteAttachment to create an EncryptedEncodedContent Object val encryptedAttachment: EncryptedEncodedContent = MultiRemoteAttachmentCodec.buildEncryptAttachmentResult(remoteAttachment, encryptedPayload) - + // Decrypt payload val encodedContent: EncodedContent = MultiRemoteAttachmentCodec.decryptAttachment(encryptedAttachment) - + // Convert EncodedContent to Attachment val attachment = attachmentCodec.decode(encodedContent) decryptedAttachments.add(attachment) @@ -10487,19 +10776,17 @@ pnpm add @xmtp/content-type-remote-attachment :::code-group ```jsx [Browser] -import { - AttachmentCodec, -} from "@xmtp/content-type-remote-attachment"; +import { AttachmentCodec } from '@xmtp/content-type-remote-attachment'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new AttachmentCodec()], }); ``` ```jsx [React Native] const client = await Client.create(signer, { - env: "production", + env: 'production', codecs: [new StaticAttachmentCodec()], }); ``` @@ -10522,9 +10809,9 @@ Client.register(AttachmentCodec()); ```tsx // Read local file and extract its details -const file = fs.readFileSync("xmtp.png"); -const filename = path.basename("xmtp.png"); -const extname = path.extname("xmtp.png"); +const file = fs.readFileSync('xmtp.png'); +const filename = path.basename('xmtp.png'); +const extname = path.extname('xmtp.png'); console.log(`Filename: ${filename}`); console.log(`File Type: ${extname}`); ``` @@ -10542,7 +10829,7 @@ const attachment = { data: imgArray, }; -console.log("Attachment created", attachment); +console.log('Attachment created', attachment); await conversation.send(attachment, { contentType: ContentTypeAttachment }); ``` @@ -10658,7 +10945,7 @@ Here is the current standard content type: An app built with XMTP uses the `TextCodec` (plain text) standard content type by default. This means that if your app is sending plain text messages only, you don't need to perform any additional steps related to content types. ```jsx [Node] -await conversation.send("gm"); +await conversation.send('gm'); ``` ## Standards-track content types @@ -10717,17 +11004,17 @@ After importing the package, you can register the codec. :::code-group ```js [Browser] -import { ReplyCodec } from "@xmtp/content-type-reply"; +import { ReplyCodec } from '@xmtp/content-type-reply'; // Create the XMTP client const xmtp = await Client.create(signer, { - env: "dev", + env: 'dev', codecs: [new ReplyCodec()], }); ``` ```js [React Native] const client = await Client.create(signer, { - env: "production", + env: 'production', codecs: [new ReplyCodec()], }); ``` @@ -10755,14 +11042,14 @@ Once you've created a reply, you can send it. Replies are represented as objects :::code-group ```ts [Browser] -import { ContentTypeText } from "@xmtp/content-type-text"; -import { ContentTypeReply } from "@xmtp/content-type-reply"; -import type { Reply } from "@xmtp/content-type-reply"; +import { ContentTypeText } from '@xmtp/content-type-text'; +import { ContentTypeReply } from '@xmtp/content-type-reply'; +import type { Reply } from '@xmtp/content-type-reply'; const reply: Reply = { reference: someMessageID, contentType: ContentTypeText, - content: "I concur", + content: 'I concur', }; await conversation.send(reply, { @@ -10776,7 +11063,7 @@ const replyContent = { reply: { reference: messageId, // ID of the message you're replying to content: { - text: "This is a reply", // Content of the reply + text: 'This is a reply', // Content of the reply }, }, }; @@ -10876,7 +11163,16 @@ However, with XMTP, you can give your users chats that are **spam-free spaces fo This video provides a walkthrough of consent, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. - + ## How user consent preferences work @@ -10891,7 +11187,6 @@ For example: 1. `alix.id` starts a conversation with `bo.id`. At this time, `alix.id` is unknown to `bo.id` and the conversation displays in a message requests UI. 2. When `bo.id` views the message request, they express their user consent preference to **Block** or **Accept** `alix.id` as a contact. - - If `bo.id` accepts `alix.id` as a contact, their conversation displays in `bo.id`'s main inbox. Because only contacts `bo.id` accepts display in their main inbox, their inbox remains spam-free. - If `bo.id` blocks contact with `alix.id`, remove the conversation from `bo.id`'s view. In an appropriate location in your app, give the user the option to unblock the contact. @@ -10969,7 +11264,7 @@ Check the current consent state of a specific conversation: :::code-group ```js [Browser] -import { ConsentEntityType } from "@xmtp/browser-sdk"; +import { ConsentEntityType } from '@xmtp/browser-sdk'; // get consent state from the client const conversationConsentState = await client.getConsentState( @@ -10978,14 +11273,13 @@ const conversationConsentState = await client.getConsentState( ); // or get consent state directly from a conversation -const groupConversation = await client.conversations.findConversationById( - groupId -); +const groupConversation = + await client.conversations.findConversationById(groupId); const groupConversationConsentState = await groupConversation.consentState(); ``` ```js [Node] -import { ConsentEntityType } from "@xmtp/node-sdk"; +import { ConsentEntityType } from '@xmtp/node-sdk'; // get consent state from the client const conversationConsentState = await client.getConsentState( @@ -10994,9 +11288,8 @@ const conversationConsentState = await client.getConsentState( ); // or get consent state directly from a conversation -const groupConversation = await client.conversations.findConversationById( - groupId -); +const groupConversation = + await client.conversations.findConversationById(groupId); const groupConversationConsentState = await groupConversation.consentState(); ``` @@ -11021,7 +11314,7 @@ Update the consent state of a conversation to allow or deny messages: :::code-group ```js [Browser] -import { ConsentEntityType, ConsentState } from "@xmtp/browser-sdk"; +import { ConsentEntityType, ConsentState } from '@xmtp/browser-sdk'; // set consent state from the client (can set multiple states at once) await client.setConsentStates([ @@ -11033,14 +11326,13 @@ await client.setConsentStates([ ]); // set consent state directly on a conversation -const groupConversation = await client.conversations.findConversationById( - groupId -); +const groupConversation = + await client.conversations.findConversationById(groupId); await groupConversation.updateConsentState(ConsentState.Allowed); ``` ```js [Node] -import { ConsentEntityType, ConsentState } from "@xmtp/node-sdk"; +import { ConsentEntityType, ConsentState } from '@xmtp/node-sdk'; // set consent state from the client (can set multiple states at once) await client.setConsentStates([ @@ -11052,14 +11344,13 @@ await client.setConsentStates([ ]); // set consent state directly on a conversation -const groupConversation = await client.conversations.findConversationById( - groupId -); +const groupConversation = + await client.conversations.findConversationById(groupId); await groupConversation.updateConsentState(ConsentState.Allowed); ``` ```tsx [React Native] -await conversation.updateConsent("allowed"); // 'allowed' | 'denied' +await conversation.updateConsent('allowed'); // 'allowed' | 'denied' ``` ```kotlin [Kotlin] @@ -11083,21 +11374,21 @@ Listen for real-time updates to consent preferences: const stream = await client.preferences.streamConsent({ onValue: (updates) => { // Received consent updates - console.log("Consent updates:", updates); + console.log('Consent updates:', updates); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Consent stream failed"); - } + console.log('Consent stream failed'); + }, }); // Or use for-await loop for await (const updates of stream) { // Received consent updates - console.log("Consent updates:", updates); + console.log('Consent updates:', updates); } ``` @@ -11106,26 +11397,26 @@ for await (const updates of stream) { const stream = await client.preferences.streamConsent({ onValue: (updates) => { // Received consent updates - console.log("Consent updates:", updates); + console.log('Consent updates:', updates); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { - console.log("Consent stream failed"); - } + console.log('Consent stream failed'); + }, }); // Or use for-await loop for await (const updates of stream) { // Received consent updates - console.log("Consent updates:", updates); + console.log('Consent updates:', updates); } ``` ```tsx [React Native] -await client.preferences.streamConsent() +await client.preferences.streamConsent(); ``` ```kotlin [Kotlin] @@ -11153,7 +11444,7 @@ You may want to enable users to deny or allow a users on an individual basis. Yo :::code-group ```js [Browser] -import { ConsentEntityType, ConsentState } from "@xmtp/browser-sdk"; +import { ConsentEntityType, ConsentState } from '@xmtp/browser-sdk'; await client.setConsentStates([ { @@ -11165,7 +11456,7 @@ await client.setConsentStates([ ``` ```js [Node] -import { ConsentEntityType, ConsentState } from "@xmtp/node-sdk"; +import { ConsentEntityType, ConsentState } from '@xmtp/node-sdk'; // set consent state from the client (can set multiple states at once) await client.setConsentStates([ @@ -11180,7 +11471,7 @@ await client.setConsentStates([ ```tsx [React Native] await client.preferences.setConsentState( new ConsentRecord(inboxId, 'inbox_id', 'denied') -) +); ``` ```kotlin [Kotlin] @@ -11199,7 +11490,7 @@ client.preferences.setConsentState( try await client.preferences.setConsentState( entries: [ ConsentRecord( - value: inboxID, + value: inboxID, entryType: .inbox_id, consentType: .denied) ]) @@ -11218,7 +11509,7 @@ You may want to enable users to deny or allow a users on an individual basis. Yo :::code-group ```js [Browser] -import { ConsentEntityType } from "@xmtp/browser-sdk"; +import { ConsentEntityType } from '@xmtp/browser-sdk'; const inboxConsentState = await client.getConsentState( ConsentEntityType.InboxId, @@ -11227,7 +11518,7 @@ const inboxConsentState = await client.getConsentState( ``` ```js [Node] -import { ConsentEntityType } from "@xmtp/node-sdk"; +import { ConsentEntityType } from '@xmtp/node-sdk'; const inboxConsentState = await client.getConsentState( ConsentEntityType.InboxId, @@ -11237,12 +11528,12 @@ const inboxConsentState = await client.getConsentState( ```tsx [React Native] // Get consent directly on the member -const memberConsentStates = (await group.members()).map( - (member) => member.consentState() -) +const memberConsentStates = (await group.members()).map((member) => + member.consentState() +); // Get consent from the inboxId -const inboxConsentState = await client.preferences.inboxIdConsentState(inboxId) +const inboxConsentState = await client.preferences.inboxIdConsentState(inboxId); ``` ```kotlin [Kotlin] @@ -11268,8 +11559,8 @@ let inboxConsentState = try await client.preferences.inboxIdState(inboxId: inbox Get the inbox ID of the individual who added you to a group or created the group to check the consent state for it: ```tsx [React Native] -group.addedByInboxId -await group.creatorInboxId() +group.addedByInboxId; +await group.creatorInboxId(); ``` ```kotlin [Kotlin] @@ -11320,7 +11611,11 @@ val ethAddresses = identities You can then display appropriate messages on a **You might know** tab, for example.
-Screenshot of a mobile chat app showing a 'You might know' tab with filtered conversation requests + Screenshot of a mobile chat app showing a 'You might know' tab with filtered conversation requests
### Identify contacts the user might not know, including spammy or scammy requests @@ -11328,7 +11623,11 @@ You can then display appropriate messages on a **You might know** tab, for examp To identify contacts the user might not know or not want to know, which might include spam, you can consciously decide to scan messages in an unencrypted state to find messages that might contain spammy or scammy content. You can also look for an absence of onchain interaction data between the addresses, which might indicate that there is no affinity between addresses. You can then filter the appropriate messages to display on a **Hidden requests** tab, for example.
-Screenshot of a mobile chat app showing a 'Hidden requests' tab with filtered messages from unknown contacts or potentially spammy content + Screenshot of a mobile chat app showing a 'Hidden requests' tab with filtered messages from unknown contacts or potentially spammy content
The decision to scan unencrypted messages is yours as the app developer. If you take this approach: