diff --git a/.github/workflows/permit-generator.yml b/.github/workflows/permit-generator.yml new file mode 100644 index 0000000..d2ad565 --- /dev/null +++ b/.github/workflows/permit-generator.yml @@ -0,0 +1,46 @@ +name: Permit Generator + +on: + workflow_dispatch: + inputs: + PAYMENT_REQUESTS: + description: "A JSON array containing usernames and associated amounts" + required: true + # example: '[{"user1": 100}, {"user2": 150}]' + +jobs: + run: + runs-on: ubuntu-latest + permissions: write-all + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.10.0" + + - name: Install dependencies + run: yarn install --immutable --immutable-cache --check-cache + + - name: Process permit requests + id: parse + run: | + echo "Received input: ${{ github.event.inputs.PAYMENT_REQUESTS }}" + npx tsx scripts/github-action-permit-generator.ts permits.txt + shell: bash + env: + X25519_PRIVATE_KEY: ${{ secrets.X25519_PRIVATE_KEY }} + EVM_NETWORK_ID: ${{ secrets.EVM_NETWORK_ID }} + EVM_PRIVATE_KEY: ${{ secrets.EVM_PRIVATE_KEY }} + EVM_TOKEN_ADDRESS: ${{ secrets.EVM_TOKEN_ADDRESS }} + SUPABASE_URL: ${{ secrets.SUPABASE_URL}} + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PAYMENT_REQUESTS: ${{ github.event.inputs.PAYMENT_REQUESTS }} + + - name: Log permits data + run: | + export PERMITS=$(cat permits.txt) + echo $PERMITS diff --git a/package.json b/package.json index 6c47b59..10bd847 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "@uniswap/permit2-sdk": "^1.2.0", "dotenv": "^16.4.4", "ethers": "^5.7.2", - "libsodium-wrappers": "^0.7.13" + "libsodium-wrappers": "^0.7.13", + "uuid": "^10.0.0" }, "devDependencies": { "@commitlint/cli": "^18.6.1", @@ -48,6 +49,7 @@ "@jest/types": "29.6.3", "@types/libsodium-wrappers": "^0.7.8", "@types/node": "^20.11.19", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/parser": "^7.0.1", "cspell": "^8.4.0", diff --git a/scripts/github-action-permit-generator.ts b/scripts/github-action-permit-generator.ts new file mode 100644 index 0000000..95cfeac --- /dev/null +++ b/scripts/github-action-permit-generator.ts @@ -0,0 +1,94 @@ +import { Octokit } from "@octokit/rest"; +import { createClient } from "@supabase/supabase-js"; +import { createAdapters } from "../src/adapters"; +import { Database } from "../src/adapters/supabase/types/database"; +import { generatePayoutPermit } from "../src/handlers"; +import { Context } from "../src/types/context"; +import { PermitGenerationSettings, PermitRequest } from "../src/types/plugin-input"; +import * as fs from "fs"; + +function getEnvVar(key: string) { + return ( + process.env[key] || + (() => { + throw new Error(`Environment variable ${key} is required`); + })() + ); +} +/** + * Generates all the permits based on the current github workflow dispatch. + */ +export async function generatePermitsFromGithubWorkflowDispatch() { + const EVM_NETWORK_ID = getEnvVar("EVM_NETWORK_ID"); + const EVM_PRIVATE_KEY = getEnvVar("EVM_PRIVATE_KEY"); + const EVM_TOKEN_ADDRESS = getEnvVar("EVM_TOKEN_ADDRESS"); + const PAYMENT_REQUESTS = getEnvVar("PAYMENT_REQUESTS"); + const GITHUB_TOKEN = getEnvVar("GITHUB_TOKEN"); + const SUPABASE_URL = getEnvVar("SUPABASE_URL"); + const SUPABASE_KEY = getEnvVar("SUPABASE_KEY"); + + console.log(`Received: ${PAYMENT_REQUESTS}`); + const userAmounts = JSON.parse(PAYMENT_REQUESTS); + + // Populate the permitRequests from the user_amounts payload + + const permitRequests: PermitRequest[] = userAmounts.flatMap((userObj: { [key: string]: number }) => + Object.entries(userObj).map(([user, amount]) => ({ + type: "ERC20", + username: user, + amount: amount, + tokenAddress: EVM_TOKEN_ADDRESS, + })) + ); + + const config: PermitGenerationSettings = { + evmNetworkId: Number(EVM_NETWORK_ID), + evmPrivateEncrypted: EVM_PRIVATE_KEY, + permitRequests: permitRequests, + }; + + const octokit = new Octokit({ auth: GITHUB_TOKEN }); + const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY); + + const context: Context = { + eventName: "workflow_dispatch", + config: config, + octokit, + payload: userAmounts, + env: undefined, + logger: { + debug(message: unknown, ...optionalParams: unknown[]) { + console.debug(message, ...optionalParams); + }, + info(message: unknown, ...optionalParams: unknown[]) { + console.log(message, ...optionalParams); + }, + warn(message: unknown, ...optionalParams: unknown[]) { + console.warn(message, ...optionalParams); + }, + error(message: unknown, ...optionalParams: unknown[]) { + console.error(message, ...optionalParams); + }, + fatal(message: unknown, ...optionalParams: unknown[]) { + console.error(message, ...optionalParams); + }, + }, + adapters: {} as ReturnType, + }; + + context.adapters = createAdapters(supabaseClient, context); + + const permits = await generatePayoutPermit(context, config.permitRequests); + const out = Buffer.from(JSON.stringify(permits)).toString("base64"); + fs.writeFile(process.argv[2], out, (err) => { + if (err) { + throw err; + } + }); +} + +generatePermitsFromGithubWorkflowDispatch() + .then((result) => console.log(`result: ${result}`)) + .catch((error) => { + console.error(error); + }); diff --git a/src/adapters/supabase/helpers/wallet.ts b/src/adapters/supabase/helpers/wallet.ts index f94af08..d6cb5d2 100644 --- a/src/adapters/supabase/helpers/wallet.ts +++ b/src/adapters/supabase/helpers/wallet.ts @@ -15,8 +15,11 @@ export class Wallet extends Super { throw error; } - console.info("Successfully fetched wallet", { userId, address: data.wallets?.address }); - return data.wallets?.address; + // Check if wallets is an array, if so, return the first element's address + const address = Array.isArray(data.wallets) ? data.wallets[0]?.address : data.wallets?.address; + + console.info("Successfully fetched wallet", { userId, address: address }); + return address; } async upsertWallet(userId: number, address: string) { diff --git a/src/handlers/generate-erc20-permit.ts b/src/handlers/generate-erc20-permit.ts index b4c00f2..2cd49dd 100644 --- a/src/handlers/generate-erc20-permit.ts +++ b/src/handlers/generate-erc20-permit.ts @@ -4,6 +4,7 @@ import { Context, Logger } from "../types/context"; import { PermitReward, TokenType } from "../types"; import { decrypt, parseDecryptedPrivateKey } from "../utils"; import { getFastestProvider } from "../utils/get-fastest-provider"; +import { v4 as uuid } from "uuid"; export interface Payload { evmNetworkId: number; @@ -55,7 +56,7 @@ export async function generateErc20PermitSignature( } else if ("pull_request" in contextOrPayload.payload) { issueNodeId = contextOrPayload.payload.pull_request.node_id; } else { - throw new Error("Issue Id is missing"); + issueNodeId = uuid(); } } diff --git a/src/handlers/generate-erc721-permit.ts b/src/handlers/generate-erc721-permit.ts index 39ebced..a1fd07a 100644 --- a/src/handlers/generate-erc721-permit.ts +++ b/src/handlers/generate-erc721-permit.ts @@ -67,6 +67,9 @@ export async function generateErc721PermitSignature( _repositoryName = contextOrPermitPayload.repositoryName; _userId = contextOrPermitPayload.userId; } else { + if (!contextOrPermitPayload.env) { + throw new Error(`env is undefined`); + } const { NFT_MINTER_PRIVATE_KEY, NFT_CONTRACT_ADDRESS } = contextOrPermitPayload.env; const { evmNetworkId } = contextOrPermitPayload.config; const adapters = contextOrPermitPayload.adapters; diff --git a/src/types/context.ts b/src/types/context.ts index 17d773f..5c94c2a 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -20,6 +20,6 @@ export interface Context { octokit: InstanceType; adapters: ReturnType; config: PermitGenerationSettings; - env: Env; + env?: Env; logger: Logger; } diff --git a/src/utils/keys.ts b/src/utils/keys.ts index b2326b4..fea705a 100644 --- a/src/utils/keys.ts +++ b/src/utils/keys.ts @@ -10,14 +10,12 @@ export async function decrypt(encryptedText: string, x25519PrivateKey: string): await sodium.ready; const publicKey = await getPublicKey(x25519PrivateKey); - + const binaryPublic = sodium.from_base64(publicKey, sodium.base64_variants.URLSAFE_NO_PADDING); const binaryPrivate = sodium.from_base64(x25519PrivateKey, sodium.base64_variants.URLSAFE_NO_PADDING); const binaryEncryptedText = sodium.from_base64(encryptedText, sodium.base64_variants.URLSAFE_NO_PADDING); - const decryptedText = sodium.crypto_box_seal_open(binaryEncryptedText, binaryPublic, binaryPrivate, "text"); - - return decryptedText; + return sodium.crypto_box_seal_open(binaryEncryptedText, binaryPublic, binaryPrivate, "text"); } /** @@ -36,40 +34,40 @@ export async function getPublicKey(x25519PrivateKey: string): Promise { * 1. Private key * 2. Organization id where this private key is allowed to be used * 3. Repository id where this private key is allowed to be used - * - * The issue with "plain" encryption of wallet private keys is that if partner accidentally shares - * his encrypted private key then a malicious user will be able to use that leaked private key + * + * The issue with "plain" encryption of wallet private keys is that if partner accidentally shares + * his encrypted private key then a malicious user will be able to use that leaked private key * in another organization with permits generated from a leaked partner's wallet. - * + * * Partner private key (`evmPrivateEncrypted` config param in `conversation-rewards` plugin) supports 3 formats: * 1. PRIVATE_KEY * 2. PRIVATE_KEY:GITHUB_ORGANIZATION_ID * 3. PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID - * + * * Format "PRIVATE_KEY" can be used only for `ubiquity` and `ubiquibot` organizations. It is * kept for backwards compatibility in order not to update private key formats for our existing * values set in the `evmPrivateEncrypted` param. - * + * * Format "PRIVATE_KEY:GITHUB_ORGANIZATION_ID" restricts in which particular organization this private * key can be used. It can be set either in the organization wide config either in the repository wide one. - * - * Format "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" restricts organization and a particular + * + * Format "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID" restricts organization and a particular * repository where private key is allowed to be used. - * - * @param decryptedPrivateKey Decrypted private key string (in any of the 3 different formats) + * + * @param decryptedPrivateKey Decrypted private key string (in any of the 3 different formats) * @returns Parsed private key object: private key, organization id and repository id */ export function parseDecryptedPrivateKey(decryptedPrivateKey: string) { - let result: { - privateKey: string | null, - allowedOrganizationId: number | null, - allowedRepositoryId: number | null, + const result: { + privateKey: string | null; + allowedOrganizationId: number | null; + allowedRepositoryId: number | null; } = { privateKey: null, allowedOrganizationId: null, allowedRepositoryId: null, }; - + // split private key const privateKeyParts = decryptedPrivateKey.split(":"); diff --git a/yarn.lock b/yarn.lock index 71fe119..b87bef6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2130,6 +2130,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== + "@types/ws@^8.5.10": version "8.5.12" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" @@ -6157,16 +6162,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"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: +"string-width-cjs@npm:string-width@^4.2.0", 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== @@ -6238,14 +6234,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"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: +"strip-ansi-cjs@npm:strip-ansi@^6.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== @@ -6655,6 +6644,11 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -6779,16 +6773,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"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: +"wrap-ansi-cjs@npm:wrap-ansi@^7.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==