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
118 changes: 59 additions & 59 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);
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);
}
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;
}
Loading
Loading