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.
Building a custom content type enables you to manage data in a way that's more personalized or specialized to the needs of your app.
For example, if you need a content type that isn't covered by a standard or standards-track content type, you can create a custom content type and begin using it immediately in your app.
Fallback plain text is "alt text"-like description text that you can associate with a custom content type if you are concerned that a receiving app might not be able to handle the content. If the receiving app is unable to handle the custom content, it displays the fallback plain text instead.
If another app wants to display your custom content type, they must implement your custom content type in their code exactly as it's defined in your code.
For more common content types, you can usually find a standard or standards-track content type to serve your needs.
If your custom content type generates interest within the developer community, consider proposing it as a standard content type through the XIP process.
This document describes how to build custom content types using two examples:
- Build a basic custom content type that multiplies numbers
- Build an advanced custom content type that sends a transaction hash
Basic: Build a custom content type that multiplies numbers
Create the content type
Create the custom content type by creating a new file
import { content } from '@xmtp/proto'
import {JSContentCodec, Client, Conversation, DecodedMessage } from '@xmtp/react-native-sdk';
type EncodedContent = content.EncodedContent
type ContentTypeId = content.ContentTypeId
const ContentTypeMultiplyNumbers: ContentTypeId = {
authorityId: 'com.example',
typeId: 'multiplyNumbers',
versionMajor: 1,
versionMinor: 1,
}
class MultiplyNumbers {
public readonly num1: number
public readonly num2: number
public readonly result: number
constructor(num1: number, num2: number, result: number) {
this.num1 = num1
this.num2 = num2
this.result = result
}
}
class ContentTypeMultiplyNumberCodec
implements JSContentCodec<MultiplyNumbers>
{
get contentType() {
return ContentTypeMultiplyNumbers
}
encode(decoded: MultiplyNumbers): EncodedContent {
return {
type: ContentTypeMultiplyNumbers,
parameters: {
num1: decoded.num1.toString(),
num2: decoded.num2.toString(),
},
content: new Uint8Array(),
}
}
decode(encoded: EncodedContent): MultiplyNumbers {
const num1 = parseFloat(encoded.parameters['num1'] ?? '0')
const num2 = parseFloat(encoded.parameters['num2'] ?? '0')
return new MultiplyNumbers(num1, num2, num1 * num2)
}
fallback(content: MultiplyNumbers): string {
return `MultiplyNumbersCodec is not supported`
}
// This method is optional and can be used to determine if the content type should trigger a push notification
shouldPush(): boolean {
return true;
}
}
Configure the content type
Import and register the custom content type.
import { ContentTypeMultiplyNumberCodec } from "./xmtp-content-type-number";
const client = await Client.create({
env: "production",
codecs: [new ContentTypeMultiplyNumberCodec()],
});
Send the content
Send a message using the custom content type. This code sample demonstrates how to use the MultiplyCodec
custom content type to perform multiplication operations.
const multiplyNumbers = new MultiplyNumbers(3, 7);
await bobConvo.send(multiplyNumbers, {
contentType: ContentTypeMultiplyNumbers,
});
Receive the content
To use the result of the multiplication operation, add a renderer for the custom content type.
To handle unsupported content types, see the fallback section.
// Because of this message content is now a function which returns the actual content.
// You can get that content by call `message.content()` now instead of message.content.
// This may involve more filtering on the message side to make sure you are handling different contentTypes appropriately.
if (message.contentTypeId === "com.example/multiplyNumbers:1.1") {
return message.content(); // 21
}
Advanced: Build a custom content type to send a transaction hash
This tutorial covers how to build a custom content type that sends transaction hashes on the Polygon blockchain. This example also describes how to use the custom content type to render the transaction hash.
Create the custom content type
Create a new file, xmtp-content-type-transaction-hash.tsx
. This file hosts the TransactionHash
class for encoding and decoding the custom content type.
import { ContentTypeId } from "@xmtp/xmtp-js";
export const ContentTypeTransactionHash = new ContentTypeId({
authorityId: "your.domain",
typeId: "transaction-hash",
versionMajor: 1,
versionMinor: 0,
});
export class ContentTypeTransactionHashCodec {
get contentType() {
return ContentTypeTransactionHash;
}
encode(hash) {
return {
type: ContentTypeTransactionHash,
parameters: {},
content: new TextEncoder().encode(hash),
};
}
decode(content: { content: any }) {
const uint8Array = content.content;
const hash = new TextDecoder().decode(uint8Array);
return hash;
}
}
Import and register the custom content type
import {
ContentTypeTransactionHash,
ContentTypeTransactionHashCodec,
} from "./xmtp-content-type-transaction-hash";
const xmtp = await Client.create(signer, {
env: "dev",
});
xmtp.registerCodec(new ContentTypeTransactionHashCodec());
Send a message using the custom content type
This code sample demonstrates how to use the TransactionHash
content type to send a transaction.
// Create a wallet from a known private key
const wallet = new ethers.Wallet(privateKey);
console.log(`Wallet address: ${wallet.address}`);
//im using a burner wallet with MATIC from a faucet
//https://faucet.polygon.technology/
// Set up provider for Polygon Testnet (Mumbai)
const provider = new ethers.providers.JsonRpcProvider(
"https://rpc-mumbai.maticvigil.com",
);
// Connect the wallet to the provider
const signer = wallet.connect(provider);
// Define the recipient address and amount
const amount = ethers.utils.parseEther("0.01"); // Amount in ETH (0.01 in this case)
// Create a transaction
const transaction = {
to: recipientAddress,
value: amount,
};
// Sign and send the transaction
const tx = await signer.sendTransaction(transaction);
console.log(`Transaction hash: ${tx.hash}`);
const conversation = await xmtp.conversations.newConversation(WALLET_TO);
await conversation
.send(tx.hash, {
contentType: ContentTypeTransactionHash,
})
.then(() => {
console.log("Transaction data sent", tx.hash);
})
.catch((error) => {
console.log("Error sending transaction data: ", error);
});
Use the result of the hash
Add an async renderer for the custom content type.
if (message.contentType.sameAs(ContentTypeTransactionHash)) {
// Handle ContentTypeAttachment
return (
<TransactionMonitor key={message.id} encodedContent={message.content} />
);
}
const TransactionMonitor = ({ encodedContent }) => {
const [retryCount, setRetryCount] = useState(0);
const [transactionValue, setTransactionValue] = useState(null);
useEffect(() => {
const fetchTransactionReceipt = async () => {
console.log(encodedContent);
const provider = new ethers.providers.JsonRpcProvider(
"https://rpc-mumbai.maticvigil.com",
);
const receipt = await provider.getTransactionReceipt(encodedContent);
const tx = await provider.getTransaction(encodedContent);
if (tx && tx.value) {
setTransactionValue(ethers.utils.formatEther(tx.value));
}
};
fetchTransactionReceipt();
}, [encodedContent, retryCount]);
return transactionValue ? (
<div>Transaction value: {transactionValue} ETH</div>
) : (
<div>
Waiting for transaction to be mined...
<button onClick={() => setRetryCount(retryCount + 1)}>
Refresh Status 🔄
</button>
</div>
);
};