diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 82c537bb4..8b1378917 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @0xcodercrane
\ No newline at end of file
+
diff --git a/.github/ubiquibot-config.yml b/.github/ubiquibot-config.yml
index 48a9f91c4..f79f61652 100644
--- a/.github/ubiquibot-config.yml
+++ b/.github/ubiquibot-config.yml
@@ -1,6 +1,6 @@
---
-chain-id: 100
-base-multiplier: 1500
+evm-network-id: 100
+price-multiplier: 1.5
time-labels:
- name: "Time: <1 Hour"
weight: 0.125
@@ -28,6 +28,12 @@ priority-labels:
weight: 4
- name: "Priority: 4 (Emergency)"
weight: 5
+default-labels:
+ - "Time: <1 Hour"
+ - "Priority: 0 (Normal)"
+ - "Test"
auto-pay-mode: true
-analytics-mode: true
+comment-incentives: true
max-concurrent-bounties: 2
+promotion-comment: "\n
If you enjoy the DevPool experience, please follow Ubiquity on GitHub and star this repo to show your support. It helps a lot!
"
+register-wallet-with-verification: false
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8843bee39..973953d53 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -23,3 +23,26 @@ jobs:
- name: Lint
run: yarn lint
+
+ run-migration:
+ runs-on: ubuntu-latest
+ needs: build
+ if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/development')
+ env:
+ SUPABASE_ACCESS_TOKEN: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SUPABASE_ACCESS_TOKEN || secrets.STAGING_SUPABASE_ACCESS_TOKEN }}
+ SUPABASE_DB_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SUPABASE_DB_PASSWORD || secrets.STAGING_SUPABASE_DB_PASSWORD }}
+ PROJECT_ID: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SUPABASE_PROJECT_ID || secrets.STAGING_SUPABASE_PROJECT_ID }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - uses: supabase/setup-cli@v1
+ with:
+ version: latest
+
+ - name: Link Supabase project
+ run: supabase link --project-ref $PROJECT_ID
+
+ - name: Run migrations
+ run: supabase db push
diff --git a/README.md b/README.md
index 5ad4a1eeb..0311933d0 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,37 @@ To test the bot, you can:
2. Add a time label, ex: `Time: <1 Day`
3. At this point the bot should add a price label.
+## Configuration
+
+`chain-id` is ID of the EVM-compatible network that will be used for payouts.
+
+`base-multiplier` is a base number that will be used to calculate bounty price based on the following formula: `price = base-multiplier * time-label-weight * priority-label-weight / 10`
+
+`time-labels` are labels for marking the time limit of the bounty:
+
+- `name` is a human-readable name
+- `weight` is a number that will be used to calculate the bounty price
+- `value` is number of seconds that corresponds to the time limit of the bounty
+
+`priority-labels` are labels for marking the priority of the bounty:
+
+- `name` is a human-readable name
+- `weight` is a number that will be used to calculate the bounty price
+
+`default-labels` are labels that are applied when an issue is created without any time or priority labels.
+
+`auto-pay-mode` can be `true` or `false` that enables or disables automatic payout of bounties when the issue is closed.
+
+`analytics-mode` can be `true` or `false` that enables or disables weekly analytics collection by Ubiquity.
+
+`incentive-mode` can be `true` or `false` that enables or disables comment incentives. These are comments in the issue by either the creator of the bounty or other users.
+
+`issue-creator-multiplier` is a number that defines a base multiplier for calculating incentive reward for the creator of the issue.
+
+`comment-element-pricing` defines how much is a part of the comment worth. For example `text: 0.1` means that any text in the comment will be multiplied by 0.1
+
+`max-concurrent-bounties` is the maximum number of bounties that can be assigned to a bounty hunter at once. This excludes bounties with pending pull request reviews.
+
## How to run locally
1. Create a new project at [Supabase](https://supabase.com/). Add `Project URL` and `API Key` to the `.env` file:
diff --git a/src/adapters/supabase/helpers/client.ts b/src/adapters/supabase/helpers/client.ts
index cfa3d68d9..335ba0e95 100644
--- a/src/adapters/supabase/helpers/client.ts
+++ b/src/adapters/supabase/helpers/client.ts
@@ -290,6 +290,22 @@ export const getWalletMultiplier = async (username: string): Promise =>
else return data?.multiplier;
};
+/**
+ * Queries both the wallet multiplier and address in one request registered previously
+ *
+ * @param username The username you want to find an address for
+ * @returns The Multiplier and ERC-20 Address, returns 1 if not found
+ *
+ */
+
+export const getWalletInfo = async (username: string): Promise<{ multiplier: number | null; address: string | null } | number | undefined> => {
+ const { supabase } = getAdapters();
+
+ const { data } = await supabase.from("wallets").select("multiplier, address").eq("user_name", username).single();
+ if (data?.multiplier == null || data?.address == null) return 1;
+ else return { multiplier: data?.multiplier, address: data?.address };
+};
+
export const getMultiplierReason = async (username: string): Promise => {
const { supabase } = getAdapters();
const { data } = await supabase.from("wallets").select("reason").eq("user_name", username).single();
diff --git a/src/adapters/supabase/types/database.d.ts b/src/adapters/supabase/types/database.ts
similarity index 68%
rename from src/adapters/supabase/types/database.d.ts
rename to src/adapters/supabase/types/database.ts
index c727abb19..48a6256bf 100644
--- a/src/adapters/supabase/types/database.d.ts
+++ b/src/adapters/supabase/types/database.ts
@@ -1,4 +1,4 @@
-export type Json = string | number | boolean | null | { [key: string]: Json } | Json[];
+export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
export interface Database {
graphql_public: {
@@ -11,10 +11,10 @@ export interface Database {
Functions: {
graphql: {
Args: {
- operationName: string;
- query: string;
- variables: Json;
- extensions: Json;
+ operationName?: string;
+ query?: string;
+ variables?: Json;
+ extensions?: Json;
};
Returns: Json;
};
@@ -22,9 +22,45 @@ export interface Database {
Enums: {
[_ in never]: never;
};
+ CompositeTypes: {
+ [_ in never]: never;
+ };
};
public: {
Tables: {
+ access: {
+ Row: {
+ created_at: string | null;
+ multiplier_access: boolean | null;
+ price_access: boolean | null;
+ priority_access: boolean | null;
+ repository: string | null;
+ time_access: boolean | null;
+ updated_at: string | null;
+ user_name: string;
+ };
+ Insert: {
+ created_at?: string | null;
+ multiplier_access?: boolean | null;
+ price_access?: boolean | null;
+ priority_access?: boolean | null;
+ repository?: string | null;
+ time_access?: boolean | null;
+ updated_at?: string | null;
+ user_name: string;
+ };
+ Update: {
+ created_at?: string | null;
+ multiplier_access?: boolean | null;
+ price_access?: boolean | null;
+ priority_access?: boolean | null;
+ repository?: string | null;
+ time_access?: boolean | null;
+ updated_at?: string | null;
+ user_name?: string;
+ };
+ Relationships: [];
+ };
issues: {
Row: {
assignees: string[] | null;
@@ -86,6 +122,7 @@ export interface Database {
txhash?: string[] | null;
updated_at?: string | null;
};
+ Relationships: [];
};
users: {
Row: {
@@ -154,26 +191,49 @@ export interface Database {
user_type?: string | null;
wallet_address?: string | null;
};
+ Relationships: [];
};
wallets: {
Row: {
created_at: string | null;
+ multiplier: number | null;
+ reason: string | null;
updated_at: string | null;
user_name: string;
wallet_address: string | null;
};
Insert: {
created_at?: string | null;
+ multiplier?: number | null;
+ reason?: string | null;
updated_at?: string | null;
user_name: string;
wallet_address?: string | null;
};
Update: {
created_at?: string | null;
+ multiplier?: number | null;
+ reason?: string | null;
updated_at?: string | null;
user_name?: string;
wallet_address?: string | null;
};
+ Relationships: [];
+ };
+ weekly: {
+ Row: {
+ created_at: string | null;
+ last_time: string | null;
+ };
+ Insert: {
+ created_at?: string | null;
+ last_time?: string | null;
+ };
+ Update: {
+ created_at?: string | null;
+ last_time?: string | null;
+ };
+ Relationships: [];
};
};
Views: {
@@ -185,12 +245,18 @@ export interface Database {
Enums: {
issue_status: "READY_TO_START" | "IN_PROGRESS" | "IN_REVIEW" | "DONE";
};
+ CompositeTypes: {
+ [_ in never]: never;
+ };
};
storage: {
Tables: {
buckets: {
Row: {
+ allowed_mime_types: string[] | null;
+ avif_autodetection: boolean | null;
created_at: string | null;
+ file_size_limit: number | null;
id: string;
name: string;
owner: string | null;
@@ -198,7 +264,10 @@ export interface Database {
updated_at: string | null;
};
Insert: {
+ allowed_mime_types?: string[] | null;
+ avif_autodetection?: boolean | null;
created_at?: string | null;
+ file_size_limit?: number | null;
id: string;
name: string;
owner?: string | null;
@@ -206,13 +275,24 @@ export interface Database {
updated_at?: string | null;
};
Update: {
+ allowed_mime_types?: string[] | null;
+ avif_autodetection?: boolean | null;
created_at?: string | null;
+ file_size_limit?: number | null;
id?: string;
name?: string;
owner?: string | null;
public?: boolean | null;
updated_at?: string | null;
};
+ Relationships: [
+ {
+ foreignKeyName: "buckets_owner_fkey";
+ columns: ["owner"];
+ referencedRelation: "users";
+ referencedColumns: ["id"];
+ }
+ ];
};
migrations: {
Row: {
@@ -233,6 +313,7 @@ export interface Database {
id?: number;
name?: string;
};
+ Relationships: [];
};
objects: {
Row: {
@@ -245,6 +326,7 @@ export interface Database {
owner: string | null;
path_tokens: string[] | null;
updated_at: string | null;
+ version: string | null;
};
Insert: {
bucket_id?: string | null;
@@ -256,6 +338,7 @@ export interface Database {
owner?: string | null;
path_tokens?: string[] | null;
updated_at?: string | null;
+ version?: string | null;
};
Update: {
bucket_id?: string | null;
@@ -267,39 +350,72 @@ export interface Database {
owner?: string | null;
path_tokens?: string[] | null;
updated_at?: string | null;
+ version?: string | null;
};
+ Relationships: [
+ {
+ foreignKeyName: "objects_bucketId_fkey";
+ columns: ["bucket_id"];
+ referencedRelation: "buckets";
+ referencedColumns: ["id"];
+ },
+ {
+ foreignKeyName: "objects_owner_fkey";
+ columns: ["owner"];
+ referencedRelation: "users";
+ referencedColumns: ["id"];
+ }
+ ];
};
};
Views: {
[_ in never]: never;
};
Functions: {
+ can_insert_object: {
+ Args: {
+ bucketid: string;
+ name: string;
+ owner: string;
+ metadata: Json;
+ };
+ Returns: undefined;
+ };
extension: {
- Args: { name: string };
+ Args: {
+ name: string;
+ };
Returns: string;
};
filename: {
- Args: { name: string };
+ Args: {
+ name: string;
+ };
Returns: string;
};
foldername: {
- Args: { name: string };
- Returns: string[];
+ Args: {
+ name: string;
+ };
+ Returns: unknown;
};
get_size_by_bucket: {
Args: Record;
- Returns: { size: number; bucket_id: string }[];
+ Returns: {
+ size: number;
+ bucket_id: string;
+ }[];
};
search: {
Args: {
prefix: string;
bucketname: string;
- limits: number;
- levels: number;
- offsets: number;
- search: string;
- sortcolumn: string;
- sortorder: string;
+ limits?: number;
+ levels?: number;
+ offsets?: number;
+ search?: string;
+ sortcolumn?: string;
+ sortorder?: string;
};
Returns: {
name: string;
@@ -314,5 +430,8 @@ export interface Database {
Enums: {
[_ in never]: never;
};
+ CompositeTypes: {
+ [_ in never]: never;
+ };
};
}
diff --git a/src/adapters/supabase/types/index.ts b/src/adapters/supabase/types/index.ts
index f211e1c1a..c30cd664d 100644
--- a/src/adapters/supabase/types/index.ts
+++ b/src/adapters/supabase/types/index.ts
@@ -1 +1 @@
-export * from "./database.d";
+export * from "./database";
diff --git a/src/bindings/config.ts b/src/bindings/config.ts
index 05d7b64c9..d354c9785 100644
--- a/src/bindings/config.ts
+++ b/src/bindings/config.ts
@@ -2,7 +2,7 @@ import ms from "ms";
import { BotConfig, BotConfigSchema } from "../types";
import { DEFAULT_BOT_DELAY, DEFAULT_DISQUALIFY_TIME, DEFAULT_FOLLOWUP_TIME, DEFAULT_PERMIT_BASE_URL } from "../configs";
-import { getPayoutConfigByChainId } from "../helpers";
+import { getPayoutConfigByNetworkId } from "../helpers";
import { ajv } from "../utils";
import { Context } from "probot";
import { getScalarKey, getWideConfig } from "../utils/private";
@@ -15,15 +15,18 @@ export const loadConfig = async (context: Context): Promise => {
priorityLabels,
commentElementPricing,
autoPayMode,
- analyticsMode,
+ disableAnalytics,
bountyHunterMax,
incentiveMode,
- chainId,
+ networkId,
issueCreatorMultiplier,
+ defaultLabels,
+ promotionComment,
+ registerWalletWithVerification,
} = await getWideConfig(context);
const publicKey = await getScalarKey(process.env.X25519_PRIVATE_KEY);
- const { rpc, paymentToken } = getPayoutConfigByChainId(chainId);
+ const { rpc, paymentToken } = getPayoutConfigByNetworkId(networkId);
const botConfig: BotConfig = {
log: {
@@ -36,9 +39,13 @@ export const loadConfig = async (context: Context): Promise => {
timeLabels,
priorityLabels,
commentElementPricing,
+ defaultLabels,
+ },
+ comments: {
+ promotionComment: promotionComment,
},
payout: {
- chainId: chainId,
+ networkId: networkId,
rpc: rpc,
privateKey: privateKey,
paymentToken: paymentToken,
@@ -58,7 +65,7 @@ export const loadConfig = async (context: Context): Promise => {
},
mode: {
autoPayMode: autoPayMode,
- analyticsMode: analyticsMode,
+ disableAnalytics: disableAnalytics,
incentiveMode: incentiveMode,
},
assign: {
@@ -68,6 +75,9 @@ export const loadConfig = async (context: Context): Promise => {
privateKey: process.env.X25519_PRIVATE_KEY ?? "",
publicKey: publicKey ?? "",
},
+ wallet: {
+ registerWalletWithVerification: registerWalletWithVerification,
+ },
};
if (botConfig.log.ingestionKey == "") {
diff --git a/src/bindings/event.ts b/src/bindings/event.ts
index f85cc680d..77518c99c 100644
--- a/src/bindings/event.ts
+++ b/src/bindings/event.ts
@@ -50,6 +50,7 @@ export const bindEvents = async (context: Context): Promise => {
unassign: botConfig.unassign,
mode: botConfig.mode,
log: botConfig.log,
+ wallet: botConfig.wallet,
})}`
);
const allowedEvents = Object.values(GithubEvent) as string[];
diff --git a/src/configs/shared.ts b/src/configs/shared.ts
index 65d499402..2d817b7a0 100644
--- a/src/configs/shared.ts
+++ b/src/configs/shared.ts
@@ -1,5 +1,3 @@
-import { generateHelpMenu } from "../handlers";
-
export const COLORS = {
default: "ededed",
price: "1f883d",
@@ -7,6 +5,7 @@ export const COLORS = {
export const DEFAULT_BOT_DELAY = 100; // 100ms
export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE = 24;
export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED = true;
+export const ASSIGN_COMMAND_ENABLED = true;
/**
* ms('2 days') // 172800000
* ms('1d') // 86400000
@@ -24,7 +23,6 @@ export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED = true;
export const DEFAULT_FOLLOWUP_TIME = "4 days"; // 4 days
export const DEFAULT_DISQUALIFY_TIME = "7 days"; // 7 days
-export const DEFAULT_CHAIN_ID = 1; // ethereum
+export const DEFAULT_NETWORK_ID = 1; // ethereum
export const DEFAULT_RPC_ENDPOINT = "https://rpc-bot.ubq.fi/v1/mainnet";
export const DEFAULT_PERMIT_BASE_URL = "https://pay.ubq.fi";
-export const COMMAND_INSTRUCTIONS = generateHelpMenu();
diff --git a/src/configs/strings.ts b/src/configs/strings.ts
index ef96a0f2b..9bc705a20 100644
--- a/src/configs/strings.ts
+++ b/src/configs/strings.ts
@@ -1,4 +1,5 @@
export const GLOBAL_STRINGS = {
unassignComment: "Releasing the bounty back to dev pool because the allocated duration already ended!",
askUpdate: "Do you have any updates",
+ assignCommandDisabledComment: "The `/assign` command is disabled for this repository.",
};
diff --git a/src/handlers/assign/action.ts b/src/handlers/assign/action.ts
index 951ad3314..bd71d65c3 100644
--- a/src/handlers/assign/action.ts
+++ b/src/handlers/assign/action.ts
@@ -60,7 +60,7 @@ export const commentWithAssignMessage = async (): Promise => {
const curDate = new Date();
const curDateInMillisecs = curDate.getTime();
const endDate = new Date(curDateInMillisecs + duration * 1000);
- const commit_msg = `${flattened_assignees} ${deadLinePrefix} ${endDate.toUTCString()}`;
+ const commit_msg = `${flattened_assignees} ${deadLinePrefix} ${endDate.toUTCString().replace("GMT", "UTC")}`;
logger.debug(`Creating an issue comment, commit_msg: ${commit_msg}`);
await addCommentToIssue(commit_msg, payload.issue?.number);
diff --git a/src/handlers/assign/auto.ts b/src/handlers/assign/auto.ts
index bc11cc48e..a065327f4 100644
--- a/src/handlers/assign/auto.ts
+++ b/src/handlers/assign/auto.ts
@@ -1,5 +1,5 @@
import { getBotContext, getLogger } from "../../bindings";
-import { addAssignees, getIssueByNumber, getPullRequests } from "../../helpers";
+import { addAssignees, getIssueByNumber, getPullByNumber, getPullRequests } from "../../helpers";
import { gitLinkedIssueParser } from "../../helpers/parser";
import { Payload } from "../../types";
@@ -29,6 +29,14 @@ export const checkPullRequests = async () => {
continue;
}
+ const connectedPull = await getPullByNumber(context, pull.number);
+
+ // Newly created PULL (draft or direct) pull does have same `created_at` and `updated_at`.
+ if (connectedPull?.created_at !== connectedPull?.updated_at) {
+ logger.debug("It's an updated Pull Request, reverting");
+ continue;
+ }
+
const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1);
// Check if the pull request opener is assigned to the issue
diff --git a/src/handlers/comment/action.ts b/src/handlers/comment/action.ts
index faa7a24ae..1857b7851 100644
--- a/src/handlers/comment/action.ts
+++ b/src/handlers/comment/action.ts
@@ -16,15 +16,16 @@ export const handleComment = async (): Promise => {
}
const body = comment.body;
- const commands = commentParser(body);
+ const commentedCommands = commentParser(body);
- if (commands.length === 0) {
+ if (commentedCommands.length === 0) {
await verifyFirstCheck();
return;
}
- for (const command of commands) {
- const userCommand = userCommands.find((i) => i.id == command);
+ const allCommands = userCommands();
+ for (const command of commentedCommands) {
+ const userCommand = allCommands.find((i) => i.id == command);
if (userCommand) {
const { handler, callback, successComment, failureComment } = userCommand;
@@ -37,13 +38,13 @@ export const handleComment = async (): Promise => {
try {
const response = await handler(body);
const callbackComment = response ?? successComment ?? "";
- if (callbackComment) await callback(issue.number, callbackComment);
+ if (callbackComment) await callback(issue.number, callbackComment, payload.action, payload.comment);
} catch (err: unknown) {
// Use failureComment for failed command if it is available
if (failureComment) {
- await callback(issue.number, failureComment);
+ await callback(issue.number, failureComment, payload.action, payload.comment);
}
- await callback(issue.number, `Error: ${err}`);
+ await callback(issue.number, `Error: ${err}`, payload.action, payload.comment);
}
} else {
logger.info(`Skipping for a command: ${command}`);
diff --git a/src/handlers/comment/commands.ts b/src/handlers/comment/commands.ts
index 98c52990f..878727dcb 100644
--- a/src/handlers/comment/commands.ts
+++ b/src/handlers/comment/commands.ts
@@ -5,7 +5,7 @@ export enum IssueCommentCommands {
WALLET = "/wallet", // register wallet address
PAYOUT = "/payout", // request permit payout
MULTIPLIER = "/multiplier", // set bounty multiplier (for treasury)
-
+ QUERY = "/query",
// Access Controls
ALLOW = "/allow",
diff --git a/src/handlers/comment/handlers/set-access.ts b/src/handlers/comment/handlers/allow.ts
similarity index 93%
rename from src/handlers/comment/handlers/set-access.ts
rename to src/handlers/comment/handlers/allow.ts
index 1c855690c..52d22e9bc 100644
--- a/src/handlers/comment/handlers/set-access.ts
+++ b/src/handlers/comment/handlers/allow.ts
@@ -50,6 +50,6 @@ export const setAccess = async (body: string) => {
return `Updated access for @${username} successfully!\t Access: **${accessType}** for "${repo.full_name}"`;
} else {
logger.error("Invalid body for allow command");
- return `Invalid body for allow command`;
+ return `Invalid syntax for allow \n usage: '/allow set-(access type) @user true|false' \n ex-1 /allow set-multiplier @user false`;
}
};
diff --git a/src/handlers/comment/handlers/assign.ts b/src/handlers/comment/handlers/assign.ts
index 9d3fddc64..2721171f4 100644
--- a/src/handlers/comment/handlers/assign.ts
+++ b/src/handlers/comment/handlers/assign.ts
@@ -1,10 +1,11 @@
-import { addAssignees, getAssignedIssues, getCommentsOfIssue, getAvailableOpenedPullRequests } from "../../../helpers";
+import { addAssignees, getAssignedIssues, getAvailableOpenedPullRequests, getAllIssueComments, addCommentToIssue } from "../../../helpers";
import { getBotConfig, getBotContext, getLogger } from "../../../bindings";
import { Payload, LabelItem, Comment, IssueType } from "../../../types";
import { deadLinePrefix } from "../../shared";
import { getWalletAddress, getWalletMultiplier, getMultiplierReason } from "../../../adapters/supabase";
import { tableComment } from "./table";
import { bountyInfo } from "../../wildcard";
+import { ASSIGN_COMMAND_ENABLED, GLOBAL_STRINGS } from "../../../configs";
export const assign = async (body: string) => {
const { payload: _payload } = getBotContext();
@@ -19,18 +20,20 @@ export const assign = async (body: string) => {
return "Skipping '/assign' because of no issue instance";
}
- const opened_prs = await getAvailableOpenedPullRequests(payload.sender.login);
-
- logger.info(`Opened Pull Requests with no reviews but over 24 hours have passed: ${JSON.stringify(opened_prs)}`);
+ if (!ASSIGN_COMMAND_ENABLED) {
+ logger.info(`Ignore '/assign' command from user: ASSIGN_COMMAND_ENABLED config is set false`);
+ await addCommentToIssue(GLOBAL_STRINGS.assignCommandDisabledComment, issue.number);
+ return;
+ }
- const assigned_issues = await getAssignedIssues(payload.sender.login);
+ const openedPullRequests = await getAvailableOpenedPullRequests(payload.sender.login);
+ logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: ${JSON.stringify(openedPullRequests)}`);
+ const assignedIssues = await getAssignedIssues(payload.sender.login);
logger.info(`Max issue allowed is ${config.assign.bountyHunterMax}`);
- const issue_number = issue.number;
-
// check for max and enforce max
- if (assigned_issues.length - opened_prs.length >= config.assign.bountyHunterMax) {
+ if (assignedIssues.length - openedPullRequests.length >= config.assign.bountyHunterMax) {
return `Too many assigned issues, you have reached your max of ${config.assign.bountyHunterMax}`;
}
@@ -55,10 +58,10 @@ export const assign = async (body: string) => {
const timeLabelsDefined = config.price.timeLabels;
const timeLabelsAssigned: LabelItem[] = [];
for (const _label of labels) {
- const _label_type = typeof _label;
- const _label_name = _label_type === "string" ? _label.toString() : _label_type === "object" ? _label.name : "unknown";
+ const _labelType = typeof _label;
+ const _labelName = _labelType === "string" ? _label.toString() : _labelType === "object" ? _label.name : "unknown";
- const timeLabel = timeLabelsDefined.find((item) => item.name === _label_name);
+ const timeLabel = timeLabelsDefined.find((item) => item.name === _labelName);
if (timeLabel) {
timeLabelsAssigned.push(timeLabel);
}
@@ -73,54 +76,48 @@ export const assign = async (body: string) => {
const targetTimeLabel = sorted[0];
const duration = targetTimeLabel.value;
if (!duration) {
- logger.info(`Missing configure for timelabel: ${targetTimeLabel.name}`);
+ logger.info(`Missing configure for time label: ${targetTimeLabel.name}`);
return "Skipping `/assign` since configuration is missing for the following labels";
}
- const curDate = new Date();
- const curDateInMillisecs = curDate.getTime();
- const endDate = new Date(curDateInMillisecs + duration * 1000);
- const deadline_msg = endDate.toUTCString();
-
- let wallet_msg, multiplier_msg, reason_msg, bounty_msg;
-
- const commit_msg = `@${payload.sender.login} ${deadLinePrefix} ${endDate.toUTCString()}`;
+ const startTime = new Date().getTime();
+ const endTime = new Date(startTime + duration * 1000);
+
+ const comment = {
+ deadline: endTime.toUTCString().replace("GMT", "UTC"),
+ wallet: (await getWalletAddress(payload.sender.login)) || "Please set your wallet address to use `/wallet 0x0000...0000`",
+ multiplier: "1.00",
+ reason: await getMultiplierReason(payload.sender.login),
+ bounty: `Permit generation skipped since price label is not set`,
+ commit: `@${payload.sender.login} ${deadLinePrefix} ${endTime.toUTCString()}`,
+ tips: `Tips:
+
+ - Use
/wallet 0x0000...0000
if you want to update your registered payment wallet address @user.
+ - Be sure to open a draft pull request as soon as possible to communicate updates on your progress.
+ - Be sure to provide timely updates to us when requested, or you will be automatically unassigned from the bounty.
+ `,
+ };
if (!assignees.map((i) => i.login).includes(payload.sender.login)) {
logger.info(`Adding the assignee: ${payload.sender.login}`);
- // assign default bounty account to the issue
- await addAssignees(issue_number, [payload.sender.login]);
+ await addAssignees(issue.number, [payload.sender.login]);
}
// double check whether the assign message has been already posted or not
- logger.info(`Creating an issue comment: ${commit_msg}`);
- const issue_comments = await getCommentsOfIssue(issue_number);
- const comments = issue_comments.sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
- const latest_comment = comments.length > 0 ? comments[0].body : undefined;
- if (latest_comment && commit_msg != latest_comment) {
- const recipient = await getWalletAddress(payload.sender.login);
- if (!recipient) {
- //no wallet found
- wallet_msg = "Please set your wallet address to use `/wallet 0x4FDE...BA18`";
- } else {
- //wallet found
- wallet_msg = recipient;
- }
+ logger.info(`Creating an issue comment: ${comment.commit}`);
+ const issueComments = await getAllIssueComments(issue.number);
+ const comments = issueComments.sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
+ const latestComment = comments.length > 0 ? comments[0].body : undefined;
+ if (latestComment && comment.commit != latestComment) {
const multiplier = await getWalletMultiplier(payload.sender.login);
- if (!multiplier) {
- multiplier_msg = "1.00";
- } else {
- multiplier_msg = multiplier.toFixed(2);
+ if (multiplier) {
+ comment.multiplier = multiplier.toFixed(2);
}
const issueDetailed = bountyInfo(issue);
- if (!issueDetailed.priceLabel) {
- bounty_msg = `Permit generation skipped since price label is not set`;
- } else {
- bounty_msg = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString() + " USD";
+ if (issueDetailed.priceLabel) {
+ comment.bounty = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString() + " USD";
}
- const reason = await getMultiplierReason(payload.sender.login);
- reason_msg = reason ?? "";
- return tableComment(deadline_msg, wallet_msg, multiplier_msg, reason_msg, bounty_msg);
+ return tableComment(comment) + comment.tips;
}
return;
};
diff --git a/src/handlers/comment/handlers/first.ts b/src/handlers/comment/handlers/first.ts
index 13aa8163a..c14d50702 100644
--- a/src/handlers/comment/handlers/first.ts
+++ b/src/handlers/comment/handlers/first.ts
@@ -1,7 +1,7 @@
import { getBotContext, getLogger } from "../../../bindings";
-import { COMMAND_INSTRUCTIONS } from "../../../configs";
-import { addCommentToIssue } from "../../../helpers";
+import { upsertCommentToIssue } from "../../../helpers";
import { Payload } from "../../../types";
+import { generateHelpMenu } from "./help";
export const verifyFirstCheck = async (): Promise => {
const context = getBotContext();
@@ -25,8 +25,8 @@ export const verifyFirstCheck = async (): Promise => {
const isFirstComment = resp.data.filter((item) => item.user?.login === payload.sender.login).length === 1;
if (isFirstComment) {
//first_comment
- const msg = `${COMMAND_INSTRUCTIONS}\n@${payload.sender.login}`;
- await addCommentToIssue(msg, payload.issue.number);
+ const msg = `${generateHelpMenu()}\n@${payload.sender.login}`;
+ await upsertCommentToIssue(payload.issue.number, msg, payload.action, payload.comment);
}
}
} catch (error: unknown) {
diff --git a/src/handlers/comment/handlers/help.ts b/src/handlers/comment/handlers/help.ts
index 84764c00b..9c461e84a 100644
--- a/src/handlers/comment/handlers/help.ts
+++ b/src/handlers/comment/handlers/help.ts
@@ -1,5 +1,6 @@
import { userCommands } from ".";
import { getBotContext, getLogger } from "../../../bindings";
+import { ASSIGN_COMMAND_ENABLED } from "../../../configs";
import { IssueType, Payload } from "../../../types";
import { IssueCommentCommands } from "../commands";
@@ -28,19 +29,20 @@ export const listAvailableCommands = async (body: string) => {
export const generateHelpMenu = () => {
let helpMenu = "### Available commands\n```";
-
- userCommands.map((command) => {
+ const commands = userCommands();
+ commands.map((command) => {
// if first command, add a new line
- if (command.id === userCommands[0].id) {
+ if (command.id === commands[0].id) {
helpMenu += `\n`;
+ if (!ASSIGN_COMMAND_ENABLED) return;
}
helpMenu += `- ${command.id}: ${command.description}`;
// if not last command, add a new line (fixes too much space below)
- if (command.id !== userCommands[userCommands.length - 1].id) {
+ if (command.id !== commands[commands.length - 1].id) {
helpMenu += `\n`;
}
});
- helpMenu += "```";
+ if (!ASSIGN_COMMAND_ENABLED) helpMenu += "```\n***_To assign yourself to an issue, please open a draft pull request that is linked to it._***";
return helpMenu;
};
diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts
index c5fd31427..a304d1a10 100644
--- a/src/handlers/comment/handlers/index.ts
+++ b/src/handlers/comment/handlers/index.ts
@@ -1,5 +1,4 @@
-import { getBotConfig } from "../../../bindings";
-import { Payload, UserCommands } from "../../../types";
+import { Comment, Payload, UserCommands } from "../../../types";
import { IssueCommentCommands } from "../commands";
import { assign } from "./assign";
import { listAvailableCommands } from "./help";
@@ -7,11 +6,12 @@ import { listAvailableCommands } from "./help";
// import { payout } from "./payout";
import { unassign } from "./unassign";
import { registerWallet } from "./wallet";
-import { setAccess } from "./set-access";
+import { setAccess } from "./allow";
import { multiplier } from "./multiplier";
-import { addCommentToIssue, createLabel, addLabelToIssue } from "../../../helpers";
-import { getBotContext } from "../../../bindings";
+import { addCommentToIssue, createLabel, addLabelToIssue, getLabel, upsertCommentToIssue } from "../../../helpers";
+import { getBotConfig, getBotContext } from "../../../bindings";
import { handleIssueClosed } from "../../payout";
+import { query } from "./query";
export * from "./assign";
export * from "./wallet";
@@ -19,6 +19,7 @@ export * from "./unassign";
export * from "./payout";
export * from "./help";
export * from "./multiplier";
+export * from "./query";
/**
* Parses the comment body and figure out the command name a user wants
@@ -27,12 +28,19 @@ export * from "./multiplier";
* @param body - The comment body
* @returns The list of command names the comment includes
*/
+
export const commentParser = (body: string): IssueCommentCommands[] => {
- // TODO: As a starting point, it may be simple but there could be cases for the comment to includes one or more commands
- // We need to continuously improve to parse even complex comments. Right now, we implement it simply.
- const commandList = Object.values(IssueCommentCommands) as string[];
- const result = commandList.filter((command: string) => body.startsWith(command));
- return result as IssueCommentCommands[];
+ const regex = /^\/(\w+)\b/; // Regex pattern to match the command at the beginning of the body
+
+ const matches = regex.exec(body);
+ if (matches) {
+ const command = matches[0] as IssueCommentCommands;
+ if (Object.values(IssueCommentCommands).includes(command)) {
+ return [command];
+ }
+ }
+
+ return [];
};
/**
@@ -41,15 +49,12 @@ export const commentParser = (body: string): IssueCommentCommands[] => {
export const issueClosedCallback = async (): Promise => {
const { payload: _payload } = getBotContext();
+ const { comments } = getBotConfig();
const issue = (_payload as Payload).issue;
if (!issue) return;
try {
const comment = await handleIssueClosed();
- if (comment) await addCommentToIssue(comment, issue.number);
- await addCommentToIssue(
- `If you enjoy the DevPool experience, please follow Ubiquity on GitHub and star this repo to show your support. It helps a lot!`,
- issue.number
- );
+ if (comment) await addCommentToIssue(comment + comments.promotionComment, issue.number);
} catch (err: unknown) {
return await addCommentToIssue(`Error: ${err}`, issue.number);
}
@@ -66,16 +71,16 @@ export const issueCreatedCallback = async (): Promise => {
if (!issue) return;
const labels = issue.labels;
try {
- const timeLabelConfigs = config.price.timeLabels.sort((label1, label2) => label1.weight - label2.weight);
- const priorityLabelConfigs = config.price.priorityLabels.sort((label1, label2) => label1.weight - label2.weight);
const timeLabels = config.price.timeLabels.filter((item) => labels.map((i) => i.name).includes(item.name));
const priorityLabels = config.price.priorityLabels.filter((item) => labels.map((i) => i.name).includes(item.name));
- if (timeLabels.length === 0 && timeLabelConfigs.length > 0) await createLabel(timeLabelConfigs[0].name);
- if (priorityLabels.length === 0 && priorityLabelConfigs.length > 0) await createLabel(priorityLabelConfigs[0].name);
- await addLabelToIssue(timeLabelConfigs[0].name);
- await addLabelToIssue(priorityLabelConfigs[0].name);
- return;
+ if (timeLabels.length === 0 && priorityLabels.length === 0) {
+ for (const label of config.price.defaultLabels) {
+ const exists = await getLabel(label);
+ if (!exists) await createLabel(label);
+ await addLabelToIssue(label);
+ }
+ }
} catch (err: unknown) {
return await addCommentToIssue(`Error: ${err}`, issue.number);
}
@@ -88,52 +93,65 @@ export const issueCreatedCallback = async (): Promise => {
* @param issue_number - The issue number
* @param comment - Comment string
*/
-const commandCallback = async (issue_number: number, comment: string) => {
- await addCommentToIssue(comment, issue_number);
+
+const commandCallback = async (issue_number: number, comment: string, action: string, reply_to?: Comment) => {
+ await upsertCommentToIssue(issue_number, comment, action, reply_to);
};
-export const userCommands: UserCommands[] = [
- {
- id: IssueCommentCommands.ASSIGN,
- description: "Assign the origin sender to the issue automatically.",
- handler: assign,
- callback: commandCallback,
- },
- {
- id: IssueCommentCommands.UNASSIGN,
- description: "Unassign the origin sender from the issue automatically.",
- handler: unassign,
- callback: commandCallback,
- },
- {
- handler: listAvailableCommands,
- id: IssueCommentCommands.HELP,
- description: "List all available commands.",
- callback: commandCallback,
- },
- // Commented out until Gnosis Safe is integrated (https://github.com/ubiquity/ubiquibot/issues/353)
- /*{
+export const userCommands = (): UserCommands[] => {
+ const config = getBotConfig();
+
+ return [
+ {
+ id: IssueCommentCommands.ASSIGN,
+ description: "Assign the origin sender to the issue automatically.",
+ handler: assign,
+ callback: commandCallback,
+ },
+ {
+ id: IssueCommentCommands.UNASSIGN,
+ description: "Unassign the origin sender from the issue automatically.",
+ handler: unassign,
+ callback: commandCallback,
+ },
+ {
+ handler: listAvailableCommands,
+ id: IssueCommentCommands.HELP,
+ description: "List all available commands.",
+ callback: commandCallback,
+ },
+ // Commented out until Gnosis Safe is integrated (https://github.com/ubiquity/ubiquibot/issues/353)
+ /*{
id: IssueCommentCommands.PAYOUT,
description: "Disable automatic payment for the issue.",
handler: payout,
callback: commandCallback,
},*/
- {
- id: IssueCommentCommands.MULTIPLIER,
- description: `Set bounty multiplier (for treasury)`,
- handler: multiplier,
- callback: commandCallback,
- },
- {
- id: IssueCommentCommands.ALLOW,
- description: `Set access control. (Admin Only)`,
- handler: setAccess,
- callback: commandCallback,
- },
- {
- id: IssueCommentCommands.WALLET,
- description: `: Register the hunter's wallet address. \n ex1: /wallet 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 \n ex2: /wallet vitalik.eth\n`,
- handler: registerWallet,
- callback: commandCallback,
- },
-];
+ {
+ id: IssueCommentCommands.QUERY,
+ description: `Comments the users multiplier and address`,
+ handler: query,
+ callback: commandCallback,
+ },
+ {
+ id: IssueCommentCommands.MULTIPLIER,
+ description: `Set the bounty payout multiplier for a specific contributor, and provide the reason for why. \n example usage: "/wallet @user 0.5 'Multiplier reason'"`,
+ handler: multiplier,
+ callback: commandCallback,
+ },
+ {
+ id: IssueCommentCommands.ALLOW,
+ description: `Set access control. (Admin Only)`,
+ handler: setAccess,
+ callback: commandCallback,
+ },
+ {
+ id: IssueCommentCommands.WALLET,
+ description: config.wallet.registerWalletWithVerification
+ ? ` : Register the hunter's wallet address. \n Your message to sign is: DevPool\n You can generate SIGNATURE_HASH at https://etherscan.io/verifiedSignatures\n ex1: /wallet 0x0000000000000000000000000000000000000000 0xe2a3e34a63f3def2c29605de82225b79e1398190b542be917ef88a8e93ff9dc91bdc3ef9b12ed711550f6d2cbbb50671aa3f14a665b709ec391f3e603d0899a41b\n ex2: /wallet vitalik.eth 0x75329f883590507e581cd6dfca62680b6cd12e1f1665db8097f9e642ed70025146b5cf9f777dde90c4a9cbd41500a6bf76bc394fd0b0cae2aab09f7a6f30e3b31b\n`
+ : `: Register the hunter's wallet address. \n ex1: /wallet 0x0000000000000000000000000000000000000000\n ex2: /wallet vitalik.eth\n`,
+ handler: registerWallet,
+ callback: commandCallback,
+ },
+ ];
+};
diff --git a/src/handlers/comment/handlers/multiplier.ts b/src/handlers/comment/handlers/multiplier.ts
index c54ac2ad3..bef7a68b3 100644
--- a/src/handlers/comment/handlers/multiplier.ts
+++ b/src/handlers/comment/handlers/multiplier.ts
@@ -49,7 +49,7 @@ export const multiplier = async (body: string) => {
reason += part.replace(/['"]/g, "") + " ";
}
}
-
+ username = username || sender;
// check if sender is admin or billing_manager
// passing in context so we don't have to make another request to get the user
const permissionLevel = await getUserPermission(sender, context);
@@ -66,12 +66,14 @@ export const multiplier = async (body: string) => {
}
}
+ logger.info(`Upserting to the wallet table, username: ${username}, bountyMultiplier: ${bountyMultiplier}, reason: ${reason}}`);
+
await upsertWalletMultiplier(username, bountyMultiplier?.toString(), reason);
return `Successfully changed the payout multiplier for @${username} to ${bountyMultiplier}. The reason ${
reason ? `provided is "${reason}"` : "is not provided"
}.`;
} else {
logger.error("Invalid body for bountyMultiplier command");
- return `Invalid body for bountyMultiplier command`;
+ return `Invalid syntax for wallet command \n example usage: "/multiplier @user 0.5 'Multiplier reason'"`;
}
};
diff --git a/src/handlers/comment/handlers/query.ts b/src/handlers/comment/handlers/query.ts
new file mode 100644
index 000000000..a31b71b6c
--- /dev/null
+++ b/src/handlers/comment/handlers/query.ts
@@ -0,0 +1,34 @@
+import { getWalletInfo } from "../../../adapters/supabase";
+import { getBotContext, getLogger } from "../../../bindings";
+import { Payload } from "../../../types";
+
+export const query = async (body: string) => {
+ const context = getBotContext();
+ const logger = getLogger();
+ const payload = context.payload as Payload;
+ const sender = payload.sender.login;
+
+ logger.info(`Received '/query' command from user: ${sender}`);
+
+ const issue = payload.issue;
+ if (!issue) {
+ logger.info(`Skipping '/query' because of no issue instance`);
+ return;
+ }
+
+ const regex = /\/query @([A-Za-z0-9_]+)/gm;
+ const matches = body.match(regex);
+ const user = matches?.[1];
+
+ if (user) {
+ const walletInfo = await getWalletInfo(user);
+ if (typeof walletInfo == "number") {
+ return `Error retrieving multiplier and wallet address for @${user}`;
+ } else {
+ return `@${user}'s wallet address is ${walletInfo?.address} and multiplier is ${walletInfo?.multiplier}`;
+ }
+ } else {
+ logger.error("Invalid body for query command");
+ return `Invalid syntax for query command \n usage /query @user`;
+ }
+};
diff --git a/src/handlers/comment/handlers/table.ts b/src/handlers/comment/handlers/table.ts
index 547e0d866..fef8e181e 100644
--- a/src/handlers/comment/handlers/table.ts
+++ b/src/handlers/comment/handlers/table.ts
@@ -1,28 +1,44 @@
-export const tableComment = (deadline: string, wallet: string, multiplier: string, reason: string, bouty: string) => {
- return `
+export const tableComment = ({
+ deadline,
+ wallet,
+ multiplier,
+ reason,
+ bounty,
+}: {
+ deadline: string;
+ wallet: string;
+ multiplier: string;
+ reason: string;
+ bounty: string;
+}) => {
+ return `
+
+
+
- |
- |
+ |
+ |
- Deadline |
- ${deadline} |
+ Deadline |
+ ${deadline} |
- Registered Wallet |
- ${wallet} |
+ Registered Wallet |
+ ${wallet} |
- Payment Multiplier |
- ${multiplier} |
+ Payment Multiplier |
+ ${multiplier} |
- Multiplier Reason |
- ${reason} |
+ Multiplier Reason |
+ ${reason} |
- Total Bounty |
- ${bouty} |
+ Total Bounty |
+ ${bounty} |
-
`;
+
+`;
};
diff --git a/src/handlers/comment/handlers/unassign.ts b/src/handlers/comment/handlers/unassign.ts
index fb44edae9..3289ed0b0 100644
--- a/src/handlers/comment/handlers/unassign.ts
+++ b/src/handlers/comment/handlers/unassign.ts
@@ -2,6 +2,7 @@ import { removeAssignees } from "../../../helpers";
import { getBotContext, getLogger } from "../../../bindings";
import { Payload } from "../../../types";
import { IssueCommentCommands } from "../commands";
+import { closePullRequestForAnIssue } from "../../assign/index";
export const unassign = async (body: string) => {
const { payload: _payload } = getBotContext();
@@ -27,6 +28,7 @@ export const unassign = async (body: string) => {
logger.debug(`Unassigning sender: ${payload.sender.login.toLowerCase()}, assignee: ${assignees[0].login.toLowerCase()}, shouldUnassign: ${shouldUnassign}`);
if (shouldUnassign) {
+ await closePullRequestForAnIssue();
await removeAssignees(
issue_number,
assignees.map((i) => i.login)
diff --git a/src/handlers/comment/handlers/wallet.ts b/src/handlers/comment/handlers/wallet.ts
index a100360a1..6cabfd357 100644
--- a/src/handlers/comment/handlers/wallet.ts
+++ b/src/handlers/comment/handlers/wallet.ts
@@ -1,10 +1,11 @@
+import { ethers } from "ethers";
import { upsertWalletAddress } from "../../../adapters/supabase";
-import { getBotContext, getLogger } from "../../../bindings";
+import { getBotConfig, getBotContext, getLogger } from "../../../bindings";
import { resolveAddress } from "../../../helpers";
import { Payload } from "../../../types";
import { formatEthAddress } from "../../../utils";
import { IssueCommentCommands } from "../commands";
-
+import { constants } from "ethers";
// Extracts ensname from raw text.
const extractEnsName = (text: string): string | undefined => {
// Define a regular expression to match ENS names
@@ -23,6 +24,7 @@ const extractEnsName = (text: string): string | undefined => {
export const registerWallet = async (body: string) => {
const { payload: _payload } = getBotContext();
+ const config = getBotConfig();
const logger = getLogger();
const payload = _payload as Payload;
const sender = payload.sender.login;
@@ -34,7 +36,10 @@ export const registerWallet = async (body: string) => {
if (!address && !ensName) {
logger.info("Skipping to register a wallet address because both address/ens doesn't exist");
- return;
+ if (config.wallet.registerWalletWithVerification) {
+ return `Please include your wallet or ENS address.\n usage: /wallet 0x0000000000000000000000000000000000000000 0x0830f316c982a7fd4ff050c8fdc1212a8fd92f6bb42b2337b839f2b4e156f05a359ef8f4acd0b57cdedec7874a865ee07076ab2c81dc9f9de28ced55228587f81c`;
+ }
+ return `Please include your wallet or ENS address.\n usage: /wallet 0x0000000000000000000000000000000000000000`;
}
if (!address && ensName) {
@@ -47,7 +52,33 @@ export const registerWallet = async (body: string) => {
logger.info(`Resolved address from Ens name: ${ensName}, address: ${address}`);
}
+ if (config.wallet.registerWalletWithVerification) {
+ const regexForSigHash = /(0x[a-fA-F0-9]{130})/g;
+ const sigHashMatches = body.match(regexForSigHash);
+ const sigHash = sigHashMatches ? sigHashMatches[0] : null;
+
+ const messageToSign = "DevPool";
+ const failedSigLogMsg = `Skipping to register the wallet address because you have not provided a valid SIGNATURE_HASH.`;
+ const failedSigResponse = `Skipping to register the wallet address because you have not provided a valid SIGNATURE_HASH. \nUse [etherscan](https://etherscan.io/verifiedSignatures) to sign the message \`${messageToSign}\` and register your wallet by appending the signature hash.\n\n**Usage:**\n/wallet \n\n**Example:**\n/wallet 0x0000000000000000000000000000000000000000 0x0830f316c982a7fd4ff050c8fdc1212a8fd92f6bb42b2337b839f2b4e156f05a359ef8f4acd0b57cdedec7874a865ee07076ab2c81dc9f9de28ced55228587f81c`;
+ try {
+ //verifyMessage throws an error when some parts(r,s,v) of the signature are correct but some are not
+ const isSigHashValid = address && sigHash && ethers.utils.verifyMessage(messageToSign, sigHash) == ethers.utils.getAddress(address);
+ if (!isSigHashValid) {
+ logger.info(failedSigLogMsg);
+ return failedSigResponse;
+ }
+ } catch (e) {
+ logger.info(`Exception thrown by verifyMessage for /wallet: ${e}`);
+ logger.info(failedSigLogMsg);
+ return failedSigResponse;
+ }
+ }
+
if (address) {
+ if (address == constants.AddressZero) {
+ logger.info("Skipping to register a wallet address because user is trying to set their address to null address");
+ return `Cannot set address to null address`;
+ }
await upsertWalletAddress(sender, address);
return `Updated the wallet address for @${sender} successfully!\t Your new address: ${formatEthAddress(address)}`;
}
diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts
index 4d33917a5..e998bba5c 100644
--- a/src/handlers/payout/action.ts
+++ b/src/handlers/payout/action.ts
@@ -1,7 +1,7 @@
import { getWalletAddress, getWalletMultiplier } from "../../adapters/supabase";
import { getBotConfig, getBotContext, getLogger } from "../../bindings";
import { addLabelToIssue, deleteLabel, generatePermit2Signature, getAllIssueComments, getTokenSymbol } from "../../helpers";
-import { Payload, StateReason } from "../../types";
+import { UserType, Payload, StateReason } from "../../types";
import { shortenEthAddress } from "../../utils";
import { bountyInfo } from "../wildcard";
@@ -48,7 +48,7 @@ export const handleIssueClosed = async () => {
const multiplier = await getWalletMultiplier(assignee.login);
if (multiplier === 0) {
- const errMsg = "Refusing to generate the payment permit because" + `@${assignee.login}` + "'s payment `multiplier` is `0`";
+ const errMsg = "Refusing to generate the payment permit because " + `@${assignee.login}` + "'s payment `multiplier` is `0`";
logger.info(errMsg);
return errMsg;
}
@@ -57,15 +57,7 @@ export const handleIssueClosed = async () => {
const priceInEth = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString();
if (!recipient || recipient?.trim() === "") {
logger.info(`Recipient address is missing`);
- return (
- "Please set your wallet address by using the `/wallet` command.\n" +
- "```\n" +
- "/wallet example.eth\n" +
- "/wallet 0xBf...CdA\n" +
- "```\n" +
- "@" +
- assignee.login
- );
+ return;
}
const payoutUrl = await generatePermit2Signature(recipient, priceInEth, issue.node_id);
@@ -74,9 +66,8 @@ export const handleIssueClosed = async () => {
logger.info(`Posting a payout url to the issue, url: ${payoutUrl}`);
const comment = `### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```";
const comments = await getAllIssueComments(issue.number);
- const commentContents = comments.map((i) => i.body);
- const exist = commentContents.find((content) => content.includes(comment));
- if (exist) {
+ const permitComments = comments.filter((content) => content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot);
+ if (permitComments.length > 0) {
logger.info(`Skip to generate a permit url because it has been already posted`);
return `Permit generation skipped because it was already posted to this issue.`;
}
diff --git a/src/handlers/processors.ts b/src/handlers/processors.ts
index 340c091f3..ecd47fcd5 100644
--- a/src/handlers/processors.ts
+++ b/src/handlers/processors.ts
@@ -3,7 +3,7 @@ import { closePullRequestForAnIssue, commentWithAssignMessage } from "./assign";
import { pricingLabelLogic, validatePriceLabels } from "./pricing";
import { checkBountiesToUnassign, collectAnalytics, checkWeeklyUpdate } from "./wildcard";
import { nullHandler } from "./shared";
-import { handleComment, issueClosedCallback, issueCreatedCallback } from "./comment";
+import { handleComment, issueClosedCallback } from "./comment";
import { checkPullRequests } from "./assign/auto";
import { createDevPoolPR } from "./pull-request";
import { runOnPush } from "./push";
@@ -12,7 +12,7 @@ import { incentivizeComments, incentivizeCreatorComment } from "./payout";
export const processors: Record = {
[GithubEvent.ISSUES_OPENED]: {
pre: [nullHandler],
- action: [issueCreatedCallback],
+ action: [nullHandler], // SHOULD not set `issueCreatedCallback` until the exploit issue resolved. https://github.com/ubiquity/ubiquibot/issues/535
post: [nullHandler],
},
[GithubEvent.ISSUES_LABELED]: {
diff --git a/src/handlers/pull-request/create-devpool-pr.ts b/src/handlers/pull-request/create-devpool-pr.ts
index 75689171f..08046bfbc 100644
--- a/src/handlers/pull-request/create-devpool-pr.ts
+++ b/src/handlers/pull-request/create-devpool-pr.ts
@@ -8,7 +8,7 @@ export const createDevPoolPR = async () => {
const payload = context.payload as Payload;
const devPoolOwner = "ubiquity";
- const devPoolRepo = "devpool";
+ const devPoolRepo = "devpool-directory";
if (!payload.repositories_added) {
return;
diff --git a/src/handlers/push/update-base.ts b/src/handlers/push/update-base.ts
index 6f10418de..4141d811f 100644
--- a/src/handlers/push/update-base.ts
+++ b/src/handlers/push/update-base.ts
@@ -21,12 +21,12 @@ export const updateBaseRate = async (context: Context, payload: PushPayload, fil
const previousContent = Buffer.from(preFileContent, "base64").toString();
const previousConfig = await parseYAML(previousContent);
- if (!previousConfig || !previousConfig["base-multiplier"]) {
+ if (!previousConfig || !previousConfig["price-multiplier"]) {
logger.debug("No multiplier found in file object");
return;
}
- const previousBaseRate = previousConfig["base-multiplier"];
+ const previousBaseRate = previousConfig["price-multiplier"];
// fetch all labels
const repoLabels = await listLabelsForRepo();
diff --git a/src/handlers/shared/pricing.ts b/src/handlers/shared/pricing.ts
index f422ca84c..65c1079d0 100644
--- a/src/handlers/shared/pricing.ts
+++ b/src/handlers/shared/pricing.ts
@@ -4,7 +4,7 @@ export const calculateBountyPrice = (timeValue: number, priorityValue: number, b
const botConfig = getBotConfig();
const base = baseValue ?? botConfig.price.baseMultiplier;
const priority = priorityValue / 10; // floats cause bad math
- const price = base * timeValue * priority;
+ const price = 1000 * base * timeValue * priority;
return price;
};
diff --git a/src/handlers/wildcard/analytics.ts b/src/handlers/wildcard/analytics.ts
index e88e91898..40e861ac7 100644
--- a/src/handlers/wildcard/analytics.ts
+++ b/src/handlers/wildcard/analytics.ts
@@ -43,10 +43,10 @@ export const bountyInfo = (
export const collectAnalytics = async (): Promise => {
const logger = getLogger();
const {
- mode: { analyticsMode },
+ mode: { disableAnalytics },
} = getBotConfig();
- if (!analyticsMode) {
- logger.info(`Skipping to collect analytics, reason: mode=${analyticsMode}`);
+ if (disableAnalytics) {
+ logger.info(`Skipping to collect analytics, reason: mode=${disableAnalytics}`);
return;
}
logger.info("Collecting analytics information...");
diff --git a/src/handlers/wildcard/unassign.ts b/src/handlers/wildcard/unassign.ts
index c5f4854fc..b94b2c1c7 100644
--- a/src/handlers/wildcard/unassign.ts
+++ b/src/handlers/wildcard/unassign.ts
@@ -1,11 +1,12 @@
+import { closePullRequestForAnIssue } from "../assign";
import { getBotConfig, getLogger } from "../../bindings";
import { GLOBAL_STRINGS } from "../../configs/strings";
import {
addCommentToIssue,
- getCommentsOfIssue,
+ getAllIssueComments,
getCommitsOnPullRequest,
getOpenedPullRequestsForAnIssue,
- listIssuesForRepo,
+ listAllIssuesForRepo,
removeAssignees,
} from "../../helpers";
import { Comment, Issue, IssueType } from "../../types";
@@ -20,7 +21,7 @@ export const checkBountiesToUnassign = async () => {
// List all the issues in the repository. It may include `pull_request`
// because GitHub's REST API v3 considers every pull request an issue
- const issues_opened = await listIssuesForRepo(IssueType.OPEN);
+ const issues_opened = await listAllIssuesForRepo(IssueType.OPEN);
const assigned_issues = issues_opened.filter((issue) => issue.assignee);
@@ -37,15 +38,16 @@ const checkBountyToUnassign = async (issue: Issue): Promise => {
logger.info(`Checking the bounty to unassign, issue_number: ${issue.number}`);
const { unassignComment, askUpdate } = GLOBAL_STRINGS;
const assignees = issue.assignees.map((i) => i.login);
- const comments = await getCommentsOfIssue(issue.number);
+ const comments = await getAllIssueComments(issue.number);
if (!comments || comments.length == 0) return false;
const askUpdateComments = comments
.filter((comment: Comment) => comment.body.includes(askUpdate))
.sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
+
const lastAskTime = askUpdateComments.length > 0 ? new Date(askUpdateComments[0].created_at).getTime() : new Date(issue.created_at).getTime();
const curTimestamp = new Date().getTime();
- const lastActivity = await lastActivityTime(issue);
+ const lastActivity = await lastActivityTime(issue, comments);
const passedDuration = curTimestamp - lastActivity.getTime();
if (passedDuration >= disqualifyTime || passedDuration >= followUpTime) {
@@ -53,9 +55,10 @@ const checkBountyToUnassign = async (issue: Issue): Promise => {
logger.info(
`Unassigning... lastActivityTime: ${lastActivity.getTime()}, curTime: ${curTimestamp}, passedDuration: ${passedDuration}, followUpTime: ${followUpTime}, disqualifyTime: ${disqualifyTime}`
);
+ await closePullRequestForAnIssue();
// remove assignees from the issue
await removeAssignees(issue.number, assignees);
- await addCommentToIssue(`${unassignComment} \nLast activity time: ${lastActivity}`, issue.number);
+ await addCommentToIssue(`@${assignees[0]} - ${unassignComment} \nLast activity time: ${lastActivity}`, issue.number);
return true;
} else if (passedDuration >= followUpTime) {
@@ -78,14 +81,14 @@ const checkBountyToUnassign = async (issue: Issue): Promise => {
return false;
};
-const lastActivityTime = async (issue: Issue): Promise => {
+const lastActivityTime = async (issue: Issue, comments: Comment[]): Promise => {
const logger = getLogger();
logger.info(`Checking the latest activity for the issue, issue_number: ${issue.number}`);
const assignees = issue.assignees.map((i) => i.login);
const activities: Date[] = [new Date(issue.created_at)];
// get last comment on the issue
- const lastCommentsOfHunterForIssue = (await getCommentsOfIssue(issue.number))
+ const lastCommentsOfHunterForIssue = comments
.filter((comment) => assignees.includes(comment.user.login))
.sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
@@ -98,9 +101,9 @@ const lastActivityTime = async (issue: Issue): Promise => {
const commits = (await getCommitsOnPullRequest(pr.number))
.filter((it) => it.commit.committer?.date)
.sort((a, b) => new Date(b.commit.committer?.date ?? 0).getTime() - new Date(a.commit.committer?.date ?? 0).getTime());
- const prComments = (await getCommentsOfIssue(pr.number))
+ const prComments = (await getAllIssueComments(pr.number))
.filter((comment) => comment.user.login === assignees[0])
- .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
if (commits.length > 0) activities.push(new Date(commits[0].commit.committer?.date ?? 0));
if (prComments.length > 0) activities.push(new Date(prComments[0].created_at));
diff --git a/src/handlers/wildcard/weekly.ts b/src/handlers/wildcard/weekly.ts
index 7f05cf077..3f52bcd5b 100644
--- a/src/handlers/wildcard/weekly.ts
+++ b/src/handlers/wildcard/weekly.ts
@@ -7,10 +7,10 @@ const SEVEN_DAYS = 604800; // 7 days in seconds
export const checkWeeklyUpdate = async () => {
const { log } = getBotContext();
const {
- mode: { analyticsMode },
+ mode: { disableAnalytics },
} = getBotConfig();
- if (!analyticsMode) {
- log.info(`Skipping to collect the weekly analytics, reason: mode=${analyticsMode}`);
+ if (disableAnalytics) {
+ log.info(`Skipping to collect the weekly analytics, reason: mode=${disableAnalytics}`);
return;
}
const curTime = Date.now() / 1000;
diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts
index 01cf8bf85..ae9508f00 100644
--- a/src/helpers/issue.ts
+++ b/src/helpers/issue.ts
@@ -60,6 +60,8 @@ export const listIssuesForRepo = async (state: "open" | "closed" | "all" = "open
page,
});
+ await checkRateLimitGit(response.headers);
+
if (response.status === 200) {
return response.data;
} else {
@@ -67,6 +69,24 @@ export const listIssuesForRepo = async (state: "open" | "closed" | "all" = "open
}
};
+export const listAllIssuesForRepo = async (state: "open" | "closed" | "all" = "open") => {
+ const issuesArr = [];
+ let fetchDone = false;
+ const perPage = 100;
+ let curPage = 1;
+ while (!fetchDone) {
+ const issues = await listIssuesForRepo(state, perPage, curPage);
+
+ // push the objects to array
+ issuesArr.push(...issues);
+
+ if (issues.length < perPage) fetchDone = true;
+ else curPage++;
+ }
+
+ return issuesArr;
+};
+
export const addCommentToIssue = async (msg: string, issue_number: number) => {
const context = getBotContext();
const logger = getLogger();
@@ -84,6 +104,56 @@ export const addCommentToIssue = async (msg: string, issue_number: number) => {
}
};
+export const updateCommentOfIssue = async (msg: string, issue_number: number, reply_to: Comment) => {
+ const context = getBotContext();
+ const logger = getLogger();
+ const payload = context.payload as Payload;
+
+ try {
+ const appResponse = await context.octokit.apps.getAuthenticated();
+ const { name, slug } = appResponse.data;
+ logger.info(`App name/slug ${name}/${slug}`);
+
+ const editCommentBy = `${slug}[bot]`;
+ logger.info(`Bot slug: ${editCommentBy}`);
+
+ const comments = await context.octokit.issues.listComments({
+ owner: payload.repository.owner.login,
+ repo: payload.repository.name,
+ issue_number: issue_number,
+ since: reply_to.created_at,
+ per_page: 30,
+ });
+
+ const comment_to_edit = comments.data.find((comment) => {
+ return comment?.user?.login == editCommentBy && comment.id > reply_to.id;
+ });
+
+ if (comment_to_edit) {
+ logger.info(`For comment_id: ${reply_to.id} found comment_to_edit with id: ${comment_to_edit.id}`);
+ await context.octokit.issues.updateComment({
+ owner: payload.repository.owner.login,
+ repo: payload.repository.name,
+ comment_id: comment_to_edit.id,
+ body: msg,
+ });
+ } else {
+ logger.info(`Falling back to add comment. Couldn't find response to edit for comment_id: ${reply_to.id}`);
+ await addCommentToIssue(msg, issue_number);
+ }
+ } catch (e: unknown) {
+ logger.debug(`Upading a comment failed!, reason: ${e}`);
+ }
+};
+
+export const upsertCommentToIssue = async (issue_number: number, comment: string, action: string, reply_to?: Comment) => {
+ if (action == "edited" && reply_to) {
+ await updateCommentOfIssue(comment, issue_number, reply_to);
+ } else {
+ await addCommentToIssue(comment, issue_number);
+ }
+};
+
export const getCommentsOfIssue = async (issue_number: number): Promise => {
const context = getBotContext();
const logger = getLogger();
@@ -344,6 +414,18 @@ export const getIssueByNumber = async (context: Context, issue_number: number) =
}
};
+export const getPullByNumber = async (context: Context, pull_number: number) => {
+ const logger = getLogger();
+ const payload = context.payload as Payload;
+ try {
+ const { data: pull } = await context.octokit.rest.pulls.get({ owner: payload.repository.owner.login, repo: payload.repository.name, pull_number });
+ return pull;
+ } catch (error) {
+ logger.debug(`Fetching pull failed!, reason: ${error}`);
+ return;
+ }
+};
+
// Get issues assigned to a username
export const getAssignedIssues = async (username: string) => {
const issuesArr = [];
@@ -415,7 +497,10 @@ export const getAvailableOpenedPullRequests = async (username: string) => {
const pr = opened_prs[i];
const reviews = await getAllPullRequestReviews(context, pr.number);
- if (reviews.length > 0) result.push(pr);
+ if (reviews.length > 0) {
+ const approvedReviews = reviews.find((review) => review.state === "APPROVED");
+ if (approvedReviews) result.push(pr);
+ }
if (reviews.length === 0 && (new Date().getTime() - new Date(pr.created_at).getTime()) / (1000 * 60 * 60) >= DEFAULT_TIME_RANGE_FOR_MAX_ISSUE) {
result.push(pr);
diff --git a/src/helpers/payout.ts b/src/helpers/payout.ts
index 361920060..7ef127f77 100644
--- a/src/helpers/payout.ts
+++ b/src/helpers/payout.ts
@@ -16,7 +16,7 @@ import { DEFAULT_RPC_ENDPOINT } from "../configs";
import { PayoutConfigSchema } from "../types";
// available tokens for payouts
-const PAYMENT_TOKEN_PER_CHAIN: Record = {
+const PAYMENT_TOKEN_PER_NETWORK: Record = {
"1": {
rpc: DEFAULT_RPC_ENDPOINT,
token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI
@@ -27,17 +27,17 @@ const PAYMENT_TOKEN_PER_CHAIN: Record =
},
};
-type PayoutConfigPartial = Omit, "chainId" | "privateKey" | "permitBaseUrl">;
+type PayoutConfigPartial = Omit, "networkId" | "privateKey" | "permitBaseUrl">;
/**
- * Returns payout config for a particular chain
- * @param chainId chain id
+ * Returns payout config for a particular network
+ * @param networkId network id
* @returns RPC URL and payment token
*/
-export const getPayoutConfigByChainId = (chainId: number): PayoutConfigPartial => {
- const paymentToken = PAYMENT_TOKEN_PER_CHAIN[chainId.toString()];
+export const getPayoutConfigByNetworkId = (networkId: number): PayoutConfigPartial => {
+ const paymentToken = PAYMENT_TOKEN_PER_NETWORK[networkId.toString()];
if (!paymentToken) {
- throw new Error(`No config setup for chainId: ${chainId}`);
+ throw new Error(`No config setup for networkId: ${networkId}`);
}
return {
diff --git a/src/helpers/permit.ts b/src/helpers/permit.ts
index 290064230..de17b98eb 100644
--- a/src/helpers/permit.ts
+++ b/src/helpers/permit.ts
@@ -3,7 +3,7 @@ import { BigNumber, ethers } from "ethers";
import { getBotConfig, getLogger } from "../bindings";
import { keccak256, toUtf8Bytes } from "ethers/lib/utils";
-const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all chains
+const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all networks
/**
* Generates permit2 signature data with `spender` and `amountInETH`
@@ -15,7 +15,7 @@ const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on
*/
export const generatePermit2Signature = async (spender: string, amountInEth: string, identifier: string): Promise => {
const {
- payout: { chainId, privateKey, permitBaseUrl, rpc, paymentToken },
+ payout: { networkId, privateKey, permitBaseUrl, rpc, paymentToken },
} = getBotConfig();
const logger = getLogger();
const provider = new ethers.providers.JsonRpcProvider(rpc);
@@ -35,7 +35,7 @@ export const generatePermit2Signature = async (spender: string, amountInEth: str
deadline: MaxUint256,
};
- const { domain, types, values } = SignatureTransfer.getPermitData(permitTransferFromData, PERMIT2_ADDRESS, chainId);
+ const { domain, types, values } = SignatureTransfer.getPermitData(permitTransferFromData, PERMIT2_ADDRESS, networkId);
const signature = await adminWallet._signTypedData(domain, types, values);
const txData = {
@@ -57,7 +57,7 @@ export const generatePermit2Signature = async (spender: string, amountInEth: str
const base64encodedTxData = Buffer.from(JSON.stringify(txData)).toString("base64");
- const result = `${permitBaseUrl}?claim=${base64encodedTxData}&network=${chainId}`;
+ const result = `${permitBaseUrl}?claim=${base64encodedTxData}&network=${networkId}`;
logger.info(`Generated permit2 url: ${result}`);
return result;
};
diff --git a/src/types/config.ts b/src/types/config.ts
index 56d48079a..855ba2049 100644
--- a/src/types/config.ts
+++ b/src/types/config.ts
@@ -16,6 +16,7 @@ export const PriceConfigSchema = Type.Object({
timeLabels: Type.Array(LabelItemSchema),
priorityLabels: Type.Array(LabelItemSchema),
commentElementPricing: CommentElementPricingSchema,
+ defaultLabels: Type.Array(Type.String()),
});
export type PriceConfig = Static;
@@ -30,7 +31,7 @@ export const TelegramBotConfigSchema = Type.Object({
});
export const PayoutConfigSchema = Type.Object({
- chainId: Type.Number(),
+ networkId: Type.Number(),
rpc: Type.String(),
privateKey: Type.String(),
paymentToken: Type.String(),
@@ -44,7 +45,7 @@ export const UnassignConfigSchema = Type.Object({
export const ModeSchema = Type.Object({
autoPayMode: Type.Boolean(),
- analyticsMode: Type.Boolean(),
+ disableAnalytics: Type.Boolean(),
incentiveMode: Type.Boolean(),
});
@@ -62,6 +63,14 @@ export const SodiumSchema = Type.Object({
privateKey: Type.String(),
});
+export const CommentsSchema = Type.Object({
+ promotionComment: Type.String(),
+});
+
+export const WalletSchema = Type.Object({
+ registerWalletWithVerification: Type.Boolean(),
+});
+
export const BotConfigSchema = Type.Object({
log: LogConfigSchema,
price: PriceConfigSchema,
@@ -72,6 +81,8 @@ export const BotConfigSchema = Type.Object({
mode: ModeSchema,
assign: AssignSchema,
sodium: SodiumSchema,
+ comments: CommentsSchema,
+ wallet: WalletSchema,
});
export type BotConfig = Static;
diff --git a/src/types/handlers.ts b/src/types/handlers.ts
index 804f8769d..5a79f4dd0 100644
--- a/src/types/handlers.ts
+++ b/src/types/handlers.ts
@@ -1,6 +1,8 @@
+import { Comment } from "./payload";
+
export type CommandsHandler = (args: string) => Promise;
export type ActionHandler = (args?: string) => Promise;
-export type CallbackHandler = (issue_number: number, text: string) => Promise;
+export type CallbackHandler = (issue_number: number, text: string, action: string, reply_to?: Comment) => Promise;
export type PreActionHandler = ActionHandler;
export type PostActionHandler = ActionHandler;
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index e8e41fc6c..415305870 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -7,23 +7,23 @@ interface Configs {
parsedDefault: WideRepoConfig;
}
-export const getChainId = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => {
- if (parsedRepo && parsedRepo["chain-id"] && !Number.isNaN(Number(parsedRepo["chain-id"]))) {
- return Number(parsedRepo["chain-id"]);
- } else if (parsedOrg && parsedOrg["chain-id"] && !Number.isNaN(Number(parsedOrg["chain-id"]))) {
- return Number(parsedOrg["chain-id"]);
+export const getNetworkId = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => {
+ if (parsedRepo && parsedRepo["evm-network-id"] && !Number.isNaN(Number(parsedRepo["evm-network-id"]))) {
+ return Number(parsedRepo["evm-network-id"]);
+ } else if (parsedOrg && parsedOrg["evm-network-id"] && !Number.isNaN(Number(parsedOrg["evm-network-id"]))) {
+ return Number(parsedOrg["evm-network-id"]);
} else {
- return Number(parsedDefault["chain-id"]!);
+ return Number(parsedDefault["evm-network-id"]);
}
};
export const getBaseMultiplier = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => {
- if (parsedRepo && parsedRepo["base-multiplier"] && !Number.isNaN(Number(parsedRepo["base-multiplier"]))) {
- return Number(parsedRepo["base-multiplier"]);
- } else if (parsedOrg && parsedOrg["base-multiplier"] && !Number.isNaN(Number(parsedOrg["base-multiplier"]))) {
- return Number(parsedOrg["base-multiplier"]);
+ if (parsedRepo && parsedRepo["price-multiplier"] && !Number.isNaN(Number(parsedRepo["price-multiplier"]))) {
+ return Number(parsedRepo["price-multiplier"]);
+ } else if (parsedOrg && parsedOrg["price-multiplier"] && !Number.isNaN(Number(parsedOrg["price-multiplier"]))) {
+ return Number(parsedOrg["price-multiplier"]);
} else {
- return Number(parsedDefault["base-multiplier"]!);
+ return Number(parsedDefault["price-multiplier"]);
}
};
@@ -43,7 +43,7 @@ export const getTimeLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Configs)
} else if (parsedOrg && parsedOrg["time-labels"] && Array.isArray(parsedOrg["time-labels"]) && parsedOrg["time-labels"].length > 0) {
return parsedOrg["time-labels"];
} else {
- return parsedDefault["time-labels"]!;
+ return parsedDefault["time-labels"] as WideLabel[];
}
};
@@ -53,7 +53,7 @@ export const getPriorityLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Conf
} else if (parsedOrg && parsedOrg["priority-labels"] && Array.isArray(parsedOrg["priority-labels"]) && parsedOrg["priority-labels"].length > 0) {
return parsedOrg["priority-labels"];
} else {
- return parsedDefault["priority-labels"]!;
+ return parsedDefault["priority-labels"] as WideLabel[];
}
};
@@ -63,7 +63,7 @@ export const getCommentItemPrice = ({ parsedRepo, parsedOrg, parsedDefault }: Co
} else if (parsedOrg && parsedOrg["comment-element-pricing"]) {
return parsedOrg["comment-element-pricing"];
} else {
- return parsedDefault["comment-element-pricing"]!;
+ return parsedDefault["comment-element-pricing"] as CommentElementPricing;
}
};
@@ -73,36 +73,66 @@ export const getAutoPayMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs
} else if (parsedOrg && parsedOrg["auto-pay-mode"] && typeof parsedOrg["auto-pay-mode"] === "boolean") {
return parsedOrg["auto-pay-mode"];
} else {
- return parsedDefault["auto-pay-mode"]!;
+ return parsedDefault["auto-pay-mode"] as boolean;
}
};
export const getAnalyticsMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => {
- if (parsedRepo && parsedRepo["analytics-mode"] && typeof parsedRepo["analytics-mode"] === "boolean") {
- return parsedRepo["analytics-mode"];
- } else if (parsedOrg && parsedOrg["analytics-mode"] && typeof parsedOrg["analytics-mode"] === "boolean") {
- return parsedOrg["analytics-mode"];
+ if (parsedRepo && parsedRepo["disable-analytics"] && typeof parsedRepo["disable-analytics"] === "boolean") {
+ return parsedRepo["disable-analytics"];
+ } else if (parsedOrg && parsedOrg["disable-analytics"] && typeof parsedOrg["disable-analytics"] === "boolean") {
+ return parsedOrg["disable-analytics"];
} else {
- return parsedDefault["analytics-mode"]!;
+ return parsedDefault["disable-analytics"] as boolean;
+ }
+};
+
+export const getPromotionComment = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): string => {
+ if (parsedRepo && parsedRepo["promotion-comment"] && typeof parsedRepo["promotion-comment"] === "string") {
+ return parsedRepo["promotion-comment"];
+ } else if (parsedOrg && parsedOrg["promotion-comment"] && typeof parsedOrg["promotion-comment"] === "string") {
+ return parsedOrg["promotion-comment"];
+ } else {
+ return parsedDefault["promotion-comment"] as string;
}
};
export const getIncentiveMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => {
- if (parsedRepo && parsedRepo["incentive-mode"] && typeof parsedRepo["incentive-mode"] === "boolean") {
- return parsedRepo["incentive-mode"];
- } else if (parsedOrg && parsedOrg["incentive-mode"] && typeof parsedOrg["incentive-mode"] === "boolean") {
- return parsedOrg["incentive-mode"];
+ if (parsedRepo && parsedRepo["comment-incentives"] && typeof parsedRepo["comment-incentives"] === "boolean") {
+ return parsedRepo["comment-incentives"];
+ } else if (parsedOrg && parsedOrg["comment-incentives"] && typeof parsedOrg["comment-incentives"] === "boolean") {
+ return parsedOrg["comment-incentives"];
} else {
- return parsedDefault["incentive-mode"]!;
+ return parsedDefault["comment-incentives"] as boolean;
}
};
export const getBountyHunterMax = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => {
- if (parsedRepo && parsedRepo["max-concurrent-bounties"] && !Number.isNaN(Number(parsedRepo["max-concurrent-bounties"]))) {
- return Number(parsedRepo["max-concurrent-bounties"]);
- } else if (parsedOrg && parsedOrg["max-concurrent-bounties"] && !Number.isNaN(Number(parsedOrg["max-concurrent-bounties"]))) {
- return Number(parsedOrg["max-concurrent-bounties"]);
+ if (parsedRepo && parsedRepo["max-concurrent-assigns"] && !Number.isNaN(Number(parsedRepo["max-concurrent-assigns"]))) {
+ return Number(parsedRepo["max-concurrent-assigns"]);
+ } else if (parsedOrg && parsedOrg["max-concurrent-assigns"] && !Number.isNaN(Number(parsedOrg["max-concurrent-assigns"]))) {
+ return Number(parsedOrg["max-concurrent-assigns"]);
+ } else {
+ return Number(parsedDefault["max-concurrent-assigns"]);
+ }
+};
+
+export const getDefaultLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): string[] => {
+ if (parsedRepo && parsedRepo["default-labels"]) {
+ return parsedRepo["default-labels"];
+ } else if (parsedOrg && parsedOrg["default-labels"]) {
+ return parsedOrg["default-labels"];
+ } else {
+ return parsedDefault["default-labels"] as string[];
+ }
+};
+
+export const getRegisterWalletWithVerification = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => {
+ if (parsedRepo && parsedRepo["register-wallet-with-verification"] && typeof parsedRepo["register-wallet-with-verification"] === "boolean") {
+ return Boolean(parsedRepo["register-wallet-with-verification"]);
+ } else if (parsedOrg && parsedOrg["register-wallet-with-verification"] && typeof parsedOrg["register-wallet-with-verification"] === "boolean") {
+ return Boolean(parsedOrg["register-wallet-with-verification"]);
} else {
- return Number(parsedDefault["max-concurrent-bounties"]!);
+ return Boolean(parsedDefault["register-wallet-with-verification"]);
}
};
diff --git a/src/utils/private.ts b/src/utils/private.ts
index 66e07bd47..0a6e98064 100644
--- a/src/utils/private.ts
+++ b/src/utils/private.ts
@@ -10,10 +10,13 @@ import {
getCreatorMultiplier,
getBountyHunterMax,
getIncentiveMode,
- getChainId,
+ getNetworkId,
getPriorityLabels,
getTimeLabels,
getCommentItemPrice,
+ getDefaultLabels,
+ getPromotionComment,
+ getRegisterWalletWithVerification,
} from "./helpers";
const CONFIG_REPO = "ubiquibot-config";
@@ -48,16 +51,19 @@ export interface WideLabel {
}
export interface WideConfig {
- "chain-id"?: number;
- "base-multiplier"?: number;
+ "evm-network-id"?: number;
+ "price-multiplier"?: number;
"issue-creator-multiplier": number;
"time-labels"?: WideLabel[];
"priority-labels"?: WideLabel[];
"auto-pay-mode"?: boolean;
- "analytics-mode"?: boolean;
- "incentive-mode"?: boolean;
- "max-concurrent-bounties"?: number;
+ "promotion-comment"?: string;
+ "disable-analytics"?: boolean;
+ "comment-incentives"?: boolean;
+ "max-concurrent-assigns"?: number;
"comment-element-pricing"?: Record;
+ "default-labels"?: string[];
+ "register-wallet-with-verification"?: boolean;
}
export type WideRepoConfig = WideConfig;
@@ -80,7 +86,7 @@ export const parseYAML = (data?: string): WideConfig | undefined => {
export const getDefaultConfig = (): WideRepoConfig => {
const defaultConfig = readFileSync(`${__dirname}/../../ubiquibot-config-default.yml`, "utf8");
- return parseYAML(defaultConfig)!;
+ return parseYAML(defaultConfig) as WideRepoConfig;
};
export const getPrivateKey = async (cipherText: string): Promise => {
@@ -134,17 +140,20 @@ export const getWideConfig = async (context: Context) => {
const configs = { parsedRepo, parsedOrg, parsedDefault };
const configData = {
- chainId: getChainId(configs),
+ networkId: getNetworkId(configs),
privateKey: privateKeyDecrypted ?? "",
baseMultiplier: getBaseMultiplier(configs),
issueCreatorMultiplier: getCreatorMultiplier(configs),
timeLabels: getTimeLabels(configs),
priorityLabels: getPriorityLabels(configs),
autoPayMode: getAutoPayMode(configs),
- analyticsMode: getAnalyticsMode(configs),
+ disableAnalytics: getAnalyticsMode(configs),
bountyHunterMax: getBountyHunterMax(configs),
incentiveMode: getIncentiveMode(configs),
commentElementPricing: getCommentItemPrice(configs),
+ defaultLabels: getDefaultLabels(configs),
+ promotionComment: getPromotionComment(configs),
+ registerWalletWithVerification: getRegisterWalletWithVerification(configs),
};
return configData;
diff --git a/supabase/README.md b/supabase/README.md
index 636cfd57c..bdfa8ab90 100644
--- a/supabase/README.md
+++ b/supabase/README.md
@@ -2,20 +2,18 @@
[Supabase](https://supabase.com/) is used to store bounty hunters profiles and bounties information.
-
-
### How to setup supabase project locally
1. To get started with supabase, you have to create a project at [Supabase](https://supabase.com/).
-Once you create a project, please put both variables into `.env` file.
+ Once you create a project, please put both variables into `.env` file.
```
SUPABASE_URL=XXX
SUPABASE_KEY=XXX
```
-2.
-[The Supabase CLI](https://supabase.com/docs/guides/resources/supabase-cli) available as a node package through the dev dependencies provides tools to develop your project locally and deploy to the Supabase Platform.
-Most common useful commands are
+
+2. [The Supabase CLI](https://supabase.com/docs/guides/resources/supabase-cli) available as a node package through the dev dependencies provides tools to develop your project locally and deploy to the Supabase Platform.
+ Most common useful commands are
- Run Supabase locally
@@ -51,7 +49,7 @@ yarn supabase gen types
```sh
yarn supabase link -p PASSWORD --project-ref PROJECT_REF
-```
+```
For more information about arguments, please go through [here](https://supabase.com/docs/reference/cli/supabase-link)
@@ -59,4 +57,4 @@ For more information about arguments, please go through [here](https://supabase.
- `supabase migration new MIGRATION_NAME`: It will create a migration file in supabase/migrations folder.
- `supabase db push -p PASSWORD`: Update database schema on supabase platform
-- `supabase gen types typescript > src/adapters/supabase/types/database.types.ts --linked`: Generate typescript types from the supabase project linked
+- `supabase gen types typescript > src/adapters/supabase/types/database.ts --linked`: Generate typescript types from the supabase project linked
diff --git a/ubiquibot-config-default.yml b/ubiquibot-config-default.yml
index fe7100f6f..5b0c70ac9 100644
--- a/ubiquibot-config-default.yml
+++ b/ubiquibot-config-default.yml
@@ -1,6 +1,6 @@
---
-chain-id: 1
-base-multiplier: 1000
+evm-network-id: 1
+price-multiplier: 1000
issue-creator-multiplier: 2000
time-labels:
- name: "Time: <1 Hour"
@@ -30,12 +30,15 @@ priority-labels:
- name: "Priority: 4 (Emergency)"
weight: 5
auto-pay-mode: true
-analytics-mode: true
-incentive-mode: false
-max-concurrent-bounties: 2
+disable-analytics: true
+comment-incentives: false
+max-concurrent-assigns: 2
comment-element-pricing:
text: 0.1
link: 0.5
list: 0.5
code: 5
- image: 5
\ No newline at end of file
+ image: 5
+default-labels: []
+promotion-comment: "\nIf you enjoy the DevPool experience, please follow Ubiquity on GitHub and star this repo to show your support. It helps a lot!
"
+register-wallet-with-verification: false