create-message-kit 1.1.9 → 1.1.10-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/index.js +12 -36
  2. package/package.json +2 -2
  3. package/templates/agent/.yarnrc.yml +4 -0
  4. package/templates/agent/package.json +20 -0
  5. package/templates/agent/src/handlers/check.ts +42 -0
  6. package/templates/agent/src/handlers/cool.ts +52 -0
  7. package/templates/agent/src/handlers/info.ts +65 -0
  8. package/templates/agent/src/handlers/register.ts +40 -0
  9. package/templates/agent/src/handlers/renew.ts +52 -0
  10. package/templates/agent/src/handlers/reset.ts +19 -0
  11. package/templates/agent/src/handlers/tip.ts +29 -0
  12. package/templates/agent/src/index.ts +55 -55
  13. package/templates/agent/src/prompt.ts +52 -0
  14. package/templates/gated/.env.example +3 -0
  15. package/templates/gated/.yarnrc.yml +4 -0
  16. package/templates/gated/package.json +23 -0
  17. package/templates/gated/src/index.ts +64 -0
  18. package/templates/gated/src/lib/gated.ts +51 -0
  19. package/templates/gated/src/lib/nft.ts +37 -0
  20. package/templates/gated/src/skills.ts +24 -0
  21. package/templates/gpt/.yarnrc.yml +4 -0
  22. package/templates/gpt/package.json +20 -0
  23. package/templates/gpt/src/index.ts +8 -29
  24. package/templates/gpt/src/prompt.ts +8 -18
  25. package/templates/group/.yarnrc.yml +4 -0
  26. package/templates/group/package.json +20 -0
  27. package/templates/group/src/{handler → handlers}/game.ts +19 -3
  28. package/templates/group/src/{handler → handlers}/helpers.ts +22 -3
  29. package/templates/group/src/handlers/payment.ts +51 -0
  30. package/templates/group/src/handlers/tipping.ts +60 -0
  31. package/templates/group/src/index.ts +34 -21
  32. package/templates/group/src/prompt.ts +21 -16
  33. package/templates/agent/src/handler.ts +0 -174
  34. package/templates/agent/src/skills.ts +0 -88
  35. package/templates/ens-agent-pro/.env.example +0 -2
  36. package/templates/ens-agent-pro/src/index.ts +0 -90
  37. package/templates/ens-agent-pro/src/prompt.ts +0 -19
  38. package/templates/ens-agent-pro/src/skills.ts +0 -233
  39. package/templates/group/src/handler/payment.ts +0 -29
  40. package/templates/group/src/handler/tipping.ts +0 -40
  41. package/templates/group/src/skills.ts +0 -87
