Skip to content

Commit

Permalink
Updating ethers service, adding eas, adding worldcoin verification
Browse files Browse the repository at this point in the history
  • Loading branch information
LovishB committed Aug 9, 2024
1 parent 15e8aba commit 37beead
Show file tree
Hide file tree
Showing 14 changed files with 901 additions and 34 deletions.
8 changes: 8 additions & 0 deletions packages/github-bot-nestjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@apollo/client": "^3.11.4",
"@ethereum-attestation-service/eas-sdk": "^2.5.0",
"@nestjs/apollo": "^12.2.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/graphql": "^12.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.0",
"@worldcoin/idkit": "^1.2.2",
"cross-fetch": "^4.0.0",
"ethers": "^6.13.2",
"express": "^4.19.2",
"graphql": "^16.9.0",
"graphql-request": "^7.1.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
Expand Down
4 changes: 3 additions & 1 deletion packages/github-bot-nestjs/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Module } from '@nestjs/common';
import { BotModule } from './bot-github/probot.module';
import { ConfigModule } from '@nestjs/config';
import { Web3Module } from './web3/web3.module';
import { EasModule } from './eas/eas.module';
import { WorldcoinModule } from './worldcoin/worldcoin.module';

@Module({
imports: [
BotModule,
ConfigModule.forRoot(), Web3Module,
ConfigModule.forRoot(), Web3Module, EasModule, WorldcoinModule,
]
})
export class AppModule {}
65 changes: 55 additions & 10 deletions packages/github-bot-nestjs/src/bot-github/bot.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,69 @@
import { Injectable } from '@nestjs/common';
import { AttestationService } from 'src/eas/attestation.service';
import { Web3Service } from 'src/web3/web3.service';

@Injectable()
export class BotService {

constructor(
private readonly web3Service: Web3Service,
private attestationService: AttestationService
) {}

async handleIssueClosed(id: string, name: string, body: any) {
const issue = JSON.parse(JSON.stringify(body.issue));
const user = JSON.parse(JSON.stringify(issue.user));
const repository = JSON.parse(JSON.stringify(body.repository));
const issueId = issue.id;
const repoId = repository.id;
const contributorGithubId = user.id;
const contributorAddress = '1234'; //get this from smart contract
console.log(`issueID: ${issueId}, repoId: ${repoId},
contributorId: ${contributorGithubId}, contributorAddress: ${contributorAddress}`);
this.web3Service.callDistribution(issueId, repoId, contributorGithubId, contributorAddress);
try {
//parsing data from github events
const issue = JSON.parse(JSON.stringify(body.issue));
const user = JSON.parse(JSON.stringify(issue.user));
const repository = JSON.parse(JSON.stringify(body.repository));
const organisation = JSON.parse(JSON.stringify(body.organization));
const issueId = issue.id;
const issueTitle = issue.title;
const repoId = repository.id;
const repoTitle = repository.name;
const organisationName = organisation.login;
const contributorGithubId = user.id;
const contributorGithubUsername = user.login;
const contributorGithubUrl = user.html_url;
const issueUrl = issue.html_url;
const githubRepositoryUrl = repository.html_url;
const githubOrganisationUrl = organisation.url;
const attestorName = 'Repo Rewards';
const achivement = `Successfully contributed to ${organisationName}'s open-source repo ${repoTitle} by creating and submitting a pull request, demonstrating collaborative software development skills and commitment to improving opensource codebases.`;

// Fetching Contributor WalletAddress From Contract
const contributorAddress = await this.web3Service.getUserWalletByUsername('optSepoliaNet', contributorGithubUsername);

//logging for debug purpose
console.log(`
issueID: ${issueId},
issueTitle: ${issueTitle},
repoId: ${repoId},
repoTitle: ${repoTitle},
contributorId: ${contributorGithubId},
contributorAddress: ${contributorAddress},
contributorGithubUsername ${contributorGithubUsername},
organisationName: ${organisationName}`
);

console.log(`
contributorGithubUrl: ${contributorGithubUrl},
issueUrl: ${issueUrl},
githubRepositoryUrl: ${githubRepositoryUrl},
githubOrganisationUrl: ${githubOrganisationUrl}`
);

// Distributing Reward on Contract
await this.web3Service.distributeReward('optSepoliaNet', repoId, issueId, contributorAddress);

//Attesting Open Source Contribution
this.attestationService.createAttestation(contributorGithubId, contributorGithubUsername,
issueId, issueTitle, repoId, repoTitle, organisationName, attestorName, achivement,
contributorGithubUrl, issueUrl, githubRepositoryUrl, githubOrganisationUrl, contributorAddress
);
} catch(error) {
console.error('BotService Error:', error);
}
}

}
4 changes: 3 additions & 1 deletion packages/github-bot-nestjs/src/bot-github/probot.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { BotService } from './bot.service';
import { WebhookController } from './webhook.controller';
import { Web3Module } from 'src/web3/web3.module';
import { EasModule } from 'src/eas/eas.module';
import { WorldcoinModule } from 'src/worldcoin/worldcoin.module';

