From e8ad685cb454fe2b94d9ad1fa685a687ea4959ff Mon Sep 17 00:00:00 2001 From: Caleb Tuttle <1calebtuttle@gmail.com> Date: Tue, 18 Jun 2024 19:40:39 -0400 Subject: [PATCH] add /sbts routes --- README.md | 40 +++++++++ src/routes/sbts.js | 14 +++ src/server.js | 2 + src/services/sbts.js | 204 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 260 insertions(+) create mode 100644 src/routes/sbts.js create mode 100644 src/services/sbts.js diff --git a/README.md b/README.md index 1086351..29b4c8f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ We plan to support more chains in the future. If you would like to use Holonym o ## Endpoints +- **GET** `/sbts//` - **GET** `/sybil-resistance/gov-id/` - **GET** `/sybil-resistance/epassport/` - **GET** `/sybil-resistance/phone/` @@ -21,6 +22,45 @@ We plan to support more chains in the future. If you would like to use Holonym o - **GET** `/attestation/attestor` - **GET** `/attestation/sbts/gov-id` +### **GET** `/sbts/?address=` + +(Differs slightly from sybil-resistance endpoints.) + +- Parameters + + | name | description | type | in | required | + | ----------------- | ------------------------------ | ------ | ----- | -------- | + | `credential-type` | 'kyc', 'epassport', or 'phone' | string | path | true | + | `address` | User's blockchain address | string | query | true | + +- Example + + ```JavaScript + const resp = await fetch('https://api.holonym.io/sbts/kyc?address=0x0000000000000000000000000000000000000000'); + const { hasValidSbt, message } = await resp.json(); + ``` + +- Responses + + - 200 + + ```JSON + { + "hasValidSbt": true, + } + ``` + + - 200 + + Result if user's SBT has expired + + ```JSON + { + "hasValidSbt": false, + "message": "SBT is expired or does not exist" + } + ``` + ### **GET** `/sybil-resistance//?user=&action-id=` Get whether the user has registered for the given action-id. diff --git a/src/routes/sbts.js b/src/routes/sbts.js new file mode 100644 index 0000000..244e3bc --- /dev/null +++ b/src/routes/sbts.js @@ -0,0 +1,14 @@ +import express from "express"; +import { + getHasValidKycSbt, + getHasValidEPassportSbt, + getHasPhoneSbt +} from "../services/sbts.js"; + +const router = express.Router(); + +router.get("/kyc/", getHasValidKycSbt); +router.get("/epassport/", getHasValidEPassportSbt); +router.get("/phone/", getHasPhoneSbt); + +export default router; diff --git a/src/server.js b/src/server.js index 86a33f6..82a8abf 100644 --- a/src/server.js +++ b/src/server.js @@ -6,6 +6,7 @@ import sybilResistance from "./routes/sybil-resistance.js"; import snapshotStrategies from "./routes/snapshot-strategies.js"; import metrics from "./routes/metrics.js"; import sbtAttestation from "./routes/sbt-attestation.js"; +import sbts from "./routes/sbts.js"; // ---------------------------- // Setup express app @@ -27,6 +28,7 @@ app.use("/sybil-resistance", sybilResistance); app.use("/snapshot-strategies", snapshotStrategies); app.use("/metrics", metrics); app.use("/attestation", sbtAttestation); +app.use("/sbts", sbts); app.get("/", (req, res) => { console.log(`${new Date().toISOString()} GET /`); diff --git a/src/services/sbts.js b/src/services/sbts.js new file mode 100644 index 0000000..5d0ecc5 --- /dev/null +++ b/src/services/sbts.js @@ -0,0 +1,204 @@ +/** + * These endpoints are very similar to the ones in sybil-resistance.js. + * But these endpoints exist because we need to simply check whether + * a user has a valid V3 SBT, which is slightly different from what + * the endpoints in sybil-resistance.js do. + */ +import { ethers } from "ethers"; +import { providers } from "../init.js"; +import { logWithTimestamp, assertValidAddress } from "../utils/utils.js"; +import { blocklistGetAddress } from "../utils/dynamodb.js"; +import { + hubV3Address, + govIdIssuerAddress, + phoneIssuerAddress, + v3KYCSybilResistanceCircuitId, + v3PhoneSybilResistanceCircuitId, + v3EPassportSybilResistanceCircuitId, + ePassportIssuerMerkleRoot, +} from "../constants/misc.js"; +import HubV3ABI from "../constants/HubV3ABI.js"; + +/** + * Parse and validate query params. + */ +function parseV3SbtParams(req) { + const address = req.query.address; + // We hardcode actionId right now because there's only one actionId in use. + const actionId = 123456789; + // const actionId = req.query["action-id"]; + if (!address) { + return { error: "Request query params do not include user address" }; + } + if (!actionId) { + return { error: "Request query params do not include action-id" }; + } + if (!assertValidAddress(address)) { + return { error: "Invalid user address" }; + } + if (!parseInt(actionId)) { + return { error: "Invalid action-id" }; + } + + return { + address, + actionId, + } +} + +export async function getHasValidKycSbt(req, res) { + try { + const result = parseV3SbtParams(req); + if (result.error) return res.status(400).json({ error: result.error }); + const { address, actionId } = result; + + // Check blocklist first + const blockListResult = await blocklistGetAddress(address); + if (blockListResult.Item) { + return res.status(200).json({ + hasValidSbt: false, + message: "Address is on blocklist" + }); + } + + const hubV3Contract = new ethers.Contract(hubV3Address, HubV3ABI, providers.optimism); + + // Check v3 contract for KYC SBT + try { + const sbt = await hubV3Contract.getSBT(address, v3KYCSybilResistanceCircuitId); + + const publicValues = sbt[1]; + const actionIdInSBT = publicValues[2].toString(); + const issuerAddress = publicValues[4].toHexString(); + + const actionIdIsValid = actionId == actionIdInSBT; + const issuerIsValid = govIdIssuerAddress == issuerAddress; + const isExpired = new Date(sbt[0].toNumber()) < (Date.now() / 1000); + const isRevoked = sbt[2]; + + return res.status(200).json({ + hasValidSbt: actionIdIsValid && issuerIsValid && !isRevoked && !isExpired, + }); + + } catch (err) { + // Do nothing + if ((err.errorArgs?.[0] ?? "").includes("SBT is expired or does not exist")) { + return res.status(200).json({ + hasValidSbt: false, + message: "SBT is expired or does not exist" + }); + } + + throw err + } + } catch (err) { + console.log(err); + logWithTimestamp( + "getHasValidKycSbt: Encountered error while calling smart contract. Exiting" + ); + return res.status(500).json({ error: "An unexpected error occured" }); + } +} + +export async function getHasValidEPassportSbt(req, res) { + try { + const result = parseV3SbtParams(req); + if (result.error) return res.status(400).json({ error: result.error }); + const { address, actionId } = result; + + // Check blocklist first + const blockListResult = await blocklistGetAddress(address); + if (blockListResult.Item) { + return res.status(200).json({ + hasValidSbt: false, + message: "Address is on blocklist" + }); + } + + const hubV3Contract = new ethers.Contract(hubV3Address, HubV3ABI, providers.optimism); + + // Check v3 contract for ePassport SBT + try { + const sbt = await hubV3Contract.getSBT(address, v3EPassportSybilResistanceCircuitId); + + const publicValues = sbt[1]; + const merkleRoot = publicValues[2].toHexString(); + const isExpired = new Date(sbt[0].toNumber()) < (Date.now() / 1000); + const isRevoked = sbt[2]; + + return res.status(200).json({ + hasValidSbt: merkleRoot === ePassportIssuerMerkleRoot && !isRevoked && !isExpired, + }); + } catch (err) { + if ((err.errorArgs?.[0] ?? "").includes("SBT is expired or does not exist")) { + return res.status(200).json({ + hasValidSbt: false, + message: "SBT is expired or does not exist" + }); + } + + throw err; + } + } catch (err) { + console.log(err); + logWithTimestamp( + "getHasValidEPassportSbt: Encountered error while calling smart contract. Exiting" + ); + return res.status(500).json({ error: "An unexpected error occured" }); + } + +} + +export async function getHasPhoneSbt(req, res) { + try { + const result = parseV3SbtParams(req); + if (result.error) return res.status(400).json({ error: result.error }); + const { address, actionId } = result; + + // Check blocklist first + const blockListResult = await blocklistGetAddress(address); + if (blockListResult.Item) { + return res.status(200).json({ + hasValidSbt: false, + message: "Address is on blocklist" + }); + } + + const provider = providers.optimism; + + // Check v3 contract + try { + const hubV3Contract = new ethers.Contract(hubV3Address, HubV3ABI, provider); + + const sbt = await hubV3Contract.getSBT(address, v3PhoneSybilResistanceCircuitId); + + const publicValues = sbt[1]; + const actionIdInSBT = publicValues[2].toString(); + const issuerAddress = publicValues[4].toHexString(); + + const actionIdIsValid = actionId == actionIdInSBT; + const issuerIsValid = phoneIssuerAddress == issuerAddress; + const isExpired = new Date(sbt[0].toNumber()) < (Date.now() / 1000); + const isRevoked = sbt[2]; + + return res.status(200).json({ + hasValidSbt: issuerIsValid && actionIdIsValid && !isRevoked && !isExpired, + }); + } catch (err) { + if ((err.errorArgs?.[0] ?? "").includes("SBT is expired or does not exist")) { + return res.status(200).json({ + hasValidSbt: false, + message: "SBT is expired or does not exist" + }); + } + + throw err; + } + } catch (err) { + console.log(err); + logWithTimestamp( + "getHasPhoneSbt: Encountered error while calling smart contract. Exiting" + ); + return res.status(500).json({ error: "An unexpected error occured" }); + } +}