This commit is contained in:
gzuuus
2025-02-08 12:21:16 +01:00
parent b5e8ba4cbc
commit 1625661356
13 changed files with 665 additions and 0 deletions

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
# Nostr
PRIVATE_KEY=your_private_key_here
RELAY_URLS=wss://relay.damus.io,wss://relay.nostr.band,wss://nos.lol
# MCP Service Info
MCP_SERVICE_NAME="DVM MCP Bridge"
MCP_SERVICE_ABOUT="MCP-enabled DVM providing AI and computational tools"
# MCP Client Connection
MCP_CLIENT_NAME="DVM MCP Bridge Client"
MCP_CLIENT_VERSION="1.0.0"
MCP_SERVER_COMMAND=bun # The command to start the MCP server
MCP_SERVER_ARGS=run,src/external-mcp-server.ts # Comma-separated args to pass to the server command

179
.gitignore vendored Normal file
View File

@@ -0,0 +1,179 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
#
codebase*
.aider*

View File

@@ -0,0 +1,8 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80,
"bracketSpacing": true
}

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# dvmcp
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

98
bun.lock Normal file
View File

@@ -0,0 +1,98 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "dvmcp",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"@noble/hashes": "^1.7.1",
"dotenv": "^16.4.7",
"nostr-tools": "^2.10.4",
"prettier": "^3.4.2",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5.0.0",
},
},
},
"packages": {
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.4.1", "", { "dependencies": { "content-type": "^1.0.5", "eventsource": "^3.0.2", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g=="],
"@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="],
"@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="],
"@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
"@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="],
"@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="],
"@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="],
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
"@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="],
"eventsource": ["eventsource@3.0.5", "", { "dependencies": { "eventsource-parser": "^3.0.0" } }, "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw=="],
"eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"nostr-tools": ["nostr-tools@2.10.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1" }, "optionalDependencies": { "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg=="],
"nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="],
"prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.1", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w=="],
"@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="],
"@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
"@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="],
"nostr-tools/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="],
}
}

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "dvmcp",
"module": "src/dvm-bridge.ts",
"type": "module",
"scripts": {
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
"dev": "bun --watch src/dvm-bridge.ts",
"start": "bun run src/dvm-bridge.ts",
"typecheck": "tsc --noEmit",
"lint": "bun run typecheck && bun run format"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.4.1",
"@noble/hashes": "^1.7.1",
"dotenv": "^16.4.7",
"nostr-tools": "^2.10.4",
"prettier": "^3.4.2"
}
}

21
src/config.ts Normal file
View File

@@ -0,0 +1,21 @@
import { config } from 'dotenv';
config();
export const CONFIG = {
nostr: {
privateKey: process.env.PRIVATE_KEY!,
relayUrls: process.env.RELAY_URLS!.split(',').map((url) => url.trim()),
},
mcp: {
// Service info
name: process.env.MCP_SERVICE_NAME || 'DVM MCP Bridge',
about:
process.env.MCP_SERVICE_ABOUT ||
'MCP-enabled DVM providing AI and computational tools',
// Client connection info
clientName: process.env.MCP_CLIENT_NAME!,
clientVersion: process.env.MCP_CLIENT_VERSION!,
serverCommand: process.env.MCP_SERVER_COMMAND!,
serverArgs: process.env.MCP_SERVER_ARGS!.split(','),
},
};

108
src/dvm-bridge.ts Normal file
View File

@@ -0,0 +1,108 @@
import { MCPClientHandler } from './mcp-client';
import { NostrAnnouncer } from './nostr/announcer';
import { RelayHandler } from './nostr/relay';
import { keyManager } from './nostr/keys';
import { CONFIG } from './config';
import type { Event } from 'nostr-tools/pure';
export class DVMBridge {
private mcpClient: MCPClientHandler;
private nostrAnnouncer: NostrAnnouncer;
private relayHandler: RelayHandler;
constructor() {
this.mcpClient = new MCPClientHandler();
this.nostrAnnouncer = new NostrAnnouncer();
this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls);
}
async start() {
await this.mcpClient.connect();
const tools = await this.mcpClient.listTools();
console.log('Available MCP tools:', tools);
await this.nostrAnnouncer.announceService();
this.relayHandler.subscribeToRequests(this.handleRequest.bind(this));
}
private async handleRequest(event: Event) {
try {
if (event.kind === 5000) {
const tools = await this.mcpClient.listTools();
const response = keyManager.signEvent({
...keyManager.createEventTemplate(6000),
content: JSON.stringify({
schema_version: '1.0',
tools,
}),
tags: [
['e', event.id],
['p', event.pubkey],
],
});
await this.relayHandler.publishEvent(response);
} else if (event.kind === 5001) {
const { name, parameters } = JSON.parse(event.content);
const processingStatus = keyManager.signEvent({
...keyManager.createEventTemplate(7000),
tags: [
['status', 'processing'],
['e', event.id],
['p', event.pubkey],
],
});
await this.relayHandler.publishEvent(processingStatus);
try {
const result = await this.mcpClient.callTool(name, parameters);
const successStatus = keyManager.signEvent({
...keyManager.createEventTemplate(7000),
tags: [
['status', 'success'],
['e', event.id],
['p', event.pubkey],
],
});
await this.relayHandler.publishEvent(successStatus);
const response = keyManager.signEvent({
...keyManager.createEventTemplate(6001),
content: JSON.stringify(result),
tags: [
['e', event.id],
['p', event.pubkey],
],
});
await this.relayHandler.publishEvent(response);
} catch (error) {
const errorStatus = keyManager.signEvent({
...keyManager.createEventTemplate(7000),
tags: [
[
'status',
'error',
error instanceof Error ? error.message : 'Unknown error',
],
['e', event.id],
['p', event.pubkey],
],
});
await this.relayHandler.publishEvent(errorStatus);
}
}
} catch (error) {
console.error('Error handling request:', error);
}
}
async stop() {
await this.mcpClient.disconnect();
this.relayHandler.cleanup();
}
}

