diff --git a/src/helpers/web3.ts b/src/helpers/web3.ts index e448b501..723f5f40 100644 --- a/src/helpers/web3.ts +++ b/src/helpers/web3.ts @@ -2,13 +2,21 @@ import { RPCHandler, HandlerConstructorConfig, NetworkId } from "@ubiquity-dao/r import { Context } from "@ubiquity-os/permit-generation"; import { ethers, utils } from "ethers"; +// Required ERC20 ABI functions +export const ERC20_ABI = [ + "function symbol() view returns (string)", + "function decimals() public view returns (uint8)", + "function balanceOf(address) view returns (uint256)", + "function transfer(address,uint256) public returns (bool)", +]; + /** * Returns the funding wallet * @param privateKey of the funding wallet * @param provider ethers.Provider * @returns the funding wallet */ -async function getFundingWallet(privateKey: string, provider: ethers.providers.Provider) { +export async function getFundingWallet(privateKey: string, provider: ethers.providers.Provider) { try { return new ethers.Wallet(privateKey, provider); } catch (error) { @@ -24,14 +32,7 @@ async function getFundingWallet(privateKey: string, provider: ethers.providers.P * @returns ERC20 token contract */ -async function getErc20TokenContract(networkId: number, tokenAddress: string) { - const abi = [ - "function symbol() view returns (string)", - "function decimals() public view returns (uint8)", - "function balanceOf(address) view returns (uint256)", - "function transfer(address,uint256) public returns (bool)", - ]; - +export async function getErc20TokenContract(networkId: number, tokenAddress: string) { // get fastest RPC const config: HandlerConstructorConfig = { networkName: null, @@ -52,7 +53,7 @@ async function getErc20TokenContract(networkId: number, tokenAddress: string) { const handler = new RPCHandler(config); const provider = await handler.getFastestRpcProvider(); - return new ethers.Contract(tokenAddress, abi, provider); + return new ethers.Contract(tokenAddress, ERC20_ABI, provider); } /** * Returns ERC20 token symbol @@ -70,7 +71,7 @@ export async function getErc20TokenSymbol(networkId: number, tokenAddress: strin * @param tokenAddress ERC20 token address * @returns ERC20 token decimals */ -async function getErc20TokenDecimals(networkId: number, tokenAddress: string) { +export async function getErc20TokenDecimals(networkId: number, tokenAddress: string) { return await (await getErc20TokenContract(networkId, tokenAddress)).decimals(); } diff --git a/tests/__mocks__/results/valid-configuration.json b/tests/__mocks__/results/valid-configuration.json index a4dfd79c..5d338460 100644 --- a/tests/__mocks__/results/valid-configuration.json +++ b/tests/__mocks__/results/valid-configuration.json @@ -5,6 +5,7 @@ }, "erc20RewardToken": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", "evmNetworkId": 100, + "autoTransferMode": false, "evmPrivateEncrypted": "qGslGsT9bfY0C3uc_utvFhBN_Zwggf0sONEFOfF67E_gWDXqnV3qC-LnGj2ufIHJwdHxFLz3VVzup2MrGtFtHhZ2WZZvq_8NQi48LFbr5b6OIxL-vOXooAyQYfb2mjLmcsYaxXc5eu1mUfO9rfENSAw9dFeWfi9VKQ", "incentives": { "collaboratorOnlyPaymentInvocation": true, diff --git a/tests/helpers/web3.test.ts b/tests/helpers/web3.test.ts index 29889cf1..bf170903 100644 --- a/tests/helpers/web3.test.ts +++ b/tests/helpers/web3.test.ts @@ -1,4 +1,13 @@ -import { getErc20TokenSymbol } from "../../src/helpers/web3"; +import { BigNumber } from "ethers"; +import { + getErc20TokenSymbol, + getErc20TokenDecimals, + getFundingWalletBalance, + getFundingWallet, + getErc20TokenContract, + ERC20_ABI, +} from "../../src/helpers/web3"; +import { Interface } from "ethers/lib/utils"; describe("web3.ts", () => { describe("getERC20TokenSymbol()", () => { @@ -9,4 +18,47 @@ describe("web3.ts", () => { expect(tokenSymbol).toEqual("WXDAI"); }, 120000); }); + describe("getErc20TokenDecimals()", () => { + it("Should return ERC20 token decimals", async () => { + const networkId = 100; // gnosis + const tokenAddress = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"; // WXDAI + const tokenDecimals = await getErc20TokenDecimals(networkId, tokenAddress); + expect(tokenDecimals).toEqual(18); + }, 120000); + }); + describe("getFundingWalletBalance()", () => { + it("Should return ERC20 token balance of the funding wallet", async () => { + const networkId = 100; // gnosis + const tokenAddress = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"; // WXDAI + const tokenBalance: BigNumber = await getFundingWalletBalance( + networkId, + tokenAddress, + "100958e64966448354216e91d4d4b9418c3fa0cb0a21b935535ced1df8145a0e" + ); + expect(tokenBalance.isZero()).toEqual(true); + }, 120000); + }); + describe("getFundingWallet()", () => { + it("Should return ERC20 token decimals", async () => { + const networkId = 100; // gnosis + const tokenAddress = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"; // WXDAI + const contract = await getErc20TokenContract(networkId, tokenAddress); + const fundingWallet = await getFundingWallet( + "100958e64966448354216e91d4d4b9418c3fa0cb0a21b935535ced1df8145a0e", + contract.provider + ); + const fundingWalledAddress = await fundingWallet.getAddress(); + expect(fundingWalledAddress.toLowerCase()).toEqual("0x94d7a85efef179560f9b821cadd20056600fdb9d"); + }, 120000); + }); + + describe("getErc20TokenContract()", () => { + it("Should return ERC20 token contract", async () => { + const networkId = 100; // gnosis + const tokenAddress = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"; // WXDAI + const contract = await getErc20TokenContract(networkId, tokenAddress); + expect(contract.address).toEqual(tokenAddress); + expect(contract.interface).toEqual(new Interface(ERC20_ABI)); + }, 120000); + }); }); diff --git a/tests/parser/permit-generation-module.test.ts b/tests/parser/permit-generation-module.test.ts index a4dccb5f..5c691cc4 100644 --- a/tests/parser/permit-generation-module.test.ts +++ b/tests/parser/permit-generation-module.test.ts @@ -97,13 +97,13 @@ const resultOriginal: Result = { }, }; -const { PermitGenerationModule } = await import("../../src/parser/permit-generation-module"); +const { PaymentModule } = await import("../../src/parser/payment-module"); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); -describe("permit-generation-module.ts", () => { +describe("payment-module.ts", () => { describe("applyFees()", () => { beforeEach(() => { ctx.env.PERMIT_FEE_RATE = "10"; @@ -125,9 +125,9 @@ describe("permit-generation-module.ts", () => { it("Should not apply fees if PERMIT_FEE_RATE is empty", async () => { ctx.env.PERMIT_FEE_RATE = ""; - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "info"); - await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + await paymentModule._applyFees(resultOriginal, WXDAI_ADDRESS); const logCallArgs = spyConsoleLog.mock.calls.map((call) => call[0]); expect(logCallArgs[0]).toMatch(/.*PERMIT_FEE_RATE is not set, skipping permit fee generation/); spyConsoleLog.mockReset(); @@ -135,9 +135,9 @@ describe("permit-generation-module.ts", () => { it("Should not apply fees if PERMIT_FEE_RATE is 0", async () => { ctx.env.PERMIT_FEE_RATE = "0"; - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "info"); - await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + await paymentModule._applyFees(resultOriginal, WXDAI_ADDRESS); const logCallArgs = spyConsoleLog.mock.calls.map((call) => call[0]); expect(logCallArgs[0]).toMatch(/.*PERMIT_FEE_RATE is not set, skipping permit fee generation/); spyConsoleLog.mockReset(); @@ -146,18 +146,18 @@ describe("permit-generation-module.ts", () => { it("Should not apply fees if PERMIT_TREASURY_GITHUB_USERNAME is empty", async () => { process.env.PERMIT_TREASURY_GITHUB_USERNAME = ""; ctx.env.PERMIT_TREASURY_GITHUB_USERNAME = ""; - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "info"); - await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + await paymentModule._applyFees(resultOriginal, WXDAI_ADDRESS); const logCallArgs = spyConsoleLog.mock.calls.map((call) => call[0]); expect(logCallArgs[0]).toMatch(/.*PERMIT_TREASURY_GITHUB_USERNAME is not set, skipping permit fee generation/); spyConsoleLog.mockReset(); }); it("Should not apply fees if ERC20 reward token is included in PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "info"); - await permitGenerationModule._applyFees(resultOriginal, DOLLAR_ADDRESS); + await paymentModule._applyFees(resultOriginal, DOLLAR_ADDRESS); const logCallArgs = spyConsoleLog.mock.calls.map((call) => call[0]); expect(logCallArgs[0]).toMatch( new RegExp(`.*Token address ${DOLLAR_ADDRESS} is whitelisted to be fee free, skipping permit fee generation`) @@ -166,8 +166,8 @@ describe("permit-generation-module.ts", () => { }); it("Should apply fees", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); - const resultAfterFees = await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + const paymentModule = new PaymentModule(ctx); + const resultAfterFees = await paymentModule._applyFees(resultOriginal, WXDAI_ADDRESS); // check that 10% fee is subtracted from rewards expect(resultAfterFees["user1"].total).toEqual(90); @@ -189,7 +189,7 @@ describe("permit-generation-module.ts", () => { }); it("Should return false if private key could not be decrypted", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "warn"); // format: "PRIVATE_KEY" @@ -198,7 +198,7 @@ describe("permit-generation-module.ts", () => { const githubContextOrganizationId = 1; const githubContextRepositoryId = 2; - const isAllowed = await permitGenerationModule._isPrivateKeyAllowed( + const isAllowed = await paymentModule._isPrivateKeyAllowed( privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId, @@ -212,7 +212,7 @@ describe("permit-generation-module.ts", () => { }); it("Should return false if private key is used in unallowed organization", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "info"); // format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID" @@ -222,7 +222,7 @@ describe("permit-generation-module.ts", () => { const githubContextOrganizationId = 99; const githubContextRepositoryId = 2; - const isAllowed = await permitGenerationModule._isPrivateKeyAllowed( + const isAllowed = await paymentModule._isPrivateKeyAllowed( privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId, @@ -236,7 +236,7 @@ describe("permit-generation-module.ts", () => { }); it("Should return true if private key is used in allowed organization", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); // format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID" // encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:1" @@ -245,7 +245,7 @@ describe("permit-generation-module.ts", () => { const githubContextOrganizationId = 1; const githubContextRepositoryId = 2; - const isAllowed = await permitGenerationModule._isPrivateKeyAllowed( + const isAllowed = await paymentModule._isPrivateKeyAllowed( privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId, @@ -256,7 +256,7 @@ describe("permit-generation-module.ts", () => { }); it("Should return false if private key is used in un-allowed organization and allowed repository", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "info"); // format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" @@ -266,7 +266,7 @@ describe("permit-generation-module.ts", () => { const githubContextOrganizationId = 99; const githubContextRepositoryId = 2; - const isAllowed = await permitGenerationModule._isPrivateKeyAllowed( + const isAllowed = await paymentModule._isPrivateKeyAllowed( privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId, @@ -282,7 +282,7 @@ describe("permit-generation-module.ts", () => { }); it("Should return false if private key is used in allowed organization and unallowed repository", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "info"); // format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" @@ -292,7 +292,7 @@ describe("permit-generation-module.ts", () => { const githubContextOrganizationId = 1; const githubContextRepositoryId = 99; - const isAllowed = await permitGenerationModule._isPrivateKeyAllowed( + const isAllowed = await paymentModule._isPrivateKeyAllowed( privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId, @@ -308,7 +308,7 @@ describe("permit-generation-module.ts", () => { }); it("Should return true if private key is used in allowed organization and repository", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); // format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" // encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:1:2" @@ -317,7 +317,7 @@ describe("permit-generation-module.ts", () => { const githubContextOrganizationId = 1; const githubContextRepositoryId = 2; - const isAllowed = await permitGenerationModule._isPrivateKeyAllowed( + const isAllowed = await paymentModule._isPrivateKeyAllowed( privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId, @@ -328,7 +328,7 @@ describe("permit-generation-module.ts", () => { }); it("Should return false if private key format is invalid", async () => { - const permitGenerationModule = new PermitGenerationModule(ctx); + const paymentModule = new PaymentModule(ctx); const spyConsoleLog = jest.spyOn(console, "warn"); // format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" @@ -338,7 +338,7 @@ describe("permit-generation-module.ts", () => { const githubContextOrganizationId = 1; const githubContextRepositoryId = 2; - const isAllowed = await permitGenerationModule._isPrivateKeyAllowed( + const isAllowed = await paymentModule._isPrivateKeyAllowed( privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId, diff --git a/tests/permit-generatable.test.ts b/tests/permit-generatable.test.ts index 05ff6441..2fb9e7f4 100644 --- a/tests/permit-generatable.test.ts +++ b/tests/permit-generatable.test.ts @@ -12,6 +12,7 @@ import dbSeed from "./__mocks__/db-seed.json"; import { server } from "./__mocks__/node"; import permitGenerationResults from "./__mocks__/results/permit-generation-results.json"; import cfg from "./__mocks__/results/valid-configuration.json"; +import { parseUnits } from "ethers/lib/utils"; const issueUrl = process.env.TEST_ISSUE_URL ?? "https://github.com/ubiquity-os/conversation-rewards/issues/5"; @@ -19,6 +20,10 @@ jest.unstable_mockModule("../src/helpers/web3", () => ({ getErc20TokenSymbol() { return "WXDAI"; }, + getFundingWalletBalance() { + return parseUnits("100", 18); + }, + transferFromFundingWallet() {}, })); jest.unstable_mockModule("@actions/github", () => ({ @@ -159,7 +164,7 @@ const { IssueActivity } = await import("../src/issue-activity"); const { ContentEvaluatorModule } = await import("../src/parser/content-evaluator-module"); const { DataPurgeModule } = await import("../src/parser/data-purge-module"); const { FormattingEvaluatorModule } = await import("../src/parser/formatting-evaluator-module"); -const { PermitGenerationModule } = await import("../src/parser/permit-generation-module"); +const { PaymentModule } = await import("../src/parser/payment-module"); const { Processor } = await import("../src/parser/processor"); const { UserExtractorModule } = await import("../src/parser/user-extractor-module"); @@ -176,7 +181,7 @@ afterAll(() => { server.close(); }); -describe("Permit Generation Module Tests", () => { +describe("Payment Module Tests", () => { const issue = parseGitHubUrl(issueUrl); const activity = new IssueActivity(ctx, issue); @@ -227,7 +232,7 @@ describe("Permit Generation Module Tests", () => { new DataPurgeModule(ctx), new FormattingEvaluatorModule(ctx), new ContentEvaluatorModule(ctx), - new PermitGenerationModule(ctx), + new PaymentModule(ctx), ]; server.use(http.post("https://*", () => passthrough())); @@ -246,7 +251,7 @@ describe("Permit Generation Module Tests", () => { new DataPurgeModule(ctx), new FormattingEvaluatorModule(ctx), new ContentEvaluatorModule(ctx), - new PermitGenerationModule(ctx), + new PaymentModule(ctx), ]; server.use(http.post("https://*", () => passthrough())); @@ -271,7 +276,7 @@ describe("Permit Generation Module Tests", () => { new DataPurgeModule(ctx), new FormattingEvaluatorModule(ctx), new ContentEvaluatorModule(ctx), - new PermitGenerationModule(ctx), + new PaymentModule(ctx), ]; server.use(http.post("https://*", () => passthrough())); @@ -290,7 +295,7 @@ describe("Permit Generation Module Tests", () => { new DataPurgeModule(ctx), new FormattingEvaluatorModule(ctx), new ContentEvaluatorModule(ctx), - new PermitGenerationModule(ctx), + new PaymentModule(ctx), ]; server.use(http.post("https://*", () => passthrough())); diff --git a/tests/process.issue.test.ts b/tests/process.issue.test.ts index 5e8bbf87..ca8c8495 100644 --- a/tests/process.issue.test.ts +++ b/tests/process.issue.test.ts @@ -21,6 +21,7 @@ import cfg from "./__mocks__/results/valid-configuration.json"; import { customOctokit as Octokit } from "@ubiquity-os/plugin-sdk/octokit"; import { CommentAssociation } from "../src/configuration/comment-types"; import { GitHubIssue } from "../src/github-types"; +import { parseUnits } from "ethers/lib/utils"; const issueUrl = process.env.TEST_ISSUE_URL ?? "https://github.com/ubiquity-os/conversation-rewards/issues/5"; @@ -28,6 +29,10 @@ jest.unstable_mockModule("../src/helpers/web3", () => ({ getErc20TokenSymbol() { return "WXDAI"; }, + getFundingWalletBalance() { + return parseUnits("100", 18); + }, + transferFromFundingWallet() {}, })); jest.unstable_mockModule("@actions/github", () => ({ @@ -156,7 +161,7 @@ const { ContentEvaluatorModule } = await import("../src/parser/content-evaluator const { DataPurgeModule } = await import("../src/parser/data-purge-module"); const { FormattingEvaluatorModule } = await import("../src/parser/formatting-evaluator-module"); const { GithubCommentModule } = await import("../src/parser/github-comment-module"); -const { PermitGenerationModule } = await import("../src/parser/permit-generation-module"); +const { PaymentModule } = await import("../src/parser/payment-module"); const { Processor } = await import("../src/parser/processor"); const { UserExtractorModule } = await import("../src/parser/user-extractor-module"); @@ -270,7 +275,7 @@ describe("Modules tests", () => { new DataPurgeModule(ctx), new FormattingEvaluatorModule(ctx), new ContentEvaluatorModule(ctx), - new PermitGenerationModule(ctx), + new PaymentModule(ctx), ]; // This catches calls by getFastestRpc server.use(http.post("https://*", () => passthrough())); @@ -286,7 +291,7 @@ describe("Modules tests", () => { new DataPurgeModule(ctx), new FormattingEvaluatorModule(ctx), new ContentEvaluatorModule(ctx), - new PermitGenerationModule(ctx), + new PaymentModule(ctx), new GithubCommentModule(ctx), ]; // This catches calls by getFastestRpc diff --git a/tests/rewards.test.ts b/tests/rewards.test.ts index 14fcfdda..481b85ef 100644 --- a/tests/rewards.test.ts +++ b/tests/rewards.test.ts @@ -13,6 +13,7 @@ import dbSeed from "./__mocks__/db-seed.json"; import { server } from "./__mocks__/node"; import rewardSplitResult from "./__mocks__/results/reward-split.json"; import cfg from "./__mocks__/results/valid-configuration.json"; +import { parseUnits } from "ethers/lib/utils"; const issueUrl = "https://github.com/ubiquity/work.ubq.fi/issues/69"; @@ -89,6 +90,10 @@ jest.unstable_mockModule("../src/helpers/web3", () => ({ getErc20TokenSymbol() { return "WXDAI"; }, + getFundingWalletBalance() { + return parseUnits("100", 18); + }, + transferFromFundingWallet() {}, })); jest.unstable_mockModule("../src/helpers/get-comment-details", () => ({ @@ -125,7 +130,7 @@ const { ContentEvaluatorModule } = await import("../src/parser/content-evaluator const { DataPurgeModule } = await import("../src/parser/data-purge-module"); const { FormattingEvaluatorModule } = await import("../src/parser/formatting-evaluator-module"); const { GithubCommentModule } = await import("../src/parser/github-comment-module"); -const { PermitGenerationModule } = await import("../src/parser/permit-generation-module"); +const { PaymentModule } = await import("../src/parser/payment-module"); const { Processor } = await import("../src/parser/processor"); const { UserExtractorModule } = await import("../src/parser/user-extractor-module"); @@ -189,7 +194,7 @@ describe("Rewards tests", () => { new DataPurgeModule(ctx), new FormattingEvaluatorModule(ctx), new ContentEvaluatorModule(ctx), - new PermitGenerationModule(ctx), + new PaymentModule(ctx), new GithubCommentModule(ctx), ]; server.use(http.post("https://*", () => passthrough()));