From c7d396ef1e0950a1d74403ef4751390997edcb98 Mon Sep 17 00:00:00 2001 From: hh Date: Tue, 7 Jan 2025 19:58:22 +0330 Subject: [PATCH] feat: add the payment module --- src/helpers/web3.ts | 40 +++++- ...generation-module.ts => payment-module.ts} | 118 +++++++++++------- src/parser/processor.ts | 4 +- src/types/results.ts | 1 + 4 files changed, 115 insertions(+), 48 deletions(-) rename src/parser/{permit-generation-module.ts => payment-module.ts} (86%) diff --git a/src/helpers/web3.ts b/src/helpers/web3.ts index 87b2884e..aab0c02b 100644 --- a/src/helpers/web3.ts +++ b/src/helpers/web3.ts @@ -65,7 +65,45 @@ export async function getFundingWalletBalance(networkId: number, tokenAddress: s const handler = new RPCHandler(config); const provider = await handler.getFastestRpcProvider(); - // fetch token symbol + // fetch token balance const contract = new ethers.Contract(tokenAddress, abi, provider); return await contract.balanceOf(fundingWalledAddress); } + +/** + * Returns Transaction for the ERC20 token transfer + * @param networkId Network id + * @param tokenAddress ERC20 token address + * @param _evmPrivateEncrypted encrypted private key of the funding wallet address + * @param to reciever address + * @param amount Amount of ERC20 token to be transfered + * @returns Transaction for the ERC20 token transfer + */ +export async function transferTo(networkId: number, tokenAddress: string, _evmPrivateEncrypted: string, to: string, amount: string) { + const abi = ["function transfer(address,uint256) public returns (bool)"]; + + // get fastest RPC + const config: HandlerConstructorConfig = { + networkName: null, + networkRpcs: null, + proxySettings: { + retryCount: 5, + retryDelay: 500, + logTier: null, + logger: null, + strictLogs: false, + }, + runtimeRpcs: null, + networkId: String(networkId) as NetworkId, + rpcTimeout: 1500, + autoStorage: false, + cacheRefreshCycles: 10, + }; + const handler = new RPCHandler(config); + const provider = await handler.getFastestRpcProvider(); + + // send the transaction + // TODO: sign the transaction + const contract = new ethers.Contract(tokenAddress, abi, provider); + return await contract.transfer(to, amount); +} diff --git a/src/parser/permit-generation-module.ts b/src/parser/payment-module.ts similarity index 86% rename from src/parser/permit-generation-module.ts rename to src/parser/payment-module.ts index c537fce3..026ff0e7 100644 --- a/src/parser/permit-generation-module.ts +++ b/src/parser/payment-module.ts @@ -35,7 +35,7 @@ interface Payload { issue: { node_id: string }; } -export class PermitGenerationModule extends BaseModule { +export class PaymentModule extends BaseModule { readonly _configuration: PermitGenerationConfiguration | null = this.context.config.incentives.permitGeneration; readonly _autoTransferMode: boolean = this.context.config.automaticTransferMode; readonly _fundingWalletAddress: string = this.context.config.fundingWalletAddress; @@ -44,10 +44,10 @@ export class PermitGenerationModule extends BaseModule { readonly _supabase = createClient(this.context.env.SUPABASE_URL, this.context.env.SUPABASE_KEY); async transform(data: Readonly, result: Result): Promise { - const canGeneratePermits = await this._canGeneratePermit(data); + const canMakePayment = await this._canMakePayment(data); - if (!canGeneratePermits) { - this.context.logger.error("[PermitGenerationModule] Non collaborative issue detected, skipping."); + if (!canMakePayment) { + this.context.logger.error("[PaymentModule] Non collaborative issue detected, skipping."); return Promise.resolve(result); } @@ -58,12 +58,13 @@ export class PermitGenerationModule extends BaseModule { this._fundingWalletAddress ); + let shouldTransferDirectly = false; if (this._autoTransferMode && sumPayouts < fundingWalletBalance) { this.context.logger.debug( - "[PermitGenerationModule] AutoTransformMode is enabled, " + - "and sufficient funds are available in the funding wallet, skipping." + "[PaymentModule] AutoTransformMode is enabled, " + + "and sufficient funds are available in the funding wallet, skipping." ); - return Promise.resolve(result); + shouldTransferDirectly = true; } const payload: Context["payload"] & Payload = { @@ -86,7 +87,7 @@ export class PermitGenerationModule extends BaseModule { ); if (!isPrivateKeyAllowed) { this.context.logger.error( - "[PermitGenerationModule] Private key is not allowed to be used in this organization/repository." + "[PaymentModule] Private key is not allowed to be used in this organization/repository." ); return Promise.resolve(result); } @@ -117,21 +118,23 @@ export class PermitGenerationModule extends BaseModule { for (const [key, value] of Object.entries(result)) { this.context.logger.debug(`Updating result for user ${key}`); - try { - const config: Context["config"] = { - evmNetworkId: payload.evmNetworkId, - evmPrivateEncrypted: payload.evmPrivateEncrypted, - permitRequests: [ - { - amount: value.total, - username: key, - contributionType: "", - type: TokenType.ERC20, - tokenAddress: payload.erc20RewardToken, - }, - ], - }; - const permits = await generatePayoutPermit( + const config: Context["config"] = { + evmNetworkId: payload.evmNetworkId, + evmPrivateEncrypted: payload.evmPrivateEncrypted, + permitRequests: [ + { + amount: value.total, + username: key, + contributionType: "", + type: TokenType.ERC20, + tokenAddress: payload.erc20RewardToken, + }, + ], + }; + + if (shouldTransferDirectly) { + + const txid = await transferErc20( { env, eventName, @@ -151,17 +154,56 @@ export class PermitGenerationModule extends BaseModule { }, config.permitRequests ); - result[key].permitUrl = `https://pay.ubq.fi?claim=${encodePermits(permits)}`; - await this._savePermitsToDatabase(result[key].userId, { issueUrl: payload.issueUrl, issueId }, permits); - } catch (e) { - this.context.logger.error(`[PermitGenerationModule] Failed to generate permits for user ${key}`, { e }); + result[key].permitUrl = `https://gnosisscan.io/tx/${txid}`; + //TODO: We need to save this record in the database + } else { + try { + const permits = await generatePayoutPermit( + { + env, + eventName, + logger: permitLogger, + payload, + adapters: createAdapters(this._supabase, { + env, + eventName, + octokit, + config, + logger: permitLogger, + payload, + adapters, + }), + octokit, + config, + }, + config.permitRequests + ); + result[key].permitUrl = `https://pay.ubq.fi?claim=${encodePermits(permits)}`; + await this._savePermitsToDatabase(result[key].userId, { issueUrl: payload.issueUrl, issueId }, permits); + // remove treasury item from final result in order not to display permit fee in GitHub comments + if (env.PERMIT_TREASURY_GITHUB_USERNAME) delete result[env.PERMIT_TREASURY_GITHUB_USERNAME]; + } catch (e) { + this.context.logger.error(`[PaymentModule] Failed to generate permits for user ${key}`, { e }); + } } } + return result; + } - // remove treasury item from final result in order not to display permit fee in GitHub comments - if (env.PERMIT_TREASURY_GITHUB_USERNAME) delete result[env.PERMIT_TREASURY_GITHUB_USERNAME]; + async _sumPayouts(result: Result) { + let sumPayouts = 0; + for (const value of Object.values(result)) { + sumPayouts += value.total; + } + return sumPayouts; + } - return result; + async _canMakePayment(data: Readonly) { + if (!data.self?.closed_by || !data.self.user) return false; + + if (await isAdmin(data.self.user.login, this.context)) return true; + + return isCollaborative(data); } /** @@ -220,21 +262,7 @@ export class PermitGenerationModule extends BaseModule { return this._deductFeeFromReward(result, treasuryGithubData); } - async _canGeneratePermit(data: Readonly) { - if (!data.self?.closed_by || !data.self.user) return false; - if (await isAdmin(data.self.user.login, this.context)) return true; - - return isCollaborative(data); - } - - async _sumPayouts(result: Result) { - let sumPayouts = 0; - for (const value of Object.values(result)) { - sumPayouts += value.total; - } - return sumPayouts; - } _deductFeeFromReward( result: Result, @@ -417,7 +445,7 @@ export class PermitGenerationModule extends BaseModule { get enabled(): boolean { if (!Value.Check(permitGenerationConfigurationType, this._configuration)) { - this.context.logger.error("Invalid / missing configuration detected for PermitGenerationModule, disabling."); + this.context.logger.error("Invalid / missing configuration detected for PaymentModule, disabling."); return false; } return true; diff --git a/src/parser/processor.ts b/src/parser/processor.ts index b87e738c..9f100bbb 100644 --- a/src/parser/processor.ts +++ b/src/parser/processor.ts @@ -9,7 +9,7 @@ import { ContentEvaluatorModule } from "./content-evaluator-module"; import { DataPurgeModule } from "./data-purge-module"; import { FormattingEvaluatorModule } from "./formatting-evaluator-module"; import { GithubCommentModule } from "./github-comment-module"; -import { PermitGenerationModule } from "./permit-generation-module"; +import { PaymentModule } from "./payment-module"; import { UserExtractorModule } from "./user-extractor-module"; import { getTaskReward } from "../helpers/label-price-extractor"; import { GitHubIssue } from "../github-types"; @@ -25,7 +25,7 @@ export class Processor { .add(new DataPurgeModule(context)) .add(new FormattingEvaluatorModule(context)) .add(new ContentEvaluatorModule(context)) - .add(new PermitGenerationModule(context)) + .add(new PaymentModule(context)) .add(new GithubCommentModule(context)); this._context = context; this._configuration = this._context.config.incentives; diff --git a/src/types/results.ts b/src/types/results.ts index 8f405152..f0c746c5 100644 --- a/src/types/results.ts +++ b/src/types/results.ts @@ -10,6 +10,7 @@ export interface Result { }; feeRate?: number; permitUrl?: string; + explorerUrl?: string; userId: number; evaluationCommentHtml?: string; };