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..6281552 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: yarn i --immutable --immutable-cache --check-cache - name: Generate Permit run: npx tsx ./src/index.ts diff --git a/.github/workflows/jest-testing.yml b/.github/workflows/jest-testing.yml index 9f0a9c4..e86b3bc 100644 --- a/.github/workflows/jest-testing.yml +++ b/.github/workflows/jest-testing.yml @@ -20,9 +20,8 @@ jobs: fetch-depth: 0 - name: Build & Run test suite run: | - npm i -g bun - bun install - bun test | tee ./coverage.txt && exit ${PIPESTATUS[0]} + yarn install --immutable --immutable-cache --check-cache + yarn test | tee ./coverage.txt && exit ${PIPESTATUS[0]} - name: Jest Coverage Comment # Ensures this step is run even on previous step failure (e.g. test failed) if: always() 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..3c9318d 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..bddee04 100644 --- a/src/handlers/generate-erc20-permit.ts +++ b/src/handlers/generate-erc20-permit.ts @@ -1,55 +1,48 @@ -import { MaxUint256, PERMIT2_ADDRESS, PermitTransferFrom, SignatureTransfer } from "@uniswap/permit2-sdk"; -import { BigNumber, ethers } from "ethers"; -import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; -import { getPayoutConfigByNetworkId } from "../utils/payoutConfigByNetworkId"; -import { decryptKeys } from "../utils/keys"; -import { PermitTransactionData } from "../types/permits"; +import { PERMIT2_ADDRESS, PermitTransferFrom, SignatureTransfer } from "@uniswap/permit2-sdk"; +import { ethers, keccak256, MaxInt256, parseUnits, toUtf8Bytes } from "ethers"; import { Context } from "../types/context"; +import { Permit } from "../types/permits"; +import { decryptKeys } from "../utils/keys"; +import { getPayoutConfigByNetworkId } from "../utils/payoutConfigByNetworkId"; -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); - let issueId: number | null = null; - + const userId = await user.getUserIdByUsername(username); + const walletAddress = await wallet.getWalletByUserId(userId); + let issueId: string; if ("issue" in context.payload) { - issueId = context.payload.issue.number; + issueId = context.payload.issue.id.toString(); } else if ("pull_request" in context.payload) { - issueId = context.payload.pull_request.number; - } - - if (!beneficiary) { - logger.error("No beneficiary found for permit"); - return "Permit not generated: No beneficiary found for permit"; + issueId = context.payload.pull_request.id.toString(); + } else { + throw new Error("Issue Id is missing"); } if (!userId) { - logger.error("No wallet found for user"); - return "Permit not generated: no wallet found for user"; + throw new Error("User was not found"); + } + if (!walletAddress) { + const errorMessage = "ERC20 Permit generation error: Wallet not found"; + logger.error(errorMessage); + throw new Error(errorMessage); } - 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"); + if (!privateKey) { + const errorMessage = "Private key is not defined"; + logger.fatal(errorMessage); + throw new Error(errorMessage); + } 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 +55,47 @@ 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}`))), - deadline: MaxUint256, + spender: walletAddress, + nonce: BigInt(keccak256(toUtf8Bytes(`${userId}-${issueId}`))), + deadline: MaxInt256, }; 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) => { + const errorMessage = `Failed to sign typed data ${error}`; + logger.error(errorMessage); + throw new Error(errorMessage); + }); + + 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..4413098 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,13 +39,11 @@ 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 errorMesage = "NFT contract address is not defined"; + logger.error(errorMesage); + throw new Error(errorMesage); } const beneficiary = await adapters.supabase.wallet.getWalletByUsername(username); @@ -58,12 +56,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 +77,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..08ede78 100644 --- a/src/handlers/generate-payout-permit.ts +++ b/src/handlers/generate-payout-permit.ts @@ -1,123 +1,36 @@ -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. + * @param permitRequests * @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/handlers/register-wallet.ts b/src/handlers/register-wallet.ts index a434a3a..915d5fd 100644 --- a/src/handlers/register-wallet.ts +++ b/src/handlers/register-wallet.ts @@ -1,4 +1,4 @@ -import { constants, ethers } from "ethers"; +import { ethers, ZeroAddress } from "ethers"; import { Context } from "../types/context"; export async function registerWallet(context: Context, address: string | null) { @@ -26,7 +26,7 @@ export async function registerWallet(context: Context, address: string | null) { return false; } - if (address == constants.AddressZero) { + if (address === ZeroAddress) { logger.error("Skipping to register a wallet address because user is trying to set their address to null address"); return false; } @@ -58,7 +58,7 @@ export async function registerWallet(context: Context, address: string | null) { export async function resolveAddress(ensName: string): Promise { // Gets the Ethereum address associated with an ENS (Ethereum Name Service) name // Explicitly set provider to Ethereum mainnet - const provider = new ethers.providers.JsonRpcProvider(`https://eth.llamarpc.com`); // mainnet required for ENS + const provider = new ethers.JsonRpcProvider(`https://eth.llamarpc.com`); // mainnet required for ENS return await provider.resolveName(ensName).catch((err) => { console.trace({ err }); return null; 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..6a16c90 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -2,8 +2,9 @@ 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"; +export type SupportedEvents = "issue_comment.created" | "workflow_dispatch" | "pull_request.closed" | "issues.closed"; export interface Context { eventName: T; @@ -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..f7bc151 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..8668f98 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/keys.ts b/src/utils/keys.ts index e32a3a2..16ae0e4 100644 --- a/src/utils/keys.ts +++ b/src/utils/keys.ts @@ -18,6 +18,10 @@ export async function decryptKeys(cipherText: string): Promise<{ privateKey: str console.warn("Public key is null"); return { privateKey: null, publicKey: null }; } + if (!cipherText?.length) { + console.warn("No cipherText was provided"); + return { privateKey: null, publicKey: null }; + } const binaryPublic = sodium.from_base64(_public, sodium.base64_variants.URLSAFE_NO_PADDING); const binaryPrivate = sodium.from_base64(X25519_PRIVATE_KEY, sodium.base64_variants.URLSAFE_NO_PADDING); 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; } diff --git a/tests/constants.ts b/tests/constants.ts index a0da9cf..b08ee93 100644 --- a/tests/constants.ts +++ b/tests/constants.ts @@ -1,7 +1,9 @@ import { Context } from "../src/types/context"; export const NFT_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000003"; -export const SPENDER = "0x0000000000000000000000000000000000000001"; +export const SPENDER = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"; + +export const WALLET_ADDRESS = "0xefC0e701A824943b469a694aC564Aa1efF7Ab7dd"; // cSpell: disable export const cypherText = @@ -18,6 +20,7 @@ export const mockContext = { }, issue: { number: 123, + id: 123, }, pull_request: { number: 123, @@ -40,13 +43,13 @@ export const mockContext = { deleteUser: jest.fn(), upsertUser: jest.fn(), getUserIdByWallet: jest.fn().mockReturnValue(123), - getUserIdByUsername: jest.fn(), + getUserIdByUsername: jest.fn().mockReturnValue(1), getUsernameById: jest.fn(), }, wallet: { upsertWallet: jest.fn().mockImplementation(() => Promise.resolve()), - getWalletByUserId: jest.fn(), - getWalletByUsername: jest.fn(), + getWalletByUserId: jest.fn().mockReturnValue(WALLET_ADDRESS), + getWalletByUsername: jest.fn().mockReturnValue(WALLET_ADDRESS), }, }, }, diff --git a/tests/generate-erc20-permit.test.ts b/tests/generate-erc20-permit.test.ts index 1c979bb..cb20546 100644 --- a/tests/generate-erc20-permit.test.ts +++ b/tests/generate-erc20-permit.test.ts @@ -38,7 +38,7 @@ describe("generateErc20PermitSignature", () => { beforeEach(() => { /** * 5. **Update GitHub Secrets** - - Copy the newly generated private key and update it on your GitHub Actions secret. + - Copy the newly generated private key and update it on your GitHub Actions secret. Find the field labeled `x25519_PRIVATE_KEY` and replace its content with your generated x25519 private key. */ // cSpell: ignore bHH4PDnwb2bsG9nmIu1KeIIX71twQHS-23wCPfKONls @@ -47,7 +47,7 @@ describe("generateErc20PermitSignature", () => { context = { ...mockContext, config: { - evmNetworkId: 1, + evmNetworkId: 100, }, } as unknown as Context; }); @@ -65,22 +65,23 @@ describe("generateErc20PermitSignature", () => { expect(context.logger.info).toHaveBeenCalledWith("Generated ERC20 permit2 signature", expect.any(Object)); }); - it("should return error message when no wallet found for user", async () => { - const amount = 100; - context.config.evmPrivateEncrypted = cypherText; + it("should throw error when evmPrivateEncrypted is not defined", async () => { + const amount = 0; - (context.adapters.supabase.user.getUserIdByWallet as jest.Mock).mockReturnValue(null); + await expect(generateErc20PermitSignature(context, SPENDER, amount)).rejects.toThrow("Private key is not defined"); + expect(context.logger.fatal).toHaveBeenCalledWith("Private key is not defined"); + }); - const result = await generateErc20PermitSignature(context, SPENDER, amount); + it("should return error message when no wallet found for user", async () => { + const amount = 0; + context.config.evmPrivateEncrypted = cypherText; - expect(result).toBe("Permit not generated: no wallet found for user"); - expect(context.logger.error).toHaveBeenCalledWith("No wallet found for user"); - }); + (context.adapters.supabase.wallet.getWalletByUserId as jest.Mock).mockReturnValue(null); - it("should throw error when evmPrivateEncrypted is not defined", async () => { - const amount = 100; + await expect(async () => { + await generateErc20PermitSignature(context, SPENDER, amount); + }).rejects.toThrow(); - await expect(generateErc20PermitSignature(context, SPENDER, amount)).rejects.toThrow("EVM configuration is not defined"); - expect(context.logger.fatal).toHaveBeenCalledWith("EVM configuration is not defined"); + expect(context.logger.error).toHaveBeenCalledWith("ERC20 Permit generation error: Wallet not found"); }); }); diff --git a/tests/generate-erc721-permit.test.ts b/tests/generate-erc721-permit.test.ts index 7ab6c59..f834417 100644 --- a/tests/generate-erc721-permit.test.ts +++ b/tests/generate-erc721-permit.test.ts @@ -1,33 +1,18 @@ -import { BigNumber, utils } from "ethers"; +import { MaxUint256 } from "@uniswap/permit2-sdk"; +import { keccak256, toUtf8Bytes } from "ethers"; import { generateErc721PermitSignature } from "../src/handlers/generate-erc721-permit"; import { Context } from "../src/types/context"; -import { NFT_CONTRACT_ADDRESS, SPENDER, cypherText, mockContext } from "./constants"; +import { Env } from "../src/types/env"; +import { cypherText, mockContext, NFT_CONTRACT_ADDRESS, SPENDER } from "./constants"; describe("generateErc721PermitSignature", () => { let context: Context; + const userId = 123; // cSpell: disable jest.autoMockOn(); - jest.mock("@supabase/supabase-js", () => { - return { - createClient: jest.fn().mockReturnValue({ - from: jest.fn().mockReturnValue({ - select: jest.fn().mockReturnValue({ - eq: jest.fn().mockReturnValue({ - select: jest.fn().mockReturnValue({ - eq: jest.fn().mockReturnValue({ - single: jest.fn().mockResolvedValue({ id: 123 }), - }), - }), - }), - }), - }), - }), - }; - }); - beforeEach(() => { process.env.X25519_PRIVATE_KEY = "bHH4PDnwb2bsG9nmIu1KeIIX71twQHS-23wCPfKONls"; process.env.NFT_CONTRACT_ADDRESS = NFT_CONTRACT_ADDRESS; @@ -36,7 +21,7 @@ describe("generateErc721PermitSignature", () => { context = { ...mockContext, config: { - evmNetworkId: 1, + evmNetworkId: 100, evmPrivateEncrypted: cypherText, isNftRewardEnabled: true, nftMinterPrivateKey: process.env.NFT_MINTER_PRIVATE_KEY, @@ -54,6 +39,27 @@ describe("generateErc721PermitSignature", () => { issueID: 123, }, } as unknown as Context; + context.env = process.env as Env; + context.eventName = "issues.closed"; + jest.mock("@supabase/supabase-js", () => { + return { + createClient: jest.fn().mockReturnValue({ + from: jest.fn().mockReturnValue({ + select: jest.fn().mockReturnValue({ + eq: jest.fn().mockReturnValue({ + select: jest.fn().mockReturnValue({ + eq: jest.fn().mockReturnValue({ + single: jest.fn().mockResolvedValue({ id: 123 }), + }), + }), + }), + }), + }), + }), + }; + }); + (context.adapters.supabase.wallet.getWalletByUsername as jest.Mock).mockReturnValue(SPENDER); + (context.adapters.supabase.user.getUserIdByWallet as jest.Mock).mockReturnValue(userId); }); afterEach(() => { @@ -65,30 +71,26 @@ describe("generateErc721PermitSignature", () => { const contributionType = "contribution"; const username = "tester"; - (context.adapters.supabase.wallet.getWalletByUsername as jest.Mock).mockReturnValue(SPENDER); - (context.adapters.supabase.user.getUserIdByWallet as jest.Mock).mockReturnValue(123); - - const result = await generateErc721PermitSignature(context, issueId, contributionType, username); + const result = await generateErc721PermitSignature(context, username, contributionType); const organizationName = "test"; const repositoryName = "test"; const issueNumber = issueId.toString(); - const userId = context.config.userId; const keys = ["GITHUB_ORGANIZATION_NAME", "GITHUB_REPOSITORY_NAME", "GITHUB_ISSUE_ID", "GITHUB_USERNAME", "GITHUB_CONTRIBUTION_TYPE"]; if (result && typeof result === "object") { expect(result).toBeDefined(); - expect(result.type).toBe("erc721-permit"); - expect(result.permit.permitted.token).toBe(process.env.NFT_CONTRACT_ADDRESS); - expect(result.permit.permitted.amount).toBe("1"); - expect(result.nftMetadata).toBeDefined(); - expect(result.request.beneficiary).toBe(context.config.spender); - expect(result.request.deadline).toBe(BigNumber.from("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").toString()); - expect(result.request.nonce).toBe(BigNumber.from(utils.keccak256(utils.toUtf8Bytes(`${userId}-${issueId}`))).toString()); - expect(result.request.values).toEqual([organizationName, repositoryName, issueNumber, username, contributionType]); + expect(result.tokenType).toBe("ERC721"); + expect(result.tokenAddress).toBe(process.env.NFT_CONTRACT_ADDRESS); + expect(result.amount).toBe("1"); + expect(result.erc721Request?.metadata).toBeDefined(); + expect(result.beneficiary).toBe(SPENDER); + expect(result.deadline).toBe(MaxUint256.toString()); + expect(result.nonce).toBe(BigInt(keccak256(toUtf8Bytes(`${userId}-${issueId}`))).toString()); + expect(result.erc721Request?.values).toEqual([organizationName, repositoryName, issueNumber, username, contributionType]); expect(result.networkId).toBe(context.config.evmNetworkId); - const keysHashed = keys.map((key) => utils.keccak256(utils.toUtf8Bytes(key))); - expect(result.request.keys).toEqual(keysHashed); + const keysHashed = keys.map((key) => keccak256(toUtf8Bytes(key))); + expect(result.erc721Request?.keys).toEqual(keysHashed); } expect(context.logger.error).not.toHaveBeenCalled(); @@ -96,19 +98,19 @@ describe("generateErc721PermitSignature", () => { it("should throw an error if RPC is not defined", async () => { context.config.evmNetworkId = 123; - await expect(generateErc721PermitSignature(context, 123, "contribution", "tester")).rejects.toThrow("No config setup for evmNetworkId: 123"); + await expect(generateErc721PermitSignature(context, "tester", "contribution")).rejects.toThrow("No config setup for evmNetworkId: 123"); }); it("should throw an error if NFT minter private key is not defined", async () => { delete process.env.NFT_MINTER_PRIVATE_KEY; - await expect(generateErc721PermitSignature(context, 123, "contribution", "tester")).rejects.toThrow("NFT minter private key is not defined"); - expect(context.logger.error).toHaveBeenCalledWith("NFT minter private key is not defined"); + await expect(generateErc721PermitSignature(context, "tester", "contribution")).rejects.toThrow("Failed to instantiate wallet"); + expect(context.logger.error).toHaveBeenCalled(); }); it("should throw an error if NFT contract address is not defined", async () => { delete process.env.NFT_CONTRACT_ADDRESS; - await expect(generateErc721PermitSignature(context, 123, "contribution", "tester")).rejects.toThrow("NFT contract address is not defined"); - expect(context.logger.error).toHaveBeenCalledWith("NFT contract address is not defined"); + await expect(generateErc721PermitSignature(context, "tester", "contribution")).rejects.toThrow("NFT contract address is not defined"); + expect(context.logger.error).toHaveBeenCalled(); }); it("should throw an error if no wallet found for user", async () => { @@ -119,7 +121,7 @@ describe("generateErc721PermitSignature", () => { (context.adapters.supabase.user.getUserIdByWallet as jest.Mock).mockReturnValue(null); - await expect(generateErc721PermitSignature(context, 123, "contribution", "tester")).rejects.toThrow("No wallet found for user"); + await expect(generateErc721PermitSignature(context, "tester", "contribution")).rejects.toThrow("No wallet found for user"); expect(context.logger.error).toHaveBeenCalledWith("No wallet found for user"); }); }); diff --git a/tests/generate-payout-permit.test.ts b/tests/generate-payout-permit.test.ts index b6b11e9..e5055a7 100644 --- a/tests/generate-payout-permit.test.ts +++ b/tests/generate-payout-permit.test.ts @@ -1,15 +1,15 @@ // import { generateErc20PermitSignature } from "../src/handlers/generate-erc20-permit"; +import { generateErc20PermitSignature } from "../src/handlers/generate-erc20-permit"; // import { generateErc721PermitSignature } from "../src/handlers/generate-erc721-permit"; -import { generatePayoutForWorkflowDispatch } from "../src/handlers/generate-payout-permit"; +import { generatePayoutPermit } from "../src/handlers/generate-payout-permit"; import { Context } from "../src/types/context"; -import { unpackInputs } from "../src/utils/helpers"; -import { SPENDER, cypherText, mockContext } from "./constants"; +import { cypherText, mockContext, SPENDER } from "./constants"; jest.mock("../src/utils/helpers"); jest.mock("../src/handlers/generate-erc20-permit"); jest.mock("../src/handlers/generate-erc721-permit"); -describe("generatePayoutForWorkflowDispatch", () => { +describe("generatePayoutPermit", () => { let context: Context; beforeEach(() => { @@ -39,7 +39,7 @@ describe("generatePayoutForWorkflowDispatch", () => { issueID: 123, }, } as unknown as Context; - (unpackInputs as jest.Mock).mockReturnValue({ + (generateErc20PermitSignature as jest.Mock).mockReturnValue({ erc20: { token: "TOKEN_ADDRESS", amount: 100, @@ -53,36 +53,25 @@ describe("generatePayoutForWorkflowDispatch", () => { jest.clearAllMocks(); }); - // TODO: valids - - it("should return error message when no config found for permit generation", async () => { - (unpackInputs as jest.Mock).mockReturnValue({}); - - const result = await generatePayoutForWorkflowDispatch(context, false); - - expect(result).toBe("Permit not generated: no config found for permit generation"); - expect(context.logger.error).toHaveBeenCalledWith("No config found for permit generation"); - }); - it("should return error message when no token, amount, spender, or networkId found for ERC20 permit", async () => { - (unpackInputs as jest.Mock).mockReturnValue({ - erc20: {}, - }); - - const result = await generatePayoutForWorkflowDispatch(context, false); - - expect(result).toBe("Permit not generated: no token, amount, spender, or networkId found for ERC20 permit"); - expect(context.logger.error).toHaveBeenCalledWith("No token, amount, spender, or networkId found for ERC20 permit"); - }); - - it("should return error message when no username or issueID found for ERC721 permit", async () => { - (unpackInputs as jest.Mock).mockReturnValue({ - erc721: {}, - }); - - const result = await generatePayoutForWorkflowDispatch(context, true); + const result = await generatePayoutPermit(context, [ + { + type: "ERC20", + amount: 100, + username: "username", + contributionType: "ISSUE", + }, + { + type: "ERC20", + amount: 100, + username: "username", + contributionType: "ISSUE", + }, + ]); - expect(result).toBe("Permit not generated: no username or issueID found for ERC721 permit"); - expect(context.logger.error).toHaveBeenCalledWith("No username or issueID found for ERC721 permit"); + expect(result).toMatchObject([ + { erc20: { amount: 100, networkId: 1, spender: SPENDER, token: "TOKEN_ADDRESS" } }, + { erc20: { amount: 100, networkId: 1, spender: SPENDER, token: "TOKEN_ADDRESS" } }, + ]); }); }); diff --git a/tsconfig.json b/tsconfig.json index bb2f7f6..cff607a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ diff --git a/yarn.lock b/yarn.lock index 1f83315..71b0ff0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,6 +33,11 @@ tunnel "^0.0.6" undici "^5.25.4" +"@adraffy/ens-normalize@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" + integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -1926,6 +1931,18 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2467,6 +2484,11 @@ dependencies: undici-types "~5.26.4" +"@types/node@18.15.13": + version "18.15.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" + integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== + "@types/node@^20.11.19": version "20.11.19" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" @@ -2657,6 +2679,11 @@ aes-js@3.0.0: resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -4007,7 +4034,20 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -ethers@^5.3.1, ethers@^5.7.2: +ethers@6.11.1: + version "6.11.1" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.11.1.tgz#96aae00b627c2e35f9b0a4d65c7ab658259ee6af" + integrity sha512-mxTAE6wqJQAbp5QAe/+o+rXOID7Nw91OZXvgpjDa1r4fAbq2Nu314oEZSbjoRLacuCzs7kUC3clEvkCQowffGg== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "18.15.13" + aes-js "4.0.0-beta.5" + tslib "2.4.0" + ws "8.5.0" + +ethers@^5.3.1: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -6849,7 +6889,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6924,7 +6973,14 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7134,6 +7190,11 @@ ts-node@10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^2.2.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" @@ -7501,7 +7562,16 @@ wrangler@^3.23.0: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -7556,6 +7626,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" + integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== + ws@^8.11.0, ws@^8.14.2: version "8.16.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"