Skip to content

Commit

Permalink
feat(iam): add support for passport attestations (passportxyz#1415)
Browse files Browse the repository at this point in the history
* feat(iam): add support for passport attestations

* feat(iam): fixing linter complaints

* feat(iam): save providers to bitmap and include expiration dates

* feat(app): use eas/passport endpoint to bring stamps on chain

* chore(iam): update and test sorting and bitmap operations for on chain passport

* chore(iam): check sorting of issuance and expiration dates

* feat(iam): build bit map from static stampMetadata json

* chore(iam): unit test eas/passport

* chore(iam): remove outdated test

* feat(iam): statically generate stamp metada info from script

* feat(iam): include bitMap version in eas schema

---------

Co-authored-by: schultztimothy <[email protected]>
  • Loading branch information
nutrina and tim-schultz authored Jul 6, 2023
1 parent ae97e1c commit 837037d
Show file tree
Hide file tree
Showing 12 changed files with 784 additions and 7 deletions.
2 changes: 1 addition & 1 deletion app/components/SyncToChainButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const SyncToChainButton = () => {

const { data }: { data: EasPayload } = await axios({
method: "post",
url: `${process.env.NEXT_PUBLIC_PASSPORT_IAM_URL}v0.0.0/eas`,
url: `${process.env.NEXT_PUBLIC_PASSPORT_IAM_URL}v0.0.0/eas/passport`,
data: payload,
headers: {
"Content-Type": "application/json",
Expand Down
4 changes: 4 additions & 0 deletions iam/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,9 @@ SCORER_API_KEY=abc

EAS_GITCOIN_STAMP_SCHEMA=0xd83994d5459162a259c3a18d3db267ca05982f1e1e261d5388a8bfe2a2a2c7f9
EAS_GITCOIN_SCORE_SCHEMA=0xd43bc76510fcedd3ee4cac1a439ed4a130736b8faba0f4a814050e948942cdc0
EAS_GITCOIN_PASSPORT_SCHEMA=0x49d9700408d4ed2c132a3d6410ada3ed794e9d927accfdc02a9ceae75de7af97

FF_NEW_GITHUB_STAMPS=on

PASSPORT_STAMP_METADATA_PATH=http://localhost:3000/stampMetadata.json

270 changes: 270 additions & 0 deletions iam/__tests__/easPassportSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import * as easPassportModule from "../src/utils/easPassportSchema";
import * as easStampModule from "../src/utils/easStampSchema";

import { VerifiableCredential } from "@gitcoin/passport-types";
import { BigNumber } from "ethers";
import { NO_EXPIRATION, ZERO_BYTES32 } from "@ethereum-attestation-service/eas-sdk";

jest.mock("../src/utils/scorerService", () => ({
fetchPassportScore: jest.fn(),
}));

const defaultRequestData = {
recipient: "0x123",
expirationTime: NO_EXPIRATION,
revocable: true,
refUID: ZERO_BYTES32,
value: 0,
};

describe("formatMultiAttestationRequest", () => {
it("should return formatted attestation request", async () => {
jest.spyOn(easPassportModule, "encodeEasPassport").mockReturnValue("0x00000000000000000000000");
jest.spyOn(easStampModule, "encodeEasScore").mockReturnValue("0x00000000000000000000000");

const validatedCredentials = [
{
credential: {
credentialSubject: {
provider: "mockCredential1",
hash: "v0.0.0:QdjFB8E6FbvBT8HP+4mr7VBjal+CC7aDcAAqGAKsXos=",
},
issuanceDate: "2023-05-10T11:00:14.986Z",
expirationDate: "2023-08-10T11:00:14.986Z",
} as unknown as VerifiableCredential,
verified: true,
},
{
credential: {
credentialSubject: {
provider: "mockCredential2",
hash: "v0.0.0:QdjFB8E6FbvBT8HP+4mr7VBjal+CC7aDcAAqGAKsXos=",
},
issuanceDate: "2023-05-10T11:00:14.986Z",
expirationDate: "2023-08-10T11:00:14.986Z",
} as unknown as VerifiableCredential,
verified: false,
},
];

const recipient = "0x123";

const result = await easPassportModule.formatMultiAttestationRequest(validatedCredentials, recipient);

expect(result).toEqual([
{
schema: process.env.EAS_GITCOIN_PASSPORT_SCHEMA,
data: [
{
...defaultRequestData,
data: "0x00000000000000000000000",
},
],
},
{
schema: process.env.EAS_GITCOIN_SCORE_SCHEMA,
data: [
{
...defaultRequestData,
data: "0x00000000000000000000000",
},
],
},
]);
});
});

describe("formatPassportAttestationData", () => {
it("should format attestation data correctly", async () => {
// Prepare a mock credential
const mockCredential: VerifiableCredential = {
"@context": [],
type: [],
credentialSubject: {
hash: "v0.0.0:8JZcQJy6uwNGPDZnvfGbEs6mf5OZVD1mUOdhKNrOHls=",
provider: "mockProvider",
id: "",
"@context": [{}],
},
issuer: "string",
issuanceDate: "2023-01-01T00:00:00.000Z",
expirationDate: "2023-12-31T23:59:59.999Z",
proof: {
type: "string",
proofPurpose: "string",
verificationMethod: "string",
created: "string",
jws: "string",
},
};

// Prepare a mock stamp
const mockStamp = {
name: "mockProvider",
index: 0,
bit: 1,
};

const passportAttestationStampMap: Map<string, easPassportModule.PassportAttestationStamp> = new Map();

passportAttestationStampMap.set(mockCredential.credentialSubject.provider, mockStamp);

jest.spyOn(easPassportModule, "buildProviderBitMap").mockReturnValue(passportAttestationStampMap);

const result: easPassportModule.PassportAttestationData = await easPassportModule.formatPassportAttestationData([
mockCredential,
]);

// Check that the resulting providers array has the expected value
// For the mockStamp, it should be [BigNumber.from(1 << 1)]
expect(result.providers).toEqual([BigNumber.from(1 << mockStamp.bit)]);

// Check that the info array contains an object with the expected values
expect(result.info).toHaveLength(1);
expect(result.info[0]).toMatchObject({
hash: "0x" + Buffer.from(mockCredential.credentialSubject.hash.split(":")[1], "base64").toString("hex"),
issuanceDate: BigNumber.from(Math.floor(new Date(mockCredential.issuanceDate).getTime() / 1000)),
expirationDate: BigNumber.from(Math.floor(new Date(mockCredential.expirationDate).getTime() / 1000)),
stampInfo: mockStamp,
});
});
it("should return multiple provider bitmaps/bns if index exceeds range", async () => {
// Prepare a mock credential
const mockCredential = {
credentialSubject: {
hash: "v0.0.0:8JZcQJy6uwNGPDZnvfGbEs6mf5OZVD1mUOdhKNrOHls=",
provider: "mockProvider",
},
issuer: "string",
issuanceDate: "2023-01-01T00:00:00.000Z",
expirationDate: "2023-12-31T23:59:59.999Z",
} as unknown as VerifiableCredential;

const mockCredential1 = {
credentialSubject: {
hash: "v0.0.0:QdjFB8E6FbvBT8HP+4mr7VBjal+CC7aDcAAqGAKsXos=",
provider: "mockProvider1",
},
issuer: "string",
issuanceDate: "2023-01-01T00:00:00.000Z",
expirationDate: "2023-12-31T23:59:59.999Z",
} as unknown as VerifiableCredential;

// Prepare a mock stamp
const mockStamp = {
name: "mockProvider",
index: 0,
bit: 1,
};

const mockStamp1 = {
name: "mockProvider1",
index: 1,
bit: 0,
};

const passportAttestationStampMap: Map<string, easPassportModule.PassportAttestationStamp> = new Map();

// Add the mock stamps to the map
passportAttestationStampMap.set(mockCredential.credentialSubject.provider, mockStamp);
passportAttestationStampMap.set(mockCredential1.credentialSubject.provider, mockStamp1);

jest.spyOn(easPassportModule, "buildProviderBitMap").mockReturnValue(passportAttestationStampMap);

const result: easPassportModule.PassportAttestationData = await easPassportModule.formatPassportAttestationData([
mockCredential,
mockCredential1,
]);

expect(result.providers).toEqual([BigNumber.from(1 << mockStamp.bit), BigNumber.from(1 << mockStamp1.bit)]);

// Check that the info array contains an object with the expected values
expect(result.info).toHaveLength(2);
expect(result.info[0]).toMatchObject({
hash: "0x" + Buffer.from(mockCredential.credentialSubject.hash.split(":")[1], "base64").toString("hex"),
issuanceDate: BigNumber.from(Math.floor(new Date(mockCredential.issuanceDate).getTime() / 1000)),
expirationDate: BigNumber.from(Math.floor(new Date(mockCredential.expirationDate).getTime() / 1000)),
stampInfo: mockStamp,
});
});
});

describe("sortPassportAttestationData", () => {
beforeEach(() => {
jest.restoreAllMocks();
});
it("should correctly sort the Attestation data", () => {
const stamp1 = {
name: "TestStamp1",
index: 2,
bit: 1,
};

const stamp2 = {
name: "TestStamp2",
index: 1,
bit: 2,
};

const stamp3 = {
name: "TestStamp3",
index: 2,
bit: 0,
};

const attestation: easPassportModule.PassportAttestationData = {
providers: [BigNumber.from(3), BigNumber.from(1), BigNumber.from(2)],
info: [
{
hash: "0x123",
issuanceDate: BigNumber.from(1000),
expirationDate: BigNumber.from(2000),
stampInfo: stamp1,
},
{
hash: "0x456",
issuanceDate: BigNumber.from(1001),
expirationDate: BigNumber.from(2001),
stampInfo: stamp2,
},
{
hash: "0x789",
issuanceDate: BigNumber.from(1002),
expirationDate: BigNumber.from(2002),
stampInfo: stamp3,
},
],
};

const sortedAttestation: easPassportModule.AttestationData =
easPassportModule.sortPassportAttestationData(attestation);

expect(sortedAttestation.hashes).toEqual(["0x456", "0x789", "0x123"]);
expect(sortedAttestation.issuancesDates).toEqual([
BigNumber.from(1001),
BigNumber.from(1002),
BigNumber.from(1000),
]);
expect(sortedAttestation.expirationDates).toEqual([
BigNumber.from(2001),
BigNumber.from(2002),
BigNumber.from(2000),
]);
});
});

describe("buildProviderBitMapInfo", () => {
it("should correctly utilize the first bit of the new bitmap when a new bitmap is created", async () => {
const stampNames = Array(257)
.fill(null)
.map((_, idx) => `stamp${idx}`);
const groupStamps = stampNames.map((name) => ({ name }));
const group = { name: "group1", stamps: groupStamps };
const stampMetadata: easPassportModule.StampMetadata = [{ id: "1", name: "metadata1", groups: [group] }];

const bitmapInfo = easPassportModule.mapBitMapInfo(stampMetadata);

expect(bitmapInfo[bitmapInfo.length - 2]).toEqual({ bit: 255, index: 0, name: "stamp255" });
expect(bitmapInfo[bitmapInfo.length - 1]).toEqual({ bit: 0, index: 1, name: "stamp256" });
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as easStampModule from "../src/utils/easSchema";
import * as easStampModule from "../src/utils/easStampSchema";
import { VerifiableCredential } from "@gitcoin/passport-types";
import { BigNumber } from "ethers";
import { NO_EXPIRATION, ZERO_BYTES32 } from "@ethereum-attestation-service/eas-sdk";
Expand Down
Loading

0 comments on commit 837037d

Please sign in to comment.