Skip to content

Commit

Permalink
wip: add wallet linking
Browse files Browse the repository at this point in the history
  • Loading branch information
peterferguson committed Apr 10, 2024
1 parent 552ca20 commit b4114f1
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 74 deletions.
213 changes: 141 additions & 72 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
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'
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,
ChainAwareAddressLiteral,
HexLiteral,
} 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";
import { getLinkWalletEIP712TypedData } from "./lib/eth/link-wallet-sign-typed-data";
import { verifyTypedData } from "viem";
import { TypedDataDomain } from "abitype/zod";

/**
* This service is responsible for keeping xmtp group chat members in sync with the members of a safe.
Expand All @@ -36,7 +46,7 @@ import { bot } from './lib/xmtp/client'
export default new Elysia()
.use(
cron({
name: 'heartbeat',
name: "heartbeat",
pattern: Patterns.EVERY_10_SECONDS,
run() {
console.log(
Expand All @@ -45,108 +55,167 @@ 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('/', async () => {
if (process.env.NODE_ENV === 'development') {
console.log('groups', await bot.listGroups())
await bot.send('5eb5b1fa27adc585a75cdedd6a1d4d5d', 'Hello').catch(console.error)
.get("/", async () => {
if (process.env.NODE_ENV === "development") {
console.log("groups", await bot.listGroups());
await bot
.send("5eb5b1fa27adc585a75cdedd6a1d4d5d", "Hello")
.catch(console.error);
}

return 'Onit XMTP bot 🤖'
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)
.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);

console.log('safes ->', safes)
console.log("safes ->", safes);

// - check for groups with the safe address
return (await getGroupsByWalletAddresses(safes)) || []
})
})
.group('/group/:groupId', (app) => {
// - 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 || []
})
.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'
.get("/wallets", async ({ params: { groupId } }) => {
if (!groupId) return "Invalid group id";
return (await getGroup(groupId))?.wallets || [];
})
.group(
"/link-wallet",
{
params: t.Object({
chainAwareAddress: ChainAwareAddressLiteral,
groupId: t.String(),
}),
},
(app) => {
return app
.get(
"/:chainAwareAddress",
async ({ params: { groupId, chainAwareAddress } }) => {
return getLinkWalletEIP712TypedData(chainAwareAddress, groupId);
},
)
.post(
"/:chainAwareAddress",
async ({ params: { groupId, chainAwareAddress }, body }) => {
const group = await getGroup(groupId);

if (!group) throw new Error("Group not found");

const { signature } = body;

console.log("signature", signature);

const signTypedData = getLinkWalletEIP712TypedData(
chainAwareAddress,
groupId,
);

// 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 await Promise.any(
group?.members.map(async (member) => {
return await verifyTypedData({
address: member.address,
...signTypedData,
// @ts-expect-error: still throws even after parsing
domain: TypedDataDomain.parse(signTypedData.domain),
signature,
});
}),
);
},
{
body: t.Object({ signature: HexLiteral }),
},
);
},
);
})
.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}`);
});
4 changes: 3 additions & 1 deletion src/lib/eth/eip3770-shortnames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ export const chainShortNames = {
71402: "gw-mainnet-v1",
73799: "vt",
80001: "maticmum",
84531: "base-gor",
84531: "basegor",
84532: "basesep",
200101: "milktada",
200202: "milktalgo",
333999: "olympus",
Expand All @@ -145,6 +146,7 @@ export const chainShortNames = {
622277: "rth",
7777777: "zora",
11155111: "sep",
11155420: "opsep",
245022926: "neonevm-devnet",
245022934: "neonevm-mainnet",
1313161554: "aurora",
Expand Down
60 changes: 60 additions & 0 deletions src/lib/eth/link-wallet-sign-typed-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getAddress, type Address } from "viem";
import type { ChainAwareAddress } from "../../db/schema";
import { chainShortNames } from "./eip3770-shortnames";
import type { TypedDataDomain } from "abitype/zod";

type EIP712Domain = Zod.infer<typeof TypedDataDomain>;

type LinkingMessage = {
address: Address;
groupId: string;
};

export function getLinkWalletEIP712TypedData(
safeAddress: ChainAwareAddress,
groupId: string,
) {
const [chainShortName, addressFromSplit] = safeAddress.split(":");

if (!chainShortName || !addressFromSplit) {
throw new Error("Invalid safe address");
}

const address = getAddress(addressFromSplit);

const [chainId] =
Object.entries(chainShortNames).find(
([_, shortName]) => shortName === chainShortName,
) ?? [];

if (!chainId) {
throw new Error(`Chain ${chainShortName} not supported`);
}

const domain: EIP712Domain = {
name: "Link Safe With XMTP Group Chat",
version: "1",
chainId: Number(chainId),
};

const types = {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
],
LinkingMessage: [
{ name: "address", type: "address" },
{ name: "groupId", type: "string" },
],
};

const message: LinkingMessage = { address, groupId };

return {
domain,
types,
primaryType: "LinkingMessage" as const,
message,
};
}
1 change: 1 addition & 0 deletions src/lib/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const uuidv7Schema = z.string().length(36).brand("Uuidv7");
export type Uuidv7 = z.infer<typeof uuidv7Schema>;
export type Uuid25 = z.infer<typeof uuid25Schema>;

export const HexLiteral = t.TemplateLiteral("0x${string}");
export const AddressLiteral = t.TemplateLiteral("0x${string}");
export const ChainAwareAddressLiteral = t.TemplateLiteral(
"${string}:0x${string}",
Expand Down
12 changes: 11 additions & 1 deletion test/local-api-test.http
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ POST http://localhost:8080/bot/create HTTP/1.1
content-type: application/json

{
"members": ["0xD452ba2fB26fB7b529178b3fa4B96b2719ca8D46"]
"members": ["0xDdD77754e23f2EA8bCd05D43C25084c6C81e82D8", "0xAF3fCab790DC38bCFcD6a19422c559d0aB57b29F"]
}

###
Expand Down Expand Up @@ -66,3 +66,13 @@ GET http://localhost:8080/group/4cf971dcb124e9e57fad653cdc6242f1/wallets HTTP/1.
###
GET http://localhost:8080/0x524bF2086D4b5BBdA06f4c16Ec36f06AAd4E1Cad HTTP/1.1

###
GET http://localhost:8080/group/ed4ac987e6556dc8802bb7d0fce39d1d/link-wallet/basesep:0xaC03aD602D6786e7E87566192b48e30666e327Ad HTTP/1.1

###
POST http://localhost:8080/group/ed4ac987e6556dc8802bb7d0fce39d1d/link-wallet/basesep:0xaC03aD602D6786e7E87566192b48e30666e327Ad HTTP/1.1
Content-Type: application/json

{
"signature": "0x123"
}

0 comments on commit b4114f1

Please sign in to comment.