Skip to content

Commit

Permalink
feat: permits
Browse files Browse the repository at this point in the history
  • Loading branch information
whilefoo committed Mar 29, 2024
1 parent bcde7a0 commit 3b928a1
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 462 deletions.
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: npm install -g bun && bun install

- name: Generate Permit
run: npx tsx ./src/index.ts
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",
"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
95 changes: 39 additions & 56 deletions src/handlers/generate-erc20-permit.ts
Original file line number Diff line number Diff line change
@@ -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<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;
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);
}
Expand All @@ -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;
}
124 changes: 54 additions & 70 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,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) {
Expand All @@ -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");
Expand All @@ -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<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

0 comments on commit 3b928a1

Please sign in to comment.