diff --git a/README.md b/README.md index e8d09c845..6c4c121d7 100644 --- a/README.md +++ b/README.md @@ -224,22 +224,6 @@ Bounty bot is built using the [probot](https://probot.github.io/) framework so i ├── utils A set of utility functions -## Default Config Notes (`ubiquibot-config-default.ts`) - -We can't use a `jsonc` file due to limitations with Netlify. Here is a snippet of some values with notes next to them. - -```jsonc -{ - "payment-permit-max-price": 9007199254740991, // Number.MAX_SAFE_INTEGER - "max-concurrent-assigns": 9007199254740991, // Number.MAX_SAFE_INTEGER - "comment-element-pricing": { - /* https://github.com/syntax-tree/mdast#nodes */ - "strong": 0 // Also includes italics, unfortunately https://github.com/syntax-tree/mdast#strong - /* https://github.com/syntax-tree/mdast#gfm */ - } -} -``` - ## Supabase Cron Job (`logs-cleaner`) ##### Dashboard > Project > Database > Extensions diff --git a/src/bindings/config.ts b/src/bindings/config.ts index 55d0fb8be..43b5718fa 100644 --- a/src/bindings/config.ts +++ b/src/bindings/config.ts @@ -65,9 +65,7 @@ export const loadConfig = async (context: Context): Promise => { permitBaseUrl: process.env.PERMIT_BASE_URL || permitBaseUrl, }, unassign: { - timeRangeForMaxIssue: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE - ? Number(process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE) - : timeRangeForMaxIssue, + timeRangeForMaxIssue: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE ? Number(process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE) : timeRangeForMaxIssue, timeRangeForMaxIssueEnabled: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED ? process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED == "true" : timeRangeForMaxIssueEnabled, diff --git a/src/configs/ubiquibot-config-default.ts b/src/configs/ubiquibot-config-default.ts index 9598ef782..5fce38d64 100644 --- a/src/configs/ubiquibot-config-default.ts +++ b/src/configs/ubiquibot-config-default.ts @@ -4,8 +4,8 @@ export const DefaultConfig: MergedConfig = { evmNetworkId: 100, priceMultiplier: 1, issueCreatorMultiplier: 2, - paymentPermitMaxPrice: 9007199254740991, - maxConcurrentAssigns: 9007199254740991, + paymentPermitMaxPrice: Number.MAX_SAFE_INTEGER, + maxConcurrentAssigns: Number.MAX_SAFE_INTEGER, assistivePricing: false, disableAnalytics: false, commentIncentives: false, @@ -87,7 +87,31 @@ export const DefaultConfig: MergedConfig = { ], incentives: { comment: { - elements: {}, + elements: { + h1: 0, + h2: 0, + h3: 0, + h4: 0, + h5: 0, + h6: 0, + a: 0, + ul: 0, + li: 0, + p: 0, + img: 0, + code: 0, + table: 0, + td: 0, + tr: 0, + br: 0, + blockquote: 0, + em: 0, + strong: 0, + hr: 0, + del: 0, + pre: 0, + ol: 0, + }, totals: { word: 0, }, diff --git a/src/handlers/comment/handlers/ask.ts b/src/handlers/comment/handlers/ask.ts index 63777d4ae..27c841574 100644 --- a/src/handlers/comment/handlers/ask.ts +++ b/src/handlers/comment/handlers/ask.ts @@ -21,7 +21,7 @@ export const ask = async (body: string) => { } if (!issue) { - return `This command can only be used on issues`; + return `This command can only be used on issues or pull requests`; } const chatHistory: CreateChatCompletionRequestMessage[] = []; diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index 4dcbf7f21..43d62946a 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -291,7 +291,7 @@ export const userCommands = (): UserCommands[] => { }, { id: IssueCommentCommands.ASK, - description: `Ask a technical question to the Ubiquity AI. \n example usage: "/ask How do I do X?"`, + description: `Ask a technical question to UbiquiBot. \n example usage: "/ask How do I do X?"`, handler: ask, callback: commandCallback, }, diff --git a/src/handlers/comment/handlers/payout.ts b/src/handlers/comment/handlers/payout.ts index d5aabc7d6..2255f8599 100644 --- a/src/handlers/comment/handlers/payout.ts +++ b/src/handlers/comment/handlers/payout.ts @@ -1,4 +1,4 @@ -import { getBotContext, getLogger } from "../../../bindings"; +import { getBotConfig, getBotContext, getLogger } from "../../../bindings"; import { Payload } from "../../../types"; import { IssueCommentCommands } from "../commands"; import { @@ -14,6 +14,9 @@ import { GLOBAL_STRINGS } from "../../../configs"; export const payout = async (body: string) => { const { payload: _payload } = getBotContext(); + const { + payout: { permitBaseUrl }, + } = getBotConfig(); const logger = getLogger(); if (body != IssueCommentCommands.PAYOUT && body.replace(/`/g, "") != IssueCommentCommands.PAYOUT) { logger.info(`Skipping to payout. body: ${body}`); @@ -39,7 +42,7 @@ export const payout = async (body: string) => { return `Permit generation failed due to internal GitHub Error`; } - const hasPosted = IssueComments.find((e) => e.user.type === "Bot" && e.body.includes("https://pay.ubq.fi?claim")); + const hasPosted = IssueComments.find((e) => e.user.type === "Bot" && e.body.includes(permitBaseUrl)); if (hasPosted) { logger.info(`Permit already generated for ${payload.issue?.number}`); return; diff --git a/src/handlers/comment/handlers/table.ts b/src/handlers/comment/handlers/table.ts index 97f154e0b..a54769f77 100644 --- a/src/handlers/comment/handlers/table.ts +++ b/src/handlers/comment/handlers/table.ts @@ -16,23 +16,51 @@ export const tableComment = ({ days?: number; }) => { return ` - -${ - isBountyStale - ? `` - : `` -} - - - - - - - - -${multiplier ? `` : ``} -${reason ? `` : ``} -${bounty ? `` : ``} -
Warning! This task was created over ${days} days ago. Please confirm that this issue specification is accurate before starting.
Deadline${deadline}
Registered Wallet${wallet}
Payment Multiplier${multiplier}
Multiplier Reason${reason}
Total Bounty${bounty}
`; + ${ + isBountyStale + ? ` + + Warning! + This task was created over ${days} days ago. Please confirm that this issue specification is accurate before starting. + ` + : `` + } + + Deadline + ${deadline} + + + Registered Wallet + ${wallet} + + ${ + multiplier + ? ` + + Payment Multiplier + ${multiplier} + ` + : `` + } + ${ + reason + ? ` + + Multiplier Reason + ${reason} + ` + : `` + } + ${ + bounty + ? ` + + Total Bounty + ${bounty} + ` + : `` + } + +`; }; diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 25bf01562..5b47f87b9 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -93,8 +93,8 @@ export const incentivesCalculation = async (): Promise e.user.type === UserType.Bot && e.body.match(claimUrlRegex)); + const claimUrlRegex = new RegExp(`\\((${permitBaseUrl}\\S+)\\)`); + const permitCommentIdx = comments.findIndex((e) => e.user.type === UserType.Bot && e.body.match(claimUrlRegex) && e.body.includes("Task Assignee Reward")); if (wasReopened && permitCommentIdx !== -1) { const permitComment = comments[permitCommentIdx]; diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index e1ef60e76..74d32d440 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -12,7 +12,7 @@ import { BigNumber } from "ethers"; export interface CreatorCommentResult { title: string; account?: string | undefined; - amountInETH?: Decimal | undefined; + rewardInTokens?: Decimal | undefined; userId?: string | undefined; tokenSymbol?: string | undefined; node_id?: string | undefined; @@ -120,14 +120,14 @@ export const calculateIssueCreatorReward = async (incentivesCalculation: Incenti const description = await getIssueDescription(incentivesCalculation.issue.number, "html"); if (!description) { - logger.info(`Skipping to generate a permit url because issue description is empty. description: ${description}`); - return { error: `Skipping to generate a permit url because issue description is empty. description: ${description}` }; + logger.info(`Skipping issue creator reward because issue description is empty. description: ${description}`); + return { error: `Skipping issue creator reward because issue description is empty. description: ${description}` }; } logger.info(`Getting the issue description done. description: ${description}`); const creator = incentivesCalculation.issue.user; - if (creator.type === UserType.Bot || creator.login === incentivesCalculation.issue.assignee) { - logger.info("Issue creator assigneed himself or Bot created this issue."); - return { error: "Issue creator assigneed himself or Bot created this issue." }; + if (creator.type === UserType.Bot) { + logger.info("Skipping issue creator reward because Bot created this issue."); + return { error: "Skipping issue creator reward because Bot created this issue." }; } const result = await generatePermitForComments( @@ -138,8 +138,8 @@ export const calculateIssueCreatorReward = async (incentivesCalculation: Incenti incentivesCalculation.paymentPermitMaxPrice ); - if (!result || !result.account || !result.amountInETH) { - throw new Error("Failed to generate permit for issue creator because of missing account or amountInETH"); + if (!result || !result.account || !result.rewardInTokens) { + throw new Error("Failed to generate permit for issue creator because of missing account or rewardInTokens"); } return { @@ -149,7 +149,7 @@ export const calculateIssueCreatorReward = async (incentivesCalculation: Incenti username: creator.login, reward: [ { - priceInEth: result?.amountInETH ?? new Decimal(0), + priceInEth: result?.rewardInTokens ?? new Decimal(0), account: result?.account, userId: creator.id, user: "", @@ -269,7 +269,7 @@ const generatePermitForComments = async ( multiplier: number, incentives: Incentives, paymentPermitMaxPrice: number -): Promise => { +): Promise => { const logger = getLogger(); const commentsByNode = await parseComments(comments, ItemsToExclude); const rewardValue = calculateRewardValue(commentsByNode, incentives); @@ -279,15 +279,15 @@ const generatePermitForComments = async ( } logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue.sum}`); const account = await getWalletAddress(user); - const amountInETH = rewardValue.sum.mul(multiplier); - if (amountInETH.gt(paymentPermitMaxPrice)) { + const rewardInTokens = rewardValue.sum.mul(multiplier); + if (rewardInTokens.gt(paymentPermitMaxPrice)) { logger.info(`Skipping issue creator reward for user ${user} because reward is higher than payment permit max price`); return; } if (account) { - return { account, amountInETH }; + return { account, rewardInTokens }; } else { - return { account: "0x", amountInETH: new Decimal(0) }; + return { account: "0x", rewardInTokens: new Decimal(0) }; } }; /** diff --git a/src/handlers/push/index.ts b/src/handlers/push/index.ts index 340143439..2abc418c8 100644 --- a/src/handlers/push/index.ts +++ b/src/handlers/push/index.ts @@ -1,6 +1,6 @@ import { getBotContext, getLogger } from "../../bindings"; import { createCommitComment, getFileContent } from "../../helpers"; -import { CommitsPayload, PushPayload, WideConfigSchema } from "../../types"; +import { CommitsPayload, PushPayload, ConfigSchema } from "../../types"; import { parseYAML } from "../../utils/private"; import { updateBaseRate } from "./update-base"; import { validate } from "../../utils/ajv"; @@ -87,7 +87,7 @@ export const validateConfigChange = async () => { if (configFileContent) { const decodedConfig = Buffer.from(configFileContent, "base64").toString(); const config = parseYAML(decodedConfig); - const { valid, error } = validate(WideConfigSchema, config); + const { valid, error } = validate(ConfigSchema, config); if (!valid) { await createCommitComment(`@${payload.sender.login} Config validation failed! ${error}`, commitSha, BASE_RATE_FILE); } diff --git a/src/helpers/gpt.ts b/src/helpers/gpt.ts index 046503d79..dc78cf3e3 100644 --- a/src/helpers/gpt.ts +++ b/src/helpers/gpt.ts @@ -5,7 +5,7 @@ import OpenAI from "openai"; import { CreateChatCompletionRequestMessage } from "openai/resources/chat"; import { ErrorDiff } from "../utils/helpers"; -export const sysMsg = `You are the UbiquityAI, designed to provide accurate technical answers. \n +export const sysMsg = `You are the UbiquiBot, designed to provide accurate technical answers. \n Whenever appropriate, format your response using GitHub Flavored Markdown. Utilize tables, lists, and code blocks for clear and organized answers. \n Do not make up answers. If you are unsure, say so. \n Original Context exists only to provide you with additional information to the current question, use it to formulate answers. \n @@ -14,7 +14,7 @@ All replies MUST end with "\n\n ".\n `; export const gptContextTemplate = ` -You are the UbiquityAI, designed to review and analyze pull requests. +You are the UbiquiBot, designed to review and analyze pull requests. You have been provided with the spec of the issue and all linked issues or pull requests. Using this full context, Reply in pure JSON format, with the following structure omitting irrelvant information pertaining to the specification. You MUST provide the following structure, but you may add additional information if you deem it relevant. @@ -118,17 +118,17 @@ export const decideContextGPT = async ( { role: "system", content: "This issue/Pr context: \n" + JSON.stringify(streamlined), - name: "UbiquityAI", + name: "UbiquiBot", } as CreateChatCompletionRequestMessage, { role: "system", content: "Linked issue(s) context: \n" + JSON.stringify(linkedIssueStreamlined), - name: "UbiquityAI", + name: "UbiquiBot", } as CreateChatCompletionRequestMessage, { role: "system", content: "Linked Pr(s) context: \n" + JSON.stringify(linkedPRStreamlined), - name: "UbiquityAI", + name: "UbiquiBot", } as CreateChatCompletionRequestMessage ); diff --git a/src/helpers/permit.ts b/src/helpers/permit.ts index deddcffe0..e0fe1e6ad 100644 --- a/src/helpers/permit.ts +++ b/src/helpers/permit.ts @@ -5,6 +5,7 @@ import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; import Decimal from "decimal.js"; import { Payload } from "../types"; import { savePermit } from "../adapters/supabase"; +import { ERC20ABI } from "../configs"; const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all networks @@ -45,16 +46,16 @@ type TxData = { }; /** - * Generates permit2 signature data with `spender` and `amountInETH` + * Generates permit2 signature data with `spender` and `amountInTokens` * * @param spender The recipient address we're going to send tokens - * @param amountInETH The token amount in ETH + * @param amountInTokens The token amount * * @returns Permit2 url including base64 encoded data */ export const generatePermit2Signature = async ( spender: string, - amountInEth: Decimal, + amountInTokens: Decimal, identifier: string, userId = "" ): Promise<{ txData: TxData; payoutUrl: string }> => { @@ -65,12 +66,15 @@ export const generatePermit2Signature = async ( const provider = new ethers.providers.JsonRpcProvider(rpc); const adminWallet = new ethers.Wallet(privateKey, provider); + const tokenContract = new ethers.Contract(paymentToken, ERC20ABI, provider); + const decimals = await tokenContract.decimals(); + const permitTransferFromData: PermitTransferFrom = { permitted: { // token we are permitting to be transferred token: paymentToken, // amount we are permitting to be transferred - amount: ethers.utils.parseUnits(amountInEth.toString(), 18), + amount: ethers.utils.parseUnits(amountInTokens.toString(), decimals), }, // who can transfer the tokens spender: spender, @@ -101,9 +105,12 @@ export const generatePermit2Signature = async ( const base64encodedTxData = Buffer.from(JSON.stringify(txData)).toString("base64"); - const payoutUrl = `${permitBaseUrl}?claim=${base64encodedTxData}&network=${networkId}`; - logger.info(`Generated permit2 url: ${payoutUrl}`); - return { txData, payoutUrl }; + const payoutUrl = new URL(permitBaseUrl); + payoutUrl.searchParams.append("claim", base64encodedTxData); + payoutUrl.searchParams.append("network", networkId.toString()); + logger.info(`Generated permit2 of amount ${amountInTokens} for user ${spender}, url: ${payoutUrl}`); + + return { txData, payoutUrl: payoutUrl.toString() }; }; export const savePermitToDB = async (bountyHunterId: number, txData: TxData): Promise => { diff --git a/src/types/config.ts b/src/types/config.ts index a22683a28..eeec20253 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -181,7 +181,7 @@ export const GPTResponseSchema = Type.Object({ export type GPTResponse = Static; -export const WideConfigSchema = Type.Object( +export const ConfigSchema = Type.Object( { evmNetworkId: Type.Optional(Type.Number()), priceMultiplier: Type.Optional(Type.Number()), @@ -210,9 +210,7 @@ export const WideConfigSchema = Type.Object( } ); -export type WideConfig = Static; - -export type WideRepoConfig = WideConfig; +export type Config = Static; export const MergedConfigSchema = Type.Object({ evmNetworkId: Type.Number(), diff --git a/src/utils/private.ts b/src/utils/private.ts index 48c498f9a..90b2e577a 100644 --- a/src/utils/private.ts +++ b/src/utils/private.ts @@ -6,7 +6,7 @@ import merge from "lodash/merge"; import { DefaultConfig } from "../configs"; import { validate } from "./ajv"; -import { WideConfig, WideRepoConfig, WideConfigSchema } from "../types"; +import { Config, ConfigSchema } from "../types"; const CONFIG_REPO = "ubiquibot-config"; const CONFIG_PATH = ".github/ubiquibot-config.yml"; @@ -34,12 +34,12 @@ export const getConfigSuperset = async (context: Context, type: "org" | "repo", }; export interface MergedConfigs { - parsedRepo: WideRepoConfig | undefined; - parsedOrg: WideRepoConfig | undefined; + parsedRepo: Config | undefined; + parsedOrg: Config | undefined; parsedDefault: MergedConfig; } -export const parseYAML = (data?: string): WideConfig | undefined => { +export const parseYAML = (data?: string): Config | undefined => { try { if (data) { const parsedData = YAML.parse(data); @@ -111,17 +111,17 @@ export const getWideConfig = async (context: Context) => { const orgConfig = await getConfigSuperset(context, "org", CONFIG_PATH); const repoConfig = await getConfigSuperset(context, "repo", CONFIG_PATH); - const parsedOrg: WideRepoConfig | undefined = parseYAML(orgConfig); + const parsedOrg: Config | undefined = parseYAML(orgConfig); if (parsedOrg) { - const { valid, error } = validate(WideConfigSchema, parsedOrg); + const { valid, error } = validate(ConfigSchema, parsedOrg); if (!valid) { throw new Error(`Invalid org config: ${error}`); } } - const parsedRepo: WideRepoConfig | undefined = parseYAML(repoConfig); + const parsedRepo: Config | undefined = parseYAML(repoConfig); if (parsedRepo) { - const { valid, error } = validate(WideConfigSchema, parsedRepo); + const { valid, error } = validate(ConfigSchema, parsedRepo); if (!valid) { throw new Error(`Invalid repo config: ${error}`); } diff --git a/supabase/migrations/20230926105600_debit_rename.sql b/supabase/migrations/20230926105600_debit_rename.sql new file mode 100644 index 000000000..efcd40c75 --- /dev/null +++ b/supabase/migrations/20230926105600_debit_rename.sql @@ -0,0 +1 @@ +ALTER TABLE Debits RENAME TO debits; \ No newline at end of file