Skip to content

Commit

Permalink
Merge pull request #30 from hypercerts-org/feat/attestation_validator
Browse files Browse the repository at this point in the history
Implement attestation validator
  • Loading branch information
bitbeckers authored Feb 3, 2025
2 parents 0cf34db + c746381 commit 8921cc3
Show file tree
Hide file tree
Showing 11 changed files with 582 additions and 58 deletions.
25 changes: 25 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,66 @@ export const DEFAULT_ENVIRONMENT: Environment = "production";

// The APIs we expose

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

const ENDPOINTS: { [key: string]: string } = {
test: "https://staging-api.hypercerts.org",
production: "https://api.hypercerts.org",
};

const SUPPORTED_EAS_SCHEMAS: { [key: string]: { [key: string]: string | boolean } } = {
BASIC_EVALUATION: {
uid: "0x2f4f575d5df78ac52e8b124c4c900ec4c540f1d44f5b8825fac0af5308c91449",
schema:
"uint256 chain_id,address contract_address,uint256 token_id,uint8 evaluate_basic,uint8 evaluate_work,uint8 evaluate_contributors,uint8 evaluate_properties,string comments,string[] tags",
resolver: ZERO_ADDRESS,
revocable: true,
},
CREATOR_FEED: {
uid: "0x48e3e1be1e08084b408a7035ac889f2a840b440bbf10758d14fb722831a200c3",
schema:
"uint256 chain_id,address contract_address,uint256 token_id,string title,string description,string[] sources",
resolver: ZERO_ADDRESS,
revocable: false,
},
};

