From 982f2bd2ceb21ce42001daf0c97e5198420f07f6 Mon Sep 17 00:00:00 2001 From: awisniew207 Date: Tue, 22 Apr 2025 11:53:06 -0700 Subject: [PATCH 1/4] init --- index.ts | 3 +- lit.ts | 40 ++++ routes/delegate/user.ts | 53 ++++- tests/routes/delegate/removeusers.test.ts | 271 ++++++++++++++++++++++ 4 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 tests/routes/delegate/removeusers.test.ts diff --git a/index.ts b/index.ts index 0aad6d1..e1558c8 100644 --- a/index.ts +++ b/index.ts @@ -66,7 +66,7 @@ import { import { mintClaimedKeyId } from "./routes/auth/claim"; import { registerPayerHandler } from "./routes/delegate/register"; -import { addPayeeHandler } from "./routes/delegate/user"; +import { addPayeeHandler, removePayeeHandler } from "./routes/delegate/user"; import { sendTxnHandler } from "./routes/auth/sendTxn"; const app = express(); @@ -222,6 +222,7 @@ app.get("/auth/status/:requestId", getAuthStatusHandler); // -- Payment Delegation app.post("/register-payer", registerPayerHandler); app.post("/add-users", addPayeeHandler); +app.post("/remove-users", removePayeeHandler); // --- Send TXN app.post("/send-txn", sendTxnHandler); diff --git a/lit.ts b/lit.ts index e12200c..d2a676a 100644 --- a/lit.ts +++ b/lit.ts @@ -711,6 +711,46 @@ export async function addPaymentDelegationPayee({ } } +export async function removePaymentDelegationPayee({ + wallet, + payeeAddresses, +}: { + wallet: ethers.Wallet; + payeeAddresses: string[]; +}) { + // add payer in contract + const paymentDelegationContract = await getContractFromJsSdk( + config.network, + "PaymentDelegation", + wallet, + ); + + try { + // Estimate gas first + const estimatedGas = await paymentDelegationContract.estimateGas.undelegatePaymentsBatch( + payeeAddresses + ); + + // Add 30% buffer using proper BigNumber math + const gasLimit = estimatedGas + .mul(ethers.BigNumber.from(130)) + .div(ethers.BigNumber.from(100)); + + console.log(`Estimated gas: ${estimatedGas.toString()}, Using gas limit: ${gasLimit.toString()}`); + + const tx = await paymentDelegationContract.functions.undelegatePaymentsBatch( + payeeAddresses, + { gasLimit } + ); + console.log("tx hash for undelegatePaymentsBatch()", tx.hash); + await tx.wait(); + return tx; + } catch (err) { + console.error("Error while estimating or executing undelegatePaymentsBatch:", err); + throw err; + } +} + // export function packAuthData({ // credentialPublicKey, // credentialID, diff --git a/routes/delegate/user.ts b/routes/delegate/user.ts index 87bd8c0..512ec1b 100644 --- a/routes/delegate/user.ts +++ b/routes/delegate/user.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { deriveWallet } from './register'; -import { addPaymentDelegationPayee } from '../../lit'; +import { addPaymentDelegationPayee, removePaymentDelegationPayee } from '../../lit'; export async function addPayeeHandler(req: Request, res: Response) { const payeeAddresses = req.body as string[]; @@ -41,6 +41,57 @@ export async function addPayeeHandler(req: Request, res: Response) { error = (err as Error).toString(); } + if (error) { + res.status(500).json({ + success: false, + error + }); + } else { + res.status(200).json({ + success: true + }); + } +} + +export async function removePayeeHandler(req: Request, res: Response) { + const payeeAddresses = req.body as string[]; + const apiKey = req.header('api-key'); + const payerSecret = req.header('payer-secret-key'); + + if (!apiKey || !payerSecret) { + res.status(400).json({ + success: false, + error: 'Missing or invalid API / Payer key' + }); + + return; + } + + if (!payeeAddresses || !Array.isArray(payeeAddresses) || payeeAddresses.length < 1) { + res.status(400).json({ + success: false, + error: 'Missing or invalid payee addresses' + }); + return; + } + + const wallet = await deriveWallet(apiKey, payerSecret); + let error: string | boolean = false; + + try { + const tx = await removePaymentDelegationPayee({ + wallet, + payeeAddresses + }); + + if (!tx) { + throw new Error('Failed to remove payee: delegation transaction failed'); + } + } catch (err) { + console.error('Failed to remove payee', err); + error = (err as Error).toString(); + } + if (error) { res.status(500).json({ success: false, diff --git a/tests/routes/delegate/removeusers.test.ts b/tests/routes/delegate/removeusers.test.ts new file mode 100644 index 0000000..d5221ac --- /dev/null +++ b/tests/routes/delegate/removeusers.test.ts @@ -0,0 +1,271 @@ +import request from "supertest"; +import express from "express"; +import { addPayeeHandler, removePayeeHandler } from "../../../routes/delegate/user"; +import { registerPayerHandler } from "../../../routes/delegate/register"; +import { ethers } from "ethers"; +import { getProvider } from "../../../lit"; +import cors from "cors"; +import { Sequencer } from "../../../lib/sequencer"; +import "../../../tests/setup"; // Import setup to ensure environment variables are loaded + +describe("addPayee Integration Tests", () => { + let app: express.Application; + let provider: ethers.providers.JsonRpcProvider; + let signer: ethers.Wallet; + let apiKey: string; + let payerSecretKey: string; + let payerWalletAddress: string; + let payeeAddresses: string[] = []; + let hasNetworkConnectivity = false; + + beforeAll(async () => { + // Set up the Express app + app = express(); + app.use(express.json()); + app.use(cors()); + app.post("/register-payer", registerPayerHandler); + app.post("/add-users", addPayeeHandler); + app.post("/remove-users", removePayeeHandler); + // Generate a unique API key for testing + apiKey = `test-api-key-${Date.now()}`; + + try { + // Set up provider and signer + provider = getProvider(); + const privateKey = process.env.LIT_TXSENDER_PRIVATE_KEY!; + signer = new ethers.Wallet(privateKey, provider); + + // Check if we have network connectivity + await provider.getNetwork(); + hasNetworkConnectivity = true; + } catch (error) { + console.log("No network connectivity available for blockchain tests"); + hasNetworkConnectivity = false; + } + }); + + afterAll(async () => { + // Clean up provider and connections + if (provider) { + provider.removeAllListeners(); + } + + if (Sequencer.Instance) { + Sequencer.Instance.stop(); + } + }); + + it("should register a payer and return a payer secret key", async () => { + // Skip this test if we don't have network connectivity + if (!hasNetworkConnectivity) { + console.log("Skipping test due to lack of network connectivity"); + return; + } + + try { + const response = await request(app) + .post("/register-payer") + .set("api-key", apiKey) + .set("Content-Type", "application/json"); + + // If we have network connectivity, the test should succeed + if (response.status === 200) { + expect(response.body).toHaveProperty("success", true); + expect(response.body).toHaveProperty("payerSecretKey"); + expect(response.body).toHaveProperty("payerWalletAddress"); + + // Save the payer secret key and wallet address for the next test + payerSecretKey = response.body.payerSecretKey; + payerWalletAddress = response.body.payerWalletAddress; + + console.log(`Registered payer with address: ${payerWalletAddress}`); + } else { + // If we don't have network connectivity, log the error + console.log("Failed to register payer:", response.body.error); + } + } catch (error) { + console.error("Error in test:", error); + // Don't fail the test, just log the error + console.log("Skipping test due to error"); + } + }, 30000); // Increase timeout to 30s since we're waiting for real transactions + + it("should successfully add users with valid API key and payer secret", async () => { + // Skip this test if we don't have network connectivity or a payer secret key + if (!hasNetworkConnectivity || !payerSecretKey) { + console.log("Skipping test due to lack of network connectivity or payer registration failure"); + return; + } + + try { + // Generate random payee addresses + payeeAddresses = [ + ethers.Wallet.createRandom().address, + ethers.Wallet.createRandom().address + ]; + + const response = await request(app) + .post("/add-users") + .set("api-key", apiKey) + .set("payer-secret-key", payerSecretKey) + .set("Content-Type", "application/json") + .send(payeeAddresses); + + // If we have network connectivity, the test should succeed + if (response.status === 200) { + expect(response.body).toHaveProperty("success", true); + console.log(`Successfully added payees to payer ${payerWalletAddress}`); + } else { + // If we don't have network connectivity, log the error + console.log("Failed to add users:", response.body.error); + } + } catch (error) { + console.error("Error in test:", error); + // Don't fail the test, just log the error + console.log("Skipping test due to error"); + } + }, 30000); // Increase timeout to 30s since we're waiting for real transactions + + it("should successfully remove users with valid API key and payer secret", async () => { + // Skip this test if we don't have network connectivity, payer secret key, or payee addresses + if (!hasNetworkConnectivity || !payerSecretKey || payeeAddresses.length === 0) { + console.log("Skipping test due to lack of network connectivity, payer registration failure, or no payees added"); + return; + } + + try { + const response = await request(app) + .post("/remove-users") + .set("api-key", apiKey) + .set("payer-secret-key", payerSecretKey) + .set("Content-Type", "application/json") + .send(payeeAddresses); + + // If we have network connectivity, the test should succeed + if (response.status === 200) { + expect(response.body).toHaveProperty("success", true); + console.log(`Successfully removed payees from payer ${payerWalletAddress}`); + } else { + // If we don't have network connectivity, log the error + console.log("Failed to remove users:", response.body.error); + } + } catch (error) { + console.error("Error in test:", error); + // Don't fail the test, just log the error + console.log("Skipping test due to error"); + } + }, 30000); // Increase timeout to 30s since we're waiting for real transactions + + // Test with mock data for validation tests + describe("Input validation tests", () => { + it("should return 400 if API key is missing", async () => { + const payeeAddresses = [ + ethers.Wallet.createRandom().address, + ]; + + const response = await request(app) + .post("/add-users") + .set("payer-secret-key", "test-payer-secret-key") + .set("Content-Type", "application/json") + .send(payeeAddresses) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("success", false); + expect(response.body).toHaveProperty("error", "Missing or invalid API / Payer key"); + }); + + it("should return 400 if payer secret key is missing", async () => { + const payeeAddresses = [ + ethers.Wallet.createRandom().address, + ]; + + const response = await request(app) + .post("/add-users") + .set("api-key", "test-api-key-123") + .set("Content-Type", "application/json") + .send(payeeAddresses) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("success", false); + expect(response.body).toHaveProperty("error", "Missing or invalid API / Payer key"); + }); + + it("should return 400 if payee addresses are missing or invalid", async () => { + const response = await request(app) + .post("/add-users") + .set("api-key", "test-api-key-123") + .set("payer-secret-key", "test-payer-secret-key") + .set("Content-Type", "application/json") + .send([]) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("success", false); + expect(response.body).toHaveProperty("error", "Missing or invalid payee addresses"); + }); + + // Test with non-array input + it("should return 400 if payee addresses are not an array", async () => { + const response = await request(app) + .post("/add-users") + .set("api-key", "test-api-key-123") + .set("payer-secret-key", "test-payer-secret-key") + .set("Content-Type", "application/json") + .send("not an array"); + + // Just check that the status code is 400 + expect(response.status).toBe(400); + }); + + // Add validation tests for remove-users endpoint + it("should return 400 if API key is missing when removing users", async () => { + const payeeAddresses = [ + ethers.Wallet.createRandom().address, + ]; + + const response = await request(app) + .post("/remove-users") + .set("payer-secret-key", "test-payer-secret-key") + .set("Content-Type", "application/json") + .send(payeeAddresses) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("success", false); + expect(response.body).toHaveProperty("error", "Missing or invalid API / Payer key"); + }); + + it("should return 400 if payer secret key is missing when removing users", async () => { + const payeeAddresses = [ + ethers.Wallet.createRandom().address, + ]; + + const response = await request(app) + .post("/remove-users") + .set("api-key", "test-api-key-123") + .set("Content-Type", "application/json") + .send(payeeAddresses) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("success", false); + expect(response.body).toHaveProperty("error", "Missing or invalid API / Payer key"); + }); + + it("should return 400 if payee addresses are missing or invalid when removing users", async () => { + const response = await request(app) + .post("/remove-users") + .set("api-key", "test-api-key-123") + .set("payer-secret-key", "test-payer-secret-key") + .set("Content-Type", "application/json") + .send([]) + .expect("Content-Type", /json/) + .expect(400); + + expect(response.body).toHaveProperty("success", false); + expect(response.body).toHaveProperty("error", "Missing or invalid payee addresses"); + }); + }); +}); From c81624ce0e129465e921d71e55c9ab3ce63cb5b7 Mon Sep 17 00:00:00 2001 From: awisniew207 Date: Tue, 22 Apr 2025 12:06:32 -0700 Subject: [PATCH 2/4] fix(removePayees): comment fix --- lit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lit.ts b/lit.ts index d2a676a..b2384c2 100644 --- a/lit.ts +++ b/lit.ts @@ -718,7 +718,7 @@ export async function removePaymentDelegationPayee({ wallet: ethers.Wallet; payeeAddresses: string[]; }) { - // add payer in contract + // remove payer in contract const paymentDelegationContract = await getContractFromJsSdk( config.network, "PaymentDelegation", From d93fc0d41bec0c592a1327f37c78ca31f234306c Mon Sep 17 00:00:00 2001 From: awisniew207 Date: Tue, 22 Apr 2025 12:32:06 -0700 Subject: [PATCH 3/4] fix(sentTxn): fixed test file to avoid race condition --- tests/routes/auth/sendTxn.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/routes/auth/sendTxn.test.ts b/tests/routes/auth/sendTxn.test.ts index 8dea9fc..4e85285 100644 --- a/tests/routes/auth/sendTxn.test.ts +++ b/tests/routes/auth/sendTxn.test.ts @@ -163,11 +163,15 @@ describe("sendTxn Integration Tests", () => { const { chainId } = await provider.getNetwork(); + // Get a fresh nonce right before creating the transaction + const nonce = await provider.getTransactionCount(pkpEthAddress); + const gasPrice = await provider.getGasPrice(); + const unsignedTxn = { to: authWallet.address, - value: "0x0", - gasPrice: await provider.getGasPrice(), - nonce: await provider.getTransactionCount(pkpEthAddress), + value: ethers.utils.parseEther("0"), // Use parseEther to ensure proper formatting instead of "0x0" + gasPrice, + nonce, chainId, data: "0x", }; @@ -258,7 +262,7 @@ describe("sendTxn Integration Tests", () => { // check that the txn hash is the same as the one from the client expect(response.body.requestId).toBe(txnHashFromClient); - }, 30000); + }, 60000); // Increase timeout to 60s since we're waiting for real transactions it("should reject transaction with invalid signature", async () => { // Create a new random wallet From 7e14ed0c8a9cfa639d6fe1e7bfc05faad67b78ec Mon Sep 17 00:00:00 2001 From: awisniew207 Date: Tue, 22 Apr 2025 12:43:02 -0700 Subject: [PATCH 4/4] fix(sendTxn): only increase timeout --- tests/routes/auth/sendTxn.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/routes/auth/sendTxn.test.ts b/tests/routes/auth/sendTxn.test.ts index 4e85285..b3f6070 100644 --- a/tests/routes/auth/sendTxn.test.ts +++ b/tests/routes/auth/sendTxn.test.ts @@ -163,15 +163,11 @@ describe("sendTxn Integration Tests", () => { const { chainId } = await provider.getNetwork(); - // Get a fresh nonce right before creating the transaction - const nonce = await provider.getTransactionCount(pkpEthAddress); - const gasPrice = await provider.getGasPrice(); - const unsignedTxn = { to: authWallet.address, - value: ethers.utils.parseEther("0"), // Use parseEther to ensure proper formatting instead of "0x0" - gasPrice, - nonce, + value: "0x0", + gasPrice: await provider.getGasPrice(), + nonce: await provider.getTransactionCount(pkpEthAddress), chainId, data: "0x", };