diff --git a/.env.example b/.env.example index 81328dc4..0643146d 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,8 @@ GITHUB_TOKEN="" OPENAI_API_KEY="" +# treasury fee applied to the final permits, ex: 100 = 100%, 0.1 = 0.1% +PERMIT_FEE_RATE="" +# github account associated with EVM treasury address allowed to claim permit fees, ex: "ubiquibot-treasury" +PERMIT_TREASURY_GITHUB_USERNAME="" +# comma separated list of token addresses which should not incur any fees, ex: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d, 0x4ECaBa5870353805a9F068101A40E0f32ed605C6" +PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST="" diff --git a/.github/workflows/compute.yml b/.github/workflows/compute.yml index c0a83fe4..70b64d7e 100644 --- a/.github/workflows/compute.yml +++ b/.github/workflows/compute.yml @@ -28,8 +28,35 @@ jobs: SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} NFT_MINTER_PRIVATE_KEY: ${{ secrets.NFT_MINTER_PRIVATE_KEY }} NFT_CONTRACT_ADDRESS: ${{ secrets.NFT_CONTRACT_ADDRESS }} + PERMIT_FEE_RATE: ${{ secrets.PERMIT_FEE_RATE }} + PERMIT_TREASURY_GITHUB_USERNAME: ${{ secrets.PERMIT_TREASURY_GITHUB_USERNAME }} + PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST: ${{ secrets.PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST }} steps: + - name: Post starting comment to issue + uses: actions/github-script@v7 + id: post-comment + with: + github-token: ${{ inputs.authToken }} + script: | + const comment_body = '\`\`\`diff\n+ Evaluating results. Please wait...'; + const obj = ${{ inputs.eventPayload }} + if (obj.issue && "${{ inputs.eventName }}" === "issues.closed") { + const response = await github.rest.issues.createComment({ + owner: obj.repository.owner.login, + repo: obj.repository.name, + issue_number: obj.issue.number, + body: comment_body, + }); + core.setOutput('comment_id', response.data.id); + } + + - name: Set environment variable + run: echo "COMMENT_ID=${{ steps.post-comment.outputs.comment_id }}" >> $GITHUB_ENV + + - run: ${{ toJSON(inputs) }} + shell: cat {0} + - name: Checkout code uses: actions/checkout@v4 @@ -38,8 +65,5 @@ jobs: with: node-version: "20.10.0" - - run: ${{ toJSON(inputs) }} - shell: cat {0} - - name: Generate Rewards uses: ./ diff --git a/.github/workflows/jest-testing.yml b/.github/workflows/jest-testing.yml index c3d954ab..65bb69be 100644 --- a/.github/workflows/jest-testing.yml +++ b/.github/workflows/jest-testing.yml @@ -20,6 +20,9 @@ jobs: EVM_PRIVATE_ENCRYPTED: "kmpTKq5Wh9r9x5j3U9GqZr3NYnjK2g0HtbzeUBOuLC2y3x8ja_SKBNlB2AZ6LigXHP_HeMitftVUtzmoj8CFfVP9SqjWoL6IPku1hVTWkdTn97g1IxzmjydFxjdcf0wuDW1hvVtoq3Uw5yALABqxcQ" NFT_MINTER_PRIVATE_KEY: ${{ secrets.NFT_MINTER_PRIVATE_KEY }} NFT_CONTRACT_ADDRESS: ${{ secrets.NFT_CONTRACT_ADDRESS }} + PERMIT_FEE_RATE: "" + PERMIT_TREASURY_GITHUB_USERNAME: "" + PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST: "" steps: - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 3c432a4d..61f619e9 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -17,3 +17,4 @@ jobs: - uses: googleapis/release-please-action@v4 with: release-type: simple + target-branch: main diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e05133..a72d970e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.2.0](https://github.com/ubiquibot/conversation-rewards/compare/v1.1.0...v1.2.0) (2024-07-10) + + +### Features + +* reward is now split if there are multiple assignees ([b556238](https://github.com/ubiquibot/conversation-rewards/commit/b55623812633bc48760e07bbbd7a1c8f7509121d)) + + +### Bug Fixes + +* assignees are added to the reward even without commenting ([170cdcc](https://github.com/ubiquibot/conversation-rewards/commit/170cdcc694cf4499eb8210beff1a58885c99c5a4)) +* users with no comment now can see their issue task on multiple assignees ([615d221](https://github.com/ubiquibot/conversation-rewards/commit/615d221bc1d0a8129f58e2c0ff5c06339d177792)) + ## [1.1.0](https://github.com/ubiquibot/conversation-rewards/compare/v1.0.0...v1.1.0) (2024-07-07) diff --git a/README.md b/README.md index 743b0dff..d9cb2b22 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,9 @@ with: evmNetworkId: 100 evmPrivateEncrypted: "encrypted-key" erc20RewardToken: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d" + dataCollection: + maxAttempts: 10 + delayMs: 10000 incentives: enabled: true requirePriceLabel: true diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..8770df86 --- /dev/null +++ b/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "Conversation rewards", + "description": "Generate rewards for on topic conversation for closing issues as complete.", + "ubiquity:listeners": [ "issues.closed" ] +} diff --git a/package.json b/package.json index e7be7d63..9a011776 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@supabase/supabase-js": "2.42.0", "@ubiquibot/permit-generation": "1.3.1", "@ubiquity-dao/rpc-handler": "1.1.0", + "@ubiquity-dao/ubiquibot-logger": "1.2.0", "decimal.js": "10.4.3", "dotenv": "16.4.5", "ethers": "^6.13.0", @@ -40,6 +41,7 @@ "lodash": "4.17.21", "markdown-it": "14.1.0", "openai": "4.29.1", + "ts-retry": "4.2.5", "tsx": "4.7.1", "typebox-validators": "0.3.5", "yaml": "2.4.1" diff --git a/src/configuration/data-collection-config.ts b/src/configuration/data-collection-config.ts new file mode 100644 index 00000000..59b4982f --- /dev/null +++ b/src/configuration/data-collection-config.ts @@ -0,0 +1,14 @@ +import { Static, Type } from "@sinclair/typebox"; + +export const dataCollectionConfigurationType = Type.Object({ + /** + * The maximum amount of retries on failure. + */ + maxAttempts: Type.Number({ default: 10, minimum: 1 }), + /** + * The delay between each retry, in milliseconds. + */ + delayMs: Type.Number({ default: 10000, minimum: 100 }), +}); + +export type DataCollectionConfiguration = Static; diff --git a/src/configuration/incentives.ts b/src/configuration/incentives.ts index 122dbcc1..cb2dd070 100644 --- a/src/configuration/incentives.ts +++ b/src/configuration/incentives.ts @@ -1,6 +1,7 @@ import { StaticDecode, Type as T } from "@sinclair/typebox"; import { StandardValidator } from "typebox-validators"; import { contentEvaluatorConfigurationType } from "./content-evaluator-config"; +import { dataCollectionConfigurationType } from "./data-collection-config"; import { dataPurgeConfigurationType } from "./data-purge-config"; import { formattingEvaluatorConfigurationType } from "./formatting-evaluator-config"; import { githubCommentConfigurationType } from "./github-comment-config"; @@ -40,7 +41,9 @@ export const incentivesConfigurationSchema = T.Object({ permitGeneration: permitGenerationConfigurationType, githubComment: githubCommentConfigurationType, }), + dataCollection: dataCollectionConfigurationType, }); + export const validateIncentivesConfiguration = new StandardValidator(incentivesConfigurationSchema); export type IncentivesConfiguration = StaticDecode; diff --git a/src/helpers/github-comment-module-instance.ts b/src/helpers/github-comment-module-instance.ts new file mode 100644 index 00000000..7b7f2a1b --- /dev/null +++ b/src/helpers/github-comment-module-instance.ts @@ -0,0 +1,10 @@ +import * as github from "@actions/github"; +import { GithubCommentModule } from "../parser/github-comment-module"; + +export function getGithubWorkflowRunUrl() { + return `${github.context.payload.repository?.html_url}/actions/runs/${github.context.runId}`; +} + +const githubCommentModule = new GithubCommentModule(); + +export default githubCommentModule; diff --git a/src/helpers/label-price-extractor.ts b/src/helpers/label-price-extractor.ts index ad264825..28c1ee65 100644 --- a/src/helpers/label-price-extractor.ts +++ b/src/helpers/label-price-extractor.ts @@ -1,4 +1,4 @@ -import { GitHubIssue } from "../github-types.ts"; +import { GitHubIssue } from "../github-types"; export function getSortedPrices(labels: GitHubIssue["labels"] | undefined) { if (!labels) return []; diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts new file mode 100644 index 00000000..c37bb718 --- /dev/null +++ b/src/helpers/logger.ts @@ -0,0 +1,5 @@ +import { Logs } from "@ubiquity-dao/ubiquibot-logger"; + +const logger = new Logs("debug"); + +export default logger; diff --git a/src/index.ts b/src/index.ts index a6fee8ad..26f8b02b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import * as core from "@actions/core"; +import githubCommentModuleInstance, { getGithubWorkflowRunUrl } from "./helpers/github-comment-module-instance"; +import logger from "./helpers/logger"; import { run } from "./run"; export default run() @@ -6,8 +8,15 @@ export default run() core?.setOutput("result", result); return result; }) - .catch((e) => { - console.error("Failed to run comment evaluation:", e); - core?.setFailed(e.toString()); + .catch(async (e) => { + const errorMessage = logger.error(`Failed to run comment evaluation. ${e}`, e); + try { + await githubCommentModuleInstance.postComment( + `${errorMessage?.logMessage.diff}\n` + ); + } catch (err) { + logger.error(`Failed to update Github comment: ${err}`); + } + core?.setFailed(e); return e; }); diff --git a/src/issue-activity.ts b/src/issue-activity.ts index a149ef7c..a2063d0d 100644 --- a/src/issue-activity.ts +++ b/src/issue-activity.ts @@ -1,4 +1,7 @@ +import { retryAsyncUntilDefinedDecorator } from "ts-retry"; import { CommentType } from "./configuration/comment-types"; +import configuration from "./configuration/config-reader"; +import { DataCollectionConfiguration } from "./configuration/data-collection-config"; import { collectLinkedMergedPulls } from "./data-collection/collect-linked-pulls"; import { GitHubIssue, @@ -8,18 +11,22 @@ import { GitHubPullRequestReviewComment, GitHubPullRequestReviewState, } from "./github-types"; +import githubCommentModuleInstance from "./helpers/github-comment-module-instance"; +import logger from "./helpers/logger"; import { - IssueParams, - PullParams, getIssue, getIssueComments, getIssueEvents, getPullRequest, getPullRequestReviewComments, getPullRequestReviews, + IssueParams, + PullParams, } from "./start"; export class IssueActivity { + readonly _configuration: DataCollectionConfiguration = configuration.dataCollection; + constructor(private _issueParams: IssueParams) {} self: GitHubIssue | null = null; @@ -28,11 +35,32 @@ export class IssueActivity { linkedReviews: Review[] = []; async init() { + function fn(func: () => Promise) { + return func(); + } + const decoratedFn = retryAsyncUntilDefinedDecorator(fn, { + delay: this._configuration.delayMs, + maxTry: this._configuration.maxAttempts, + async onError(error) { + try { + const content = "Failed to retrieve activity. Retrying..."; + const message = logger.error(content, { error }); + await githubCommentModuleInstance.postComment(message?.logMessage.diff || content); + } catch (e) { + logger.error(`${e}`); + } + }, + async onMaxRetryFunc(error) { + logger.error("Failed to retrieve activity after 10 attempts. See logs for more details.", { + error, + }); + }, + }); [this.self, this.events, this.comments, this.linkedReviews] = await Promise.all([ - getIssue(this._issueParams), - getIssueEvents(this._issueParams), - getIssueComments(this._issueParams), - this._getLinkedReviews(), + decoratedFn(() => getIssue(this._issueParams)), + decoratedFn(() => getIssueEvents(this._issueParams)), + decoratedFn(() => getIssueComments(this._issueParams)), + decoratedFn(() => this._getLinkedReviews()), ]); } diff --git a/src/parser/github-comment-module.ts b/src/parser/github-comment-module.ts index 2a51427e..f84bfef4 100644 --- a/src/parser/github-comment-module.ts +++ b/src/parser/github-comment-module.ts @@ -6,11 +6,12 @@ import { CommentType } from "../configuration/comment-types"; import configuration from "../configuration/config-reader"; import { GithubCommentConfiguration, githubCommentConfigurationType } from "../configuration/github-comment-config"; import { getOctokitInstance } from "../get-authentication-token"; +import { getGithubWorkflowRunUrl } from "../helpers/github-comment-module-instance"; +import logger from "../helpers/logger"; +import { getERC20TokenSymbol } from "../helpers/web3"; import { IssueActivity } from "../issue-activity"; -import { parseGitHubUrl } from "../start"; import program from "./command-line"; import { GithubCommentScore, Module, Result } from "./processor"; -import { getERC20TokenSymbol } from "../helpers/web3"; interface SortedTasks { issues: { specification: GithubCommentScore | null; comments: GithubCommentScore[] }; @@ -23,6 +24,11 @@ interface SortedTasks { export class GithubCommentModule implements Module { private readonly _configuration: GithubCommentConfiguration = configuration.incentives.githubComment; private readonly _debugFilePath = "./output.html"; + /** + * COMMENT_ID can be set in the environment to reference the id of the last comment created during this workflow. + * See also compute.yml to understand how it is set. + */ + private _lastCommentId: number | null = process.env.COMMENT_ID ? Number(process.env.COMMENT_ID) : null; async transform(data: Readonly, result: Result): Promise { const bodyArray: (string | undefined)[] = []; @@ -31,23 +37,20 @@ export class GithubCommentModule implements Module { result[key].evaluationCommentHtml = await this._generateHtml(key, value); bodyArray.push(result[key].evaluationCommentHtml); } + // Add the workflow run url and the metadata in the GitHub's comment + bodyArray.push("\n"); const body = bodyArray.join(""); if (this._configuration.debug) { fs.writeFileSync(this._debugFilePath, body); } if (this._configuration.post) { try { - const octokit = getOctokitInstance(); - const { owner, repo, issue_number } = parseGitHubUrl(program.eventPayload.issue.html_url); - - await octokit.issues.createComment({ - body, - repo, - owner, - issue_number, - }); + await this.postComment(body); } catch (e) { - console.error(`Could not post GitHub comment: ${e}`); + logger.error(`Could not post GitHub comment: ${e}`); } } return result; @@ -55,12 +58,33 @@ export class GithubCommentModule implements Module { get enabled(): boolean { if (!Value.Check(githubCommentConfigurationType, this._configuration)) { - console.warn("Invalid configuration detected for GithubContentModule, disabling."); + logger.error("Invalid configuration detected for GithubContentModule, disabling."); return false; } return true; } + async postComment(body: string, updateLastComment = true) { + const { eventPayload } = program; + if (updateLastComment && this._lastCommentId !== null) { + await getOctokitInstance().issues.updateComment({ + body, + repo: eventPayload.repository.name, + owner: eventPayload.repository.owner.login, + issue_number: eventPayload.issue.number, + comment_id: this._lastCommentId, + }); + } else { + const comment = await getOctokitInstance().issues.createComment({ + body, + repo: eventPayload.repository.name, + owner: eventPayload.repository.owner.login, + issue_number: eventPayload.issue.number, + }); + this._lastCommentId = comment.data.id; + } + } + _createContributionRows(result: Result[0], sortedTasks: SortedTasks | undefined) { const content: string[] = []; diff --git a/src/parser/permit-generation-module.ts b/src/parser/permit-generation-module.ts index 0d6b7b6a..dfeac0b9 100644 --- a/src/parser/permit-generation-module.ts +++ b/src/parser/permit-generation-module.ts @@ -11,6 +11,7 @@ import { SupportedEvents, TokenType, } from "@ubiquibot/permit-generation/core"; +import Decimal from "decimal.js"; import configuration from "../configuration/config-reader"; import { PermitGenerationConfiguration, @@ -69,6 +70,9 @@ export class PermitGenerationModule implements Module { }; const adapters = {} as ReturnType; + // apply fees + result = await this._applyFees(result, payload.erc20RewardToken); + for (const [key, value] of Object.entries(result)) { try { const config: Context["config"] = { @@ -110,6 +114,85 @@ export class PermitGenerationModule implements Module { console.error(e); } } + + // remove treasury item from final result in order not to display permit fee in github comments + if (process.env.PERMIT_TREASURY_GITHUB_USERNAME) delete result[process.env.PERMIT_TREASURY_GITHUB_USERNAME]; + + return result; + } + + /** + * Applies fees to the final result. + * How it works: + * 1. Fee (read from ENV variable) is subtracted from all of the final result items (user.total, user.task.reward, user.comments[].reward) + * 2. Total fee is calculated + * 3. A new item is added to the final result object, example: + * ``` + * { + * ...other items + * "ubiquibot-treasury": { + * total: 10.00, + * userId: 1 + * } + * } + * ``` + * This method is meant to be called before the final permit generation. + * @param result Result object + * @param erc20RewardToken ERC20 address of the reward token + * @returns Result object + */ + async _applyFees(result: Result, erc20RewardToken: string): Promise { + // validate fee related env variables + if (!process.env.PERMIT_FEE_RATE || +process.env.PERMIT_FEE_RATE === 0) { + console.log("PERMIT_FEE_RATE is not set, skipping permit fee generation"); + return result; + } + if (!process.env.PERMIT_TREASURY_GITHUB_USERNAME) { + console.log("PERMIT_TREASURY_GITHUB_USERNAME is not set, skipping permit fee generation"); + return result; + } + if (process.env.PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST) { + const erc20TokensNoFee = process.env.PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST.split(","); + if (erc20TokensNoFee.includes(erc20RewardToken)) { + console.log(`Token address ${erc20RewardToken} is whitelisted to be fee free, skipping permit fee generation`); + return result; + } + } + + // Get treasury github user id + const octokit = getOctokitInstance(); + const { data: treasuryGithubData } = await octokit.users.getByUsername({ username: process.env.PERMIT_TREASURY_GITHUB_USERNAME }); + if (!treasuryGithubData) { + console.log(`GitHub user was not found for username ${process.env.PERMIT_TREASURY_GITHUB_USERNAME}, skipping permit fee generation`); + return result; + } + + // Subtract fees from the final result: + // - user.total + // - user.task.reward + // - user.comments[].reward + const feeRateDecimal = new Decimal(100).minus(process.env.PERMIT_FEE_RATE).div(100); + let permitFeeAmountDecimal = new Decimal(0); + for (const [_, rewardResult] of Object.entries(result)) { + // accumulate total permit fee amount + const totalAfterFee = +(new Decimal(rewardResult.total).mul(feeRateDecimal)); + permitFeeAmountDecimal = permitFeeAmountDecimal.add(new Decimal(rewardResult.total).minus(totalAfterFee)); + // subtract fees + rewardResult.total = +totalAfterFee.toFixed(2); + if (rewardResult.task) rewardResult.task.reward = +(new Decimal(rewardResult.task.reward).mul(feeRateDecimal).toFixed(2)); + if (rewardResult.comments) { + for (let comment of rewardResult.comments) { + if (comment.score) comment.score.reward = +(new Decimal(comment.score.reward).mul(feeRateDecimal).toFixed(2)); + } + } + } + + // Add a new result item for treasury + result[process.env.PERMIT_TREASURY_GITHUB_USERNAME] = { + total: +permitFeeAmountDecimal.toFixed(2), + userId: treasuryGithubData.id, + }; + return result; } diff --git a/src/parser/processor.ts b/src/parser/processor.ts index d2344142..eb2fd464 100644 --- a/src/parser/processor.ts +++ b/src/parser/processor.ts @@ -2,11 +2,12 @@ import Decimal from "decimal.js"; import * as fs from "fs"; import { CommentType } from "../configuration/comment-types"; import configuration from "../configuration/config-reader"; +import githubCommentModuleInstance from "../helpers/github-comment-module-instance"; +import logger from "../helpers/logger"; import { IssueActivity } from "../issue-activity"; 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 { UserExtractorModule } from "./user-extractor-module"; @@ -21,7 +22,7 @@ export class Processor { .add(new FormattingEvaluatorModule()) .add(new ContentEvaluatorModule()) .add(new PermitGenerationModule()) - .add(new GithubCommentModule()); + .add(githubCommentModuleInstance); } add(transformer: Module) { @@ -31,7 +32,7 @@ export class Processor { async run(data: Readonly) { if (!this._configuration.enabled) { - console.log("Module is disabled. Skipping..."); + logger.debug("Module is disabled. Skipping..."); return; } for (const transformer of this._transformers) { @@ -67,7 +68,7 @@ export class Processor { 2 ); if (!file) { - console.log(result); + logger.debug(result); } else { fs.writeFileSync(file, result); } diff --git a/src/parser/user-extractor-module.ts b/src/parser/user-extractor-module.ts index c13c7618..282c4e9c 100644 --- a/src/parser/user-extractor-module.ts +++ b/src/parser/user-extractor-module.ts @@ -3,7 +3,7 @@ import Decimal from "decimal.js"; import configuration from "../configuration/config-reader"; import { UserExtractorConfiguration, userExtractorConfigurationType } from "../configuration/user-extractor-config"; import { GitHubIssue } from "../github-types"; -import { getSortedPrices } from "../helpers/label-price-extractor.ts"; +import { getSortedPrices } from "../helpers/label-price-extractor"; import { IssueActivity } from "../issue-activity"; import { Module, Result } from "./processor"; diff --git a/src/run.ts b/src/run.ts index d98587e1..2e6de60b 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,43 +1,32 @@ -import { getSortedPrices } from "./helpers/label-price-extractor.ts"; +import configuration from "./configuration/config-reader"; +import githubCommentModuleInstance from "./helpers/github-comment-module-instance"; +import { getSortedPrices } from "./helpers/label-price-extractor"; +import logger from "./helpers/logger"; import { IssueActivity } from "./issue-activity"; import program from "./parser/command-line"; import { Processor } from "./parser/processor"; import { parseGitHubUrl } from "./start"; -import { getOctokitInstance } from "./get-authentication-token.ts"; -import configuration from "./configuration/config-reader.ts"; export async function run() { const { eventPayload, eventName } = program; if (eventName === "issues.closed") { if (eventPayload.issue.state_reason !== "completed") { - const result = "# Issue was not closed as completed. Skipping."; - await getOctokitInstance().issues.createComment({ - body: `\`\`\`text\n${result}\n\`\`\``, - repo: eventPayload.repository.name, - owner: eventPayload.repository.owner.login, - issue_number: eventPayload.issue.number, - }); - return result; + const result = logger.info("Issue was not closed as completed. Skipping."); + await githubCommentModuleInstance.postComment(result?.logMessage.diff || ""); + return result?.logMessage.raw; } const issue = parseGitHubUrl(eventPayload.issue.html_url); const activity = new IssueActivity(issue); await activity.init(); if (configuration.incentives.requirePriceLabel && !getSortedPrices(activity.self?.labels).length) { - const result = "! No price label has been set. Skipping permit generation."; - await getOctokitInstance().issues.createComment({ - body: `\`\`\`text\n${result}\n\`\`\``, - repo: eventPayload.repository.name, - owner: eventPayload.repository.owner.login, - issue_number: eventPayload.issue.number, - }); - return result; + const result = logger.error("No price label has been set. Skipping permit generation."); + await githubCommentModuleInstance.postComment(result?.logMessage.diff || ""); + return result?.logMessage.raw; } const processor = new Processor(); await processor.run(activity); return processor.dump(); } else { - const result = `${eventName} is not supported, skipping.`; - console.warn(result); - return result; + return logger.error(`${eventName} is not supported, skipping.`)?.logMessage.raw; } } diff --git a/src/types/env-type.ts b/src/types/env-type.ts index 5fa39f93..4110e18d 100644 --- a/src/types/env-type.ts +++ b/src/types/env-type.ts @@ -8,6 +8,9 @@ const envConfigSchema = Type.Object({ OPENAI_API_KEY: Type.String(), NFT_MINTER_PRIVATE_KEY: Type.String({ default: "" }), NFT_CONTRACT_ADDRESS: Type.String({ default: "" }), + PERMIT_FEE_RATE: Type.String(), + PERMIT_TREASURY_GITHUB_USERNAME: Type.String(), + PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST: Type.String(), }); export type EnvConfigType = Static; diff --git a/src/types/env.d.ts b/src/types/env.d.ts index cbe80985..f2190a5b 100644 --- a/src/types/env.d.ts +++ b/src/types/env.d.ts @@ -8,6 +8,10 @@ declare global { SUPABASE_URL: string; NFT_CONTRACT_ADDRESS: string; NFT_MINTER_PRIVATE_KEY: string; + PERMIT_FEE_RATE: string, + PERMIT_TREASURY_GITHUB_USERNAME: string, + PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST: string, + COMMENT_ID: string | undefined; } } } diff --git a/tests/__mocks__/results/output-reward-split.html b/tests/__mocks__/results/output-reward-split.html index 253c8232..c6f00da9 100644 --- a/tests/__mocks__/results/output-reward-split.html +++ b/tests/__mocks__/results/output-reward-split.html @@ -1 +1,132 @@ -