// These are the deployments we manage
const DEPLOYMENTS: { [key in SupportedChainIds]: Deployment } = {
10: {
chainId: 10,
addresses: deployments[10],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
} as const,
42220: {
chainId: 42220,
addresses: deployments[42220],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
},
8453: {
chainId: 8453,
addresses: deployments[8453],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
} as const,
11155111: {
chainId: 11155111,
addresses: deployments[11155111],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: true,
} as const,
84532: {
chainId: 84532,
addresses: deployments[84532],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: true,
} as const,
42161: {
chainId: 42161,
addresses: deployments[42161],
easSchemas: SUPPORTED_EAS_SCHEMAS,
isTestnet: false,
} as const,
421614: {
Expand Down
3 changes: 2 additions & 1 deletion src/types/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ export type Contracts =
| "StrategyHypercertFractionOffer";

/**
* Represents a deployment of a contract on a specific network.
* Represents the hypercerts deployments on a specific network.
*/
export type Deployment = {
chainId: SupportedChainIds;
/** The address of the deployed contract. */
addresses: Partial<Record<Contracts, `0x${string}`>>;
isTestnet: boolean;
easSchemas?: { [key: string]: { [key: string]: string | boolean } };
};

/**
Expand Down
61 changes: 61 additions & 0 deletions src/utils/tokenIds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// https://github.com/hypercerts-org/hypercerts/blob/7671d06762c929bc2890a31e5dc392f8a30065c6/contracts/test/foundry/protocol/Bitshifting.t.sol

/**
* The maximum value that can be represented as an uint256.
* @type {BigInt}
*/
const MAX = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");

/**
* A mask that represents the base id of the token. It is created by shifting the maximum uint256 value left by 128 bits.
* @type {BigInt}
*/
const TYPE_MASK = MAX << BigInt(128);

/**
* A mask that represents the index of a non-fungible token. It is created by shifting the maximum uint256 value right by 128 bits.
* @type {BigInt}
*/
const NF_INDEX_MASK = MAX >> BigInt(128);

/**
* Checks if a token ID represents a base type token.
*
* A token ID is considered to represent a base type token if:
* - The bitwise AND of the token ID and the TYPE_MASK equals the token ID.
* - The bitwise AND of the token ID and the NF_INDEX_MASK equals 0.
*
* @param {BigInt} id - The token ID to check.
* @returns {boolean} - Returns true if the token ID represents a base type token, false otherwise.
*/
const isBaseType = (id: bigint) => {
return (id & TYPE_MASK) === id && (id & NF_INDEX_MASK) === BigInt(0);
};

/**
* Checks if a token ID represents a claim token.
*
* A token ID is considered to represent a claim token if it is not null and it represents a base type token.
*
* @param {BigInt} tokenId - The token ID to check. It can be undefined.
* @returns {boolean} - Returns true if the token ID represents a claim token, false otherwise.
*/
export const isHypercertToken = (tokenId?: bigint) => {
if (!tokenId) {
return false;
}
return isBaseType(tokenId);
};

/**
* Gets the claim token ID from a given token ID.
*
* The claim token ID is obtained by applying the TYPE_MASK to the given token ID using the bitwise AND operator.
* The result is logged to the console for debugging purposes.
*
* @param {BigInt} tokenId - The token ID to get the claim token ID from.
* @returns {BigInt} - Returns the claim token ID.
*/
export const getHypercertTokenId = (tokenId: bigint) => {
return tokenId & TYPE_MASK;
};
51 changes: 46 additions & 5 deletions src/validator/base/SchemaValidator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import Ajv, { Schema, ErrorObject } from "ajv";
import { IValidator, ValidationError, ValidationResult } from "../interfaces";
import Ajv, { Schema as AjvSchema, ErrorObject } from "ajv";
import { z } from "zod";

export abstract class SchemaValidator<T> implements IValidator<T> {
// Base interface for all validators
export interface ISchemaValidator<T> extends IValidator<T> {
validate(data: unknown): ValidationResult<T>;
}

// AJV-based validator
export abstract class AjvSchemaValidator<T> implements ISchemaValidator<T> {
protected ajv: Ajv;
protected schema: Schema;
protected schema: AjvSchema;

constructor(schema: Schema, additionalSchemas: Schema[] = []) {
constructor(schema: AjvSchema, additionalSchemas: AjvSchema[] = []) {
this.ajv = new Ajv({ allErrors: true });
// Add any additional schemas first
additionalSchemas.forEach((schema) => this.ajv.addSchema(schema));
this.schema = schema;
}
Expand Down Expand Up @@ -38,3 +44,38 @@ export abstract class SchemaValidator<T> implements IValidator<T> {
}));
}
}

// Zod-based validator
export abstract class ZodSchemaValidator<T> implements ISchemaValidator<T> {
protected schema: z.ZodType<T>;

constructor(schema: z.ZodType<T>) {
this.schema = schema;
}

validate(data: unknown): ValidationResult<T> {
const result = this.schema.safeParse(data);

if (!result.success) {
return {
isValid: false,
errors: this.formatErrors(result.error),
};
}

return {
isValid: true,
data: result.data,
errors: [],
};
}

protected formatErrors(error: z.ZodError): ValidationError[] {
return error.issues.map((issue) => ({
code: issue.code || "SCHEMA_VALIDATION_ERROR",
message: issue.message,
field: issue.path.join("."),
details: issue,
}));
}
}
115 changes: 115 additions & 0 deletions src/validator/validators/AttestationValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { z } from "zod";
import { DEPLOYMENTS } from "../../constants";
import { ZodSchemaValidator } from "../base/SchemaValidator";
import { isHypercertToken } from "src/utils/tokenIds";

const AttestationSchema = z
.object({
chain_id: z.coerce.bigint(),
contract_address: z.string(),
token_id: z.coerce.bigint(),
})
.passthrough()
.refine(
(data) => {
return Number(data.chain_id) in DEPLOYMENTS;
},
(data) => ({
code: "INVALID_CHAIN_ID",
message: `Chain ID ${data.chain_id.toString()} is not supported`,
path: ["chain_id"],
}),
)
.refine(
(data) => {
const deployment = DEPLOYMENTS[Number(data.chain_id) as keyof typeof DEPLOYMENTS];
if (!deployment?.addresses) {
return false;
}
const knownAddresses = Object.values(deployment.addresses).map((addr) => addr.toLowerCase());
return knownAddresses.includes(data.contract_address.toLowerCase());
},
(data) => ({
code: "INVALID_CONTRACT_ADDRESS",
message: `Contract address ${data.contract_address} is not deployed on chain ${data.chain_id.toString()}`,
path: ["contract_address"],
}),
)
.refine(
(data) => {
return isHypercertToken(data.token_id);
},
(data) => ({
code: "INVALID_TOKEN_ID",
message: `Token ID ${data.token_id.toString()} is not a valid hypercert token`,
path: ["token_id"],
}),
);

type AttestationData = z.infer<typeof AttestationSchema>;

// Example raw attestation

// {
// "uid": "0x4f923f7485e013d3c64b55268304c0773bb84d150b4289059c77af0e28aea3f6",
// "data": "0x000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000822f17a9a5eecfd66dbaff7946a8071c265d1d0700000000000000000000000000009c0900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b5a757a616c752032303233000000000000000000000000000000000000000000",
// "time": 1727969021,
// "refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
// "schema": "0x2f4f575d5df78ac52e8b124c4c900ec4c540f1d44f5b8825fac0af5308c91449",
// "attester": "0x676703E18b2d03Aa36d6A3124B4F58716dBf61dB",
// "recipient": "0x0000000000000000000000000000000000000000",
// "revocable": false,
// "expirationTime": 0,
// "revocationTime": 0
// }

// Example decoded attestation data

// {
// "tags": [
// "Zuzalu 2023"
// ],
// "chain_id": 10,
// "comments": "",
// "token_id": 1.3592579146656887e+43,
// "evaluate_work": 1,
// "evaluate_basic": 1,
// "contract_address": "0x822F17A9A5EeCFd66dBAFf7946a8071C265D1d07",
// "evaluate_properties": 1,
// "evaluate_contributors": 1
// }

// Example raw attestation data

// {
// "uid": "0xc6b717cfbf9df516c0cbdc670fdd7d098ae0a7d30b2fb2c1ff7bd15a822bf1f4",
// "data": "0x0000000000000000000000000000000000000000000000000000000000aa36a7000000000000000000000000a16dfb32eb140a6f3f2ac68f41dad8c7e83c4941000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000001e54657374696e67206164646974696f6e616c206174746573746174696f6e0000000000000000000000000000000000000000000000000000000000000000000877757575757575740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000003a7b2274797065223a2275726c222c22737263223a2268747470733a2f2f7078686572652e636f6d2f656e2f70686f746f2f31333833373237227d00000000000000000000000000000000000000000000000000000000000000000000000000b27b2274797065223a22696d6167652f6a706567222c226e616d65223a22676f61745f62726f776e5f616e696d616c5f6e61747572655f62696c6c795f676f61745f6d616d6d616c5f63726561747572655f686f726e732d3635363235322d313533313531373336392e6a7067222c22737263223a226261666b72656964676d613237367a326d756178717a79797467676979647437627a617073736479786b7333376737736f37377372347977776775227d0000000000000000000000000000",
// "time": 1737648084,
// "refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
// "schema": "0x48e3e1be1e08084b408a7035ac889f2a840b440bbf10758d14fb722831a200c3",
// "attester": "0xdf2C3dacE6F31e650FD03B8Ff72beE82Cb1C199A",
// "recipient": "0x0000000000000000000000000000000000000000",
// "revocable": false,
// "expirationTime": 0,
// "revocationTime": 0
// }

// Example decoded attestation data

// {
// "title": "Testing additional attestation",
// "sources": [
// "{\"type\":\"url\",\"src\":\"https://pxhere.com/en/photo/1383727\"}",
// "{\"type\":\"image/jpeg\",\"name\":\"goat_brown_animal_nature_billy_goat_mammal_creature_horns-656252-1531517369.jpg\",\"src\":\"bafkreidgma276z2muaxqzyytggiydt7bzapssdyxks37g7so77sr4ywwgu\"}"
// ],
// "chain_id": 11155111,
// "token_id": 2.0416942015256308e+41,
// "description": "wuuuuuut",
// "contract_address": "0xa16DFb32Eb140a6f3F2AC68f41dAd8c7e83C4941"
// }

export class AttestationValidator extends ZodSchemaValidator<AttestationData> {
constructor() {
super(AttestationSchema);
}
}
6 changes: 3 additions & 3 deletions src/validator/validators/MetadataValidator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HypercertClaimdata, HypercertMetadata } from "src/types/metadata";
import { SchemaValidator } from "../base/SchemaValidator";
import { AjvSchemaValidator } from "../base/SchemaValidator";
import claimDataSchema from "../../resources/schema/claimdata.json";
import metaDataSchema from "../../resources/schema/metadata.json";
import { PropertyValidator } from "./PropertyValidator";

