Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate erc20 / erc721 permit #4

Merged
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}"],
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/compute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
node-version: "20.10.0"

- name: Install dependencies
run: yarn
run: yarn i --immutable --immutable-cache --check-cache
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved

- name: Generate Permit
run: npx tsx ./src/index.ts
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/jest-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Binary file added bun.lockb
Binary file not shown.
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,13 @@
"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 .",
"format:cspell": "cspell **/*",
"knip": "knip",
"knip-ci": "knip --no-exit-code --reporter json",
"prepare": "husky install",
"worker": "wrangler dev --port 8789",
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
"test": "jest"
},
"keywords": [
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions src/adapters/supabase/helpers/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
116 changes: 58 additions & 58 deletions src/handlers/generate-erc20-permit.ts
Original file line number Diff line number Diff line change
@@ -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<PermitTransactionData | string> {
export async function generateErc20PermitSignature(context: Context, username: string, amount: number): Promise<Permit> {
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;
}

if (!beneficiary) {
logger.error("No beneficiary found for permit");
return "Permit not generated: No beneficiary found for permit";
issueId = context.payload.pull_request.id;
} else {
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
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);
}
Expand All @@ -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;
}
126 changes: 58 additions & 68 deletions src/handlers/generate-erc721-permit.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<PermitTransactionData | string> {
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<Permit> {
const { NFT_MINTER_PRIVATE_KEY, NFT_CONTRACT_ADDRESS } = context.env;
const { evmNetworkId } = context.config;
const adapters = context.adapters;
const logger = context.logger;
Expand All @@ -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);
Expand All @@ -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");
Expand All @@ -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<string, string>;

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;
}
Loading
Loading