[ 34.12 WXDAI ]

@0x4007
Contributions Overview
View Contribution Count Reward
Issue Task 0.5 25
Issue Specification 1 3.84
Issue Comment 2 5.28
Conversation Incentives
Comment Formatting Relevance Reward
Looks like the filters are barely useable now that we have the s…
4.8
content:
  p:
    count: 48
    score: 1
  img:
    count: 1
    score: 0
wordValue: 0.1
formattingMultiplier: 1
0.8 3.84
Okay both bots are broken @gentlementlegen We should have split…
2.6
content:
  p:
    count: 13
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 2.08
Actually, looks like it did the right thing for your reward on v…
4
content:
  p:
    count: 20
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 3.2

[ 25.82 WXDAI ]

@gentlementlegen
Contributions Overview
View Contribution Count Reward
Issue Task 0.5 25
Issue Comment 2 0.82
Conversation Incentives
Comment Formatting Relevance Reward
@0x4007 So it should be 25 each? I can confirm this is not handl…
0.6
content:
  p:
    count: 24
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.48
Ah yes because it doesn't apply the `0.5` multiplier I s…
0.425
content:
  p:
    count: 16
    score: 1
  code:
    count: 1
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.34
\ No newline at end of file +

[ 34.12 WXDAI ]

@0x4007
Contributions Overview
View Contribution Count Reward
Issue Task 0.5 25
Issue Specification 1 3.84
Issue Comment 2 5.28
Conversation Incentives
Comment Formatting Relevance Reward
Looks like the filters are barely useable now that we have the s…
4.8
content:
  p:
    count: 48
    score: 1
  img:
    count: 1
    score: 0
