Skip to content

Commit 27d20fd

Browse files
authored
Describing IPFS (#675)
1 parent f1fc4f6 commit 27d20fd

File tree

1 file changed

+101
-79
lines changed

1 file changed

+101
-79
lines changed
Lines changed: 101 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,135 @@
11
# Support attachments with your agent built with XMTP
22

3-
Use the remote attachment content type (`RemoteAttachmentCodec`) and a storage provider to send one remote attachment of any size.
3+
The Agent SDK package provides an XMTP content type (`RemoteAttachmentCodec`) to support sending file attachments through conversations.
44

55
::::tip[Quickstart]
66
To learn more, see the [Attachment example](https://github.com/xmtplabs/xmtp-agent-examples/tree/main/examples/xmtp-attachments) in the xmtp-agent-examples repo.
77
::::
88

9-
The remote attachment content type is built into the Agent SDK. No installation is required.
9+
## How remote attachments work
1010

11-
## Send a remote attachment
11+
XMTP messages have a [maximum size limit](/chat-apps/intro/faq#does-xmtp-have-a-maximum-message-size). Files that exceed this limit can't be sent inline and are instead handled as **remote attachments**. For this, the file is encrypted, uploaded to an external storage provider, and a reference URL is sent in the message. The recipient then downloads and decrypts the file using the metadata from the message.
1212

13-
Load the file. This example uses a web browser to load the file:
14-
15-
```tsx [Node]
16-
//image is the uploaded event.target.files[0];
17-
const data = await new Promise((resolve, reject) => {
18-
const reader = new FileReader();
19-
reader.onload = () => {
20-
if (reader.result instanceof ArrayBuffer) {
21-
resolve(reader.result);
22-
} else {
23-
reject(new Error('Not an ArrayBuffer'));
24-
}
25-
};
26-
reader.readAsArrayBuffer(image);
27-
});
28-
```
13+
This approach keeps messages lightweight while supporting files of any size. To send an attachment, you need three things:
2914

30-
Create an attachment object:
15+
1. **A file** to attach (image, document, etc.)
16+
2. **A storage provider** to host the encrypted file (any service that supports HTTPS GET requests)
17+
3. **An upload callback** that tells the SDK how to upload the encrypted bytes and return a download URL
3118

32-
```tsx [Node]
33-
// Local file details
34-
const attachment = {
35-
filename: image?.name,
36-
mimeType: image?.type,
37-
data: new Uint8Array(data),
38-
};
39-
```
19+
### IPFS as a storage provider
4020

41-
Use `encryptAttachment` to encrypt an attachment:
21+
[IPFS](https://en.wikipedia.org/wiki/InterPlanetary_File_System) (InterPlanetary File System) is a decentralized storage network that's a natural fit for XMTP attachments. Instead of relying on a single server, IPFS distributes files across a peer-to-peer network. In IPFS, every file receives a unique [content identifier](https://docs.pinata.cloud/ipfs-101/what-are-cids) (CID) derived from its contents. This ensures data integrity, enables decentralized serving across multiple nodes, and guarantees availability as long as at least one node pins the file.
4222

43-
```tsx [Node]
44-
import { encryptAttachment } from '@xmtp/agent-sdk';
23+
In practice, most developers use an **IPFS pinning service** like [Pinata](https://pinata.cloud/) or [Filebase](https://filebase.com/) rather than running their own IPFS node. Pinning services provide a simple API to upload files and a reliable gateway to serve them over HTTPS, which is exactly what the XMTP remote attachment flow needs.
4524

46-
const encryptedAttachment = await encryptAttachment(attachment);
25+
The good news is that the XMTP Agent SDK handles the encryption, metadata, and decryption for you. You just have to provide a callback that uploads the encrypted file and returns a download URL.
26+
27+
## Set up Pinata as your storage provider
28+
29+
All the examples below use [Pinata](https://pinata.cloud/) as the IPFS storage provider. First, install the [Pinata SDK](https://www.npmjs.com/package/pinata):
30+
31+
```bash
32+
npm install pinata
4733
```
4834

49-
Upload an encrypted attachment to a location where it will be accessible via an HTTPS GET request. This location will depend on which storage provider you use based on your needs.
35+
You'll need two environment variables from your [Pinata dashboard](https://app.pinata.cloud/):
5036

51-
Now that you have a `url`, you can create a `RemoteAttachment`:
37+
1. `PINATA_JWT` — your API authentication token
38+
2. `PINATA_GATEWAY` — your dedicated gateway URL (e.g., `your-gateway.mypinata.cloud`)
5239

53-
```tsx [Node]
54-
import type { RemoteAttachment } from '@xmtp/agent-sdk';
40+
## Create a file to send
5541

56-
const remoteAttachment: RemoteAttachment = {
57-
url: url,
58-
contentDigest: encryptedAttachment.contentDigest,
59-
salt: encryptedAttachment.salt,
60-
nonce: encryptedAttachment.nonce,
61-
secret: encryptedAttachment.secret,
62-
scheme: 'https://',
63-
filename: encryptedAttachment.filename,
64-
contentLength: encryptedAttachment.contentLength,
65-
};
42+
You can send any file as an attachment. Here's an example that programmatically creates a PNG image using the [canvas](https://www.npmjs.com/package/canvas) library:
43+
44+
```bash
45+
npm install canvas
6646
```
6747

68-
Now that you have a remote attachment, you can send it:
48+
```ts
49+
import { createCanvas } from 'canvas';
50+
51+
const createImageFile = () => {
52+
const canvas = createCanvas(400, 300);
53+
const canvasCtx = canvas.getContext('2d');
6954

70-
```tsx [Node]
71-
await ctx.conversation.sendRemoteAttachment(remoteAttachment);
55+
canvasCtx.fillStyle = 'blue';
56+
canvasCtx.fillRect(0, 0, 400, 300);
57+
canvasCtx.fillStyle = 'white';
58+
canvasCtx.font = '30px Arial';
59+
canvasCtx.fillText('Hello XMTP!', 100, 150);
60+
61+
const buffer = canvas.toBuffer('image/png');
62+
return new File([new Uint8Array(buffer)], 'hello-xmtp.png', {
63+
type: 'image/png',
64+
});
65+
};
7266
```
7367

74-
## Receive, decode, and decrypt a remote attachment
68+
This produces a simple blue image with white text. In a real application, this would be any [File](https://developer.mozilla.org/docs/Web/API/File) object.
7569

76-
Now that you can send a remote attachment, you need a way to receive it. For example:
70+
## Send a remote attachment
7771

78-
```ts [Node]
79-
import { decryptAttachment } from '@xmtp/agent-sdk';
80-
import type { RemoteAttachment } from '@xmtp/agent-sdk';
72+
Use `ctx.sendRemoteAttachment` to send a file as an encrypted remote attachment. You provide the file and an upload callback that handles storing the encrypted bytes:
73+
74+
```ts
75+
import { CommandRouter, type AttachmentUploadCallback } from '@xmtp/agent-sdk';
76+
import { PinataSDK } from 'pinata';
77+
78+
const router = new CommandRouter();
79+
80+
router.command('/send-image', async (ctx) => {
81+
const file = createImageFile();
82+
83+
const uploadCallback: AttachmentUploadCallback = async (attachment) => {
84+
const pinata = new PinataSDK({
85+
pinataJwt: `${process.env.PINATA_JWT}`,
86+
pinataGateway: `${process.env.PINATA_GATEWAY}`,
87+
});
88+
89+
const mimeType = 'application/octet-stream';
90+
const encryptedBlob = new Blob([Buffer.from(attachment.payload)], {
91+
type: mimeType,
92+
});
93+
const encryptedFile = new File(
94+
[encryptedBlob],
95+
attachment.filename || 'untitled',
96+
{
97+
type: mimeType,
98+
}
99+
);
100+
const upload = await pinata.upload.public.file(encryptedFile);
101+
102+
return pinata.gateways.public.convert(`${upload.cid}`);
103+
};
81104

82-
if (ctx.isRemoteAttachment()) {
83-
const remoteAttachment: RemoteAttachment = ctx.message.content;
84-
// Download encrypted bytes from remoteAttachment.url
85-
const encryptedBytes = await fetch(remoteAttachment.url).then(r => r.arrayBuffer());
86-
const attachment = await decryptAttachment(
87-
new Uint8Array(encryptedBytes),
88-
remoteAttachment
89-
);
90-
}
105+
await ctx.sendRemoteAttachment(file, uploadCallback);
106+
});
91107
```
92108

93-
You now have the original attachment:
109+
Here's what happens under the hood when you call `sendRemoteAttachment`:
94110

95-
```ts [Node]
96-
attachment.filename; // => "screenshot.png"
97-
attachment.mimeType; // => "image/png",
98-
attachment.data; // => [the PNG data]
99-
```
111+
1. The SDK encrypts the file contents
112+
2. Your `uploadCallback` receives the encrypted payload and uploads it to Pinata's IPFS network
113+
3. Pinata returns a CID, which is converted to a gateway URL
114+
4. The SDK sends a message containing the URL and decryption metadata (salt, nonce, secret, content digest)
115+
116+
The `attachment.payload` contains the already-encrypted bytes, so what gets stored on IPFS is unreadable without the decryption keys, which are only shared within the XMTP message.
117+
118+
## Receive and decrypt a remote attachment
100119

101-
Once you've created the attachment object, you can create a preview to show in the message input field before sending:
120+
When your agent receives an attachment, use `downloadRemoteAttachment` to download and decrypt it in one step:
102121

103-
```tsx [Node]
104-
const objectURL = URL.createObjectURL(
105-
new Blob([Buffer.from(attachment.data)], {
106-
type: attachment.mimeType,
107-
})
108-
);
122+
```ts
123+
import { downloadRemoteAttachment } from '@xmtp/agent-sdk/util';
109124

110-
const img = document.createElement('img');
111-
img.src = objectURL;
112-
img.title = attachment.filename;
125+
agent.on('attachment', async (ctx) => {
126+
const receivedAttachment = await downloadRemoteAttachment(
127+
ctx.message.content
128+
);
129+
console.log(`Received: ${receivedAttachment.filename}`);
130+
console.log(`Type: ${receivedAttachment.mimeType}`);
131+
// receivedAttachment.content contains the decrypted file bytes
132+
});
113133
```
134+
135+
The `downloadRemoteAttachment` utility handles fetching the encrypted bytes from the remote URL and decrypting them using the metadata from the message. You get back the original file with its filename, MIME type, and data.

0 commit comments

Comments
 (0)