-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pkp helper v2 on datil dev only + working on gas subsidy endpoint (#65)
* 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
Showing
5 changed files
with
323 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}`, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); | ||
}); |