wordValue: 0.1
formattingMultiplier: 1
0.8 3.84
Okay both bots are broken @gentlementlegen We should have split…
2.6
content:
  p:
    count: 13
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 2.08
Actually, looks like it did the right thing for your reward on v…
4
content:
  p:
    count: 20
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 3.2

[ 25.82 WXDAI ]

@gentlementlegen
Contributions Overview
View Contribution Count Reward
Issue Task 0.5 25
Issue Comment 2 0.82
Conversation Incentives
Comment Formatting Relevance Reward
@0x4007 So it should be 25 each? I can confirm this is not handl…
0.6
content:
  p:
    count: 24
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.48
Ah yes because it doesn't apply the `0.5` multiplier I s…
0.425
content:
  p:
    count: 16
    score: 1
  code:
    count: 1
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.34
+ \ No newline at end of file diff --git a/tests/__mocks__/results/output.html b/tests/__mocks__/results/output.html index c0463068..9a5bd4da 100644 --- a/tests/__mocks__/results/output.html +++ b/tests/__mocks__/results/output.html @@ -1 +1,476 @@ -

[ 45.5 WXDAI ]

@gitcoindev
Contributions Overview
View Contribution Count Reward
Issue Task 1 37.5
Issue Comment 1 0
Review Comment 3 8
Conversation Incentives
Comment Formatting Relevance Reward
@molecula451 I tried to override X25519_PRIVATE_KEY but it did n…
0
content:
  p:
    count: 26
    score: 1
