diff --git a/bun.lockb b/bun.lockb index fa813e2..f47b665 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml index b5a3b99..e798835 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: # override default entrypoint allows us to do `bun install` before serving entrypoint: [] # execute bun install before we start the dev server in watch mode - command: "/bin/sh -c 'bun install && bun run --watch src/index.ts'" + command: "/bin/sh -c 'bun install && NODE_ENV=development bun run --watch src/index.ts'" # expose the right ports ports: ["8080:8080"] # setup a host mounted volume to sync changes to the container diff --git a/package.json b/package.json index 04835b8..3fb57e0 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "scripts": { - "dev": "bun --watch src/index.ts", + "dev": "NODE_ENV=development bun --watch src/index.ts", "build": "bun build src/index.ts", "start": "NODE_ENV=production bun src/index.ts", "test": "bun test", @@ -34,4 +34,4 @@ "viem": "^2.9.5", "zod": "^3.22.4" } -} +} \ No newline at end of file diff --git a/src/actions/create-xmtp-group.ts b/src/actions/create-xmtp-group.ts index 30c439a..9d69bd1 100644 --- a/src/actions/create-xmtp-group.ts +++ b/src/actions/create-xmtp-group.ts @@ -9,6 +9,7 @@ import { bot } from "../lib/xmtp/client"; import { getDeployments } from "./get-deployments"; import { getGroupByWalletAddress } from "./get-group-by-wallet-address"; import { addMembers } from "./add-members"; +import type { CliError } from "../lib/xmtp/cli"; /** * TODO: ? @@ -74,15 +75,18 @@ export async function createXmtpGroup( if (groupId) await db.insert(schema.groups).values({ id: groupId }); } catch (e) { console.log("failed to create group", groupId); - console.error("error code ->", (e as any).exitCode); - console.error("error ->", (e as any).stderr.toString()); - } finally { - if (!groupId) { - console.log("Failed to create group"); - return { groupId: undefined }; + if (e instanceof Error) console.error(e.message); + if (e && typeof e === "object" && "exitCode" in e && "stderr" in e) { + console.error("error code ->", (e as CliError).exitCode); + console.error("error ->", (e as CliError).stderr.toString()); } } + if (!groupId) { + console.log("Failed to create group"); + return { groupId: undefined }; + } + let deployments: Awaited> | undefined = undefined; diff --git a/src/index.ts b/src/index.ts index cd69d9c..cf303c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,16 @@ -import { Elysia, t } from "elysia"; -import { - createXmtpGroup, - createXmtpGroupValidator, -} from "./actions/create-xmtp-group"; -import { getGroup } from "./actions/get-group"; -import { syncPendingMembers } from "./actions/sync-pending-members"; -import { AddressLiteral } from "./lib/validators"; -import { getOwnersSafes } from "./actions/get-owners-safes"; -import { getGroupsByWalletAddresses } from "./actions/get-group-by-wallet-address"; -import { addMembers } from "./actions/add-members"; -import { removeMembers } from "./actions/remove-members"; -import { cron, Patterns } from "@elysiajs/cron"; -import { db } from "./db"; -import { sql } from "drizzle-orm"; +import { Elysia, t } from 'elysia' +import { createXmtpGroup, createXmtpGroupValidator } from './actions/create-xmtp-group' +import { getGroup } from './actions/get-group' +import { syncPendingMembers } from './actions/sync-pending-members' +import { AddressLiteral } from './lib/validators' +import { getOwnersSafes } from './actions/get-owners-safes' +import { getGroupsByWalletAddresses } from './actions/get-group-by-wallet-address' +import { addMembers } from './actions/add-members' +import { removeMembers } from './actions/remove-members' +import { cron, Patterns } from '@elysiajs/cron' +import { db } from './db' +import { sql } from 'drizzle-orm' +import { bot } from './lib/xmtp/client' /** * This service is responsible for keeping xmtp group chat members in sync with the members of a safe. @@ -29,6 +27,7 @@ import { sql } from "drizzle-orm"; * another call will have to be made to the service once the account is deployed. * * TODO: Add a method to link a deployed counterfactual account to the group chat + * TODO: Add the ability for a member to remove themselves from a group chat * ! only run the next two methods if the group chat is a **deployed** safe * TODO: Add a job to periodically check for new members in a safe and add them to the group chat * TODO: Add a job to periodically check for removed members in a safe and remove them from the group chat @@ -37,7 +36,7 @@ import { sql } from "drizzle-orm"; export default new Elysia() .use( cron({ - name: "heartbeat", + name: 'heartbeat', pattern: Patterns.EVERY_10_SECONDS, run() { console.log( @@ -46,111 +45,108 @@ export default new Elysia() sql`SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size();`, )[0] / 1024 } KB`, - ); + ) }, }), ) .use( cron({ - name: "sync-pending-members", + name: 'sync-pending-members', pattern: Patterns.EVERY_5_MINUTES, async run() { - console.log("try sync pending members"); - await syncPendingMembers().catch((e) => console.error(e)); + console.log('try sync pending members') + await syncPendingMembers().catch((e) => console.error(e)) }, }), ) - .get("/", () => "Onit XMTP bot 🤖") - .group( - "/wallet/:address", - { params: t.Object({ address: AddressLiteral }) }, - (app) => { - return app.get("/", async ({ params: { address } }) => { - console.log("getting groups by address", address); - // - get the addresses safes - const safes = await getOwnersSafes(address); + .get('/', async () => { + if (process.env.NODE_ENV === 'development') { + console.log('groups', await bot.listGroups()) + await bot.send('5eb5b1fa27adc585a75cdedd6a1d4d5d', 'Hello').catch(console.error) + } - console.log("safes ->", safes); + return 'Onit XMTP bot 🤖' + }) + .group('/wallet/:address', { params: t.Object({ address: AddressLiteral }) }, (app) => { + return app.get('/', async ({ params: { address } }) => { + console.log('getting groups by address', address) + // - get the addresses safes + const safes = await getOwnersSafes(address) - // - check for groups with the safe address - return (await getGroupsByWalletAddresses(safes)) || []; - }); - }, - ) - .group("/group/:groupId", (app) => { + console.log('safes ->', safes) + + // - check for groups with the safe address + return (await getGroupsByWalletAddresses(safes)) || [] + }) + }) + .group('/group/:groupId', (app) => { return app - .get("/", async ({ params: { groupId } }) => { - if (!groupId) return "Invalid group id"; - return await getGroup(groupId); + .get('/', async ({ params: { groupId } }) => { + if (!groupId) return 'Invalid group id' + return await getGroup(groupId) }) - .get("/members", async ({ params: { groupId } }) => { - if (!groupId) return "Invalid group id"; - return (await getGroup(groupId))?.pendingMembers || []; + .get('/members', async ({ params: { groupId } }) => { + if (!groupId) return 'Invalid group id' + return (await getGroup(groupId))?.pendingMembers || [] }) .post( - "/members", + '/members', async ({ params: { groupId }, body: { members, type } }) => { - const group = await getGroup(groupId); - if (!groupId || !group) return "Invalid group id"; + const group = await getGroup(groupId) + if (!groupId || !group) return 'Invalid group id' // - we only enable adding and removing members if a wallet is not already attached to the group if (group.wallets.length) - return "Members on group chat with wallets are managed by who is a signer on each of the wallet"; + return 'Members on group chat with wallets are managed by who is a signer on each of the wallet' switch (type) { - case "add": - return addMembers(groupId, members); - case "remove": - return removeMembers(groupId, members); + case 'add': + return addMembers(groupId, members) + case 'remove': + return removeMembers(groupId, members) default: - return "Invalid type"; + return 'Invalid type' } }, { body: t.Object({ members: t.Array(AddressLiteral), - type: t.Union([t.Literal("add"), t.Literal("remove")]), + type: t.Union([t.Literal('add'), t.Literal('remove')]), }), }, ) - .get("/wallets", async ({ params: { groupId } }) => { - if (!groupId) return "Invalid group id"; - return (await getGroup(groupId))?.wallets || []; + .get('/wallets', async ({ params: { groupId } }) => { + if (!groupId) return 'Invalid group id' + return (await getGroup(groupId))?.wallets || [] }) - .post("/link-wallet", async ({ params: { groupId }, body }) => { + .post('/link-wallet', async ({ params: { groupId }, body }) => { // TODO: check that each member is a member of the group // TODO: if so then add the wallet to the group // TODO: if not return an error - return "Not implemented"; - }); + return 'Not implemented' + }) }) - .group("/bot", (app) => { + .group('/bot', (app) => { return app - .get("/sync-pending-members", async () => { - const pendingMembers = await syncPendingMembers(); - return JSON.stringify(pendingMembers, null, 4); + .get('/sync-pending-members', async () => { + const pendingMembers = await syncPendingMembers() + return JSON.stringify(pendingMembers, null, 4) }) .post( - "/create", + '/create', async ({ body }) => { - const result = await createXmtpGroup(body); + const result = await createXmtpGroup(body) - const { groupId, members, pendingMembers, deployments } = result; + const { groupId, members, pendingMembers, deployments } = result - console.log( - "Created group", - groupId, - members, - pendingMembers, - deployments, - ); + console.log('Created group', groupId, members, pendingMembers, deployments) - if (!groupId) return "Failed to create group"; + if (!groupId) return 'Failed to create group' - return result; + return result }, { body: createXmtpGroupValidator }, - ); + ) }) .listen(8080, ({ hostname, port }) => { - console.log(`🦊 Elysia is running at http://${hostname}:${port}`); - }); + console.log(`🦊 Elysia is running at http://${hostname}:${port}`) + }) diff --git a/src/lib/xmtp/cli.ts b/src/lib/xmtp/cli.ts index 12f8061..61c6803 100644 --- a/src/lib/xmtp/cli.ts +++ b/src/lib/xmtp/cli.ts @@ -1,108 +1,109 @@ -import { Client } from "@xmtp/xmtp-js"; -import { $, Glob } from "bun"; -import { getWalletClient } from "../eth/clients"; +import { Client } from '@xmtp/xmtp-js' +import { $, Glob } from 'bun' +import { getWalletClient } from '../eth/clients' -const glob = new Glob("cli-binary"); +const glob = new Glob('cli-binary') // This binary was downloaded from https://github.com/xmtp/libxmtp/releases/tag/cli-a8d3dd9 // You must download an appropriate binary for your system's architecture -let BINARY_PATH: string | undefined = undefined; +let BINARY_PATH: string | undefined = undefined for (const file of glob.scanSync({ absolute: true })) { - if (file) BINARY_PATH = file; + if (file) BINARY_PATH = file } async function generateV2Client() { - const { mnemonic, walletClient } = getWalletClient(); + const { mnemonic, walletClient } = getWalletClient() // - ensure the wallet exists on the network await Client.create( // @ts-expect-error: xmtp types do not like private key accounts walletClient, - { env: "dev" }, - ); - return mnemonic; + { env: 'dev' }, + ) + return mnemonic } export async function createClient(dbPath: string) { - const runCommandTemplate = `${BINARY_PATH} --db ${dbPath} --json`; + const runCommandTemplate = `${BINARY_PATH} --db ${dbPath} --json` - let accountAddress: string | undefined = undefined; + let accountAddress: string | undefined = undefined try { accountAddress = ( - await extractDataFromOutput<{ account_address: string }>( - `${runCommandTemplate} info`, - ) - )?.account_address; + await extractDataFromOutput<{ account_address: string }>(`${runCommandTemplate} info`) + )?.account_address } catch (e) { - const seedPhrase = await generateV2Client(); + const seedPhrase = await generateV2Client() accountAddress = ( await extractDataFromOutput<{ account_address: string }>( `${runCommandTemplate} register --seed-phrase "${seedPhrase}"`, ) - )?.account_address; + )?.account_address } return { accountAddress, - async createGroup(permissions = "group-creator-is-admin") { + async createGroup(permissions = 'group-creator-is-admin') { return ( await extractDataFromOutput<{ group_id: string }>( `${runCommandTemplate} create-group ${permissions}`, ) - )?.group_id; + )?.group_id }, async addMembers(groupId: string, accountAddresses: string[]) { return await extractDataFromOutput( `${runCommandTemplate} add-group-members ${groupId} --account-addresses ${accountAddresses.join( - " ", + ' ', )}`, - ); + ) }, async removeMembers(groupId: string, accountAddresses: string[]) { return await extractDataFromOutput( `${runCommandTemplate} remove-group-members ${groupId} --account-addresses ${accountAddresses.join( - " ", + ' ', )}`, - ); + ) }, async listGroups() { - return ( - await extractDataFromOutput<{ groups: any[] }>( - `${runCommandTemplate} list-groups`, - ) - )?.groups; + return (await extractDataFromOutput<{ groups: any[] }>(`${runCommandTemplate} list-groups`)) + ?.groups }, async send(groupId: string, message: string) { - return await extractDataFromOutput( - `${runCommandTemplate} send ${groupId} --message ${message}`, - ); + return await extractDataFromOutput(`${runCommandTemplate} send ${groupId} ${message}`) }, async listMessages(groupId: string) { return ( await extractDataFromOutput<{ messages: any[] }>( `${runCommandTemplate} list-group-messages ${groupId}`, ) - )?.messages; + )?.messages }, - }; + } } async function extractDataFromOutput>( command: string, ): Promise< | ({ - level: number; - time: number; - msg: string; - command_output: boolean; + level: number + time: number + msg: string + command_output: boolean } & T) | undefined > { // @ts-expect-error for await (const line of $({ raw: [command] }).lines()) { try { - const data = JSON.parse(line); - if (data?.command_output) return data; + const data = JSON.parse(line) + if (data?.command_output) return data } catch (e) {} } } + +export class CliError { + constructor( + public exitCode: number, + public stdout: Buffer, + public stderr: Buffer, + ) {} +}