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

Permits #2

Merged
merged 1 commit into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can directly be used within https://bun.sh/guides/runtime/cicd
However I don't see where bun is used?


- 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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change and I recall that it really changes the API I hope you did this for good reason!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5.x seems to be marked legacy so eventually we should bump it up but right now that breaks the code, as it is in the latest commit of that branch.

"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
Loading