diff --git a/apps/api/src/app/credentials/credentials.service.test.ts b/apps/api/src/app/credentials/credentials.service.test.ts index ae8d7775..b00d5a4c 100644 --- a/apps/api/src/app/credentials/credentials.service.test.ts +++ b/apps/api/src/app/credentials/credentials.service.test.ts @@ -83,6 +83,7 @@ describe("CredentialsService", () => { { name: "Group1", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: JSON.stringify({ @@ -104,6 +105,7 @@ describe("CredentialsService", () => { { name: "Group2", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -186,6 +188,7 @@ describe("CredentialsService", () => { { name: "Group2", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: JSON.stringify({ @@ -232,6 +235,7 @@ describe("CredentialsService", () => { { name: "Group2", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: JSON.stringify({ @@ -337,6 +341,7 @@ describe("CredentialsService", () => { { name: "Group2", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: JSON.stringify({ @@ -387,6 +392,7 @@ describe("CredentialsService", () => { { name: "Group3", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: JSON.stringify({ @@ -439,6 +445,7 @@ describe("CredentialsService", () => { { name: "Group4", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: JSON.stringify({ @@ -492,6 +499,7 @@ describe("CredentialsService", () => { { name: "Group5", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: JSON.stringify({ diff --git a/apps/api/src/app/groups/docSchemas/group.ts b/apps/api/src/app/groups/docSchemas/group.ts index bfca9425..1175c097 100644 --- a/apps/api/src/app/groups/docSchemas/group.ts +++ b/apps/api/src/app/groups/docSchemas/group.ts @@ -8,6 +8,8 @@ export class Group { @ApiProperty() description: string @ApiProperty() + type: string + @ApiProperty() admin: string @ApiProperty() treeDepth: number diff --git a/apps/api/src/app/groups/docSchemas/groupResponse.ts b/apps/api/src/app/groups/docSchemas/groupResponse.ts index 7f890bcd..62b48760 100644 --- a/apps/api/src/app/groups/docSchemas/groupResponse.ts +++ b/apps/api/src/app/groups/docSchemas/groupResponse.ts @@ -8,6 +8,8 @@ export class GroupResponse { @ApiProperty() description: string @ApiProperty() + type: string + @ApiProperty() adminId: string @ApiProperty() treeDepth: number diff --git a/apps/api/src/app/groups/dto/create-group.dto.ts b/apps/api/src/app/groups/dto/create-group.dto.ts index 2dc49185..1051a957 100644 --- a/apps/api/src/app/groups/dto/create-group.dto.ts +++ b/apps/api/src/app/groups/dto/create-group.dto.ts @@ -8,9 +8,11 @@ import { MinLength, NotContains, IsNumberString, - IsJSON + IsJSON, + IsEnum } from "class-validator" import { ApiProperty } from "@nestjs/swagger" +import { GroupType } from "../types" export class CreateGroupDto { @IsString() @@ -30,6 +32,12 @@ export class CreateGroupDto { @ApiProperty() readonly description: string + @IsEnum(["on-chain", "off-chain"]) + @ApiProperty({ + enum: ["on-chain", "off-chain"] + }) + readonly type: GroupType + @IsNumber() @Min(16, { message: "The tree depth must be between 16 and 32." }) @Max(32, { message: "The tree depth must be between 16 and 32." }) diff --git a/apps/api/src/app/groups/entities/group.entity.ts b/apps/api/src/app/groups/entities/group.entity.ts index 05cc20c9..1c56afdd 100644 --- a/apps/api/src/app/groups/entities/group.entity.ts +++ b/apps/api/src/app/groups/entities/group.entity.ts @@ -12,6 +12,7 @@ import { import { OAuthAccount } from "../../credentials/entities/credentials-account.entity" import { Member } from "./member.entity" import { Invite } from "../../invites/entities/invite.entity" +import { GroupType } from "../types" @Entity("groups") export class Group { @@ -25,6 +26,13 @@ export class Group { @Column() description: string + @Column({ + type: "simple-enum", + enum: ["on-chain", "off-chain"], + nullable: true + }) + type: GroupType + @Column({ name: "admin_id" }) adminId: string diff --git a/apps/api/src/app/groups/groups.controller.ts b/apps/api/src/app/groups/groups.controller.ts index bf1bc24e..214d46f4 100644 --- a/apps/api/src/app/groups/groups.controller.ts +++ b/apps/api/src/app/groups/groups.controller.ts @@ -31,6 +31,7 @@ import { UpdateGroupsDto } from "./dto/update-groups.dto" import { GroupsService } from "./groups.service" import { mapGroupToResponseDTO } from "./groups.utils" import { RemoveGroupsDto } from "./dto/remove-groups.dto" +import { GroupType } from "./types" @ApiTags("groups") @Controller("groups") @@ -40,13 +41,22 @@ export class GroupsController { @Get() @ApiQuery({ name: "adminId", required: false, type: String }) @ApiQuery({ name: "memberId", required: false, type: String }) + @ApiQuery({ name: "type", required: false, type: String }) + @ApiQuery({ name: "name", required: false, type: String }) @ApiOperation({ description: "Returns the list of groups." }) @ApiCreatedResponse({ type: Group, isArray: true }) async getGroups( @Query("adminId") adminId: string, - @Query("memberId") memberId: string + @Query("memberId") memberId: string, + @Query("type") type: GroupType, + @Query("name") name: string ) { - const groups = await this.groupsService.getGroups({ adminId, memberId }) + const groups = await this.groupsService.getGroups({ + adminId, + memberId, + type, + name + }) const groupIds = groups.map((group) => group.id) const fingerprints = await this.groupsService.getFingerprints(groupIds) diff --git a/apps/api/src/app/groups/groups.service.test.ts b/apps/api/src/app/groups/groups.service.test.ts index 95792863..70219b23 100644 --- a/apps/api/src/app/groups/groups.service.test.ts +++ b/apps/api/src/app/groups/groups.service.test.ts @@ -66,6 +66,7 @@ describe("GroupsService", () => { { name: "Group1", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -77,16 +78,19 @@ describe("GroupsService", () => { describe("# createGroup", () => { it("Should create a group", async () => { - const { treeDepth, members } = await groupsService.createGroup( - { - name: "Group2", - description: "This is a description", - treeDepth: 16, - fingerprintDuration: 3600 - }, - "admin" - ) + const { type, treeDepth, members } = + await groupsService.createGroup( + { + name: "Group2", + description: "This is a description", + type: "off-chain", + treeDepth: 16, + fingerprintDuration: 3600 + }, + "admin" + ) + expect(type).toBe("off-chain") expect(treeDepth).toBe(16) expect(members).toHaveLength(0) }) @@ -96,6 +100,7 @@ describe("GroupsService", () => { { name: "Group3", description: "This is a description", + type: "off-chain", treeDepth: 15, fingerprintDuration: 3600 }, @@ -114,6 +119,7 @@ describe("GroupsService", () => { { name: "Group3", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -142,6 +148,7 @@ describe("GroupsService", () => { { name: "Group4", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { @@ -216,6 +223,7 @@ describe("GroupsService", () => { { name: "Group01", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -227,6 +235,7 @@ describe("GroupsService", () => { { name: "Group02", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -245,6 +254,7 @@ describe("GroupsService", () => { { name: "MemberGroup", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -266,6 +276,33 @@ describe("GroupsService", () => { expect(result).toHaveLength(1) }) + + it("Should return a list of groups by group type", async () => { + await groupsService.createGroup( + { + name: "OnChainGroup", + description: "This is a description", + type: "on-chain", + treeDepth: 16, + fingerprintDuration: 3600 + }, + "admin02" + ) + + const result = await groupsService.getGroups({ + type: "on-chain" + }) + + expect(result).toHaveLength(1) + }) + + it("Should return a list of groups by group name", async () => { + const result = await groupsService.getGroups({ + name: "OnChainGroup" + }) + + expect(result).toHaveLength(1) + }) }) describe("# getGroup", () => { @@ -335,6 +372,7 @@ describe("GroupsService", () => { { name: "MemberGroup", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -398,6 +436,7 @@ describe("GroupsService", () => { { name: "Group2", description: "This is a new group", + type: "off-chain", treeDepth: 21, fingerprintDuration: 3600 }, @@ -442,6 +481,7 @@ describe("GroupsService", () => { { name: "Group2", description: "This is a new group", + type: "off-chain", treeDepth: 21, fingerprintDuration: 3600 }, @@ -471,6 +511,7 @@ describe("GroupsService", () => { const groupDto: CreateGroupDto = { name: "Group", description: "This is a new group", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 } @@ -627,6 +668,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -634,6 +676,7 @@ describe("GroupsService", () => { id: "2", name: "Group2", description: "This is a new group2", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -641,6 +684,7 @@ describe("GroupsService", () => { id: "3", name: "Group3", description: "This is a new group3", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 } @@ -801,6 +845,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 } @@ -904,6 +949,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -911,6 +957,7 @@ describe("GroupsService", () => { id: "2", name: "Group2", description: "This is a new group2", + type: "off-chain", treeDepth: 32, fingerprintDuration: 7200 } @@ -1044,6 +1091,7 @@ describe("GroupsService", () => { { name: "Group2", description: "This is a new group", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -1194,6 +1242,7 @@ describe("GroupsService", () => { { name: "Group2", description: "This is a new group", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -1218,6 +1267,7 @@ describe("GroupsService", () => { { name: "Credential Group", description: "This is a new group", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { @@ -1366,6 +1416,7 @@ describe("GroupsService", () => { { name: "Group2", description: "This is a new group", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -1414,6 +1465,7 @@ describe("GroupsService", () => { { name: "Group2", description: "This is a new group", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -1441,6 +1493,7 @@ describe("GroupsService", () => { { name: "Credential Group", description: "This is a new group", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { @@ -1492,6 +1545,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 } @@ -1534,6 +1588,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -1541,6 +1596,7 @@ describe("GroupsService", () => { id: "2", name: "Group2", description: "This is a new group2", + type: "off-chain", treeDepth: 32, fingerprintDuration: 7200 } @@ -1587,6 +1643,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 } @@ -1639,6 +1696,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -1646,6 +1704,7 @@ describe("GroupsService", () => { id: "2", name: "Group2", description: "This is a new group2", + type: "off-chain", treeDepth: 32, fingerprintDuration: 7200 } @@ -1722,6 +1781,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 } @@ -1794,6 +1854,7 @@ describe("GroupsService", () => { id: "1", name: "Group1", description: "This is a new group1", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -1801,6 +1862,7 @@ describe("GroupsService", () => { id: "2", name: "Group2", description: "This is a new group2", + type: "off-chain", treeDepth: 32, fingerprintDuration: 7200 } @@ -1895,6 +1957,7 @@ describe("GroupsService", () => { { name: "Fingerprint Group", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, diff --git a/apps/api/src/app/groups/groups.service.ts b/apps/api/src/app/groups/groups.service.ts index c0c7eae2..1e29adf1 100644 --- a/apps/api/src/app/groups/groups.service.ts +++ b/apps/api/src/app/groups/groups.service.ts @@ -18,7 +18,7 @@ import { CreateGroupDto } from "./dto/create-group.dto" import { UpdateGroupDto } from "./dto/update-group.dto" import { Group } from "./entities/group.entity" import { Member } from "./entities/member.entity" -import { MerkleProof } from "./types" +import { GroupType, MerkleProof } from "./types" import { getAndCheckAdmin } from "../utils" @Injectable() @@ -149,6 +149,7 @@ export class GroupsService { id: groupId, name, description, + type, treeDepth, fingerprintDuration, credentials @@ -173,6 +174,7 @@ export class GroupsService { id: _groupId, name, description, + type, treeDepth, fingerprintDuration, credentials, @@ -820,6 +822,8 @@ export class GroupsService { async getGroups(filters?: { adminId?: string memberId?: string + type?: GroupType + name?: string }): Promise { let where = {} @@ -838,6 +842,20 @@ export class GroupsService { } } + if (filters?.type) { + where = { + type: filters.type, + ...where + } + } + + if (filters?.name) { + where = { + name: filters.name, + ...where + } + } + return this.groupRepository.find({ relations: { members: true }, where, diff --git a/apps/api/src/app/groups/groups.utils.ts b/apps/api/src/app/groups/groups.utils.ts index 5c56e519..a6f1728c 100644 --- a/apps/api/src/app/groups/groups.utils.ts +++ b/apps/api/src/app/groups/groups.utils.ts @@ -5,6 +5,7 @@ export function mapGroupToResponseDTO(group: Group, fingerprint: string = "") { id: group.id, name: group.name, description: group.description, + type: group.type, admin: group.adminId, treeDepth: group.treeDepth, fingerprint, diff --git a/apps/api/src/app/groups/types/index.ts b/apps/api/src/app/groups/types/index.ts index 478ae130..8846571e 100644 --- a/apps/api/src/app/groups/types/index.ts +++ b/apps/api/src/app/groups/types/index.ts @@ -4,3 +4,5 @@ export type MerkleProof = { siblings: any[] pathIndices: number[] } + +export type GroupType = "on-chain" | "off-chain" diff --git a/apps/api/src/app/invites/invites.service.test.ts b/apps/api/src/app/invites/invites.service.test.ts index b9513c82..47ff68b7 100644 --- a/apps/api/src/app/invites/invites.service.test.ts +++ b/apps/api/src/app/invites/invites.service.test.ts @@ -71,6 +71,7 @@ describe("InvitesService", () => { { name: "Group1", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600 }, @@ -104,6 +105,7 @@ describe("InvitesService", () => { { name: "Group2", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { @@ -159,6 +161,7 @@ describe("InvitesService", () => { { name: "Group2", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { @@ -189,6 +192,7 @@ describe("InvitesService", () => { { name: "Group3", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { @@ -283,6 +287,7 @@ describe("InvitesService", () => { { name: "Group2", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { @@ -317,6 +322,7 @@ describe("InvitesService", () => { { name: "Group3", description: "This is a description", + type: "off-chain", treeDepth: 16, fingerprintDuration: 3600, credentials: { diff --git a/apps/dashboard/src/api/bandadaAPI.ts b/apps/dashboard/src/api/bandadaAPI.ts index 8d477445..22ab216a 100644 --- a/apps/dashboard/src/api/bandadaAPI.ts +++ b/apps/dashboard/src/api/bandadaAPI.ts @@ -1,6 +1,6 @@ import { ApiKeyActions, request } from "@bandada/utils" import { SiweMessage } from "siwe" -import { Admin, Group } from "../types" +import { Admin, Group, GroupType } from "../types" import createAlert from "../utils/createAlert" const API_URL = import.meta.env.VITE_API_URL @@ -40,17 +40,23 @@ export async function generateMagicLink( * @param adminId The admin id. * @returns The list of groups or null. */ -export async function getGroups(adminId?: string): Promise { +export async function getGroups( + adminId?: string, + type?: GroupType +): Promise { try { - const url = adminId - ? `${API_URL}/groups/?adminId=${adminId}` - : `${API_URL}/groups/` + const groupType = type || "off-chain" + let url = `${API_URL}/groups/?type=${groupType}` + + if (adminId) { + url += `&?adminId=${adminId}` + } const groups = await request(url) return groups.map((group: Group) => ({ ...group, - type: "off-chain" + type: groupType })) } catch (error: any) { console.error(error) @@ -59,6 +65,28 @@ export async function getGroups(adminId?: string): Promise { } } +/** + * It returns the list of groups by group name. + * @param name + * @returns The group details. + */ +export async function getGroupByName( + name: string, + type?: GroupType +): Promise { + try { + const groupType = type || "off-chain" + + return await request( + `${API_URL}/groups/?name=${name}&?type=${groupType}` + ) + } catch (error: any) { + console.error(error) + createAlert(error.response.data.message) + return null + } +} + /** * It returns details of a specific group. * @param groupId The group id. @@ -80,12 +108,16 @@ export async function getGroup(groupId: string): Promise { * It creates a new group. * @param name The group name. * @param description The group description. + * @param type The group type ("on-chain" | "off-chain"). * @param treeDepth The Merkle tree depth. + * @param fingerprintDuration The fingerprint duration. + * @param credentials The group credentials. * @returns The group details. */ export async function createGroup( name: string, description: string, + type: GroupType, treeDepth: number, fingerprintDuration: number, credentials?: any @@ -97,6 +129,7 @@ export async function createGroup( { name, description, + type, treeDepth, fingerprintDuration, credentials: JSON.stringify(credentials) @@ -104,7 +137,7 @@ export async function createGroup( ] }) - return { ...groups.at(0), type: "off-chain" } + return { ...groups.at(0) } } catch (error: any) { console.error(error) createAlert(error.response.data.message) diff --git a/apps/dashboard/src/api/semaphoreAPI.ts b/apps/dashboard/src/api/semaphoreAPI.ts index 3409fca8..6ea01323 100644 --- a/apps/dashboard/src/api/semaphoreAPI.ts +++ b/apps/dashboard/src/api/semaphoreAPI.ts @@ -1,6 +1,7 @@ import { SemaphoreSubgraph } from "@semaphore-protocol/data" import { Group } from "../types" import parseGroupName from "../utils/parseGroupName" +import { getGroupByName } from "./bandadaAPI" const ETHEREUM_NETWORK = import.meta.env.VITE_ETHEREUM_NETWORK @@ -63,3 +64,39 @@ export async function getGroup(groupId: string): Promise { return null } } + +/** + * It returns the details of a specific on-chain group together with the associated off-chain group details. + * @param groupId + * @returns The group details. + */ +export async function getAssociatedGroup( + groupId: string +): Promise { + try { + const group = await subgraph.getGroup(groupId, { + members: true + }) + + const members = group.members as string[] + const bandadaGroup = await getGroupByName(group.id, "on-chain") + + if (bandadaGroup && bandadaGroup.length > 0) { + members.push(...bandadaGroup[0].members) + } + + return { + id: group.id, + name: parseGroupName(group.id), + treeDepth: group.merkleTree.depth, + fingerprintDuration: 3600, + members, + admin: group.admin as string, + type: "on-chain" + } + } catch (error) { + console.error(error) + + return null + } +} diff --git a/apps/dashboard/src/components/add-member-modal.tsx b/apps/dashboard/src/components/add-member-modal.tsx index 6fd5ed9a..fffb415c 100644 --- a/apps/dashboard/src/components/add-member-modal.tsx +++ b/apps/dashboard/src/components/add-member-modal.tsx @@ -147,7 +147,24 @@ ${memberIds.join("\n")} }, [onClose, _memberIds, group, signer]) const generateInviteLink = useCallback(async () => { - const inviteLink = await bandadaAPI.generateMagicLink(group.id) + let inviteLink = null + + if (group.type === "off-chain") { + inviteLink = await bandadaAPI.generateMagicLink(group.id) + } else { + const associatedGroup = await bandadaAPI.getGroupByName( + group.name, + "on-chain" + ) + + if (associatedGroup && associatedGroup.length > 0) { + inviteLink = await bandadaAPI.generateMagicLink( + associatedGroup[0].id + ) + } else { + alert("No associated on-chain group found") + } + } if (inviteLink === null) { return @@ -212,63 +229,59 @@ ${memberIds.join("\n")} )} - {group.type === "off-chain" && ( - - - {!group.credentials - ? "Share invite link" - : "Share access link"} - - - - - - - - e.preventDefault() - } - icon={ - - } - /> - - - - - {!group.credentials && ( - - )} - - )} + e.preventDefault()} + icon={ + + } + /> + + + + + {!group.credentials && ( + + )} +