@hybrd/xmtp 1.4.4 → 2.0.0
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.
- package/README.md +209 -319
- package/dist/index.cjs +138 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +131 -78
- package/dist/index.js.map +1 -1
- package/package.json +10 -6
- package/src/client.ts +19 -39
- package/src/index.ts +8 -5
- package/src/lib/jwt.ts +16 -23
- package/src/lib/secret.ts +33 -0
- package/src/plugin.ts +98 -39
- package/src/index.ts.old +0 -145
- package/src/lib/message-listener.test.ts.old +0 -369
- package/src/lib/message-listener.ts.old +0 -343
- package/src/localStorage.ts.old +0 -203
- package/src/plugin.filters.test.ts +0 -158
- package/src/service-client.ts.old +0 -309
- package/src/transactionMonitor.ts.old +0 -275
- package/src/types.ts.old +0 -157
package/src/localStorage.ts.old
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import * as fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { validateEnvironment } from "./client";
|
|
5
|
-
import type { WalletInfo, WalletStorage } from "./walletService";
|
|
6
|
-
|
|
7
|
-
const { XMTP_NETWORK_ID } = validateEnvironment(["XMTP_NETWORK_ID"]);
|
|
8
|
-
export const STORAGE_DIRS = {
|
|
9
|
-
WALLET: ".data/wallet_data",
|
|
10
|
-
XMTP: ".data/xmtp",
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generic file-based storage service
|
|
15
|
-
*/
|
|
16
|
-
export class FileStorage implements WalletStorage {
|
|
17
|
-
private initialized = false;
|
|
18
|
-
|
|
19
|
-
constructor(private baseDirs = STORAGE_DIRS) {
|
|
20
|
-
this.initialize();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Initialize storage directories
|
|
25
|
-
*/
|
|
26
|
-
public initialize(): void {
|
|
27
|
-
if (this.initialized) return;
|
|
28
|
-
|
|
29
|
-
Object.values(this.baseDirs).forEach((dir) => {
|
|
30
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
this.initialized = true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* File operations - save/read/delete
|
|
38
|
-
*/
|
|
39
|
-
private async saveToFile(
|
|
40
|
-
directory: string,
|
|
41
|
-
identifier: string,
|
|
42
|
-
data: string,
|
|
43
|
-
): Promise<boolean> {
|
|
44
|
-
const key = `${identifier}-${XMTP_NETWORK_ID}`;
|
|
45
|
-
try {
|
|
46
|
-
await fs.writeFile(path.join(directory, `${key}.json`), data);
|
|
47
|
-
return true;
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.error(`Error writing to file ${key}:`, error);
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private async readFromFile<T>(
|
|
55
|
-
directory: string,
|
|
56
|
-
identifier: string,
|
|
57
|
-
): Promise<T | null> {
|
|
58
|
-
try {
|
|
59
|
-
const key = `${identifier}-${XMTP_NETWORK_ID}`;
|
|
60
|
-
const data = await fs.readFile(
|
|
61
|
-
path.join(directory, `${key}.json`),
|
|
62
|
-
"utf-8",
|
|
63
|
-
);
|
|
64
|
-
return JSON.parse(data) as T;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (
|
|
67
|
-
error instanceof Error &&
|
|
68
|
-
(error.message.includes("ENOENT") ||
|
|
69
|
-
error.message.includes("no such file or directory"))
|
|
70
|
-
) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
public async deleteFile(directory: string, key: string): Promise<boolean> {
|
|
78
|
-
try {
|
|
79
|
-
await fs.unlink(path.join(directory, `${key}.json`));
|
|
80
|
-
return true;
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error(`Error deleting file ${key}:`, error);
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Generic data operations
|
|
89
|
-
*/
|
|
90
|
-
public async saveData(
|
|
91
|
-
category: string,
|
|
92
|
-
id: string,
|
|
93
|
-
data: unknown,
|
|
94
|
-
): Promise<boolean> {
|
|
95
|
-
if (!this.initialized) this.initialize();
|
|
96
|
-
|
|
97
|
-
// Make sure the directory exists
|
|
98
|
-
const directory = path.join(".data", category);
|
|
99
|
-
if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
|
|
100
|
-
|
|
101
|
-
return await this.saveToFile(directory, id, JSON.stringify(data));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
public async getData<T>(category: string, id: string): Promise<T | null> {
|
|
105
|
-
if (!this.initialized) this.initialize();
|
|
106
|
-
|
|
107
|
-
const directory = path.join(".data", category);
|
|
108
|
-
return this.readFromFile<T>(directory, id);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
public async listData<T>(category: string): Promise<T[]> {
|
|
112
|
-
if (!this.initialized) this.initialize();
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const directory = path.join(".data", category);
|
|
116
|
-
if (!existsSync(directory)) return [];
|
|
117
|
-
|
|
118
|
-
const files = await fs.readdir(directory);
|
|
119
|
-
const items: T[] = [];
|
|
120
|
-
|
|
121
|
-
for (const file of files.filter((f) => f.endsWith(".json"))) {
|
|
122
|
-
const id = file.replace(`-${XMTP_NETWORK_ID}.json`, "");
|
|
123
|
-
const data = await this.getData<T>(category, id);
|
|
124
|
-
if (data) items.push(data);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return items;
|
|
128
|
-
} catch (error) {
|
|
129
|
-
console.error(`Error listing data in ${category}:`, error);
|
|
130
|
-
return [];
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
public async deleteData(category: string, id: string): Promise<boolean> {
|
|
135
|
-
if (!this.initialized) this.initialize();
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const directory = path.join(".data", category);
|
|
139
|
-
const key = `${id}-${XMTP_NETWORK_ID}`;
|
|
140
|
-
return await this.deleteFile(directory, key);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
console.error(`Error deleting data ${id} from ${category}:`, error);
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Wallet Storage implementation
|
|
149
|
-
*/
|
|
150
|
-
public async saveWallet(userId: string, walletData: string): Promise<void> {
|
|
151
|
-
if (!this.initialized) this.initialize();
|
|
152
|
-
await this.saveToFile(this.baseDirs.WALLET, userId, walletData);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
public async getWallet(userId: string): Promise<WalletInfo | null> {
|
|
156
|
-
if (!this.initialized) this.initialize();
|
|
157
|
-
return this.readFromFile(this.baseDirs.WALLET, userId);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
public async getWalletByAddress(address: string): Promise<WalletInfo | null> {
|
|
161
|
-
if (!this.initialized) this.initialize();
|
|
162
|
-
try {
|
|
163
|
-
const directory = this.baseDirs.WALLET;
|
|
164
|
-
if (!existsSync(directory)) return null;
|
|
165
|
-
|
|
166
|
-
const files = await fs.readdir(directory);
|
|
167
|
-
|
|
168
|
-
for (const file of files.filter((f) => f.endsWith(".json"))) {
|
|
169
|
-
try {
|
|
170
|
-
const data = await fs.readFile(path.join(directory, file), "utf-8");
|
|
171
|
-
const walletData = JSON.parse(data) as WalletInfo;
|
|
172
|
-
|
|
173
|
-
// Check if this wallet has the target address
|
|
174
|
-
if (walletData.address.toLowerCase() === address.toLowerCase()) {
|
|
175
|
-
return walletData;
|
|
176
|
-
}
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.error(`Error parsing wallet data from ${file}:`, err);
|
|
179
|
-
// Skip files with parsing errors
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return null;
|
|
185
|
-
} catch (error) {
|
|
186
|
-
console.error(`Error finding wallet by address ${address}:`, error);
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
public async getWalletCount(): Promise<number> {
|
|
192
|
-
try {
|
|
193
|
-
const files = await fs.readdir(this.baseDirs.WALLET);
|
|
194
|
-
return files.filter((file) => file.endsWith(".json")).length;
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.error("Error getting wallet count:", error);
|
|
197
|
-
return 0;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Export a single global instance
|
|
203
|
-
export const storage = new FileStorage();
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import type { HonoVariables, PluginContext } from "@hybrd/types"
|
|
2
|
-
import { Hono } from "hono"
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
4
|
-
import { XMTPPlugin } from "./plugin"
|
|
5
|
-
|
|
6
|
-
// Mock @xmtp/agent-sdk and ./client used by the plugin
|
|
7
|
-
vi.mock("@xmtp/agent-sdk", () => {
|
|
8
|
-
const handlers: Record<string, Array<(payload: unknown) => unknown>> = {}
|
|
9
|
-
const fakeXmtp = {
|
|
10
|
-
on: (event: string, cb: (payload: unknown) => unknown) => {
|
|
11
|
-
if (!handlers[event]) handlers[event] = []
|
|
12
|
-
handlers[event].push(cb)
|
|
13
|
-
},
|
|
14
|
-
emit: async (event: string, payload: unknown) => {
|
|
15
|
-
for (const cb of handlers[event] || []) {
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
17
|
-
await cb(payload)
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
start: vi.fn(async () => {})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
Agent: { create: vi.fn(async () => fakeXmtp) },
|
|
25
|
-
createUser: vi.fn(() => ({ account: { address: "0xabc" } })),
|
|
26
|
-
createSigner: vi.fn(() => ({})),
|
|
27
|
-
getTestUrl: vi.fn(() => "http://test"),
|
|
28
|
-
XmtpEnv: {},
|
|
29
|
-
__fakeXmtp: fakeXmtp
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
vi.mock("./client", () => {
|
|
34
|
-
const fakeConversation = {
|
|
35
|
-
id: "conv1",
|
|
36
|
-
send: vi.fn(async (_text: string) => {})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const fakeClient = {
|
|
40
|
-
inboxId: "inbox-1",
|
|
41
|
-
accountIdentifier: { identifier: "0xabc" },
|
|
42
|
-
conversations: {
|
|
43
|
-
list: vi.fn(async () => [] as unknown[]),
|
|
44
|
-
getConversationById: vi.fn(async (_id: string) => fakeConversation),
|
|
45
|
-
streamAllMessages: vi.fn(async function* () {
|
|
46
|
-
// Yield one message and finish
|
|
47
|
-
yield {
|
|
48
|
-
id: "msg1",
|
|
49
|
-
senderInboxId: "other-inbox",
|
|
50
|
-
content: "hello",
|
|
51
|
-
contentType: { sameAs: () => true },
|
|
52
|
-
conversationId: "conv1"
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function getDbPath(_name: string): Promise<string> {
|
|
59
|
-
return "/tmp/xmtp-test-db"
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
createXMTPClient: vi.fn(async () => fakeClient),
|
|
64
|
-
getDbPath
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
type MockAgentSdk = {
|
|
69
|
-
__fakeXmtp: {
|
|
70
|
-
emit: (event: string, payload: unknown) => Promise<void>
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function createTestAgent() {
|
|
75
|
-
return {
|
|
76
|
-
name: "test-agent",
|
|
77
|
-
plugins: { applyAll: vi.fn(async () => {}) },
|
|
78
|
-
createRuntimeContext: vi.fn(
|
|
79
|
-
async (base: unknown) => base as Record<string, unknown>
|
|
80
|
-
),
|
|
81
|
-
generate: vi.fn(async () => ({ text: "ok" }))
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
beforeEach(() => {
|
|
86
|
-
vi.resetModules()
|
|
87
|
-
vi.clearAllMocks()
|
|
88
|
-
process.env.XMTP_WALLET_KEY = "0xabc"
|
|
89
|
-
process.env.XMTP_DB_ENCRYPTION_KEY = "secret"
|
|
90
|
-
process.env.XMTP_ENV = "dev"
|
|
91
|
-
process.env.XMTP_ENABLE_NODE_STREAM = undefined
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
afterEach(() => {
|
|
95
|
-
process.env.XMTP_WALLET_KEY = undefined
|
|
96
|
-
process.env.XMTP_DB_ENCRYPTION_KEY = undefined
|
|
97
|
-
process.env.XMTP_ENV = undefined
|
|
98
|
-
process.env.XMTP_ENABLE_NODE_STREAM = undefined
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe("XMTPPlugin behaviors", () => {
|
|
102
|
-
it("blocks node stream messages when behaviors filter out", async () => {
|
|
103
|
-
const app = new Hono<{ Variables: HonoVariables }>()
|
|
104
|
-
const agent = createTestAgent()
|
|
105
|
-
|
|
106
|
-
// Mock behaviors to filter out messages
|
|
107
|
-
const mockBehaviors = {
|
|
108
|
-
executeBefore: vi.fn(async (context: any) => {
|
|
109
|
-
context.sendOptions = { filtered: true }
|
|
110
|
-
}),
|
|
111
|
-
executeAfter: vi.fn(async () => {})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const context = {
|
|
115
|
-
agent,
|
|
116
|
-
behaviors: mockBehaviors
|
|
117
|
-
} as unknown as PluginContext
|
|
118
|
-
|
|
119
|
-
const plugin = XMTPPlugin()
|
|
120
|
-
await plugin.apply(app, context)
|
|
121
|
-
|
|
122
|
-
// Allow async stream to tick
|
|
123
|
-
await new Promise((r) => setTimeout(r, 10))
|
|
124
|
-
|
|
125
|
-
// No generation or sending should have occurred
|
|
126
|
-
expect(agent.generate).not.toHaveBeenCalled()
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it("allows text handler when behaviors don't filter", async () => {
|
|
130
|
-
const app = new Hono<{ Variables: HonoVariables }>()
|
|
131
|
-
const agent = createTestAgent()
|
|
132
|
-
|
|
133
|
-
// Mock behaviors to not filter messages
|
|
134
|
-
const mockBehaviors = {
|
|
135
|
-
executeBefore: vi.fn(async () => {}),
|
|
136
|
-
executeAfter: vi.fn(async () => {})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const context = {
|
|
140
|
-
agent,
|
|
141
|
-
behaviors: mockBehaviors
|
|
142
|
-
} as unknown as PluginContext
|
|
143
|
-
|
|
144
|
-
process.env.XMTP_ENABLE_NODE_STREAM = "false"
|
|
145
|
-
const plugin = XMTPPlugin()
|
|
146
|
-
await plugin.apply(app, context)
|
|
147
|
-
|
|
148
|
-
const mocked = (await import("@xmtp/agent-sdk")) as unknown as MockAgentSdk
|
|
149
|
-
|
|
150
|
-
// Emit a text event
|
|
151
|
-
await mocked.__fakeXmtp.emit("text", {
|
|
152
|
-
conversation: { id: "conv1", send: vi.fn(async () => {}) },
|
|
153
|
-
message: { content: "hello" }
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
expect(agent.generate).toHaveBeenCalledTimes(1)
|
|
157
|
-
})
|
|
158
|
-
})
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview XMTP Service Client Library
|
|
3
|
-
*
|
|
4
|
-
* Clean, reusable client for making HTTP calls to the XMTP listener service.
|
|
5
|
-
* Handles authentication, request formatting, and error handling.
|
|
6
|
-
*
|
|
7
|
-
* This is different from the direct XMTP client - this is for external services
|
|
8
|
-
* talking to our XMTP listener service.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type {
|
|
12
|
-
GetMessageParams,
|
|
13
|
-
SendMessageParams,
|
|
14
|
-
SendMessageResponse,
|
|
15
|
-
SendReactionParams,
|
|
16
|
-
SendReactionResponse,
|
|
17
|
-
SendReplyParams,
|
|
18
|
-
SendReplyResponse,
|
|
19
|
-
SendTransactionParams,
|
|
20
|
-
SendTransactionResponse,
|
|
21
|
-
XmtpServiceClientConfig,
|
|
22
|
-
XmtpServiceMessage,
|
|
23
|
-
XmtpServiceResponse
|
|
24
|
-
} from "./types"
|
|
25
|
-
import { logger } from "./lib/logger"
|
|
26
|
-
|
|
27
|
-
export class XmtpServiceClient {
|
|
28
|
-
private config: XmtpServiceClientConfig
|
|
29
|
-
|
|
30
|
-
constructor(config: XmtpServiceClientConfig) {
|
|
31
|
-
this.config = config
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private async request<T = unknown>(
|
|
35
|
-
endpoint: string,
|
|
36
|
-
body?: Record<string, unknown>,
|
|
37
|
-
method: "GET" | "POST" = "POST"
|
|
38
|
-
): Promise<XmtpServiceResponse<T>> {
|
|
39
|
-
const startTime = performance.now()
|
|
40
|
-
logger.debug(`🌐 [HTTP] Starting ${method} request to ${endpoint}`)
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const baseUrl = this.config.serviceUrl.replace(/\/+$/, "")
|
|
44
|
-
|
|
45
|
-
// Use Authorization header for xmtp-tools endpoints, query parameter for others
|
|
46
|
-
const isXmtpToolsEndpoint = endpoint.startsWith("/xmtp-tools/")
|
|
47
|
-
const url = `${baseUrl}${endpoint}?token=${this.config.serviceToken}`
|
|
48
|
-
|
|
49
|
-
const headers: Record<string, string> = {
|
|
50
|
-
"Content-Type": "application/json"
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Add Authorization header for xmtp-tools endpoints
|
|
54
|
-
if (isXmtpToolsEndpoint) {
|
|
55
|
-
headers.Authorization = `Bearer ${this.config.serviceToken}`
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const fetchOptions: RequestInit = {
|
|
59
|
-
method,
|
|
60
|
-
headers
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (method === "POST" && body) {
|
|
64
|
-
fetchOptions.body = JSON.stringify(body)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const sanitizedUrl = `${baseUrl}${endpoint}?token=***`
|
|
68
|
-
logger.debug(`🌐 [HTTP] Making fetch request to ${sanitizedUrl}`)
|
|
69
|
-
const fetchStartTime = performance.now()
|
|
70
|
-
|
|
71
|
-
const response = await fetch(url, fetchOptions)
|
|
72
|
-
|
|
73
|
-
const fetchEndTime = performance.now()
|
|
74
|
-
logger.debug(`🌐 [HTTP] Fetch completed in ${(fetchEndTime - fetchStartTime).toFixed(2)}ms, status: ${response.status}`)
|
|
75
|
-
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
let errorMessage = `HTTP ${response.status}`
|
|
78
|
-
try {
|
|
79
|
-
const responseText = await response.text()
|
|
80
|
-
try {
|
|
81
|
-
const errorData = JSON.parse(responseText) as { error?: string }
|
|
82
|
-
errorMessage = errorData.error || errorMessage
|
|
83
|
-
} catch {
|
|
84
|
-
errorMessage = responseText || errorMessage
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
// If we can't read the response at all, use the status
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const endTime = performance.now()
|
|
91
|
-
logger.debug(`🌐 [HTTP] Request failed in ${(endTime - startTime).toFixed(2)}ms: ${errorMessage}`)
|
|
92
|
-
throw new Error(errorMessage)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const jsonStartTime = performance.now()
|
|
96
|
-
const result = {
|
|
97
|
-
success: true,
|
|
98
|
-
data: (await response.json()) as T
|
|
99
|
-
}
|
|
100
|
-
const jsonEndTime = performance.now()
|
|
101
|
-
logger.debug(`🌐 [HTTP] JSON parsing completed in ${(jsonEndTime - jsonStartTime).toFixed(2)}ms`)
|
|
102
|
-
|
|
103
|
-
const endTime = performance.now()
|
|
104
|
-
logger.debug(`🌐 [HTTP] Total request completed in ${(endTime - startTime).toFixed(2)}ms`)
|
|
105
|
-
|
|
106
|
-
return result
|
|
107
|
-
} catch (error) {
|
|
108
|
-
const endTime = performance.now()
|
|
109
|
-
logger.error(
|
|
110
|
-
`❌ [XmtpServiceClient] Request to ${endpoint} failed in ${(endTime - startTime).toFixed(2)}ms:`,
|
|
111
|
-
error
|
|
112
|
-
)
|
|
113
|
-
return {
|
|
114
|
-
success: false,
|
|
115
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async sendMessage(
|
|
121
|
-
params: SendMessageParams
|
|
122
|
-
): Promise<XmtpServiceResponse<SendMessageResponse>> {
|
|
123
|
-
return this.request<SendMessageResponse>("/xmtp-tools/send", {
|
|
124
|
-
content: params.content
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async sendReply(
|
|
129
|
-
params: SendReplyParams
|
|
130
|
-
): Promise<XmtpServiceResponse<SendReplyResponse>> {
|
|
131
|
-
return this.request<SendReplyResponse>("/xmtp-tools/reply", {
|
|
132
|
-
content: params.content,
|
|
133
|
-
messageId: params.messageId
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async sendReaction(
|
|
138
|
-
params: SendReactionParams
|
|
139
|
-
): Promise<XmtpServiceResponse<SendReactionResponse>> {
|
|
140
|
-
return this.request<SendReactionResponse>("/xmtp-tools/react", {
|
|
141
|
-
messageId: params.messageId,
|
|
142
|
-
emoji: params.emoji,
|
|
143
|
-
action: params.action
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async sendTransaction(
|
|
148
|
-
params: SendTransactionParams
|
|
149
|
-
): Promise<XmtpServiceResponse<SendTransactionResponse>> {
|
|
150
|
-
return this.request<SendTransactionResponse>("/xmtp-tools/transaction", {
|
|
151
|
-
fromAddress: params.fromAddress,
|
|
152
|
-
chainId: params.chainId,
|
|
153
|
-
calls: params.calls.map((call) => ({
|
|
154
|
-
to: call.to,
|
|
155
|
-
data: call.data,
|
|
156
|
-
...(call.gas && { gas: call.gas }),
|
|
157
|
-
value: call.value || "0x0",
|
|
158
|
-
metadata: {
|
|
159
|
-
...call.metadata,
|
|
160
|
-
chainId: params.chainId,
|
|
161
|
-
from: params.fromAddress,
|
|
162
|
-
version: "1"
|
|
163
|
-
}
|
|
164
|
-
}))
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Get a single message by ID
|
|
170
|
-
*/
|
|
171
|
-
async getMessage(
|
|
172
|
-
params: GetMessageParams
|
|
173
|
-
): Promise<XmtpServiceResponse<XmtpServiceMessage>> {
|
|
174
|
-
return this.request<XmtpServiceMessage>(
|
|
175
|
-
`/xmtp-tools/messages/${params.messageId}`,
|
|
176
|
-
undefined,
|
|
177
|
-
"GET"
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// getConversationMessages removed - superseded by thread-based approach
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Create an XMTP service client from runtime context
|
|
186
|
-
* Expects the runtime context to have xmtpServiceUrl and xmtpServiceToken
|
|
187
|
-
*/
|
|
188
|
-
export function createXmtpServiceClient(
|
|
189
|
-
serviceUrl: string,
|
|
190
|
-
serviceToken: string
|
|
191
|
-
): XmtpServiceClient {
|
|
192
|
-
if (!serviceUrl || !serviceToken) {
|
|
193
|
-
throw new Error("Missing XMTP service URL or token from runtime context")
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return new XmtpServiceClient({
|
|
197
|
-
serviceUrl,
|
|
198
|
-
serviceToken
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export interface XmtpAuthConfig {
|
|
203
|
-
serviceUrl: string
|
|
204
|
-
serviceToken: string
|
|
205
|
-
source: "callback" | "environment"
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Get XMTP authentication configuration from multiple sources
|
|
210
|
-
* Priority: callback credentials > environment credentials
|
|
211
|
-
*/
|
|
212
|
-
export function getXmtpAuthConfig(
|
|
213
|
-
callbackUrl?: string,
|
|
214
|
-
callbackToken?: string
|
|
215
|
-
): XmtpAuthConfig | null {
|
|
216
|
-
// Priority 1: Use callback credentials if available
|
|
217
|
-
if (callbackUrl && callbackToken) {
|
|
218
|
-
console.log("🔑 [XmtpAuth] Using callback-provided credentials")
|
|
219
|
-
return {
|
|
220
|
-
serviceUrl: callbackUrl,
|
|
221
|
-
serviceToken: callbackToken,
|
|
222
|
-
source: "callback"
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Priority 2: Use environment credentials
|
|
227
|
-
const envUrl = process.env.XMTP_HOST
|
|
228
|
-
const envToken = process.env.XMTP_API_KEY
|
|
229
|
-
|
|
230
|
-
if (envUrl && envToken) {
|
|
231
|
-
console.log("🔑 [XmtpAuth] Using environment credentials")
|
|
232
|
-
return {
|
|
233
|
-
serviceUrl: envUrl,
|
|
234
|
-
serviceToken: envToken,
|
|
235
|
-
source: "environment"
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// No valid credentials found
|
|
240
|
-
console.error(
|
|
241
|
-
"❌ [XmtpAuth] No XMTP credentials found in callback or environment"
|
|
242
|
-
)
|
|
243
|
-
console.error(
|
|
244
|
-
"💡 [XmtpAuth] Expected: XMTP_HOST + XMTP_API_KEY or callback credentials"
|
|
245
|
-
)
|
|
246
|
-
return null
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Create an authenticated XMTP service client
|
|
251
|
-
* Handles both callback and environment credential sources
|
|
252
|
-
*/
|
|
253
|
-
export function createAuthenticatedXmtpClient(
|
|
254
|
-
callbackUrl?: string,
|
|
255
|
-
callbackToken?: string
|
|
256
|
-
): XmtpServiceClient {
|
|
257
|
-
const authConfig = getXmtpAuthConfig(callbackUrl, callbackToken)
|
|
258
|
-
|
|
259
|
-
if (!authConfig) {
|
|
260
|
-
throw new Error("No XMTP credentials found")
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
console.log(
|
|
264
|
-
`🔗 [XmtpAuth] Creating XMTP client (${authConfig.source} credentials)`
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
return createXmtpServiceClient(authConfig.serviceUrl, authConfig.serviceToken)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Constructs a URL for XMTP tools API endpoints with token authentication
|
|
272
|
-
*
|
|
273
|
-
* @param {string} baseUrl - The base URL of the XMTP service (e.g., "https://api.example.com")
|
|
274
|
-
* @param {string} action - The specific action/endpoint to call (e.g., "send", "receive", "status")
|
|
275
|
-
* @param {string} token - Authentication token (either JWT or API key)
|
|
276
|
-
* @returns {string} Complete URL with token as query parameter
|
|
277
|
-
*
|
|
278
|
-
* @description
|
|
279
|
-
* Builds URLs for XMTP tools endpoints using query parameter authentication.
|
|
280
|
-
* The token is appended as a query parameter for GET request authentication,
|
|
281
|
-
* following the pattern: `/xmtp-tools/{action}?token={token}`
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* ```typescript
|
|
285
|
-
* const url = getXMTPToolsUrl(
|
|
286
|
-
* "https://api.hybrid.dev",
|
|
287
|
-
* "send",
|
|
288
|
-
* "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
289
|
-
* );
|
|
290
|
-
* // Returns: "https://api.hybrid.dev/xmtp-tools/send?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
291
|
-
* ```
|
|
292
|
-
*
|
|
293
|
-
* @example
|
|
294
|
-
* ```typescript
|
|
295
|
-
* // Using with API key
|
|
296
|
-
* const url = getXMTPToolsUrl(
|
|
297
|
-
* process.env.XMTP_BASE_URL,
|
|
298
|
-
* "status",
|
|
299
|
-
* process.env.XMTP_API_KEY
|
|
300
|
-
* );
|
|
301
|
-
* ```
|
|
302
|
-
*/
|
|
303
|
-
export function getXMTPToolsUrl(
|
|
304
|
-
baseUrl: string,
|
|
305
|
-
action: string,
|
|
306
|
-
token: string
|
|
307
|
-
): string {
|
|
308
|
-
return `${baseUrl}/xmtp-tools/${action}?token=${token}`
|
|
309
|
-
}
|