Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add idOS verify step #231

Merged
merged 12 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 66 additions & 16 deletions packages/web-app/app/_server/idos/idos-grantee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { ethers } from 'ethers';
import nacl from 'tweetnacl';
import { decodeBase58, toBeHex } from 'ethers';
import { EthSigner } from '@kwilteam/kwil-js/dist/core/builders';
import { idOS } from '@idos-network/idos-sdk';

const PLAYGROUND_FRACTAL_ISSUER = 'https://vc-issuers.next.fractal.id/idos';

export function implicitAddressFromPublicKey(publicKey: string) {
const key_without_prefix = publicKey.replace(/^ed25519:/, '');
Expand Down Expand Up @@ -151,15 +154,50 @@ export class idOSGrantee {
}

async getSharedCredentialContentDecrypted(dataId: string): Promise<string> {
const credentialCopy = await this.fetchSharedCredentialFromIdos<{
content: string;
encryption_public_key: string;
}>(dataId);

return await this.noncedBox.decrypt(
credentialCopy.content,
credentialCopy.encryption_public_key,
);
try {
const credentialCopy = await this.fetchSharedCredentialFromIdos<{
content: string;
encryption_public_key: string;
}>(dataId);

return await this.noncedBox.decrypt(
credentialCopy.content,
credentialCopy.encryption_public_key,
);
} catch (error) {
console.log(
'Error fetching or decrypting shared credential from idos for dataId:',
dataId,
);
throw error;
}
}

async isValidCredential(credential: any): Promise<boolean> {
try {
// Ignore for now due to minification issues from idOS SDK side
// let result: any;

// if (process.env.NEXT_PUBLIC_ENABLE_TESTNETS === 'true') {
// result = await idOS.verifiableCredentials.verify(credential, {
// allowedIssuers: [PLAYGROUND_FRACTAL_ISSUER],
// });
// } else {
// result = await idOS.verifiableCredentials.verify(credential);
// }

// if (result !== true) {
// throw new Error('Invalid credential', result);
// }

return true;
} catch (error) {
console.log('Error verifying credential');
console.log(error);
return new Promise((resolve, _reject) => {
resolve(false);
});
}
}

async fetchUserCountriesFromSharedPlusCredential(
Expand All @@ -169,17 +207,29 @@ export class idOSGrantee {
await this.getSharedCredentialContentDecrypted(dataId);
const credential = JSON.parse(credentialString);

if (credential?.level !== 'plus')
const isValid = await this.isValidCredential(credential);
const isApproved = credential?.status === 'approved';

if (credential?.level && credential.level !== 'plus') {
console.log('Credential is not plus level, for dataId:', dataId);
}

if (!isApproved) {
console.log('Credential is not approved, for dataId:', dataId);
}

if (credential?.level === 'plus' && isValid && isApproved) {
return {
residentialCountry: undefined,
idDocumentCountry: undefined,
residentialCountry:
credential?.credentialSubject?.residential_address_country,
idDocumentCountry:
credential?.credentialSubject?.identification_document_country,
};
}

return {
residentialCountry:
credential?.credentialSubject?.residential_address_country,
idDocumentCountry:
credential?.credentialSubject?.identification_document_country,
residentialCountry: undefined,
idDocumentCountry: undefined,
};
}

Expand Down
82 changes: 72 additions & 10 deletions packages/web-app/app/_server/idos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,39 @@ const userFilter = async (grantee: idOSGrantee, userAddress: string) => {
});
const grants = grantsSchema.parse(grantsResult);

if (!grants.length) return null;
if (!grants.length) {
console.log('No grants found for user', userAddress);
return null;
}

const { residentialCountry, idDocumentCountry } =
await grantee.fetchUserCountriesFromSharedPlusCredential(
grants[0].dataId,
);

if (!residentialCountry || !idDocumentCountry) return null;
if (!residentialCountry || !idDocumentCountry) {
console.log(
'No residential or idDocument country found for user',
userAddress,
);
return null;
}

const isBlockedCountry = blockedCountries.some(
(blockedCountry) =>
blockedCountry === residentialCountry ||
blockedCountry === idDocumentCountry,
);

if (isBlockedCountry) return null;
//expected scenario
if (isBlockedCountry) {
console.log('Blocked country found for user', userAddress);
return null;
}

return userAddress;
} catch (error) {
console.log(error);
if (error instanceof Error) {
console.error(error.message);
}
Expand All @@ -49,10 +63,54 @@ const userFilter = async (grantee: idOSGrantee, userAddress: string) => {
return null;
};

const filterApplicants = async (addresses: string[], grantee: idOSGrantee) => {
const allowed = new Set<string>();
const notAllowed = new Set<string>();
const batches = Math.ceil(addresses.length / 10);

// run in batches of 10
for (let i = 0; i < batches; i++) {
console.log('==>BATCH', i);

let batch = addresses.slice(i * 10, (i + 1) * 10);
let promises = batch.map(
async (address): Promise<string | null> =>
new Promise((resolve) => resolve(userFilter(grantee, address))),
);

let results = await Promise.all(promises);

results.forEach((result, index) => {
if (result !== null) {
allowed.add(result);
} else {
notAllowed.add(batch[index]);
}
});
}

return { allowed, notAllowed };
};

const retryFailed = async (
allowed: Set<string>,
notAllowed: Set<string>,
grantee: idOSGrantee,
) => {
for (const address of notAllowed) {
const result = await userFilter(grantee, address);
if (result !== null) {
allowed.add(result);
notAllowed.delete(address);
}
}
};

export const getAllowedProjectApplicants = async (projectAddress: string) => {
try {
const applicantsResult = await getProjectApplicants(projectAddress);
const addresses = addressesListSchema.parse(applicantsResult);

const grantee = await idOSGrantee.init({
granteeSigner: evmGrantee,
encryptionSecret: ENCRYPTION_KEY_PAIR.secretKey,
Expand All @@ -61,16 +119,20 @@ export const getAllowedProjectApplicants = async (projectAddress: string) => {
? undefined
: 'x44250024a9bf9599ad7c3fcdb220d2100357dbf263014485174a1ae3',
});
const allowed = [];

for (const address of addresses) {
const result = await userFilter(grantee, address);
if (result !== null) {
allowed.push(result);
}
// first run to populate the lists
let { allowed, notAllowed } = await filterApplicants(addresses, grantee);
// retry 2 times failed addresses
let retry = 2;
while (retry > 0 && notAllowed.size > 0) {
retryFailed(allowed, notAllowed, grantee);
retry--;
}

return allowed as string[];
console.log('==>', 'NOT ALLOWED:');
console.log(notAllowed);

return Array.from(allowed);
} catch (error) {
console.error(error);

Expand Down
Loading