diff --git a/.cspell.json b/.cspell.json index db0c387..a2af128 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,7 +4,7 @@ "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log", "supabase"], "useGitignore": true, "language": "en", - "words": ["dataurl", "devpool", "outdir", "servedir"], + "words": ["dataurl", "devpool", "outdir", "servedir", "typebox"], "dictionaries": ["typescript", "node", "software-terms"], "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], "ignoreRegExpList": ["[0-9a-fA-F]{6}"], diff --git a/.github/workflows/compute.yml b/.github/workflows/compute.yml index dadd8dd..be8e555 100644 --- a/.github/workflows/compute.yml +++ b/.github/workflows/compute.yml @@ -30,7 +30,7 @@ jobs: node-version: "20.10.0" - name: Install dependencies - run: yarn + run: npm install -g bun && bun install - name: Generate Permit run: npx tsx ./src/index.ts diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..594ad89 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index ba6b03e..4baa70c 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,6 @@ "node": ">=20.10.0" }, "scripts": { - "start": "tsx build/esbuild-server.ts", - "build": "tsx build/esbuild-build.ts", "format": "run-s format:lint format:prettier format:cspell", "format:lint": "eslint --fix .", "format:prettier": "prettier --write .", @@ -18,7 +16,6 @@ "knip": "knip", "knip-ci": "knip --no-exit-code --reporter json", "prepare": "husky install", - "worker": "wrangler dev --port 8789", "test": "jest" }, "keywords": [ @@ -40,7 +37,7 @@ "blake2b": "^2.1.4", "decimal.js": "^10.4.3", "dotenv": "^16.4.4", - "ethers": "^5.7.2", + "ethers": "^6.11.1", "libsodium-wrappers": "^0.7.13", "tweetnacl": "^1.0.3", "tweetnacl-util": "^0.15.1" diff --git a/src/adapters/supabase/helpers/wallet.ts b/src/adapters/supabase/helpers/wallet.ts index e70fb86..a915e9a 100644 --- a/src/adapters/supabase/helpers/wallet.ts +++ b/src/adapters/supabase/helpers/wallet.ts @@ -15,8 +15,8 @@ export class Wallet extends Super { throw error; } - console.info("Successfully fetched wallet", { userId, address: data?.address }); - return data?.address as `0x${string}` | undefined; + console.info("Successfully fetched wallet", { userId, address: data.address }); + return data.address; } async getWalletByUsername(username: string) { diff --git a/src/handlers/generate-erc20-permit.ts b/src/handlers/generate-erc20-permit.ts index e975d65..c75f987 100644 --- a/src/handlers/generate-erc20-permit.ts +++ b/src/handlers/generate-erc20-permit.ts @@ -1,55 +1,33 @@ import { MaxUint256, PERMIT2_ADDRESS, PermitTransferFrom, SignatureTransfer } from "@uniswap/permit2-sdk"; -import { BigNumber, ethers } from "ethers"; -import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; +import { ethers, keccak256, parseUnits, toUtf8Bytes } from "ethers"; import { getPayoutConfigByNetworkId } from "../utils/payoutConfigByNetworkId"; import { decryptKeys } from "../utils/keys"; -import { PermitTransactionData } from "../types/permits"; +import { Permit } from "../types/permits"; import { Context } from "../types/context"; -export async function generateErc20PermitSignature(context: Context, wallet: `0x${string}`, amount: number): Promise { +export async function generateErc20PermitSignature(context: Context, username: string, amount: number): Promise { const config = context.config; const logger = context.logger; const { evmNetworkId, evmPrivateEncrypted } = config; + const { user, wallet } = context.adapters.supabase; - if (!evmPrivateEncrypted || !evmNetworkId) { - logger.fatal("EVM configuration is not defined"); - throw new Error("EVM configuration is not defined"); - } - - const { user } = context.adapters.supabase; - - const beneficiary = wallet; - const userId = user.getUserIdByWallet(beneficiary); + const userId = await user.getUserIdByUsername(username); + const walletAddress = await wallet.getWalletByUserId(userId); let issueId: number | null = null; - if ("issue" in context.payload) { - issueId = context.payload.issue.number; + issueId = context.payload.issue.id; } else if ("pull_request" in context.payload) { - issueId = context.payload.pull_request.number; + issueId = context.payload.pull_request.id; } - if (!beneficiary) { - logger.error("No beneficiary found for permit"); - return "Permit not generated: No beneficiary found for permit"; - } - - if (!userId) { - logger.error("No wallet found for user"); - return "Permit not generated: no wallet found for user"; - } - - if (!evmPrivateEncrypted) throw logger.warn("No bot wallet private key defined"); - const { rpc, paymentToken } = getPayoutConfigByNetworkId(evmNetworkId); + const { rpc, token, decimals } = getPayoutConfigByNetworkId(evmNetworkId); const { privateKey } = await decryptKeys(evmPrivateEncrypted); - - if (!rpc) throw logger.error("RPC is not defined"); if (!privateKey) throw logger.error("Private key is not defined"); - if (!paymentToken) throw logger.error("Payment token is not defined"); let provider; let adminWallet; try { - provider = new ethers.providers.JsonRpcProvider(rpc); + provider = new ethers.JsonRpcProvider(rpc); } catch (error) { throw logger.debug("Failed to instantiate provider", error); } @@ -62,40 +40,45 @@ export async function generateErc20PermitSignature(context: Context, wallet: `0x const permitTransferFromData: PermitTransferFrom = { permitted: { - token: paymentToken, - amount: ethers.utils.parseUnits(amount.toString(), 18), + token: token, + amount: parseUnits(amount.toString(), decimals), }, - spender: beneficiary, - nonce: BigNumber.from(keccak256(toUtf8Bytes(`${userId}-${issueId}`))), + spender: walletAddress, + nonce: BigInt(keccak256(toUtf8Bytes(`${userId}-${issueId}`))), deadline: MaxUint256, }; const { domain, types, values } = SignatureTransfer.getPermitData(permitTransferFromData, PERMIT2_ADDRESS, evmNetworkId); - const signature = await adminWallet._signTypedData(domain, types, values).catch((error) => { - throw logger.debug("Failed to sign typed data", error); - }); - - const transactionData = { - type: "erc20-permit", - permit: { - permitted: { - token: permitTransferFromData.permitted.token, - amount: permitTransferFromData.permitted.amount.toString(), + const signature = await adminWallet + .signTypedData( + { + name: domain.name, + version: domain.version, + chainId: domain.chainId ? domain.chainId.toString() : undefined, + verifyingContract: domain.verifyingContract, + salt: domain.salt?.toString(), }, - nonce: permitTransferFromData.nonce.toString(), - deadline: permitTransferFromData.deadline.toString(), - }, - transferDetails: { - to: permitTransferFromData.spender, - requestedAmount: permitTransferFromData.permitted.amount.toString(), - }, + types, + values + ) + .catch((error) => { + throw logger.debug("Failed to sign typed data", error); + }); + + const erc20Permit: Permit = { + tokenType: "erc20", + tokenAddress: permitTransferFromData.permitted.token, + beneficiary: permitTransferFromData.spender, + nonce: permitTransferFromData.nonce.toString(), + deadline: permitTransferFromData.deadline.toString(), + amount: permitTransferFromData.permitted.amount.toString(), owner: adminWallet.address, signature: signature, networkId: evmNetworkId, - } as PermitTransactionData; + }; - logger.info("Generated ERC20 permit2 signature", transactionData); + logger.info("Generated ERC20 permit2 signature", erc20Permit); - return transactionData; + return erc20Permit; } diff --git a/src/handlers/generate-erc721-permit.ts b/src/handlers/generate-erc721-permit.ts index 7369784..7378ad8 100644 --- a/src/handlers/generate-erc721-permit.ts +++ b/src/handlers/generate-erc721-permit.ts @@ -1,9 +1,18 @@ import { getPayoutConfigByNetworkId } from "../utils/payoutConfigByNetworkId"; -import { BigNumber, ethers, utils } from "ethers"; +import { ethers } from "ethers"; import { MaxUint256 } from "@uniswap/permit2-sdk"; -import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; -import { Erc721PermitSignatureData, PermitTransactionData } from "../types/permits"; +import { keccak256, toUtf8Bytes } from "ethers"; +import { Permit } from "../types/permits"; import { Context } from "../types/context"; +import { isIssueEvent } from "../types/typeguards"; + +interface Erc721PermitSignatureData { + beneficiary: string; + deadline: bigint; + keys: string[]; + nonce: bigint; + values: string[]; +} const SIGNING_DOMAIN_NAME = "NftReward-Domain"; const SIGNING_DOMAIN_VERSION = "1"; @@ -18,17 +27,8 @@ const types = { ], }; -const keys = ["GITHUB_ORGANIZATION_NAME", "GITHUB_REPOSITORY_NAME", "GITHUB_ISSUE_ID", "GITHUB_USERNAME", "GITHUB_CONTRIBUTION_TYPE"]; - -export async function generateErc721PermitSignature( - context: Context, - issueId: number, - contributionType: string, - username: string -): Promise { - const NFT_MINTER_PRIVATE_KEY = process.env.NFT_MINTER_PRIVATE_KEY; - const NFT_CONTRACT_ADDRESS = process.env.NFT_CONTRACT_ADDRESS; - +export async function generateErc721PermitSignature(context: Context, username: string, contributionType: string): Promise { + const { NFT_MINTER_PRIVATE_KEY, NFT_CONTRACT_ADDRESS } = context.env; const { evmNetworkId } = context.config; const adapters = context.adapters; const logger = context.logger; @@ -39,14 +39,6 @@ export async function generateErc721PermitSignature( logger.error("RPC is not defined"); throw new Error("RPC is not defined"); } - if (!NFT_MINTER_PRIVATE_KEY) { - logger.error("NFT minter private key is not defined"); - throw new Error("NFT minter private key is not defined"); - } - if (!NFT_CONTRACT_ADDRESS) { - logger.error("NFT contract address is not defined"); - throw new Error("NFT contract address is not defined"); - } const beneficiary = await adapters.supabase.wallet.getWalletByUsername(username); if (!beneficiary) { @@ -58,12 +50,15 @@ export async function generateErc721PermitSignature( const organizationName = context.payload.repository.owner.login; const repositoryName = context.payload.repository.name; - const issueNumber = issueId.toString(); + let issueId = ""; + if (isIssueEvent(context)) { + issueId = context.payload.issue.id.toString(); + } let provider; let adminWallet; try { - provider = new ethers.providers.JsonRpcProvider(rpc); + provider = new ethers.JsonRpcProvider(rpc); } catch (error) { logger.error("Failed to instantiate provider", error); throw new Error("Failed to instantiate provider"); @@ -76,64 +71,53 @@ export async function generateErc721PermitSignature( throw new Error("Failed to instantiate wallet"); } + const erc721Metadata = { + GITHUB_ORGANIZATION_NAME: organizationName, + GITHUB_REPOSITORY_NAME: repositoryName, + GITHUB_ISSUE_ID: issueId, + GITHUB_USERNAME: username, + GITHUB_CONTRIBUTION_TYPE: contributionType, + }; + + const metadata = Object.entries(erc721Metadata); const erc721SignatureData: Erc721PermitSignatureData = { beneficiary: beneficiary, - deadline: MaxUint256, - keys: keys.map((key) => utils.keccak256(utils.toUtf8Bytes(key))), - nonce: BigNumber.from(keccak256(toUtf8Bytes(`${userId}-${issueId}`))), - values: [organizationName, repositoryName, issueNumber, username, contributionType], + deadline: MaxUint256.toBigInt(), + keys: metadata.map(([key]) => keccak256(toUtf8Bytes(key))), + nonce: BigInt(keccak256(toUtf8Bytes(`${userId}-${issueId}`))), + values: metadata.map(([, value]) => value), }; - const signature = await adminWallet - ._signTypedData( - { - name: SIGNING_DOMAIN_NAME, - version: SIGNING_DOMAIN_VERSION, - verifyingContract: NFT_CONTRACT_ADDRESS, - chainId: evmNetworkId, - }, - types, - erc721SignatureData - ) - .catch((error: unknown) => { - logger.error("Failed to sign typed data", error); - throw new Error("Failed to sign typed data"); - }); - - const nftMetadata = {} as Record; - - keys.forEach((element, index) => { - nftMetadata[element] = erc721SignatureData.values[index]; + const domain = { + name: SIGNING_DOMAIN_NAME, + version: SIGNING_DOMAIN_VERSION, + verifyingContract: NFT_CONTRACT_ADDRESS, + chainId: evmNetworkId, + }; + + const signature = await adminWallet.signTypedData(domain, types, erc721SignatureData).catch((error: unknown) => { + logger.error("Failed to sign typed data", error); + throw new Error("Failed to sign typed data"); }); - const erc721Data: PermitTransactionData = { - type: "erc721-permit", - permit: { - permitted: { - token: NFT_CONTRACT_ADDRESS, - amount: "1", - }, - nonce: erc721SignatureData.nonce.toString(), - deadline: erc721SignatureData.deadline.toString(), - }, - transferDetails: { - to: beneficiary, - requestedAmount: "1", - }, - owner: adminWallet.address, + const erc721Permit: Permit = { + tokenType: "erc721", + tokenAddress: NFT_CONTRACT_ADDRESS, + beneficiary: beneficiary, + amount: "1", + nonce: erc721SignatureData.nonce.toString(), + deadline: erc721SignatureData.deadline.toString(), signature: signature, + owner: adminWallet.address, networkId: evmNetworkId, - nftMetadata: nftMetadata as PermitTransactionData["nftMetadata"], - request: { - beneficiary: erc721SignatureData.beneficiary, - deadline: erc721SignatureData.deadline.toString(), + erc721Request: { keys: erc721SignatureData.keys.map((key) => key.toString()), - nonce: erc721SignatureData.nonce.toString(), values: erc721SignatureData.values, + metadata: erc721Metadata, }, }; - console.info("Generated ERC721 permit signature", { erc721Data }); + console.info("Generated ERC721 permit signature", { erc721Permit }); - return erc721Data; + return erc721Permit; } diff --git a/src/handlers/generate-payout-permit.ts b/src/handlers/generate-payout-permit.ts index ad52d85..b763be3 100644 --- a/src/handlers/generate-payout-permit.ts +++ b/src/handlers/generate-payout-permit.ts @@ -1,123 +1,35 @@ -import { PermitTransactionData } from "../types/permits"; +import { Permit } from "../types/permits"; import { Context } from "../types/context"; import { generateErc20PermitSignature } from "./generate-erc20-permit"; import { generateErc721PermitSignature } from "./generate-erc721-permit"; -import { getLabelsFromLinkedIssue, getPriceFromLabels, getWalletRecord, handleNoWalletFound, unpackInputs } from "../utils/helpers"; +import { PermitRequest } from "../types/plugin-input"; /** * Generates a payout permit based on the provided context. * @param context - The context object containing the configuration and payload. * @returns A Promise that resolves to the generated permit transaction data or an error message. */ -export async function generatePayoutPermit(context: Context): Promise { - const { isNftRewardEnabled } = context.config; - const logger = context.logger; - const eventName = context.eventName; - - if (eventName == "pull_request.closed") { - const payload = context.payload as Context<"pull_request.closed">["payload"]; - return await generatePayoutForPullRequest(context, payload, isNftRewardEnabled); - } else if (eventName == "workflow_dispatch") { - return await generatePayoutForWorkflowDispatch(context, isNftRewardEnabled); - } else { - logger.error("Invalid payload"); - return "Permit not generated: invalid payload"; - } -} - -/** - * Generates a payout permit from a workflow dispatch. - * @notice All inputs must be passed in from the previous plugin/kernel. - */ -export async function generatePayoutForWorkflowDispatch(context: Context, isNftRewardEnabled: boolean): Promise { - const inputs = unpackInputs(context); - const logger = context.logger; - - let permit: PermitTransactionData | string; - - if (inputs.erc20) { - if (!inputs.erc20.token || !inputs.erc20.amount || !inputs.erc20.spender || !inputs.erc20.networkId) { - logger.error("No token, amount, spender, or networkId found for ERC20 permit"); - return "Permit not generated: no token, amount, spender, or networkId found for ERC20 permit"; +export async function generatePayoutPermit(context: Context, permitRequests: PermitRequest[]): Promise { + const permits: Permit[] = []; + + for (const permitRequest of permitRequests) { + const { type, amount, username, contributionType } = permitRequest; + + let permit: Permit; + switch (type) { + case "erc20": + permit = await generateErc20PermitSignature(context, username, amount); + break; + case "erc721": + permit = await generateErc721PermitSignature(context, username, contributionType); + break; + default: + context.logger.error(`Invalid permit type: ${type}`); + continue; } - permit = await generateErc20PermitSignature(context, inputs.erc20.spender, inputs.erc20.amount); - } else if (inputs.erc721 && isNftRewardEnabled) { - if (!inputs.erc721.username || !inputs.erc721.issueID || !inputs.erc721.contribution_type) { - logger.error("No username or issueID found for ERC721 permit"); - return "Permit not generated: no username or issueID found for ERC721 permit"; - } - - permit = await generateErc721PermitSignature(context, inputs.erc721.issueID, inputs.erc721.contribution_type, inputs.erc721.username); - } else { - logger.error("No config found for permit generation"); - return "Permit not generated: no config found for permit generation"; + permits.push(permit); } - if (typeof permit === "string") { - logger.error(permit); - return CHECK_LOGS_MESSAGE; - } else { - return permit; - } + return permits; } - -export async function generatePayoutForPullRequest( - context: Context, - payload: Context<"pull_request.closed">["payload"], - isNftRewardEnabled: boolean -): Promise { - const issue = payload.pull_request; - if (!issue.merged) { - return "Permit not generated: PR not merged\n\n ###### If this was an error tag your reviewer to process a manual permit via /permit )"; - } - - const spenderId = issue.user.id; - const walletRecord = await getWalletRecord(context, spenderId, issue.user.login); - - if (!walletRecord) { - await handleNoWalletFound(context, issue.number, issue.user.login); - return "Permit not generated: no wallet found"; - } else { - await generatePermit(context, walletRecord, isNftRewardEnabled, payload); - } - return CHECK_LOGS_MESSAGE; -} - -export async function generatePermit( - context: Context, - walletRecord: `0x${string}`, - isNftRewardEnabled: boolean, - payload: Context<"pull_request.closed">["payload"] -): Promise { - const logger = context.logger; - logger.info("Wallet found for user", { walletRecord }); - let permit: PermitTransactionData | string = ""; - - const labels = await getLabelsFromLinkedIssue(context, payload.pull_request.number); - const payoutAmount = getPriceFromLabels(labels); - - if (!payoutAmount) { - logger.error("No payout amount found on issue"); - return "Permit not generated: no payout amount found on issue"; - } - - if (isNftRewardEnabled) { - if (payoutAmount.toNumber() > 1) { - permit = await generateErc20PermitSignature(context, walletRecord, payoutAmount.toNumber()); - } else { - // permit = await generateErc721PermitSignature(context, walletRecord, "pull_request", payload.pull_request.number); - } - } else { - permit = await generateErc20PermitSignature(context, walletRecord, payoutAmount.toNumber()); - } - - if (typeof permit === "string") { - logger.error(permit); - return CHECK_LOGS_MESSAGE; - } - - return permit; -} - -const CHECK_LOGS_MESSAGE = "Permit not generated: check logs for more information"; diff --git a/src/index.ts b/src/index.ts index 14eda96..e442165 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,39 @@ import * as core from "@actions/core"; import * as github from "@actions/github"; import { Octokit } from "@octokit/rest"; -import { PluginInputs } from "./types/plugin-input"; +import { PluginInputs, permitGenerationSettingsSchema } from "./types/plugin-input"; import { Context } from "./types/context"; -import { generateErc20PermitSignature } from "./handlers/generate-erc20-permit"; import { createClient } from "@supabase/supabase-js"; import { createAdapters } from "./adapters"; import { Database } from "./adapters/supabase/types/database"; import { registerWallet } from "./handlers/register-wallet"; import { generatePayoutPermit } from "./handlers/generate-payout-permit"; -import { generateErc721PermitSignature } from "./handlers/generate-erc721-permit"; -import { PermitTransactionData } from "./types/permits"; - -const SUPABASE_URL = process.env.SUPABASE_URL; -const SUPABASE_KEY = process.env.SUPABASE_KEY; +import { Value } from "@sinclair/typebox/value"; +import { envSchema } from "./types/env"; async function run() { const webhookPayload = github.context.payload.inputs; + + const env = Value.Decode(envSchema, process.env); + const settings = Value.Decode(permitGenerationSettingsSchema, JSON.parse(webhookPayload.settings)); + const inputs: PluginInputs = { stateId: webhookPayload.stateId, eventName: webhookPayload.eventName, eventPayload: JSON.parse(webhookPayload.eventPayload), - settings: JSON.parse(webhookPayload.settings), + settings: settings, authToken: webhookPayload.authToken, ref: webhookPayload.ref, }; const octokit = new Octokit({ auth: inputs.authToken }); - - if (!SUPABASE_URL || !SUPABASE_KEY) { - throw new Error("SUPABASE_URL and SUPABASE_KEY must be provided"); - } - - const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY); + const supabaseClient = createClient(env.SUPABASE_URL, env.SUPABASE_KEY); const context: Context = { eventName: inputs.eventName, payload: inputs.eventPayload, config: inputs.settings, octokit, + env, logger: { debug(message: unknown, ...optionalParams: unknown[]) { console.debug(message, ...optionalParams); @@ -60,18 +56,12 @@ async function run() { context.adapters = createAdapters(supabaseClient, context); - if (context.eventName === "workflow_dispatch" || context.eventName === "pull_request.closed") { - const permit = await generatePayoutPermit(context); - - if (permit) { - return JSON.stringify(permit); - } else { - return "No permit generated"; - } - } else if (context.eventName === "issue_comment.created") { + if (context.eventName === "issue_comment.created") { await handleSlashCommands(context, octokit); } else { - context.logger.error(`Unsupported event: ${context.eventName}`); + const permits = await generatePayoutPermit(context, settings.permitRequests); + // TODO: return permits to kernel + return JSON.stringify(permits); } return "No permit generated"; @@ -95,44 +85,7 @@ async function handleSlashCommands(context: Context, octokit: Octokit) { body: `Failed to register wallet: ${address}`, }); } - } else { - await handlePermitSlashCommand(context, payload); - } -} - -async function handlePermitSlashCommand(context: Context, payload: Context<"issue_comment.created">["payload"]) { - const body = payload.comment.body; - - // `/permit ` || `/permit ` - const permitSlashCommand = /^\/permit\\s+((0x[a-fA-F0-9]{40})|([a-zA-Z0-9]{4,})|([a-zA-Z0-9]{3,}\\.eth))\\s+([a-zA-Z0-9]+|\\d+)$/g; - - const permitMatches = [...body.matchAll(permitSlashCommand)]; - let permit: PermitTransactionData | string | null = null; - - if (permitMatches.length > 0) { - const walletOrNftAddress = permitMatches[0][1] as `0x${string}`; - const tokenAmountOrUsername = permitMatches[0][5]; - - if (tokenAmountOrUsername === undefined || tokenAmountOrUsername === "") { - context.logger.error("tokenOrAmount is undefined or empty"); - } else { - const parsedNumber = parseFloat(tokenAmountOrUsername); - if (!isNaN(parsedNumber) && parsedNumber.toString() === tokenAmountOrUsername) { - permit = await generateErc20PermitSignature(context, walletOrNftAddress, parsedNumber); - } else { - const contributionType = "pull_request"; // TODO: must be a better way to determine this, probably with inputs - permit = await generateErc721PermitSignature(context, payload.issue.number, contributionType, tokenAmountOrUsername); - } - } - } else { - context.logger.error("No matches found for permit command"); } - - if (typeof permit === "string" || permit === null) { - throw new Error(permit || "Permit not generated"); - } - - return permit; } run() diff --git a/src/types/botConfig.ts b/src/types/botConfig.ts deleted file mode 100644 index 6c03dfe..0000000 --- a/src/types/botConfig.ts +++ /dev/null @@ -1,56 +0,0 @@ -export type BotConfig = { - keys: { - evmPrivateEncrypted?: string; - openAi?: string; - }; - features: { - assistivePricing: boolean; - defaultLabels: string[]; - newContributorGreeting: { - enabled: boolean; - header: string; - displayHelpMenu: boolean; - footer: string; - }; - publicAccessControl: { - setLabel: boolean; - fundExternalClosedIssue: boolean; - }; - isNftRewardEnabled: boolean; - }; - timers: { - reviewDelayTolerance: number; - taskStaleTimeoutDuration: number; - taskFollowUpDuration: number; - taskDisqualifyDuration: number; - }; - payments: { - maxPermitPrice: number; - evmNetworkId: number; - basePriceMultiplier: number; - issueCreatorMultiplier: number; - }; - disabledCommands: string[]; - incentives: { - comment: { - elements: Record; - totals: { - character: number; - word: number; - sentence: number; - paragraph: number; - comment: number; - }; - }; - }; - labels: { - time: string[]; - priority: string[]; - }; - miscellaneous: { - maxConcurrentTasks: number; - promotionComment: string; - registerWalletWithVerification: boolean; - openAiTokenLimit: number; - }; -}; diff --git a/src/types/context.ts b/src/types/context.ts index 5555412..4e9a5dc 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -2,6 +2,7 @@ import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as Webhook import { Octokit } from "@octokit/rest"; import { PermitGenerationSettings } from "./plugin-input"; import { createAdapters } from "../adapters"; +import { Env } from "./env"; export type SupportedEvents = "issue_comment.created" | "workflow_dispatch" | "pull_request.closed"; @@ -11,6 +12,7 @@ export interface Context { octokit: InstanceType; adapters: ReturnType; config: PermitGenerationSettings; + env: Env; logger: { fatal: (message: unknown, ...optionalParams: unknown[]) => void; error: (message: unknown, ...optionalParams: unknown[]) => void; diff --git a/src/types/env.ts b/src/types/env.ts new file mode 100644 index 0000000..8c01ac2 --- /dev/null +++ b/src/types/env.ts @@ -0,0 +1,12 @@ +import { Type as T } from "@sinclair/typebox"; +import { StaticDecode } from "@sinclair/typebox"; +import "dotenv/config"; + +export const envSchema = T.Object({ + SUPABASE_URL: T.String(), + SUPABASE_KEY: T.String(), + NFT_MINTER_PRIVATE_KEY: T.String(), + NFT_CONTRACT_ADDRESS: T.String(), +}); + +export type Env = StaticDecode; diff --git a/src/types/permits.ts b/src/types/permits.ts index 5fbb54d..cbc6d1c 100644 --- a/src/types/permits.ts +++ b/src/types/permits.ts @@ -1,65 +1,24 @@ -import { BigNumber } from "ethers"; +type TokenType = "erc20" | "erc721"; -export interface Erc721PermitSignatureData { +export interface Permit { + tokenType: TokenType; + tokenAddress: string; beneficiary: string; - deadline: BigNumber; - keys: string[]; - nonce: BigNumber; - values: string[]; -} - -export interface PermitTransactionData extends Erc20PermitTransactionData, Erc721PermitTransactionData {} - -type PermitType = "erc20-permit" | "erc721-permit"; - -interface Erc20PermitTransactionData { - type: PermitType; - permit: { - permitted: { - token: string; - amount: string; - }; - nonce: string; - deadline: string; - }; - transferDetails: { - to: string; - requestedAmount: string; - }; + amount: string; + nonce: string; + deadline: string; owner: string; signature: string; networkId: number; -} - -interface Erc721PermitTransactionData { - type: PermitType; - permit: { - permitted: { - token: string; - amount: string; - }; - nonce: string; - deadline: string; - }; - transferDetails: { - to: string; - requestedAmount: string; - }; - owner: string; - signature: string; - networkId: number; - nftMetadata: { - GITHUB_ORGANIZATION_NAME: string; - GITHUB_REPOSITORY_NAME: string; - GITHUB_ISSUE_ID: string; - GITHUB_USERNAME: string; - GITHUB_CONTRIBUTION_TYPE: string; - }; - request: { - beneficiary: string; - deadline: string; + erc721Request?: { keys: string[]; - nonce: string; values: string[]; + metadata: { + GITHUB_ORGANIZATION_NAME: string; + GITHUB_REPOSITORY_NAME: string; + GITHUB_ISSUE_ID: string; + GITHUB_USERNAME: string; + GITHUB_CONTRIBUTION_TYPE: string; + }; }; } diff --git a/src/types/plugin-input.ts b/src/types/plugin-input.ts index 300165b..60c5b52 100644 --- a/src/types/plugin-input.ts +++ b/src/types/plugin-input.ts @@ -1,5 +1,6 @@ import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks"; import { SupportedEvents } from "./context"; +import { StaticDecode, Type as T } from "@sinclair/typebox"; export interface PluginInputs { stateId: string; @@ -10,19 +11,19 @@ export interface PluginInputs { ref: string; } -export interface PermitGenerationSettings { - evmNetworkId: number; - evmPrivateEncrypted: string; - isNftRewardEnabled: boolean; +export const permitRequestSchema = T.Object({ + type: T.Union([T.Literal("erc20"), T.Literal("erc721")]), + username: T.String(), + amount: T.Number(), + contributionType: T.String(), +}); - // possible inputs from workflow_dispatch - token?: `0x${string}`; - amount?: number; - spender?: `0x${string}`; - userId?: number; +export type PermitRequest = StaticDecode; - // nft specific inputs - contribution_type?: string; - username?: string; - issueID?: number; -} +export const permitGenerationSettingsSchema = T.Object({ + evmNetworkId: T.Number(), + evmPrivateEncrypted: T.String(), + permitRequests: T.Array(permitRequestSchema), +}); + +export type PermitGenerationSettings = StaticDecode; diff --git a/src/types/typeguards.ts b/src/types/typeguards.ts index 0a71135..4279166 100644 --- a/src/types/typeguards.ts +++ b/src/types/typeguards.ts @@ -1,6 +1,6 @@ import { RestEndpointMethodTypes } from "@octokit/rest"; import { Context } from "./context"; -export function isIssueEvent(context: Context): context is Context & { issue: RestEndpointMethodTypes["issues"]["list"]["response"]["data"][0] } { +export function isIssueEvent(context: Context): context is Context & { payload: { issue: RestEndpointMethodTypes["issues"]["list"]["response"]["data"][0] } } { return context.eventName.startsWith("issues."); } diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 582d3ff..4d3788f 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -45,32 +45,6 @@ export function getPriceFromLabels(labels: string[] | null): Decimal | null { return new Decimal(payoutLabel.replace("Price:", "").trim()); } -export function unpackInputs(context: Context): { - erc721: { userId?: number; username?: string; issueID: number; contribution_type: string } | null; - erc20: { token: `0x${string}`; amount: number; spender: `0x${string}`; networkId: number } | null; -} { - const { userId, token, amount, spender, evmNetworkId: networkId, contribution_type, username, issueID } = context.config; - - let erc721 = null; - let erc20 = null; - - if (contribution_type && username && issueID) { - erc721 = { - username, - userId, - issueID, - contribution_type, - }; - } else if (token && amount && spender && networkId) { - erc20 = { token, amount, spender, networkId }; - } - - return { - erc721, - erc20, - }; -} - export async function getLabelsFromLinkedIssue(context: Context, pullRequestNumber: number): Promise { const { octokit, logger } = context; const { owner, name } = context.payload.repository; diff --git a/src/utils/payoutConfigByNetworkId.ts b/src/utils/payoutConfigByNetworkId.ts index 74081b3..5aa96a2 100644 --- a/src/utils/payoutConfigByNetworkId.ts +++ b/src/utils/payoutConfigByNetworkId.ts @@ -1,16 +1,19 @@ // available tokens for payouts -export const PAYMENT_TOKEN_PER_NETWORK: Record = { +export const PAYMENT_TOKEN_PER_NETWORK: Record = { "1": { rpc: "https://rpc.mevblocker.io", token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI + decimals: 18, }, "100": { rpc: "https://rpc.gnosis.gateway.fm", token: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", // WXDAI + decimals: 18, }, "31337": { rpc: "http://localhost:8545", token: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d", // WXDAI + decimals: 18, }, }; @@ -20,8 +23,5 @@ export function getPayoutConfigByNetworkId(evmNetworkId: number) { throw new Error(`No config setup for evmNetworkId: ${evmNetworkId}`); } - return { - rpc: paymentToken.rpc, - paymentToken: paymentToken.token, - }; + return paymentToken; }