Skip to content

Commit

Permalink
Pkp helper v2 on datil dev only + working on gas subsidy endpoint (#65)
Browse files Browse the repository at this point in the history
* a little refactoring.  added PKPHelperv2 to datil-dev

* fix tests, add test where we send PKP away after minting

* trying to get gas subsidy test working

* just in time gas sending working

* fixed tests
  • Loading branch information
glitch003 authored Feb 15, 2025
1 parent 1b7d96b commit c7232e5
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 5 deletions.
9 changes: 7 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
import { mintClaimedKeyId } from "./routes/auth/claim";
import { registerPayerHandler } from "./routes/delegate/register";
import { addPayeeHandler } from "./routes/delegate/user";
import { sendTxnHandler } from "./routes/auth/sendTxn";

const app = express();

Expand Down Expand Up @@ -226,6 +227,9 @@ app.get("/auth/status/:requestId", getAuthStatusHandler);
app.post("/register-payer", registerPayerHandler);
app.post("/add-users", addPayeeHandler);

// --- Send TXN
app.post("/send-txn", sendTxnHandler);

// *** Deprecated ***

app.post("/auth/google", googleOAuthVerifyToMintHandler);
Expand All @@ -252,7 +256,6 @@ app.get(
);
app.post("/auth/claim", mintClaimedKeyId);


if (ENABLE_HTTPS) {
const host = "0.0.0.0";
const port = 443;
Expand All @@ -276,6 +279,8 @@ if (ENABLE_HTTPS) {
const port = config.port;

http.createServer(app).listen(port, () => {
console.log(`🚀 2: Server ready at ${host}:${port} 🌶️ NETWORK: ${process.env.NETWORK} | RPC: ${process.env.LIT_TXSENDER_RPC_URL} |`);
console.log(
`🚀 2: Server ready at ${host}:${port} 🌶️ NETWORK: ${process.env.NETWORK} | RPC: ${process.env.LIT_TXSENDER_RPC_URL} |`,
);
});
}
8 changes: 5 additions & 3 deletions lit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,15 @@ function getContractFromJsSdk(
return ethersContract;
}

export function getProvider() {
return new ethers.providers.JsonRpcProvider(
export function getProvider(): ethers.providers.JsonRpcProvider {
const provider = new ethers.providers.JsonRpcProvider(
process.env.LIT_TXSENDER_RPC_URL,
);
provider.pollingInterval = 200;
return provider;
}

function getSigner() {
export function getSigner(): ethers.Wallet {
const provider = getProvider();
const privateKey = process.env.LIT_TXSENDER_PRIVATE_KEY!;
const signer = new ethers.Wallet(privateKey, provider);
Expand Down
9 changes: 9 additions & 0 deletions models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,12 @@ export interface ResolvedAuthMethod {
appId: string;
userId: string;
}

export interface SendTxnRequest {
txn: ethers.Transaction;
}

export interface SendTxnResponse {
requestId?: string;
error?: string;
}
136 changes: 136 additions & 0 deletions routes/auth/sendTxn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Request } from "express";
import { Response } from "express-serve-static-core";
import { ParsedQs } from "qs";
import { SendTxnRequest, SendTxnResponse } from "../../models";
import { getSigner } from "../../lit";
import { ethers } from "ethers";

// estimate gas, send gas, broadcast txn, return txn hash
export async function sendTxnHandler(
req: Request<
{},
SendTxnResponse,
SendTxnRequest,
ParsedQs,
Record<string, any>
>,
res: Response<SendTxnResponse, Record<string, any>, number>,
) {
try {
const signer = getSigner();
const provider = signer.provider as ethers.providers.JsonRpcProvider;
const { from } = req.body.txn;

console.log("original txn", req.body.txn);

const fixedTxn = {
...req.body.txn,
gasLimit: (req.body.txn.gasLimit as any).hex,
gasPrice: (req.body.txn.gasPrice as any).hex,
value: (req.body.txn.value as any).hex,
};

console.log("fixed txn", fixedTxn);

// get the address that signed the txn
// to make sure the "from" matches and there's no funny business
const txnWithoutSig = {
...fixedTxn,
};
delete txnWithoutSig.r;
delete txnWithoutSig.s;
delete txnWithoutSig.v;
delete txnWithoutSig.hash;
delete txnWithoutSig.from;

const signature = {
r: req.body.txn.r!,
s: req.body.txn.s!,
v: req.body.txn.v!,
};

console.log("txnWithoutSig", txnWithoutSig);
const rsTx = await ethers.utils.resolveProperties(txnWithoutSig);
const serializedTxn = ethers.utils.serializeTransaction(rsTx);
console.log("serializedTxn: ", serializedTxn);

const msgHash = ethers.utils.keccak256(serializedTxn); // as specified by ECDSA
const msgBytes = ethers.utils.arrayify(msgHash); // create binary hash

const fromViaSignature = ethers.utils.recoverAddress(
msgBytes,
signature,
);
console.log("fromViaSignature", fromViaSignature);
if (fromViaSignature !== from) {
return res.status(500).json({
error: "Invalid signature - the recovered signature does not match the from address on the txn",
});
}

// // Convert to TransactionRequest format
const txnRequest = {
...fixedTxn,
nonce: ethers.utils.hexValue(fixedTxn.nonce),
value: ethers.utils.hexValue(fixedTxn.value),
chainId: ethers.utils.hexValue(fixedTxn.chainId),
};

const stateOverrides = {
[from]: {
balance: "0xDE0B6B3A7640000", // 1 eth in wei
},
};

console.log(
"created txn request to estimate gas on server side",
txnRequest,
);

// estimate the gas
// const gasLimit = await signer.provider.estimateGas(txnRequest);
const gasLimit = await provider.send("eth_estimateGas", [
txnRequest,
"latest",
stateOverrides,
]);
console.log("gasLimit", gasLimit);
const gasToFund = ethers.BigNumber.from(gasLimit).mul(rsTx.gasPrice);

// then, send gas to fund the wallet
const gasFundingTxn = await signer.sendTransaction({
to: from,
value: gasToFund,
});
console.log("gasFundingTxn", gasFundingTxn);
// wait for confirmation
await gasFundingTxn.wait();

// serialize the txn with sig
const serializedTxnWithSig = ethers.utils.serializeTransaction(
txnWithoutSig,
signature,
);

console.log("serializedTxnWithSig", serializedTxnWithSig);

// send the txn
const txn = await signer.provider.sendTransaction(serializedTxnWithSig);
// wait for confirmation
await txn.wait();

console.info("Sent txn", {
requestId: txn.hash,
});
return res.status(200).json({
requestId: txn.hash,
});
} catch (err) {
console.error("[sendTxnHandler] Unable to send txn", {
err,
});
return res.status(500).json({
error: `[sendTxnHandler] Unable to send txn ${JSON.stringify(err)}`,
});
}
}
166 changes: 166 additions & 0 deletions tests/routes/auth/sendTxn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import request from "supertest";
import express from "express";
import { ethers } from "ethers";
import { sendTxnHandler } from "../../../routes/auth/sendTxn";
import { getProvider } from "../../../lit";
import cors from "cors";

describe("sendTxn Integration Tests", () => {
let app: express.Application;
let provider: ethers.providers.JsonRpcProvider;

beforeAll(async () => {
// Set up provider
provider = getProvider();
});

beforeEach(() => {
app = express();
app.use(express.json());
app.use(cors());
app.post("/send-txn", sendTxnHandler);
});

it("should successfully send gas and broadcast a transaction", async () => {
// Create a new random wallet
const wallet = ethers.Wallet.createRandom().connect(provider);

const { chainId } = await provider.getNetwork();

const unsignedTxn = {
to: wallet.address,
value: "0x0",
gasPrice: await provider.getGasPrice(),
nonce: await provider.getTransactionCount(wallet.address),
chainId,
data: "0x",
};

console.log("unsignedTxn", unsignedTxn);
const txnForSimulation = {
...unsignedTxn,
gasPrice: ethers.utils.hexValue(unsignedTxn.gasPrice),
nonce: ethers.utils.hexValue(unsignedTxn.nonce),
chainId: ethers.utils.hexValue(chainId),
};

const stateOverrides = {
[wallet.address]: {
balance: "0xDE0B6B3A7640000", // 1 eth in wei
},
};

const gasLimit = await provider.send("eth_estimateGas", [
txnForSimulation,
"latest",
stateOverrides,
]);

const toSign = {
...unsignedTxn,
gasLimit,
};

console.log("toSign", toSign);

// Sign the transaction
const signedTxn = await wallet.signTransaction(toSign);
console.log("signedTxn", signedTxn);
const txn = ethers.utils.parseTransaction(signedTxn);

console.log("sending txn request", txn);

const response = await request(app)
.post("/send-txn")
.send({ txn })
.expect("Content-Type", /json/)
.expect(200);

expect(response.body).toHaveProperty("requestId");
expect(response.body.requestId).toMatch(/^0x[a-fA-F0-9]{64}$/); // Should be a transaction hash

// Wait for transaction to be mined
const txReceipt = await provider.waitForTransaction(
response.body.requestId,
);
expect(txReceipt.status).toBe(1); // Transaction should be successful
}, 30000); // Increase timeout to 30s since we're waiting for real transactions

it("should reject transaction with invalid signature", async () => {
// Create a new random wallet
const wallet = ethers.Wallet.createRandom().connect(provider);
const maliciousWallet = ethers.Wallet.createRandom().connect(provider);

const { chainId } = await provider.getNetwork();

// Create a transaction but try to use a different from address
const unsignedTxn = {
to: wallet.address,
value: "0x0",
gasPrice: await provider.getGasPrice(),
nonce: await provider.getTransactionCount(wallet.address),
chainId,
data: "0x",
};

console.log("unsignedTxn", unsignedTxn);
const txnForSimulation = {
...unsignedTxn,
gasPrice: ethers.utils.hexValue(unsignedTxn.gasPrice),
nonce: ethers.utils.hexValue(unsignedTxn.nonce),
chainId: ethers.utils.hexValue(chainId),
};

const stateOverrides = {
[wallet.address]: {
balance: "0xDE0B6B3A7640000", // 1 eth in wei
},
};

const gasLimit = await provider.send("eth_estimateGas", [
txnForSimulation,
"latest",
stateOverrides,
]);

const toSign = {
...unsignedTxn,
gasLimit,
};

console.log("toSign", toSign);

// Sign with malicious wallet but keep original from address
const signedTxn = await maliciousWallet.signTransaction(toSign);
console.log("signedTxn", signedTxn);
const txn = ethers.utils.parseTransaction(signedTxn);

// Override the from address to be the original wallet
txn.from = wallet.address;

const response = await request(app)
.post("/send-txn")
.send({ txn })
.expect("Content-Type", /json/)
.expect(500);

expect(response.body).toHaveProperty("error");
expect(response.body.error).toContain("Invalid signature");
});

it("should handle errors with invalid transaction parameters", async () => {
// Create an invalid transaction missing required fields
const invalidTxn = {
to: "0x1234567890123456789012345678901234567890",
// Missing other required fields
};

const response = await request(app)
.post("/send-txn")
.send({ txn: invalidTxn })
.expect("Content-Type", /json/)
.expect(500);

expect(response.body).toHaveProperty("error");
});
});

0 comments on commit c7232e5

Please sign in to comment.