@@ -1,174 +0,0 @@
1
- import { HandlerContext, SkillResponse } from "@xmtp/message-kit";
2
- import { getUserInfo, clearInfoCache, isOnXMTP } from "@xmtp/message-kit";
3
- import { clearMemory } from "@xmtp/message-kit";
4
-
5
- export const frameUrl = "https://ens.steer.fun/";
6
- export const ensUrl = "https://app.ens.domains/";
7
- export const txpayUrl = "https://txpay.vercel.app";
8
-
9
- export async function handleEns(
10
- context: HandlerContext,
11
- ): Promise<SkillResponse | undefined> {
12
- const {
13
- message: {
14
- sender,
15
- content: { skill, params },
16
- },
17
- } = context;
18
-
19
- if (skill == "reset") {
20
- clearMemory();
21
- return { code: 200, message: "Conversation reset." };
22
- } else if (skill == "renew") {
23
- // Destructure and validate parameters for the ens
24
- const { domain } = params;
25
- // Check if the user holds the domain
26
- if (!domain) {
27
- return {
28
- code: 400,
29
- message: "Missing required parameters. Please provide domain.",
30
- };
31
- }
32
-
33
- const data = await getUserInfo(domain);
34
-
35
- if (!data?.address || data?.address !== sender?.address) {
36
- return {
37
- code: 403,
38
- message:
39
- "Looks like this domain is not registered to you. Only the owner can renew it.",
40
- };
41
- }
42
-
43
- // Generate URL for the ens
44
- let url_ens = frameUrl + "frames/manage?name=" + domain;
45
- return { code: 200, message: `${url_ens}` };
46
- } else if (skill == "register") {
47
- // Destructure and validate parameters for the ens
48
- const { domain } = params;
49
-
50
- if (!domain) {
51
- return {
52
- code: 400,
53
- message: "Missing required parameters. Please provide domain.",
54
- };
55
- }
56
- // Generate URL for the ens
57
- let url_ens = ensUrl + domain;
58
- context.send(`${url_ens}`);
59
- return { code: 200, message: `${url_ens}` };
60
- } else if (skill == "info") {
61
- const { domain } = params;
62
-
63
- const data = await getUserInfo(domain);
64
- if (!data?.ensDomain) {
65
- return {
66
- code: 404,
67
- message: "Domain not found.",
68
- };
69
- }
70
-
71
- const formattedData = {
72
- Address: data?.address,
73
- "Avatar URL": data?.ensInfo?.avatar,
74
- Description: data?.ensInfo?.description,
75
- ENS: data?.ensDomain,
76
- "Primary ENS": data?.ensInfo?.ens_primary,
77
- GitHub: data?.ensInfo?.github,
78
- Resolver: data?.ensInfo?.resolverAddress,
79
- Twitter: data?.ensInfo?.twitter,
80
- URL: `${ensUrl}${domain}`,
81
- };
82
-
83
- let message = "Domain information:\n\n";
84
- for (const [key, value] of Object.entries(formattedData)) {
85
- if (value) {
86
- message += `${key}: ${value}\n`;
87
- }
88
- }
89
- message += `\n\nWould you like to tip the domain owner for getting there first 🤣?`;
90
- message = message.trim();
91
- if (await isOnXMTP(context.client, context.v2client, sender?.address)) {
92
- await context.send(
93
- `Ah, this domains is in XMTP, you can message it directly: https://converse.xyz/dm/${domain}`,
94
- );
95
- }
96
- return { code: 200, message };
97
- } else if (skill == "check") {
98
- const { domain } = params;
99
-
100
- if (!domain) {
101
- return {
102
- code: 400,
103
- message: "Please provide a domain name to check.",
104
- };
105
- }
106
-
107
- const data = await getUserInfo(domain);
108
- if (!data?.address) {
109
- let message = `Looks like ${domain} is available! Here you can register it: ${ensUrl}${domain} or would you like to see some cool alternatives?`;
110
- return {
111
- code: 200,
112
- message,
113
- };
114
- } else {
115
- let message = `Looks like ${domain} is already registered!`;
116
- await context.executeSkill("/cool " + domain);
117
- return {
118
- code: 404,
119
- message,
120
- };
121
- }
122
- } else if (skill == "tip") {
123
- const { address } = params;
124
- if (!address) {
125
- return {
126
- code: 400,
127
- message: "Please provide an address to tip.",
128
- };
129
- }
130
- const data = await getUserInfo(address);
131
-
132
- let sendUrl = `${txpayUrl}/?&amount=1&token=USDC&receiver=${address}`;
133
-
134
- return {
135
- code: 200,
136
- message: sendUrl,
137
- };
138
- } else if (skill == "cool") {
139
- const { domain } = params;
140
- //What about these cool alternatives?\
141
- return {
142
- code: 200,
143
- message: `${generateCoolAlternatives(domain)}`,
144
- };
145
- } else {
146
- return { code: 400, message: "Skill not found." };
147
- }
148
- }
149
-
150
- export const generateCoolAlternatives = (domain: string) => {
151
- const suffixes = ["lfg", "cool", "degen", "moon", "base", "gm"];
152
- const alternatives = [];
153
- for (let i = 0; i < 5; i++) {
154
- const randomPosition = Math.random() < 0.5;
155
- const baseDomain = domain.replace(/\.eth$/, ""); // Remove any existing .eth suffix
156
- alternatives.push(
157
- randomPosition
158
- ? `${suffixes[i]}${baseDomain}.eth`
159
- : `${baseDomain}${suffixes[i]}.eth`,
160
- );
161
- }
162
-
163
- const cool_alternativesFormat = alternatives
164
- .map(
165
- (alternative: string, index: number) => `${index + 1}. ${alternative} ✨`,
166
- )
167
- .join("\n");
168
- return cool_alternativesFormat;
169
- };
170
-
171
- export async function clear() {
172
- clearMemory();
173
- clearInfoCache();
174
- }
@@ -1,88 +0,0 @@
1
- import { handleEns } from "./handler.js";
2
- import type { SkillGroup } from "@xmtp/message-kit";
3
-
4
- export const skills: SkillGroup[] = [
5
- {
6
- name: "Ens Domain Bot",
7
- tag: "@ens",
8
- description: "Register ENS domains.",
9
- skills: [
10
- {
11
- skill: "/register [domain]",
12
- handler: handleEns,
13
- description:
14
- "Register a new ENS domain. Returns a URL to complete the registration process.",
15
- examples: ["/register vitalik.eth"],
16
- params: {
17
- domain: {
18
- type: "string",
19
- },
20
- },
21
- },
22
- {
23
- skill: "/info [domain]",
24
- handler: handleEns,
25
- description:
26
- "Get detailed information about an ENS domain including owner, expiry date, and resolver.",
27
- examples: ["/info nick.eth"],
28
- params: {
29
- domain: {
30
- type: "string",
31
- },
32
- },
33
- },
34
- {
35
- skill: "/renew [domain]",
36
- handler: handleEns,
37
- description:
38
- "Extend the registration period of your ENS domain. Returns a URL to complete the renewal.",
39
- examples: ["/renew fabri.base.eth"],
40
- params: {
41
- domain: {
42
- type: "string",
43
- },
44
- },
45
- },
46
- {
47
- skill: "/check [domain]",
48
- handler: handleEns,
49
- examples: ["/check vitalik.eth", "/check fabri.base.eth"],
50
- description: "Check if a domain is available.",
51
- params: {
52
- domain: {
53
- type: "string",
54
- },
55
- },
56
- },
57
- {
58
- skill: "/cool [domain]",
59
- examples: ["/cool vitalik.eth"],
60
- handler: handleEns,
61
- description: "Get cool alternatives for a .eth domain.",
62
- params: {
63
- domain: {
64
- type: "string",
65
- },
66
- },
67
- },
68
- {
69
- skill: "/reset",
70
- examples: ["/reset"],
71
- handler: handleEns,
72
- description: "Reset the conversation.",
73
- params: {},
74
- },
75
- {
76
- skill: "/tip [address]",
77
- description: "Show a URL for tipping a domain owner.",
78
- handler: handleEns,
79
- examples: ["/tip 0x1234567890123456789012345678901234567890"],
80
- params: {
81
- address: {
82
- type: "string",
83
- },
84
- },
85
- },
86
- ],
87
- },
88
- ];
@@ -1,2 +0,0 @@
1
- KEY= # the private key of the wallet
2
- OPEN_AI_API_KEY= # sk-proj-...
@@ -1,90 +0,0 @@
1
- import { run, HandlerContext } from "@xmtp/message-kit";
2
- import { ChatOpenAI } from "@langchain/openai";
3
- import { createOpenAIFunctionsAgent, AgentExecutor } from "langchain/agents";
4
- import { tools } from "./skills.js";
5
- import { systemPrompt } from "./prompt.js";
6
- import { ChatPromptTemplate } from "@langchain/core/prompts";
7
-
8
- // Initialize OpenAI chat model
9
- const model = new ChatOpenAI({
10
- temperature: 0.7,
11
- modelName: "gpt-4o",
12
- openAIApiKey: process.env.OPEN_AI_API_KEY,
13
- });
14
-
15
- // Create the prompt template with required variables
16
- const prompt = ChatPromptTemplate.fromMessages([
17
- ["system", systemPrompt],
18
- ]).partial({
19
- tools: tools.map((tool) => `${tool.name}: ${tool.description}`).join("\n"),
20
- tool_names: tools.map((tool) => tool.name).join(", "),
21
- });
22
-
23
- // Initialize chat history storage
24
- const chatHistory = new Map<string, { role: string; content: string }[]>();
25
-
26
- // Initialize the agent and executor
27
- const initializeAgent = async () => {
28
- const agent = await createOpenAIFunctionsAgent({
29
- llm: model,
30
- tools: tools as any,
31
- prompt: await prompt,
32
- });
33
-
34
- return new AgentExecutor({
35
- agent,
36
- tools: tools as any,
37
- returnIntermediateSteps: false,
38
- verbose: true, // Set to true for checking the agent's thought process
39
- });
40
- };
41
-
42
- // Create executor instance
43
- const executor = await initializeAgent();
44
-
45
- run(
46
- async (context: HandlerContext) => {
47
- const {
48
- message: {
49
- content: { text },
50
- sender,
51
- },
52
- } = context;
53
-
54
- console.log("Received message:", text);
55
-
56
- // Get or initialize chat history for this sender
57
- if (!chatHistory.has(sender.address)) {
58
- chatHistory.set(sender.address, []);
59
- }
60
- const userHistory: any = chatHistory.get(sender.address)!;
61
-
62
- // Add user message to history
63
- userHistory.push({ role: "user", content: context.message.content.text });
64
-
65
- try {
66
- // Execute agent with user's message and chat history
67
- const result = await executor.invoke({
68
- input: text,
69
- chat_history: userHistory,
70
- });
71
-
72
- console.log("Agent response:", result.output);
73
- const output = result.output.replace(/\*/g, "");
74
-
75
- // Add assistant's response to history
76
- userHistory.push({ role: "assistant", content: output });
77
-
78
- await context.send(output);
79
- } catch (error) {
80
- console.error("Error:", error);
81
- // Add error message to history
82
- userHistory.push({
83
- role: "assistant",
84
- content: "An error occurred while processing your request.",
85
- });
86
- await context.send("An error occurred while processing your request.");
87
- }
88
- },
89
- { skills: [] },
90
- );
@@ -1,19 +0,0 @@
1
- export const systemPrompt = `You are a helpful AI assistant that can use various tools to help users.
2
-
3
- You have access to the following tools:
4
- {tools}
5
-
6
- Tool Names: {tool_names}
7
-
8
- Instructions:
9
- 1. DO NOT respond in markdown. ALWAYS respond in plain text.
10
- 2. Use tools when appropriate
11
- 3. Be friendly and helpful
12
- 4. Ask for clarification if you don't understand the user's request
13
- 5. For tipping, always confirm the amount with the user once
14
-
15
- Previous conversation history:
16
- {chat_history}
17
-
18
- User Input: {input}
19
- {agent_scratchpad}`;
@@ -1,233 +0,0 @@
1
- import { DynamicStructuredTool } from "langchain/tools";
2
- import { getUserInfo } from "@xmtp/message-kit";
3
- import { clearMemory } from "@xmtp/message-kit";
4
- import { isAddress } from "viem";
5
- import { z } from "zod";
6
-
7
- const frameUrl = "https://ens.steer.fun/";
8
- const ensUrl = "https://app.ens.domains/";
9
- const txpayUrl = "https://txpay.vercel.app";
10
- const converseUrl = "https://converse.xyz/profile/";
11
-
12
- // Add interface for Converse API response
13
- interface ConverseProfile {
14
- address: string;
15
- avatar?: string;
16
- formattedName?: string;
17
- name?: string;
18
- onXmtp: boolean;
19
- }
20
-
21
- // Add function to check XMTP status
22
- async function checkXMTPStatus(address: string): Promise<boolean> {
23
- try {
24
- const response = await fetch(converseUrl + address, {
25
- method: "POST",
26
- headers: {
27
- "Content-Type": "application/json",
28
- Accept: "application/json",
29
- },
30
- body: JSON.stringify({ address }),
31
- });
32
-
33
- if (!response.ok) {
34
- console.error(`Failed to check XMTP status: ${response.status}`);
35
- return false;
36
- }
37
-
38
- const data = (await response.json()) as ConverseProfile;
39
- return data.onXmtp;
40
- } catch (error) {
41
- console.error("Error checking XMTP status:", error);
42
- return false;
43
- }
44
- }
45
-
46
- const generateCoolAlternatives = (domain: string) => {
47
- const suffixes = ["lfg", "cool", "degen", "moon", "base", "gm"];
48
- const alternatives = [];
49
- for (let i = 0; i < 5; i++) {
50
- const randomPosition = Math.random() < 0.5;
51
- const baseDomain = domain.replace(/\.eth$/, ""); // Remove any existing .eth suffix
52
- alternatives.push(
53
- randomPosition
54
- ? `${suffixes[i]}${baseDomain}.eth`
55
- : `${baseDomain}${suffixes[i]}.eth`,
56
- );
57
- }
58
-
59
- return alternatives
60
- .map(
61
- (alternative: string, index: number) => `${index + 1}. ${alternative} ✨`,
62
- )
63
- .join("\n");
64
- };
65
-
66
- // Export tools array with all tools
67
- export const tools = [
68
- new DynamicStructuredTool({
69
- name: "reset_ens_conversation",
70
- description: "Reset the ENS conversation and clear memory",
71
- schema: z.object({}),
72
- func: async () => {
73
- clearMemory();
74
- return "Conversation reset successfully.";
75
- },
76
- }),
77
-
78
- new DynamicStructuredTool({
79
- name: "renew_ens_domain",
80
- description:
81
- "Generate renewal URL for an ENS domain. Only works if sender owns the domain",
82
- schema: z.object({
83
- domain: z.string().describe("The ENS domain to renew"),
84
- }),
85
- func: async ({ domain }) => {
86
- const data = await getUserInfo(domain);
87
- if (!data?.address) {
88
- return "Domain not found or not registered.";
89
- }
90
- return `${frameUrl}frames/manage?name=${domain}`;
91
- },
92
- }),
93
-
94
- new DynamicStructuredTool({
95
- name: "register_ens_domain",
96
- description: "Get URL to register a new ENS domain",
97
- schema: z.object({
98
- domain: z.string().describe("The ENS domain to register"),
99
- }),
100
- func: async ({ domain }) => {
101
- if (!domain) return "Please provide a domain name";
102
- return `${ensUrl}${domain}`;
103
- },
104
- }),
105
-
106
- new DynamicStructuredTool({
107
- name: "get_ens_info",
108
- description:
109
- "Get detailed information about an ENS domain including owner, avatar, description, etc",
110
- schema: z.object({
111
- domain: z.string().describe("The ENS domain to get information about"),
112
- }),
113
- func: async ({ domain }) => {
114
- const data = await getUserInfo(domain);
115
- if (!data?.ensDomain) {
116
- return "Domain not found.";
117
- }
118
-
119
- const formattedData = {
120
- Address: data?.address,
121
- "Avatar URL": data?.ensInfo?.avatar,
122
- Description: data?.ensInfo?.description,
123
- ENS: data?.ensDomain,
124
- "Primary ENS": data?.ensInfo?.ens_primary,
125
- GitHub: data?.ensInfo?.github,
126
- Resolver: data?.ensInfo?.resolverAddress,
127
- Twitter: data?.ensInfo?.twitter,
128
- URL: `${ensUrl}${domain}`,
129
- };
130
-
131
- let message = "Domain information:\n\n";
132
- for (const [key, value] of Object.entries(formattedData)) {
133
- if (value) {
134
- message += `${key}: ${value}\n`;
135
- }
136
- }
137
- message +=
138
- "\nWould you like to tip the domain owner for getting there first? 🤣";
139
-
140
- // Check XMTP status if we have an address
141
- if (data.address) {
142
- const isOnXMTP = await checkXMTPStatus(data.address);
143
- if (isOnXMTP) {
144
- message += `\n\nAh, this domain is on XMTP! You can message it directly: https://converse.xyz/dm/${domain}`;
145
- }
146
- }
147
-
148
- return message;
149
- },
150
- }),
151
-
152
- new DynamicStructuredTool({
153
- name: "check_ens_availability",
154
- description: "Check if an ENS domain is available for registration",
155
- schema: z.object({
156
- domain: z
157
- .string()
158
- .transform((str) => str.replace(/^["'](.+)["']$/, "$1")) // Remove quotes if present
159
- .transform((str) => str.toLowerCase())
160
- .describe("The ENS domain to check availability for"),
161
- }),
162
- func: async ({ domain }) => {
163
- if (!domain) return "Please provide a domain name to check.";
164
-
165
- if (domain.includes(".") && !domain.endsWith(".eth")) {
166
- return "Invalid ENS domain. Only .eth domains are supported.";
167
- }
168
-
169
- if (!domain.includes(".")) {
170
- domain = `${domain}.eth`;
171
- }
172
-
173
- const data = await getUserInfo(domain);
174
- if (!data?.address) {
175
- return `Looks like ${domain} is available! Here you can register it: ${ensUrl}${domain} or would you like to see some cool alternatives?`;
176
- } else {
177
- const alternatives = generateCoolAlternatives(domain);
178
- return `Looks like ${domain} is already registered!\n\nHere are some cool alternatives:\n${alternatives}`;
179
- }
180
- },
181
- }),
182
-
183
- new DynamicStructuredTool({
184
- name: "get_ens_alternatives",
185
- description: "Generate cool alternative names for an ENS domain",
186
- schema: z.object({
187
- domain: z
188
- .string()
189
- .describe("The ENS domain to generate alternatives for"),
190
- }),
191
- func: async ({ domain }) => {
192
- if (!domain) return "Please provide a domain name.";
193
- return `What about these cool alternatives?\n\n${generateCoolAlternatives(domain)}`;
194
- },
195
- }),
196
-
197
- new DynamicStructuredTool({
198
- name: "get_ens_tip_url",
199
- description:
200
- "Generate a URL to tip an ENS domain owner in USDC. Works with both ENS domains and Ethereum addresses.",
201
- schema: z.object({
202
- addressOrDomain: z
203
- .string()
204
- .describe("The ENS domain or Ethereum address to tip"),
205
- amount: z
206
- .number()
207
- .optional()
208
- .default(1)
209
- .describe("The amount of USDC to tip"),
210
- }),
211
- func: async ({ addressOrDomain, amount }) => {
212
- if (!addressOrDomain) {
213
- return "Please provide an address or ENS domain to tip.";
214
- }
215
-
216
- let address: string | undefined;
217
-
218
- if (isAddress(addressOrDomain)) {
219
- address = addressOrDomain;
220
- } else {
221
- const data = await getUserInfo(addressOrDomain);
222
- address = data?.address;
223
- }
224
-
225
- if (!address) {
226
- return "Could not resolve address for tipping. Please provide a valid ENS domain or Ethereum address.";
227
- }
228
-
229
- let sendUrl = `${txpayUrl}/?&amount=${amount}&token=USDC&receiver=${address}`;
230
- return sendUrl;
231
- },
232
- }),
233
- ];
@@ -1,29 +0,0 @@
1
- import { getUserInfo, HandlerContext } from "@xmtp/message-kit";
2
-
3
- export async function handler(context: HandlerContext) {
4
- const {
5
- message: {
6
- content: { skill, params },
7
- },
8
- } = context;
9
- const txpayUrl = "https://txpay.vercel.app";
10
-
11
- if (skill === "pay") {
12
- const { amount: amountSend, token: tokenSend, username } = params;
13
- console.log("username", username);
14
- let senderInfo = await getUserInfo(username);
15
- if (!amountSend || !tokenSend || !senderInfo) {
16
- context.reply(
17
- "Missing required parameters. Please provide amount, token, and username.",
18
- );
19
- return {
20
- code: 400,
21
- message:
22
- "Missing required parameters. Please provide amount, token, and username.",
23
- };
24
- }
25
-
26
- let sendUrl = `${txpayUrl}/?&amount=${amountSend}&token=${tokenSend}&receiver=${senderInfo.address}`;
27
- await context.send(`${sendUrl}`);
28
- }
29
- }
@@ -1,40 +0,0 @@
1
- import {
2
- HandlerContext,
3
- AbstractedMember,
4
- SkillResponse,
5
- } from "@xmtp/message-kit";
6
- import { getUserInfo } from "@xmtp/message-kit";
7
-
8
- export async function handler(context: HandlerContext) {
9
- const {
10
- message: {
11
- content: {
12
- skill,
13
- params: { amount, username },
14
- },
15
- sender,
16
- },
17
- } = context;
18
- let receivers: AbstractedMember[] = [];
19
-
20
- if (skill === "tip") {
21
- receivers = await Promise.all(
22
- username.map((username: string) => getUserInfo(username)),
23
- );
24
- }
25
- if (!sender || receivers.length === 0 || amount === 0) {
26
- context.reply("Sender or receiver or amount not found.");
27
- }
28
- const receiverAddresses = receivers.map((receiver) => receiver.address);
29
-
30
- context.sendTo(
31
- `You received ${amount} tokens from ${sender.address}.`,
32
- receiverAddresses,
33
- );
34
-
35
- // Notify sender of the transaction details
36
- context.sendTo(
37
- `You sent ${amount * receiverAddresses.length} tokens in total.`,
38
- [sender.address],
39
- );
40
- }