Skip to content

Commit c953fe4

Browse files
authored
Gated group (#22)
* gated * group tested * readme
1 parent f9eb1f3 commit c953fe4

File tree

6 files changed

+999
-1
lines changed

6 files changed

+999
-1
lines changed

.env.example

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ WALLET_KEY= # the private key of the wallet
22
ENCRYPTION_KEY= # a second random 32 bytes encryption key for local db encryption
33

44
# GPT agent example
5-
OPENAI_API_KEY= # the API key for the OpenAI API
5+
OPENAI_API_KEY= # the API key for the OpenAI
6+
7+
# gated group example
8+
ALCHEMY_API_KEY= # the API key for the Alchemy

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ yarn gen:keys
3434

3535
- [gm](/examples/gm/): A simple agent that replies to all text messages with "gm".
3636
- [gpt](/examples/gpt/): An example using GPT API's to answer messages.
37+
- [gated-group](/examples/gated-group/): Add members to a group that hold a certain NFT.
3738

3839
> See all the available [examples](/examples/).
3940

examples/gated-group/README.md

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Building a gated group with NFT verification
2+
3+
To create a gated group chat using XMTP, you will need an admin bot within the group to manage member additions and removals. The admin bot will create the group, assign you as the admin, and then verify NFT ownership before adding new members.
4+
5+
#### Environment variables
6+
7+
```bash
8+
WALLET_KEY= # the private key of admin bot
9+
ENCRYPTION_KEY= # a second fixed or random 32 bytes encryption key for the local db
10+
11+
12+
ALCHEMY_API_KEY= #alchemy api to check NFT ownership
13+
```
14+
15+
You can generate random keys with the following command:
16+
17+
```bash
18+
yarn gen:keys
19+
```
20+
21+
> [!WARNING]
22+
> Running the `gen:keys` script will overwrite the existing `.env` file.
23+
24+
## Start the XMTP agent
25+
26+
Start your XMTP client and begin listening to messages. The bot responds to the following commands:
27+
28+
- `/create` - Creates a new gated group
29+
- `/add <wallet_address> <group_id>` - Adds a wallet to an existing group (if they own the required NFT)
30+
31+
```tsx
32+
const client = await Client.create(signer, encryptionKey, {
33+
env,
34+
});
35+
36+
// Listen for messages
37+
const stream = client.conversations.streamAllMessages();
38+
39+
for await (const message of await stream) {
40+
// Handle /create command
41+
if (message.content === "/create") {
42+
console.log("Creating group");
43+
const group = await client.conversations.newGroup([]);
44+
await group.addMembersByInboxId([message.senderInboxId]);
45+
await group.addSuperAdmin(message.senderInboxId);
46+
47+
await conversation.send(
48+
`Group created!\n- ID: ${group.id}\n- Group URL: https://xmtp.chat/conversations/${group.id}`,
49+
);
50+
return;
51+
}
52+
53+
// Handle /add command
54+
if (message.content.startsWith("/add")) {
55+
const walletAddress = message.content.split(" ")[1];
56+
const groupId = message.content.split(" ")[2];
57+
58+
const result = await checkNft(walletAddress, "XMTPeople");
59+
if (!result) {
60+
console.log("User can't be added to the group");
61+
return;
62+
} else {
63+
await group.addMembers([walletAddress]);
64+
await conversation.send(
65+
`User added to the group\n- Group ID: ${groupId}\n- Wallet Address: ${walletAddress}`,
66+
);
67+
}
68+
}
69+
}
70+
```
71+
72+
## Verify NFT ownership
73+
74+
The bot checks if a wallet owns the required NFT using Alchemy's API:
75+
76+
```tsx
77+
async function checkNft(
78+
walletAddress: string,
79+
collectionSlug: string,
80+
): Promise<boolean> {
81+
const alchemy = new Alchemy(settings);
82+
try {
83+
const nfts = await alchemy.nft.getNftsForOwner(walletAddress);
84+
85+
const ownsNft = nfts.ownedNfts.some(
86+
(nft) =>
87+
nft.contract.name?.toLowerCase() === collectionSlug.toLowerCase(),
88+
);
89+
console.log("is the nft owned: ", ownsNft);
90+
return ownsNft;
91+
} catch (error) {
92+
console.error("Error fetching NFTs from Alchemy:", error);
93+
}
94+
return false;
95+
}
96+
```
97+
98+
## Usage
99+
100+
1. Start the bot with your environment variables configured
101+
2. Message the bot at its address to create a new group using `/create`
102+
3. Once you have the group ID, you can add members using `/add <wallet_address> <group_id>`
103+
4. The bot will verify NFT ownership and add the wallet if they own the required NFT
104+
105+
The bot will automatically make the group creator a super admin and can optionally make new members admins as well.

examples/gated-group/index.ts

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { ContentTypeText } from "@xmtp/content-type-text";
2+
import { Client, type XmtpEnv } from "@xmtp/node-sdk";
3+
import { Alchemy, Network } from "alchemy-sdk";
4+
import { createSigner, getEncryptionKeyFromHex } from "@/helpers";
5+
6+
const settings = {
7+
apiKey: process.env.ALCHEMY_API_KEY, // Replace with your Alchemy API key
8+
network: Network.BASE_MAINNET, // Use the appropriate network
9+
};
10+
11+
const { WALLET_KEY, ENCRYPTION_KEY } = process.env;
12+
13+
if (!WALLET_KEY) {
14+
throw new Error("WALLET_KEY must be set");
15+
}
16+
17+
if (!ENCRYPTION_KEY) {
18+
throw new Error("ENCRYPTION_KEY must be set");
19+
}
20+
21+
const signer = createSigner(WALLET_KEY);
22+
const encryptionKey = getEncryptionKeyFromHex(ENCRYPTION_KEY);
23+
24+
const env: XmtpEnv = "dev";
25+
26+
async function main() {
27+
console.log(`Creating client on the '${env}' network...`);
28+
const client = await Client.create(signer, encryptionKey, {
29+
env,
30+
});
31+
32+
console.log("Syncing conversations...");
33+
await client.conversations.sync();
34+
35+
console.log(
36+
`Agent initialized on ${client.accountAddress}\nSend a message on http://xmtp.chat/dm/${client.accountAddress}`,
37+
);
38+
39+
console.log("Waiting for messages...");
40+
const stream = client.conversations.streamAllMessages();
41+
42+
for await (const message of await stream) {
43+
if (
44+
!message ||
45+
!message.contentType ||
46+
!ContentTypeText.sameAs(message.contentType)
47+
) {
48+
console.log("Invalid message, skipping", message?.contentType?.typeId);
49+
continue;
50+
}
51+
52+
// Ignore own messages
53+
if (message.senderInboxId === client.inboxId) {
54+
continue;
55+
}
56+
57+
console.log(
58+
`Received message: ${message.content as string} by ${message.senderInboxId}`,
59+
);
60+
61+
const conversation = client.conversations.getConversationById(
62+
message.conversationId,
63+
);
64+
65+
if (!conversation) {
66+
console.log("Unable to find conversation, skipping");
67+
continue;
68+
}
69+
if (message.content === "/create") {
70+
console.log("Creating group");
71+
const group = await client.conversations.newGroup([]);
72+
console.log("Group created", group.id);
73+
// First add the sender to the group
74+
await group.addMembersByInboxId([message.senderInboxId]);
75+
// Then make the sender a super admin
76+
await group.addSuperAdmin(message.senderInboxId);
77+
console.log(
78+
"Sender is superAdmin",
79+
group.isSuperAdmin(message.senderInboxId),
80+
);
81+
await group.send(
82+
`Welcome to the new group!\nYou are now the admin of this group as well as the bot`,
83+
);
84+
85+
await conversation.send(
86+
`Group created!\n- ID: ${group.id}\n- Group URL: https://xmtp.chat/conversations/${group.id}: \n- This url will deeplink to the group created\n- Once in the other group you can share the invite with your friends.\n- You can add more members to the group by using the /add <group.id> <wallet-address>.`,
87+
);
88+
return;
89+
} else if (
90+
typeof message.content === "string" &&
91+
message.content.startsWith("/add")
92+
) {
93+
const groupId = message.content.split(" ")[1];
94+
if (!groupId) {
95+
await conversation.send("Please provide a group id");
96+
return;
97+
}
98+
const group = client.conversations.getConversationById(groupId);
99+
if (!group) {
100+
await conversation.send("Please provide a valid group id");
101+
return;
102+
}
103+
const walletAddress = message.content.split(" ")[2];
104+
if (!walletAddress) {
105+
await conversation.send("Please provide a wallet address");
106+
return;
107+
}
108+
109+
const result = await checkNft(walletAddress, "XMTPeople");
110+
if (!result) {
111+
console.log("User can't be added to the group");
112+
return;
113+
} else {
114+
await group.addMembers([walletAddress]);
115+
await conversation.send(
116+
`User added to the group\n- Group ID: ${groupId}\n- Wallet Address: ${walletAddress}`,
117+
);
118+
}
119+
} else {
120+
await conversation.send(
121+
"👋 Welcome to the Gated Bot Group!\nTo get started, type /create to set up a new group. 🚀\nThis example will check if the user has a particular nft and add them to the group if they do.\nOnce your group is created, you'll receive a unique Group ID and URL.\nShare the URL with friends to invite them to join your group!",
122+
);
123+
}
124+
}
125+
}
126+
127+
main().catch(console.error);
128+
129+
async function checkNft(
130+
walletAddress: string,
131+
collectionSlug: string,
132+
): Promise<boolean> {
133+
const alchemy = new Alchemy(settings);
134+
try {
135+
const nfts = await alchemy.nft.getNftsForOwner(walletAddress);
136+
137+
const ownsNft = nfts.ownedNfts.some(
138+
(nft) =>
139+
nft.contract.name?.toLowerCase() === collectionSlug.toLowerCase(),
140+
);
141+
console.log("is the nft owned: ", ownsNft);
142+
return ownsNft;
143+
} catch (error) {
144+
console.error("Error fetching NFTs from Alchemy:", error);
145+
}
146+
147+
return false;
148+
}

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"scripts": {
77
"clean": "rimraf node_modules && yarn clean:dbs",
88
"clean:dbs": "rimraf *.db3* ||:",
9+
"examples:gated": "tsx --env-file=.env examples/gated-group/index.ts",
910
"examples:gm": "tsx --env-file=.env examples/gm/index.ts",
1011
"examples:gpt": "tsx --env-file=.env examples/gpt/index.ts",
1112
"format": "prettier -w .",
@@ -17,6 +18,7 @@
1718
"dependencies": {
1819
"@xmtp/content-type-text": "^2.0.0",
1920
"@xmtp/node-sdk": "0.0.40",
21+
"alchemy-sdk": "^3.0.0",
2022
"openai": "latest",
2123
"tsx": "^4.19.2",
2224
"uint8arrays": "^5.1.0",

0 commit comments

Comments
 (0)