Skip to content

Commit

Permalink
Basic implementation of Email PODs in Z API (#2194)
Browse files Browse the repository at this point in the history
This adds a special "virtual collection" called Emails, which contains
the user's Email PODs. Zapps can request permission to this collection,
which will allow them to read the Email PODs directly, and to make
proofs about them. Updates to the collection are not allowed.
  • Loading branch information
robknight authored Jan 28, 2025
1 parent 79efcda commit 46e1888
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 127 deletions.
10 changes: 7 additions & 3 deletions apps/passport-client/src/zapp/ZappServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ import { appConfig } from "../appConfig";
import { StateContextValue } from "../dispatch";
import { EmbeddedScreenType } from "../embedded";
import { getGPCArtifactsURL } from "../util";
import { collectionIdToFolderName, getPODsForCollections } from "./collections";
import {
collectionIdToFolderName,
getPODsForCollections,
VIRTUAL_COLLECTIONS
} from "./collections";
import { QuerySubscriptionManager } from "./query_subscription_manager";
import { ListenMode } from "./useZappServer";

Expand Down Expand Up @@ -159,7 +163,7 @@ class ZupassPODRPC extends BaseZappServer implements ParcnetPODRPC {
public async insert(collectionId: string, podData: PODData): Promise<void> {
if (
!this.getPermissions().INSERT_POD?.collections.includes(collectionId) ||
collectionId === "Devcon SEA"
VIRTUAL_COLLECTIONS.includes(collectionId)
) {
throw new MissingPermissionError("INSERT_POD", "pod.insert");
}
Expand Down Expand Up @@ -195,7 +199,7 @@ class ZupassPODRPC extends BaseZappServer implements ParcnetPODRPC {
public async delete(collectionId: string, signature: string): Promise<void> {
if (
!this.getPermissions().DELETE_POD?.collections.includes(collectionId) ||
collectionId === "Devcon SEA"
VIRTUAL_COLLECTIONS.includes(collectionId)
) {
throw new MissingPermissionError("DELETE_POD", "pod.delete");
}
Expand Down
27 changes: 22 additions & 5 deletions apps/passport-client/src/zapp/collections.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { PCDCollection } from "@pcd/pcd-collection";
import type { POD } from "@pcd/pod";
import { POD } from "@pcd/pod";
import { PODEmailPCD, isPODEmailPCD } from "@pcd/pod-email-pcd";
import { isPODPCD } from "@pcd/pod-pcd";
import { PODTicketPCD, isPODTicketPCD, ticketToPOD } from "@pcd/pod-ticket-pcd";

export const COLLECTIONS_ROOT_FOLDER_NAME = "Collections";

export const VIRTUAL_COLLECTIONS = ["Devcon SEA", "Email"];

export function collectionIdToFolderName(collectionId: string): string {
return `${COLLECTIONS_ROOT_FOLDER_NAME}/${collectionId}`;
}
Expand All @@ -13,16 +16,30 @@ export function getCollectionNames(pcds: PCDCollection): string[] {
return pcds.getFoldersInFolder(COLLECTIONS_ROOT_FOLDER_NAME);
}

function emailPCDToPOD(pcd: PODEmailPCD): POD {
return POD.load(
pcd.claim.podEntries,
pcd.proof.signature,
pcd.claim.signerPublicKey
);
}

export function getPODsForCollections(
pcds: PCDCollection,
collectionIds: string[]
): POD[] {
return collectionIds
.flatMap((collectionId) =>
collectionId === "Devcon SEA"
? pcds.getAllPCDsInFolder("Devcon SEA")
VIRTUAL_COLLECTIONS.includes(collectionId)
? pcds.getAllPCDsInFolder(collectionId)
: pcds.getAllPCDsInFolder(collectionIdToFolderName(collectionId))
)
.filter((pcd) => isPODPCD(pcd) || isPODTicketPCD(pcd))
.map((pcd) => (isPODPCD(pcd) ? pcd.pod : ticketToPOD(pcd as PODTicketPCD)));
.filter((pcd) => isPODPCD(pcd) || isPODTicketPCD(pcd) || isPODEmailPCD(pcd))
.map((pcd) =>
isPODPCD(pcd)
? pcd.pod
: isPODTicketPCD(pcd)
? ticketToPOD(pcd as PODTicketPCD)
: emailPCDToPOD(pcd as PODEmailPCD)
);
}
10 changes: 5 additions & 5 deletions examples/test-zapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
},
"dependencies": {
"@pcd/pod": "^0.5.0",
"@parcnet-js/podspec": "^1.1.3",
"@parcnet-js/ticket-spec": "^1.1.8",
"@parcnet-js/client-rpc": "^1.1.6",
"@parcnet-js/app-connector": "^1.1.8",
"@parcnet-js/app-connector-react": "^1.0.3",
"@parcnet-js/podspec": "^1.2.0",
"@parcnet-js/ticket-spec": "^1.1.9",
"@parcnet-js/client-rpc": "^1.2.0",
"@parcnet-js/app-connector": "^1.1.10",
"@parcnet-js/app-connector-react": "^1.0.5",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
Expand Down
89 changes: 89 additions & 0 deletions examples/test-zapp/src/apis/GPC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,95 @@ const gpcProof = await z.gpc.prove({ request: request.schema });
</pre>
)}
</div>

<div>
<p>
Generating an email proof is done like this:
<code className="block text-xs font-base rounded-md p-2 whitespace-pre-wrap">
{`
const request: PodspecProofRequest = {
pods: {
pod1: {
pod: {
entries: {
emailAddress: {
type: "string"
},
semaphoreV4PublicKey: {
type: "eddsa_pubkey"
},
pod_type: {
type: "string",
isMemberOf: [
{ type: "string", value: "zupass.email" }
]
}
},
meta: {
labelEntry: "emailAddress"
}
},
revealed: {
emailAddress: true,
semaphoreV4PublicKey: true,
pod_type: false
}
}
}
};
const gpcProof = await z.gpc.prove({ request });
`}
</code>
</p>
<TryIt
onClick={async () => {
try {
const request: PodspecProofRequest = {
pods: {
pod1: {
pod: {
entries: {
emailAddress: {
type: "string"
},
semaphoreV4PublicKey: {
type: "eddsa_pubkey"
},
pod_type: {
type: "string",
isMemberOf: [
{ type: "string", value: "zupass.email" }
]
}
},
meta: {
labelEntry: "emailAddress"
}
},
revealed: {
emailAddress: true,
semaphoreV4PublicKey: true,
pod_type: false
}
}
}
};

const gpcProof = await z.gpc.prove({ request });
setProveResult(gpcProof);
} catch (e) {
console.log(e);
}
}}
label="Get Email Proof"
/>
{proveResult && (
<pre className="whitespace-pre-wrap">
{JSONBig.stringify(proveResult, null, 2)}
</pre>
)}
</div>
</div>
</div>
);
Expand Down
83 changes: 76 additions & 7 deletions examples/test-zapp/src/apis/PODSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export function PODSection(): ReactNode {
<div className="prose">
<h2 className="text-lg font-bold mt-4">Query PODs</h2>
<QueryPODs z={z} />
<h2 className="text-lg font-bold mt-4">Query Email PODs</h2>
<QueryEmailPODs z={z} />
<h2 className="text-lg font-bold mt-4">Sign POD</h2>
<SignPOD z={z} setSignedPOD={setPOD} />
<h2 className="text-lg font-bold mt-4">Sign POD with Prefix</h2>
Expand Down Expand Up @@ -120,6 +122,73 @@ const pods = await z.pod.collection("${selectedCollection}").query(q);
);
}

function QueryEmailPODs({ z }: { z: ParcnetAPI }): ReactNode {
const [pods, setPODs] = useState<p.PODData[] | undefined>(undefined);
return (
<div>
<p>
Querying Email PODs is done like this:
<code className="block text-xs font-base rounded-md p-2 whitespace-pre">
{`const q = p.pod({
entries: {
emailAddress: {
type: "string"
},
semaphoreV4PublicKey: {
type: "eddsa_pubkey"
},
pod_type: {
type: "string",
isMemberOf: [{ type: "string", value: "zupass.email" }]
}
}
});
const pods = await z.pod.collection("Email").query(q);
`}
</code>
</p>
<TryIt
onClick={async () => {
try {
const q = p.pod({
entries: {
emailAddress: {
type: "string"
},
semaphoreV4PublicKey: {
type: "eddsa_pubkey"
},
pod_type: {
type: "string",
isMemberOf: [{ type: "string", value: "zupass.email" }]
}
}
});
const pods = await z.pod.collection("Email").query(q);
setPODs(pods);
} catch (e) {
console.error(e);
}
}}
label="Query Email PODs"
/>
{pods !== undefined && (
<pre className="whitespace-pre-wrap">
{JSONBig.stringify(
pods.map((p) => ({
entries: p.entries,
signature: p.signature,
signerPublicKey: p.signerPublicKey
})),
null,
2
)}
</pre>
)}
</div>
);
}

type Action =
| {
type: "SET_KEY";
Expand Down Expand Up @@ -193,7 +262,7 @@ const editPODReducer = function (
) {
newState[action.key] = {
type: action.podValueType as "string" | "eddsa_pubkey",
value: newState[action.key].value.toString()
value: newState[action.key].value?.toString() ?? ""
};
} else {
state[action.key].type = action.podValueType;
Expand All @@ -203,7 +272,7 @@ const editPODReducer = function (
case "SET_VALUE": {
const newState = { ...state };
if (bigintish.includes(newState[action.key].type)) {
const value = BigInt(action.value);
const value = BigInt(action.value as string);
if (value >= POD_INT_MIN && value <= POD_INT_MAX) {
newState[action.key].value = value;
}
Expand Down Expand Up @@ -271,8 +340,8 @@ ${Object.entries(entries)
.map(([key, value]) => {
return ` ${key}: { type: "${value.type}", value: ${
bigintish.includes(value.type)
? `${value.value.toString()}n`
: `"${value.value.toString()}"`
? `${value.value?.toString() ?? ""}n`
: `"${value.value?.toString() ?? ""}"`
} }`;
})
.join(",\n")}
Expand Down Expand Up @@ -366,8 +435,8 @@ ${Object.entries(entries)
.map(([key, value]) => {
return ` ${key}: { type: "${value.type}", value: ${
bigintish.includes(value.type)
? `${value.value.toString()}n`
: `"${value.value.toString()}"`
? `${value.value?.toString() ?? ""}n`
: `"${value.value?.toString() ?? ""}"`
} }`;
})
.join(",\n")}
Expand Down Expand Up @@ -458,7 +527,7 @@ function EditPODEntry({
<label className="block">
{showLabels && <span className="text-gray-700">Value</span>}
<input
value={value.toString()}
value={value?.toString() ?? ""}
type={typeof value === "string" ? "text" : "number"}
className="mt-1 block w-full rounded-md bg-gray-100 border-transparent focus:border-gray-500 focus:bg-white focus:ring-0"
placeholder=""
Expand Down
4 changes: 2 additions & 2 deletions examples/test-zapp/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import "./index.css";
const zapp: Zapp = {
name: "test-client",
permissions: {
REQUEST_PROOF: { collections: ["Apples", "Bananas"] },
REQUEST_PROOF: { collections: ["Apples", "Bananas", "Email"] },
SIGN_POD: {},
READ_POD: { collections: ["Apples", "Bananas"] },
READ_POD: { collections: ["Apples", "Bananas", "Email"] },
INSERT_POD: { collections: ["Apples", "Bananas"] },
DELETE_POD: { collections: ["Bananas"] },
READ_PUBLIC_IDENTIFIERS: {}
Expand Down
Loading

0 comments on commit 46e1888

Please sign in to comment.