49
src/mcp-client.ts Normal file
View File

@@ -0,0 +1,49 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { CONFIG } from './config';
export class MCPClientHandler {
private client: Client;
private transport: StdioClientTransport;
constructor() {
this.transport = new StdioClientTransport({
command: CONFIG.mcp.serverCommand,
args: CONFIG.mcp.serverArgs,
});
this.client = new Client(
{
name: CONFIG.mcp.clientName,
version: CONFIG.mcp.clientVersion,
},
{
capabilities: {
tools: {},
prompts: {},
resources: {},
},
}
);
}
async connect() {
await this.client.connect(this.transport);
console.log('Connected to MCP server');
}
async listTools() {
return await this.client.listTools();
}
async callTool(name: string, args: Record<string, any>) {
return await this.client.callTool({
name,
arguments: args,
});
}
async disconnect() {
await this.transport.close();
}
}

30
src/nostr/announcer.ts Normal file
View File

@@ -0,0 +1,30 @@
import { CONFIG } from '../config';
import { RelayHandler } from './relay';
import { keyManager } from './keys';
export class NostrAnnouncer {
private relayHandler: RelayHandler;
constructor() {
this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls);
}
async announceService() {
const event = keyManager.signEvent({
...keyManager.createEventTemplate(31990),
content: JSON.stringify({
name: CONFIG.mcp.name,
about: CONFIG.mcp.about,
}),
tags: [
['d', Math.random().toString(36).substring(7)],
['k', '5000'],
['k', '5001'],
['capabilities', 'mcp-1.0'],
['t', 'mcp'],
],
});
await this.relayHandler.publishEvent(event);
}
}

33
src/nostr/keys.ts Normal file
View File

@@ -0,0 +1,33 @@
import { hexToBytes } from '@noble/hashes/utils';
import { getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import type { Event } from 'nostr-tools/pure';
import { CONFIG } from '../config';
export type UnsignedEvent = Omit<Event, 'sig' | 'id'>;
export const createKeyManager = (privateKeyHex: string) => {
const privateKeyBytes = hexToBytes(privateKeyHex);
const pubkey = getPublicKey(privateKeyBytes);
class Manager {
public readonly pubkey = pubkey;
signEvent(eventInitial: UnsignedEvent): Event {
return finalizeEvent(eventInitial, privateKeyBytes);
}
createEventTemplate(kind: number): UnsignedEvent {
return {
kind,
pubkey: this.pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: '',
};
}
}
return new Manager();
};
export const keyManager = createKeyManager(CONFIG.nostr.privateKey);

59
src/nostr/relay.ts Normal file
View File

@@ -0,0 +1,59 @@
import { SimplePool } from 'nostr-tools/pool';
import type { Event } from 'nostr-tools/pure';
import type { SubCloser } from 'nostr-tools/pool';
import WebSocket from 'ws';
import { useWebSocketImplementation } from 'nostr-tools/pool';
import type { Filter } from 'nostr-tools';
useWebSocketImplementation(WebSocket);
export class RelayHandler {
private pool: SimplePool;
private relayUrls: string[];
private subscriptions: SubCloser[] = [];
constructor(relayUrls: string[]) {
this.pool = new SimplePool();
this.relayUrls = relayUrls;
}
async publishEvent(event: Event): Promise<void> {
try {
await Promise.any(this.pool.publish(this.relayUrls, event));
console.log('Event published:', event.id);
} catch (error) {
console.error('Failed to publish event:', error);
throw error;
}
}
subscribeToRequests(onRequest: (event: Event) => void): SubCloser {
const filters: Filter[] = [
{
kinds: [5000, 5001],
},
];
const sub = this.pool.subscribeMany(this.relayUrls, filters, {
onevent(event) {
onRequest(event);
},
oneose() {
console.log('Reached end of stored events');
},
});
this.subscriptions.push(sub);
return sub;
}
async queryEvents(filter: Filter): Promise<Event[]> {
return await this.pool.querySync(this.relayUrls, filter);
}
cleanup() {
this.subscriptions.forEach((sub) => sub.close());
this.subscriptions = [];
this.pool.close(this.relayUrls);
}
}

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}