export class MetadataValidator extends SchemaValidator<HypercertMetadata> {
export class MetadataValidator extends AjvSchemaValidator<HypercertMetadata> {
private propertyValidator: PropertyValidator;

constructor() {
Expand Down Expand Up @@ -36,7 +36,7 @@ export class MetadataValidator extends SchemaValidator<HypercertMetadata> {
}
}

export class ClaimDataValidator extends SchemaValidator<HypercertClaimdata> {
export class ClaimDataValidator extends AjvSchemaValidator<HypercertClaimdata> {
constructor() {
super(claimDataSchema);
}
Expand Down
4 changes: 2 additions & 2 deletions src/validator/validators/PropertyValidator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ValidationError } from "../interfaces";
import { SchemaValidator } from "../base/SchemaValidator";
import { AjvSchemaValidator } from "../base/SchemaValidator";
import { HypercertMetadata } from "src/types";
import metaDataSchema from "../../resources/schema/metadata.json";

Expand Down Expand Up @@ -65,7 +65,7 @@ class GeoJSONValidationStrategy implements PropertyValidationStrategy {
}
}

export class PropertyValidator extends SchemaValidator<PropertyValue> {
export class PropertyValidator extends AjvSchemaValidator<PropertyValue> {
private readonly validationStrategies: Record<string, PropertyValidationStrategy> = {
geoJSON: new GeoJSONValidationStrategy(),
};
Expand Down
26 changes: 26 additions & 0 deletions test/utils/tokenIds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, it, describe } from "vitest";

import { isHypercertToken, getHypercertTokenId } from "../../src/utils/tokenIds";

const claimTokenId = 340282366920938463463374607431768211456n;
const fractionTokenId = 340282366920938463463374607431768211457n;

describe("isClaimTokenId", () => {
it("should return true for a claim token id", () => {
expect(isHypercertToken(claimTokenId)).toBe(true);
});

it("should return false for a non-claim token id", () => {
expect(isHypercertToken(fractionTokenId)).toBe(false);
});
});

describe("getClaimTokenId", () => {
it("should return the claim token id", () => {
expect(getHypercertTokenId(claimTokenId)).toBe(claimTokenId);
});

it("should return the claim token id for a fraction token id", () => {
expect(getHypercertTokenId(fractionTokenId)).toBe(claimTokenId);
});
});
Loading

0 comments on commit 8921cc3

Please sign in to comment.