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

Sign up to get free protection for your applications and to get access to all the features.
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 +42 -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
- }