From bad8a03f90e44444edc7f5e5184d250d6ed2fa48 Mon Sep 17 00:00:00 2001 From: Chris Cassano Date: Tue, 16 Sep 2025 17:42:09 +0200 Subject: [PATCH 1/2] add fund pkp endpoint --- index.ts | 4 + routes/auth/fundPkp.ts | 80 ++++++++++++++ tests/routes/auth/fundPkp.test.ts | 169 ++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 routes/auth/fundPkp.ts create mode 100644 tests/routes/auth/fundPkp.test.ts diff --git a/index.ts b/index.ts index 9ff36b7..5588294 100644 --- a/index.ts +++ b/index.ts @@ -71,6 +71,7 @@ import { mintClaimedKeyId } from "./routes/auth/claim"; import { registerPayerHandler } from "./routes/delegate/register"; import { addPayeeHandler } from "./routes/delegate/user"; import { sendTxnHandler } from "./routes/auth/sendTxn"; +import { fundPkpHandler } from "./routes/auth/fundPkp"; const app = express(); @@ -229,6 +230,9 @@ app.post("/add-users", addPayeeHandler); // --- Send TXN app.post("/send-txn", sendTxnHandler); +// --- Fund PKP +app.post("/fund-pkp", fundPkpHandler); + // *** Deprecated *** app.post("/auth/google", googleOAuthVerifyToMintHandler); diff --git a/routes/auth/fundPkp.ts b/routes/auth/fundPkp.ts new file mode 100644 index 0000000..671948b --- /dev/null +++ b/routes/auth/fundPkp.ts @@ -0,0 +1,80 @@ +import { NextFunction, Request, Response } from "express"; +import { ethers } from "ethers"; +import { getProvider, getSigner } from "../../lit"; +import { executeTransactionWithRetry } from "../../lib/optimisticNonceManager"; + +export async function fundPkpHandler( + req: Request, + res: Response, + next: NextFunction, +) { + try { + const apiKey = req.header("api-key"); + + // Check if the API key matches the required Vincent API key + if (apiKey !== process.env.LIT_VINCENT_RELAYER_API_KEY) { + return res.status(403).json({ + error: "Unauthorized. Invalid API key.", + }); + } + + const { ethAddress } = req.body; + + if (!ethAddress) { + return res.status(400).json({ + error: "Missing required parameter: ethAddress", + }); + } + + // Validate ethereum address format + if (!ethers.utils.isAddress(ethAddress)) { + return res.status(400).json({ + error: "Invalid ethereum address format", + }); + } + + const provider = getProvider(); + const signer = getSigner(); + + // Check the balance of the ethereum address + const balance = await provider.getBalance(ethAddress); + + // If balance is not 0, no funding needed + if (!balance.isZero()) { + return res.status(200).json({ + message: "Address already has funds, no funding needed", + currentBalance: ethers.utils.formatEther(balance), + }); + } + + // Send 0.01 ETH to the address + const fundingAmount = ethers.utils.parseEther("0.01"); + + // Use optimistic nonce management for reliable transaction sending + const tx = await executeTransactionWithRetry( + signer, + async (nonce: number) => { + return await signer.sendTransaction({ + to: ethAddress, + value: fundingAmount, + nonce, + }); + }, + ); + + console.log(`Funded PKP address ${ethAddress} with 0.01 ETH. Tx hash: ${tx.hash}`); + + return res.status(200).json({ + message: "Successfully funded PKP address", + txHash: tx.hash, + fundedAmount: "0.01", + recipientAddress: ethAddress, + }); + + } catch (error: any) { + console.error("Error in fundPkpHandler:", error); + return res.status(500).json({ + error: "Internal server error: " + error.message, + }); + } +} \ No newline at end of file diff --git a/tests/routes/auth/fundPkp.test.ts b/tests/routes/auth/fundPkp.test.ts new file mode 100644 index 0000000..6a6d547 --- /dev/null +++ b/tests/routes/auth/fundPkp.test.ts @@ -0,0 +1,169 @@ +import request from "supertest"; +import express from "express"; +import { ethers } from "ethers"; +import { fundPkpHandler } from "../../../routes/auth/fundPkp"; +import { getProvider } from "../../../lit"; +import cors from "cors"; + +describe("fundPkp Integration Tests", () => { + let app: express.Application; + let provider: ethers.providers.JsonRpcProvider; + + beforeAll(async () => { + provider = getProvider(); + }); + + beforeEach(() => { + app = express(); + app.use(express.json()); + app.use(cors()); + app.post("/fund-pkp", fundPkpHandler); + }); + + afterAll(async () => { + if (provider) { + provider.removeAllListeners(); + } + }); + + it("should reject request without valid Vincent API key", async () => { + const testAddress = ethers.Wallet.createRandom().address; + + const response = await request(app) + .post("/fund-pkp") + .set("api-key", "invalid-key") + .send({ ethAddress: testAddress }) + .expect("Content-Type", /json/) + .expect(403); + + expect(response.body).toHaveProperty("error"); + expect(response.body.error).toContain("Unauthorized"); + }); + + it("should reject request without API key header", async () => { + const testAddress = ethers.Wallet.createRandom().address; + + const response = await request(app) + .post("/fund-pkp") + .send({ ethAddress: testAddress }) + .expect("Content-Type", /json/) + .expect(403); + + expect(response.body).toHaveProperty("error"); + expect(response.body.error).toContain("Unauthorized"); + }); + + it("should reject request without ethAddress parameter", async () => { + const response = await request(app) + .post("/fund-pkp") + .set("api-key", process.env.LIT_VINCENT_RELAYER_API_KEY || "") + .send({}) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("error"); + expect(response.body.error).toContain("Missing required parameter: ethAddress"); + }); + + it("should reject request with invalid ethereum address format", async () => { + const response = await request(app) + .post("/fund-pkp") + .set("api-key", process.env.LIT_VINCENT_RELAYER_API_KEY || "") + .send({ ethAddress: "invalid-address" }) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("error"); + expect(response.body.error).toContain("Invalid ethereum address format"); + }); + + it("should return success message if address already has funds", async () => { + // Use an address that we know has funds (the signer address) + const { getSigner } = await import("../../../lit"); + const signer = getSigner(); + const signerAddress = await signer.getAddress(); + + const response = await request(app) + .post("/fund-pkp") + .set("api-key", process.env.LIT_VINCENT_RELAYER_API_KEY || "") + .send({ ethAddress: signerAddress }) + .expect("Content-Type", /json/) + .expect(200); + + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toContain("already has funds"); + expect(response.body).toHaveProperty("currentBalance"); + }); + + it("should successfully fund an address with zero balance", async () => { + // Create a random address that will have zero balance + const randomWallet = ethers.Wallet.createRandom(); + const testAddress = randomWallet.address; + + // Verify the address has zero balance initially + const initialBalance = await provider.getBalance(testAddress); + expect(initialBalance.isZero()).toBe(true); + + const response = await request(app) + .post("/fund-pkp") + .set("api-key", process.env.LIT_VINCENT_RELAYER_API_KEY || "") + .send({ ethAddress: testAddress }) + .expect("Content-Type", /json/) + .expect(200); + + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toContain("Successfully funded"); + expect(response.body).toHaveProperty("txHash"); + expect(response.body).toHaveProperty("fundedAmount"); + expect(response.body).toHaveProperty("recipientAddress"); + expect(response.body.fundedAmount).toBe("0.01"); + expect(response.body.recipientAddress).toBe(testAddress); + expect(response.body.txHash).toMatch(/^0x[a-fA-F0-9]{64}$/); + + // Wait for transaction to be mined and verify the balance + const txReceipt = await provider.waitForTransaction(response.body.txHash); + expect(txReceipt.status).toBe(1); + + // Check that the address now has the funded amount + const finalBalance = await provider.getBalance(testAddress); + expect(finalBalance.eq(ethers.utils.parseEther("0.01"))).toBe(true); + }, 30000); // Increase timeout since we're waiting for real transactions + + it("should handle multiple consecutive funding requests properly", async () => { + // Create two random addresses + const address1 = ethers.Wallet.createRandom().address; + const address2 = ethers.Wallet.createRandom().address; + + // Fund first address + const response1 = await request(app) + .post("/fund-pkp") + .set("api-key", process.env.LIT_VINCENT_RELAYER_API_KEY || "") + .send({ ethAddress: address1 }) + .expect("Content-Type", /json/) + .expect(200); + + expect(response1.body.message).toContain("Successfully funded"); + + // Fund second address + const response2 = await request(app) + .post("/fund-pkp") + .set("api-key", process.env.LIT_VINCENT_RELAYER_API_KEY || "") + .send({ ethAddress: address2 }) + .expect("Content-Type", /json/) + .expect(200); + + expect(response2.body.message).toContain("Successfully funded"); + + // Verify both transactions succeeded + const receipt1 = await provider.waitForTransaction(response1.body.txHash); + const receipt2 = await provider.waitForTransaction(response2.body.txHash); + expect(receipt1.status).toBe(1); + expect(receipt2.status).toBe(1); + + // Verify both addresses have the correct balance + const balance1 = await provider.getBalance(address1); + const balance2 = await provider.getBalance(address2); + expect(balance1.eq(ethers.utils.parseEther("0.01"))).toBe(true); + expect(balance2.eq(ethers.utils.parseEther("0.01"))).toBe(true); + }, 45000); +}); \ No newline at end of file From 22b4b677f7ecd8287bcf90de0cbb9ff6de19c2f0 Mon Sep 17 00:00:00 2001 From: Chris Cassano Date: Wed, 17 Sep 2025 18:37:24 +0200 Subject: [PATCH 2/2] add mock api key to CI --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 934982d..5024fec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,7 @@ jobs: # address is 0x1B672D38C063c443DDBFEB5769389c597621571e echo "LIT_DELEGATION_ROOT_MNEMONIC=${{ secrets.LIT_DELEGATION_ROOT_MNEMONIC }}" >> .env echo "NETWORK=${{ matrix.network }}" >> .env + echo "LIT_VINCENT_RELAYER_API_KEY=${{ secrets.LIT_VINCENT_RELAYER_API_KEY }}" >> .env - name: Run tests run: yarn test