wordValue: 0
formattingMultiplier: 0
0.8 -
The new evmPrivateKeyEncrypted generated for address 0x3a2E44e10…
0
content:
  p:
    count: 11
    score: 1
wordValue: 0
formattingMultiplier: 0
0.8 -
@pavlovcik @molecula451 please check now again, I added to docs.
4
content:
  p:
    count: 10
    score: 1
wordValue: 0.2
formattingMultiplier: 2
0.8 3.2
No way, full details are available in plain sight, only for test…
6
content:
  p:
    count: 15
    score: 1
wordValue: 0.2
formattingMultiplier: 2
0.8 4.8

[ 1.78 WXDAI ]

@molecula451
Contributions Overview
View Contribution Count Reward
Issue Comment 6 1.4
Review Comment 2 0.38
Conversation Incentives
Comment Formatting Relevance Reward
pavlovcik i think we need to update a bit the readme ![image_202…
0.375
content:
  p:
    count: 15
    score: 1
  img:
    count: 1
    score: 0
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.3
let us know when done
0.125
content:
  p:
    count: 5
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.1
https://github.com/ubiquibot/comment-incentives/actions/runs/793…
0.175
content:
  p:
    count: 7
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.14
@pavlovcik permitted with hard debug (tho no funds in the privat…
0.3
content:
  p:
    count: 12
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.24
pavlovcik i re-generated the X25519 to trigger the permit, what …
0.725
content:
  p:
    count: 29
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.58
sure thing
0.05
content:
  p:
    count: 2
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.04
indeed
0.025
content:
  p:
    count: 1
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.02
go to go pavlovick, we'll be using this one for test only or tes…
0.45
content:
  p:
    count: 18
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.36

[ 60.24 WXDAI ]

@0x4007
Contributions Overview
View Contribution Count Reward
Issue Specification 1 4.72
Issue Comment 6 27.2
Review Comment 3 28.32
Conversation Incentives
Comment Formatting Relevance Reward
Can somebody work on generating a new `X25519_PRIVATE_KEY …
5.9
content:
  p:
    count: 56
    score: 1
  code:
    count: 3
    score: 1
  em:
    count: 6
    score: 0
wordValue: 0.1
formattingMultiplier: 1
0.8 4.72
Link below for conversation context. It was to me. Anyways you n…
4.2
content:
  p:
    count: 21
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 3.36
In the repository secrets I think I need to change the key to ma…
3
content:
  p:
    count: 15
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 2.4
I just changed it to `627H-BcWbcp_O3YmQGIA6MqgxVsFuplFCA9DK3…
13.6
content:
  p:
    count: 67
    score: 1
  code:
    count: 1
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 10.88
I don't understand what you mean by this
1.6
content:
  p:
    count: 8
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 1.28
I'll investigate more on my computer later.
1.4
content:
  p:
    count: 7
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 1.12
Will it be an issue if I revert to the commit and secret that I …
10.2
content:
  p:
    count: 51
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 8.16
Need to document a private key too
0.7
content:
  p:
    count: 7
    score: 1
wordValue: 0.1
formattingMultiplier: 1
0.8 0.56
I was editing this right now but was too slow to push.
1.2
content:
  p:
    count: 12
    score: 1
wordValue: 0.1
formattingMultiplier: 1
0.8 0.96
I am quoting some code! <task-lists sortable=""> <table…
33.5
content:
  p:
    count: 332
    score: 1
  code:
    count: 2
    score: 1
  a:
    count: 1
    score: 1
wordValue: 0.1
formattingMultiplier: 1
0.8 26.8
\ No newline at end of file +

[ 45.5 WXDAI ]

@gitcoindev
Contributions Overview
View Contribution Count Reward
Issue Task 1 37.5
Issue Comment 1 0
Review Comment 3 8
Conversation Incentives
Comment Formatting Relevance Reward
@molecula451 I tried to override X25519_PRIVATE_KEY but it did n…
0
content:
  p:
    count: 26
    score: 1
wordValue: 0
formattingMultiplier: 0
0.8 -
The new evmPrivateKeyEncrypted generated for address 0x3a2E44e10…
0
content:
  p:
    count: 11
    score: 1
wordValue: 0
formattingMultiplier: 0
0.8 -
@pavlovcik @molecula451 please check now again, I added to docs.
4
content:
  p:
    count: 10
    score: 1
wordValue: 0.2
formattingMultiplier: 2
0.8 3.2
No way, full details are available in plain sight, only for test…
6
content:
  p:
    count: 15
    score: 1
wordValue: 0.2
formattingMultiplier: 2
0.8 4.8

[ 1.78 WXDAI ]

@molecula451
Contributions Overview
View Contribution Count Reward
Issue Comment 6 1.4
Review Comment 2 0.38
Conversation Incentives
Comment Formatting Relevance Reward
pavlovcik i think we need to update a bit the readme ![image_202…
0.375
content:
  p:
    count: 15
    score: 1
  img:
    count: 1
    score: 0
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.3
let us know when done
0.125
content:
  p:
    count: 5
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.1
https://github.com/ubiquibot/comment-incentives/actions/runs/793…
0.175
content:
  p:
    count: 7
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.14
@pavlovcik permitted with hard debug (tho no funds in the privat…
0.3
content:
  p:
    count: 12
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.24
pavlovcik i re-generated the X25519 to trigger the permit, what …
0.725
content:
  p:
    count: 29
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.58
sure thing
0.05
content:
  p:
    count: 2
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.04
indeed
0.025
content:
  p:
    count: 1
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.02
go to go pavlovick, we'll be using this one for test only or tes…
0.45
content:
  p:
    count: 18
    score: 1
wordValue: 0.1
formattingMultiplier: 0.25
0.8 0.36

[ 60.24 WXDAI ]

@0x4007
Contributions Overview
View Contribution Count Reward
Issue Specification 1 4.72
Issue Comment 6 27.2
Review Comment 3 28.32
Conversation Incentives
Comment Formatting Relevance Reward
Can somebody work on generating a new `X25519_PRIVATE_KEY …
5.9
content:
  p:
    count: 56
    score: 1
  code:
    count: 3
    score: 1
  em:
    count: 6
    score: 0
wordValue: 0.1
formattingMultiplier: 1
0.8 4.72
Link below for conversation context. It was to me. Anyways you n…
4.2
content:
  p:
    count: 21
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 3.36
In the repository secrets I think I need to change the key to ma…
3
content:
  p:
    count: 15
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 2.4
I just changed it to `627H-BcWbcp_O3YmQGIA6MqgxVsFuplFCA9DK3…
13.6
content:
  p:
    count: 67
    score: 1
  code:
    count: 1
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 10.88
I don't understand what you mean by this
1.6
content:
  p:
    count: 8
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 1.28
I'll investigate more on my computer later.
1.4
content:
  p:
    count: 7
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 1.12
Will it be an issue if I revert to the commit and secret that I …
10.2
content:
  p:
    count: 51
    score: 1
wordValue: 0.2
formattingMultiplier: 1
0.8 8.16
Need to document a private key too
0.7
content:
  p:
    count: 7
    score: 1
wordValue: 0.1
formattingMultiplier: 1
0.8 0.56
I was editing this right now but was too slow to push.
1.2
content:
  p:
    count: 12
    score: 1
wordValue: 0.1
formattingMultiplier: 1
0.8 0.96
I am quoting some code! <task-lists sortable=""> <table…
33.5
content:
  p:
    count: 332
    score: 1
  code:
    count: 2
    score: 1
  a:
    count: 1
    score: 1
wordValue: 0.1
formattingMultiplier: 1
0.8 26.8
+ \ No newline at end of file diff --git a/tests/__mocks__/results/valid-configuration.json b/tests/__mocks__/results/valid-configuration.json index 43c9775c..8f4f69ff 100644 --- a/tests/__mocks__/results/valid-configuration.json +++ b/tests/__mocks__/results/valid-configuration.json @@ -2,6 +2,10 @@ "evmNetworkId": 100, "evmPrivateEncrypted": "kmpTKq5Wh9r9x5j3U9GqZr3NYnjK2g0HtbzeUBOuLC2y3x8ja_SKBNlB2AZ6LigXHP_HeMitftVUtzmoj8CFfVP9SqjWoL6IPku1hVTWkdTn97g1IxzmjydFxjdcf0wuDW1hvVtoq3Uw5yALABqxcQ", "erc20RewardToken": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", + "dataCollection": { + "delayMs": 10000, + "maxAttempts": 10 + }, "incentives": { "enabled": true, "requirePriceLabel": true, diff --git a/tests/action.test.ts b/tests/action.test.ts index 4f772e06..3ce2e9c8 100644 --- a/tests/action.test.ts +++ b/tests/action.test.ts @@ -1,7 +1,10 @@ /* eslint @typescript-eslint/no-var-requires: 0 */ import "../src/parser/command-line"; +import { http, HttpResponse } from "msw"; +import { IssueActivity } from "../src/issue-activity"; import { run } from "../src/run"; -import { server } from "./__mocks__/node.ts"; +import { parseGitHubUrl } from "../src/start"; +import { server } from "./__mocks__/node"; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); @@ -33,9 +36,63 @@ jest.mock("../src/parser/command-line", () => { }; }); +jest.mock("@actions/github", () => ({ + context: { + runId: "1", + payload: { + repository: { + html_url: "https://github.com/ubiquibot/conversation-rewards", + }, + }, + }, +})); + describe("Action tests", () => { it("Should skip when the issue is closed without the completed status", async () => { const result = await run(); - expect(result).toEqual("# Issue was not closed as completed. Skipping."); + expect(result).toEqual("Issue was not closed as completed. Skipping."); }); + + it("Should link metadata to Github's comment", async () => { + jest.mock("../src/run", () => ({ + run: jest.fn(() => { + return Promise.reject("Some error"); + }), + })); + const githubCommentModule = require("../src/parser/github-comment-module"); + const spy = jest.spyOn(githubCommentModule.GithubCommentModule.prototype, "postComment"); + const run = (await import("../src/index")) as unknown as { default: Promise }; + await expect(run.default).resolves.toEqual("Some error"); + expect(spy).toHaveBeenCalledWith(`\`\`\`diff +! Failed to run comment evaluation. Some error +\`\`\` +`); + }); + + it("Should retry to fetch on network failure", async () => { + // Fakes one crash per route retrieving the data. Should succeed on retry. Timeout for the test function needs + // to be increased since it takes 10 seconds for a retry to happen. + [ + "https://api.github.com/repos/ubiquibot/comment-incentives/issues/22", + "https://api.github.com/repos/ubiquibot/comment-incentives/issues/22/events", + "https://api.github.com/repos/ubiquibot/comment-incentives/issues/22/comments", + "https://api.github.com/repos/ubiquibot/comment-incentives/issues/22/timeline", + ].forEach((url) => { + server.use(http.get(url, () => HttpResponse.json("", { status: 500 }), { once: true })); + }); + const issueUrl = process.env.TEST_ISSUE_URL || "https://github.com/ubiquibot/comment-incentives/issues/22"; + const issue = parseGitHubUrl(issueUrl); + const activity = new IssueActivity(issue); + await activity.init(); + expect(activity.self).toBeTruthy(); + expect(activity.linkedReviews.length).toBeGreaterThan(0); + expect(activity.comments.length).toBeGreaterThan(0); + expect(activity.events.length).toBeGreaterThan(0); + }, 60000); }); diff --git a/tests/parser/permit-generation-module.test.ts b/tests/parser/permit-generation-module.test.ts new file mode 100644 index 00000000..0d3b5146 --- /dev/null +++ b/tests/parser/permit-generation-module.test.ts @@ -0,0 +1,156 @@ +import { CommentType } from "../../src/configuration/comment-types"; +import { PermitGenerationModule } from "../../src/parser/permit-generation-module"; +import { Result } from "../../src/parser/processor"; + +const DOLLAR_ADDRESS = '0xb6919Ef2ee4aFC163BC954C5678e2BB570c2D103'; +const WXDAI_ADDRESS = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d'; + +jest.mock("../../src/parser/command-line", () => { + const cfg = require("../__mocks__/results/valid-configuration.json"); + const dotenv = require("dotenv"); + dotenv.config(); + return { + stateId: 1, + eventName: "issues.closed", + authToken: process.env.GITHUB_TOKEN, + ref: "", + eventPayload: { + issue: { + html_url: "https://github.com/ubiquibot/comment-incentives/issues/22", + number: 1, + state_reason: "not_planned", + }, + repository: { + name: "conversation-rewards", + owner: { + login: "ubiquibot", + }, + }, + }, + settings: JSON.stringify(cfg), + }; +}); + +jest.mock("../../src/get-authentication-token", () => ({ + getOctokitInstance: () => ({ + users: { + getByUsername: () => ({ + data: { + id: 3 + } + }) + } + }) +})); + +jest.mock("@supabase/supabase-js", () => { + return { + createClient: jest.fn(), + }; +}); + +// original rewards object before fees are applied +const resultOriginal: Result = { + "user1": { + "total": 100, + "task": { + "reward": 90, + "multiplier": 1, + }, + "userId": 1, + "comments": [ + { + "content": "comment 3", + "url": "https://github.com/user-org/test-repo/issues/57#issuecomment-2172704421", + "type": CommentType.COMMENTED, + "score": { + "reward": 10 + } + } + ] + }, + "user2": { + "total": 11.11, + "task": { + "reward": 9.99, + "multiplier": 1, + }, + "userId": 1, + "comments": [ + { + "content": "comment 3", + "url": "https://github.com/user-org/test-repo/issues/57#issuecomment-2172704421", + "type": CommentType.COMMENTED, + "score": { + "reward": 1.12 + } + } + ] + }, +}; + +describe("permit-generation-module.ts", () => { + describe("applyFees()", () => { + beforeEach(() => { + // set fee related env variables + // treasury fee applied to the final permits, ex: 100 = 100%, 0.1 = 0.1% + process.env.PERMIT_FEE_RATE='10'; + // github account associated with EVM treasury address allowed to claim permit fees, ex: "ubiquibot-treasury" + process.env.PERMIT_TREASURY_GITHUB_USERNAME="ubiquibot-treasury" + // comma separated list of token addresses which should not incur any fees, ex: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d, 0x4ECaBa5870353805a9F068101A40E0f32ed605C6" + process.env.PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST=`${DOLLAR_ADDRESS}` + }); + + afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); + + it("Should not apply fees if PERMIT_FEE_RATE is empty", async () => { + process.env.PERMIT_FEE_RATE = ''; + const permitGenerationModule = new PermitGenerationModule(); + const spyConsoleLog = jest.spyOn(console, 'log'); + await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + expect(spyConsoleLog).toHaveBeenCalledWith('PERMIT_FEE_RATE is not set, skipping permit fee generation'); + }); + + it("Should not apply fees if PERMIT_FEE_RATE is 0", async () => { + process.env.PERMIT_FEE_RATE = '0'; + const permitGenerationModule = new PermitGenerationModule(); + const spyConsoleLog = jest.spyOn(console, 'log'); + await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + expect(spyConsoleLog).toHaveBeenCalledWith('PERMIT_FEE_RATE is not set, skipping permit fee generation'); + }); + + it("Should not apply fees if PERMIT_TREASURY_GITHUB_USERNAME is empty", async () => { + process.env.PERMIT_TREASURY_GITHUB_USERNAME = ''; + const permitGenerationModule = new PermitGenerationModule(); + const spyConsoleLog = jest.spyOn(console, 'log'); + await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + expect(spyConsoleLog).toHaveBeenCalledWith('PERMIT_TREASURY_GITHUB_USERNAME is not set, skipping permit fee generation'); + }); + + it("Should not apply fees if ERC20 reward token is included in PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST", async () => { + const permitGenerationModule = new PermitGenerationModule(); + const spyConsoleLog = jest.spyOn(console, 'log'); + await permitGenerationModule._applyFees(resultOriginal, DOLLAR_ADDRESS); + expect(spyConsoleLog).toHaveBeenCalledWith(`Token address ${DOLLAR_ADDRESS} is whitelisted to be fee free, skipping permit fee generation`); + }); + + it("Should apply fees", async () => { + const permitGenerationModule = new PermitGenerationModule(); + const resultAfterFees = await permitGenerationModule._applyFees(resultOriginal, WXDAI_ADDRESS); + + // check that 10% fee is subtracted from rewards + expect(resultAfterFees['user1'].total).toEqual(90); + expect(resultAfterFees['user1'].task?.reward).toEqual(81); + expect(resultAfterFees['user1'].comments?.[0].score?.reward).toEqual(9); + expect(resultAfterFees['user2'].total).toEqual(10); + expect(resultAfterFees['user2'].task?.reward).toEqual(8.99); + expect(resultAfterFees['user2'].comments?.[0].score?.reward).toEqual(1.01); + + // check that treasury item is added + expect(resultAfterFees['ubiquibot-treasury'].total).toEqual(11.11); + }); + }); +}); diff --git a/tests/price-label.test.ts b/tests/price-label.test.ts index e99f7915..596df015 100644 --- a/tests/price-label.test.ts +++ b/tests/price-label.test.ts @@ -1,7 +1,7 @@ /* eslint @typescript-eslint/no-var-requires: 0 */ import "../src/parser/command-line"; import { run } from "../src/run"; -import { server } from "./__mocks__/node.ts"; +import { server } from "./__mocks__/node"; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); @@ -42,6 +42,6 @@ jest.mock("../src/parser/command-line", () => { describe("Price tests", () => { it("Should skip when no price label is set", async () => { const result = await run(); - expect(result).toEqual("! No price label has been set. Skipping permit generation."); + expect(result).toEqual("No price label has been set. Skipping permit generation."); }); }); diff --git a/tests/process.issue.test.ts b/tests/process.issue.test.ts index 5cfe266e..48ce342d 100644 --- a/tests/process.issue.test.ts +++ b/tests/process.issue.test.ts @@ -35,6 +35,17 @@ jest.mock("../src/helpers/web3", () => ({ }, })); +jest.mock("@actions/github", () => ({ + context: { + runId: "1", + payload: { + repository: { + html_url: "https://github.com/ubiquibot/conversation-rewards", + }, + }, + }, +})); + jest.mock("../src/parser/command-line", () => { // Require is needed because mock cannot access elements out of scope // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -239,7 +250,9 @@ describe("Modules tests", () => { await processor.run(activity); const result = JSON.parse(processor.dump()); expect(result).toEqual(githubCommentResults); - expect(fs.readFileSync("./output.html")).toEqual(fs.readFileSync("./tests/__mocks__/results/output.html")); + expect(fs.readFileSync("./output.html", "utf-8")).toEqual( + fs.readFileSync("./tests/__mocks__/results/output.html", "utf-8") + ); }); it("Should properly generate the configuration", () => { @@ -249,7 +262,7 @@ describe("Modules tests", () => { }); it("Should do a full run", async () => { - const module = (await import("../src/index.ts")) as unknown as { default: Promise }; + const module = (await import("../src/index")) as unknown as { default: Promise }; const result = await module.default; expect(result).toBeTruthy(); }); diff --git a/tests/rewards.test.ts b/tests/rewards.test.ts index f79b3b5c..46a3636b 100644 --- a/tests/rewards.test.ts +++ b/tests/rewards.test.ts @@ -2,19 +2,19 @@ import { drop } from "@mswjs/data"; import Decimal from "decimal.js"; import fs from "fs"; import { http, HttpResponse } from "msw"; -import { IssueActivity } from "../src/issue-activity.ts"; -import { ContentEvaluatorModule } from "../src/parser/content-evaluator-module.ts"; -import { DataPurgeModule } from "../src/parser/data-purge-module.ts"; -import { FormattingEvaluatorModule } from "../src/parser/formatting-evaluator-module.ts"; -import { GithubCommentModule } from "../src/parser/github-comment-module.ts"; -import { PermitGenerationModule } from "../src/parser/permit-generation-module.ts"; -import { Processor } from "../src/parser/processor.ts"; -import { UserExtractorModule } from "../src/parser/user-extractor-module.ts"; -import { parseGitHubUrl } from "../src/start.ts"; +import githubCommentModuleInstance from "../src/helpers/github-comment-module-instance"; +import { IssueActivity } from "../src/issue-activity"; +import { ContentEvaluatorModule } from "../src/parser/content-evaluator-module"; +import { DataPurgeModule } from "../src/parser/data-purge-module"; +import { FormattingEvaluatorModule } from "../src/parser/formatting-evaluator-module"; +import { PermitGenerationModule } from "../src/parser/permit-generation-module"; +import { Processor } from "../src/parser/processor"; +import { UserExtractorModule } from "../src/parser/user-extractor-module"; +import { parseGitHubUrl } from "../src/start"; import "../src/parser/command-line"; -import { db, db as mockDb } from "./__mocks__/db.ts"; +import { db, db as mockDb } from "./__mocks__/db"; import dbSeed from "./__mocks__/db-seed.json"; -import { server } from "./__mocks__/node.ts"; +import { server } from "./__mocks__/node"; import rewardSplitResult from "./__mocks__/results/reward-split.json"; const issueUrl = "https://github.com/ubiquity/work.ubq.fi/issues/69"; @@ -23,6 +23,17 @@ jest.spyOn(ContentEvaluatorModule.prototype, "_evaluateComments").mockImplementa return Promise.resolve(comments.map(() => new Decimal(0.8))); }); +jest.mock("@actions/github", () => ({ + context: { + runId: "1", + payload: { + repository: { + html_url: "https://github.com/ubiquibot/conversation-rewards", + }, + }, + }, +})); + jest.mock("@ubiquibot/permit-generation/core", () => { const originalModule = jest.requireActual("@ubiquibot/permit-generation/core"); @@ -136,7 +147,7 @@ describe("Rewards tests", () => { new FormattingEvaluatorModule(), new ContentEvaluatorModule(), new PermitGenerationModule(), - new GithubCommentModule(), + githubCommentModuleInstance, ]; server.use( http.post("https://*", () => @@ -157,8 +168,8 @@ describe("Rewards tests", () => { await processor.run(activity); const result = JSON.parse(processor.dump()); expect(result).toEqual(rewardSplitResult); - expect(fs.readFileSync("./output.html")).toEqual( - fs.readFileSync("./tests/__mocks__/results/output-reward-split.html") + expect(fs.readFileSync("./output.html", "utf-8")).toEqual( + fs.readFileSync("./tests/__mocks__/results/output-reward-split.html", "utf-8") ); }); }); diff --git a/tsconfig.json b/tsconfig.json index 7b4680a9..54f91018 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ diff --git a/yarn.lock b/yarn.lock index fa8a19f8..d066987e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3315,7 +3315,7 @@ ethers "6.11.1" libsodium-wrappers "^0.7.13" -"@ubiquity-dao/rpc-handler@^1.1.0": +"@ubiquity-dao/rpc-handler@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@ubiquity-dao/rpc-handler/-/rpc-handler-1.1.0.tgz#5a17e98de8c611ea19315ff946166b20d6f7f629" integrity sha512-EzbwAoHx+jPEymAdJbKt1O7C9RB/R3IWcBPRbDzFs8iBOuFktClsR8e95xvFrV9n2jTmr2ZHxpXE/rOLOEWfPA== @@ -3324,6 +3324,11 @@ axios "^1.7.1" node-fetch "^3.3.2" +"@ubiquity-dao/ubiquibot-logger@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ubiquity-dao/ubiquibot-logger/-/ubiquibot-logger-1.2.0.tgz#c636edb299e22dc4f55e4cab64cc8d976d89fbf4" + integrity sha512-CgnfbiIZNc7CVrGfHKksIdbeehc6pG7qEfUMTS4vyNlPnIa0OnQeigGbn6zKvkUvIRvJ9oj0gqFATs7vGDoL4w== + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -4493,9 +4498,9 @@ easy-table@1.2.0: wcwidth "^1.0.1" electron-to-chromium@^1.4.796: - version "1.4.818" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz#7762c8bfd15a07c3833b7f5deed990e9e5a4c24f" - integrity sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA== + version "1.4.819" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.819.tgz#b1bf73d71748a44c3b719cfe7b351d75268c9044" + integrity sha512-8RwI6gKUokbHWcN3iRij/qpvf/wCbIVY5slODi85werwqUQwpFXM+dvUBND93Qh7SB0pW3Hlq3/wZsqQ3M9Jaw== elliptic@6.5.4: version "6.5.4" @@ -4774,9 +4779,9 @@ esprima@^4.0.0, esprima@^4.0.1: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -5233,9 +5238,9 @@ glob-parent@^6.0.2: is-glob "^4.0.3" glob@^10.2.2: - version "10.4.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.3.tgz#e0ba2253dd21b3d0acdfb5d507c59a29f513fc7a" - integrity sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg== + version "10.4.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.4.tgz#d60943feb6f8140522117e6576a923b715718380" + integrity sha512-XsOKvHsu38Xe19ZQupE6N/HENeHQBA05o3hV8labZZT2zYDg1+emxWHnc/Bm9AcCMPXfD6jt+QC7zC5JSFyumw== dependencies: foreground-child "^3.1.0" jackspeak "^3.1.2" @@ -5867,9 +5872,9 @@ iterable-lookahead@^1.0.0: integrity sha512-hJnEP2Xk4+44DDwJqUQGdXal5VbyeWLaPyDl2AQc242Zr7iqz4DgpQOrEzglWVMGHMDCkguLHEKxd1+rOsmgSQ== jackspeak@^3.1.2: - version "3.4.1" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.1.tgz#145422416740568e9fc357bf60c844b3c1585f09" - integrity sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg== + version "3.4.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.2.tgz#c3d1e00071d52dba8b0dac17cd2a12d0187d2989" + integrity sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: @@ -6608,9 +6613,9 @@ log-update@^6.0.0: wrap-ansi "^9.0.0" lru-cache@^10.0.1, lru-cache@^10.0.2, lru-cache@^10.2.0: - version "10.3.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.3.1.tgz#a37050586f84ccfdb570148a253bf1632a29ef44" - integrity sha512-9/8QXrtbGeMB6LxwQd4x1tIMnsmUxMvIH/qWGsccz6bt9Uln3S+sgAaqfQNhbGA8ufzs2fHuP/yqapGgP9Hh2g== + version "10.4.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.1.tgz#da9a9cb51aec89fda9b485f5a12b2fdb8f6dbe88" + integrity sha512-8h/JsUc/2+Dm9RPJnBAmObGnUqTMmsIKThxixMLOkrebSihRhTV0wLD/8BSk6OU6Pbj8hiDTbsI3fLjBJSlhDg== lru-cache@^5.1.1: version "5.1.1" @@ -8329,6 +8334,11 @@ ts-node@10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-retry@4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/ts-retry/-/ts-retry-4.2.5.tgz#ee4638e66c68bb49da975aa4994d5f16bfb61bc2" + integrity sha512-dFBa4pxMBkt/bjzdBio8EwYfbAdycEAwe0KZgzlUKKwU9Wr1WErK7Hg9QLqJuDDYJXTW4KYZyXAyqYKOdO/ehA== + tslib@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"