Skip to content

Commit

Permalink
add /sbts routes
Browse files Browse the repository at this point in the history
  • Loading branch information
calebtuttle committed Jun 18, 2024
1 parent 807abbd commit e8ad685
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 0 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ We plan to support more chains in the future. If you would like to use Holonym o

## Endpoints

- **GET** `/sbts/<credential-type>/`
- **GET** `/sybil-resistance/gov-id/<network>`
- **GET** `/sybil-resistance/epassport/<network>`
- **GET** `/sybil-resistance/phone/<network>`
Expand All @@ -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/<credential-type>?address=<user-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/<credential-type>/<network>?user=<user-address>&action-id=<action-id>`

Get whether the user has registered for the given action-id.
Expand Down
14 changes: 14 additions & 0 deletions src/routes/sbts.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 /`);
Expand Down
204 changes: 204 additions & 0 deletions src/services/sbts.js
Original file line number Diff line number Diff line change
@@ -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" });
}
}

0 comments on commit e8ad685

Please sign in to comment.