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
// A test of this content type can be found in the following PR: https://github.com/xmtp/xmtp-js/pull/509/files
import { ContentTypeId } from "@xmtp/content-type-primitives";
import type { ContentCodec, EncodedContent } from "@xmtp/content-type-primitives";
// Create a unique identifier for your content type
export const ContentTypeMultiplyNumbers = new ContentTypeId({
authorityId: 'your.domain',
typeId: 'multiply-number',
versionMajor: 1,
versionMinor: 0,
})
// Define the MultiplyNumbers class
export class MultiplyNumbers {
public num1: number
public num2: number
public result: number | undefined
constructor(num1: number, num2: number, result?: number) {
this.num1 = num1
this.num2 = num2
this.result = result
}
}
// Define the MultiplyCodec class
export class ContentTypeMultiplyNumberCodec
implements ContentCodec<MultiplyNumbers>
{
get contentType(): ContentTypeId {
return ContentTypeMultiplyNumbers
}
// The encode method accepts an object of MultiplyNumbers and encodes it as a byte array
encode(numbers: MultiplyNumbers): EncodedContent {
const { num1, num2 } = numbers
return {
type: ContentTypeMultiplyNumbers,
parameters: {},
content: new TextEncoder().encode(JSON.stringify({ num1, num2 })),
}
}
// The decode method decodes the byte array, parses the string into numbers (num1, num2), and returns their product
decode(encodedContent: EncodedContent): MultiplyNumbers {
const decodedContent = new TextDecoder().decode(encodedContent.content)
const { num1, num2 } = JSON.parse(decodedContent)
return new MultiplyNumbers(num1, num2, num1 * num2)
}
fallback(content: MultiplyNumbers): string | undefined {
return `Can’t display number content types. Number was ${JSON.stringify(
content
)}`
// return undefined to indicate that this content type should not be displayed if it's not supported by a client
}
// Set to true to enable push notifications for interoperable content types.
// Receiving clients must handle this field appropriately.
shouldPush(): boolean {
return true;
}
}
Configure the content type
Import and register the custom content type.
import { ContentTypeMultiplyNumberCodec } from "./xmtp-content-type-multiply-number";
const client = await Client.create({
env: "production",
codecs: [new ContentTypeMultiplyNumberCodec()],
});
//or
client.registerCodec(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 numbersToMultiply = new MultiplyNumbers(2, 3);
conversation.send(numbersToMultiply, {
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.
if (message.contentType.sameAs(ContentTypeMultiplyNumber)) {
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>
);
};