@Module({
imports:[Web3Module],
imports:[Web3Module, EasModule, WorldcoinModule],
providers: [BotService],
controllers: [WebhookController]
})
Expand Down
36 changes: 32 additions & 4 deletions packages/github-bot-nestjs/src/bot-github/webhook.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { Controller, Get, Post, Req, Res } from '@nestjs/common';
import { Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
import { BotService } from './bot.service';
import { Request, Response } from 'express';
import { WorldcoinService } from 'src/worldcoin/worldcoin.service';
import { Observable } from 'rxjs';
import { AttestationService } from 'src/eas/attestation.service';

@Controller('webhook')
export class WebhookController {

constructor(private botService: BotService) {}
constructor(
private botService: BotService,
private worldcoinService: WorldcoinService,
private attestationService: AttestationService
) {}

@Post()
async handleWebhook(@Req() req: Request, @Res() res: Response): Promise<any> {
Expand All @@ -16,17 +23,38 @@ export class WebhookController {
const body = JSON.parse(JSON.stringify(payload));
console.log(`Recived a New Webhook: ${name} ${body.action}`);
if(name === 'issues' && body.action === 'closed') {
await this.botService.handleIssueClosed(id, name, body);
this.botService.handleIssueClosed(id, name, body);
}
return 'OK';
} catch (error) {
console.error('Error processing webhook:', error);
console.error('WebhookController, Error processing webhook:', error);
return 'Error processing webhook';
}
}

@Post('worldIdVerification')
async handleWorldIdVerification(@Req() req: Request, @Res() res: Response): Promise<any> {
try {
const proof = req.body
this.worldcoinService.handleWorldIdVerification(proof, res);
} catch(error) {
console.error('WebhookController, Error verifying worldcoindId:', error);
}
};

@Get('attestationRecipient/:recipient')
async getAttestations(@Param('recipient') recipient: string): Promise<any> {
return await this.attestationService.getAttestationsByRecipient(recipient);
}

@Get('attestationAttester/:attester')
async getAttestationsByAttester(@Param('attester') attester: string): Promise<any> {
return this.attestationService.getAttestationsByAttester(attester);
}

@Get('status')
async getServerStatus() {
return 'OK';
}

}
163 changes: 163 additions & 0 deletions packages/github-bot-nestjs/src/eas/attestation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// attestation.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { EAS, SchemaEncoder } from "@ethereum-attestation-service/eas-sdk";
import { ethers } from 'ethers';
import { map, Observable } from 'rxjs';
import { ApolloClient, InMemoryCache, gql, HttpLink } from '@apollo/client/core';
import fetch from 'cross-fetch';

@Injectable()
export class AttestationService {
private eas: EAS;
private signer: ethers.Wallet;
private apolloClient: ApolloClient<any>;

constructor(
@Inject('Config')
private readonly config: {
apiKey: string;
baseSepoliaNet: string;
optSepoliaNet: string;
privateKey: string
}) {
const EAS_CONTRACT_ADDRESS = "0x4200000000000000000000000000000000000021"; // OP Sepolia address
const ALCHEMY_URL = `${this.config.optSepoliaNet}${this.config.apiKey}`;
const provider = new ethers.JsonRpcProvider(ALCHEMY_URL);
this.signer = new ethers.Wallet(this.config.privateKey, provider);
this.eas = new EAS(EAS_CONTRACT_ADDRESS);
this.eas.connect(this.signer);

this.apolloClient = new ApolloClient({
link: new HttpLink({ uri: 'https://optimism-sepolia.easscan.org/graphql', fetch }),
cache: new InMemoryCache(),
});
}

async createAttestation(
contributorGithubId: string,
contributorGithubUsername: string,
issueId: string,
issueTitle: string,
repoId: string,
repoTitle: string,
organisationName: string,
attestorName: string,
achivement: string,
contributorGithubUrl: string,
issueUrl: string,
githubRepositoryUrl: string,
githubOrganisationUrl: string,
contributorAddress: any
) {
try {
const schemaUID = "0xb022941841c03fab69d4e4a55de2b8c9159ead78c7d6ee62b3f7e17d16e427f1";
const schemaEncoder = new SchemaEncoder("string Contributor_Github_Username,string Achievement,string Issue_Title,string Github_Repository,string Github_Organisation,string Attestor,string Contributor_Github_Url,string Issue_Url,string Github_Repository_Url,string Github_Organisation_Url,uint40 Contributor_Github_Id,uint40 Issue_Id,uint40 Github_Repository_Id");
const encodedData = schemaEncoder.encodeData([
{ name: "Contributor_Github_Username", value: contributorGithubUsername, type: "string" },
{ name: "Achievement", value: achivement, type: "string" },
{ name: "Issue_Title", value: issueTitle, type: "string" },
{ name: "Github_Repository", value: repoTitle, type: "string" },
{ name: "Github_Organisation", value: organisationName, type: "string" },
{ name: "Attestor", value: attestorName, type: "string" },
{ name: "Contributor_Github_Url", value: contributorGithubUrl, type: "string" },
{ name: "Issue_Url", value: issueUrl, type: "string" },
{ name: "Github_Repository_Url", value: githubRepositoryUrl, type: "string" },
{ name: "Github_Organisation_Url", value: githubOrganisationUrl, type: "string" },
{ name: "Contributor_Github_Id", value: Number(contributorGithubId), type: "uint40" },
{ name: "Issue_Id", value: Number(issueId), type: "uint40" },
{ name: "Github_Repository_Id", value: Number(repoId), type: "uint40" }
]);

const tx = await this.eas.attest({
schema: schemaUID,
data: {
recipient: contributorAddress,
expirationTime: BigInt(0),
revocable: true,
data: encodedData,
},
});

const newAttestationUID = await tx.wait();
console.log("New attestation UID:", newAttestationUID);
} catch(error) {
console.error('AttestationService Error: ', error);
}
}

async getAttestationsByRecipient(recipientAddress: string): Promise<any> {
const GET_ATTESTATIONS = gql`
query AttestationsByRecipient($recipient: String!) {
attestations(
where: { recipient: { equals: $recipient }}
orderBy: { time: desc }
) {
id
attester
recipient
revocable
data
time
decodedDataJson
isOffchain
schema {
schema
}
}
}
`;

try {
const { data } = await this.apolloClient.query({
query: GET_ATTESTATIONS,
variables: {
recipient: recipientAddress,
},
});
console.log(`Attestations found for Recipient: ${recipientAddress}, Total: `, data.attestations.length);
return data.attestations;
} catch (error) {
console.error('AttestationService Error, error fetching attestations:', error);
throw error;
}
}

async getAttestationsByAttester(attesterAddress: string): Promise<any> {
const GET_ATTESTATIONS_BY_ATTESTER = gql`
query AttestationsByAttester($attester: String!) {
attestations(
where: { attester: { equals: $attester }}
orderBy: { time: desc }
take: 15
) {
id
attester
recipient
revocable
data
time
decodedDataJson
isOffchain
schema {
schema
}
}
}
`;

try {
const { data } = await this.apolloClient.query({
query: GET_ATTESTATIONS_BY_ATTESTER,
variables: {
attester: attesterAddress
}
});
console.log(`Attestations found for Attester: ${attesterAddress}, Total: `, data.attestations.length);
return data.attestations;
} catch (error) {
console.error('Error fetching attestations by attester:', error);
throw error;
}
}

}
22 changes: 22 additions & 0 deletions packages/github-bot-nestjs/src/eas/eas.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { AttestationService } from './attestation.service';
import { ConfigService } from '@nestjs/config';

@Module({
providers: [
{
provide: 'Config',
useFactory: (configService: ConfigService) => {
return {
baseSepoliaNet: configService.get<string>('ALCHEMY_BASE_SEPOLIA_URL'),
optSepoliaNet: configService.get<string>('ALCHEMY_OPT_SEPOLIA_URL'),
apiKey: configService.get<string>('ALCHEMY_API_KEY'),
privateKey: configService.get<string>('PRIVATE_KEY'),
};
},
inject: [ConfigService],
},
AttestationService],
exports: [AttestationService]
})
export class EasModule {}
6 changes: 6 additions & 0 deletions packages/github-bot-nestjs/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ config();
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const port = process.env.PORT || 3000;

app.enableCors({
origin: '*',
allowedHeaders: 'Content-Type, Accept',
methods: ['GET, POST', 'PUT', 'DELETE']
});

await app.listen(port, '0.0.0.0');

Expand Down
Loading

0 comments on commit 37beead

Please sign in to comment.