feat: add `VerifyingPaymaster` contract

feat: add VerifyingPaymaster contract

diff --git a/.env.example b/.env.example
index c7eab5b..f74fd4f 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,3 @@
 THIRDWEB_API_KEY=
-PAYMASTER_SIGNER_KEY=
\ No newline at end of file
+PAYMASTER_SIGNER_KEY=
+CELOSCAN_KEY=
\ No newline at end of file
diff --git a/contracts/VerifyingPaymaster.sol b/contracts/VerifyingPaymaster.sol
new file mode 100644
index 0000000..5c945e7
--- /dev/null
+++ b/contracts/VerifyingPaymaster.sol
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-3.0
+pragma solidity ^0.8.12;
+
+/* solhint-disable reason-string */
+/* solhint-disable no-inline-assembly */
+
+import "@account-abstraction/contracts/core/BasePaymaster.sol";
+import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
+
+/**
+ * A sample paymaster that uses external service to decide whether to pay for the UserOp.
+ * The paymaster trusts an external signer to sign the transaction.
+ * The calling user must pass the UserOp to that external signer first, which performs
+ * whatever off-chain verification before signing the UserOp.
+ * Note that this signature is NOT a replacement for the account-specific signature:
+ * - the paymaster checks a signature to agree to PAY for GAS.
+ * - the account checks a signature to prove identity and account ownership.
+ */
+contract VerifyingPaymaster is BasePaymaster {
+    using ECDSA for bytes32;
+    using UserOperationLib for UserOperation;
+
+    address public immutable verifyingSigner;
+
+    uint256 private constant VALID_TIMESTAMP_OFFSET = 20;
+
+    uint256 private constant SIGNATURE_OFFSET = 84;
+
+    constructor(
+        IEntryPoint _entryPoint,
+        address _verifyingSigner
+    ) BasePaymaster(_entryPoint) {
+        verifyingSigner = _verifyingSigner;
+    }
+
+    mapping(address => uint256) public senderNonce;
+
+    function pack(
+        UserOperation calldata userOp
+    ) public pure returns (bytes memory ret) {
+        // lighter signature scheme. must match UserOp.ts#packUserOp
+        bytes calldata pnd = userOp.paymasterAndData;
+        // copy directly the userOp from calldata up to (but not including) the paymasterAndData.
+        // this encoding depends on the ABI encoding of calldata, but is much lighter to copy
+        // than referencing each field separately.
+        assembly {
+            let ofs := userOp
+            let len := sub(sub(pnd.offset, ofs), 32)
+            ret := mload(0x40)
+            mstore(0x40, add(ret, add(len, 32)))
+            mstore(ret, len)
+            calldatacopy(add(ret, 32), ofs, len)
+        }
+    }
+
+    /**
+     * return the hash we're going to sign off-chain (and validate on-chain)
+     * this method is called by the off-chain service, to sign the request.
+     * it is called on-chain from the validatePaymasterUserOp, to validate the signature.
+     * note that this signature covers all fields of the UserOperation, except the "paymasterAndData",
+     * which will carry the signature itself.
+     */
+    function getHash(
+        UserOperation calldata userOp,
+        uint48 validUntil,
+        uint48 validAfter
+    ) public view returns (bytes32) {
+        //can't use userOp.hash(), since it contains also the paymasterAndData itself.
+
+        return
+            keccak256(
+                abi.encode(
+                    pack(userOp),
+                    block.chainid,
+                    address(this),
+                    senderNonce[userOp.getSender()],
+                    validUntil,
+                    validAfter
+                )
+            );
+    }
+
+    /**
+     * verify our external signer signed this request.
+     * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params
+     * paymasterAndData[:20] : address(this)
+     * paymasterAndData[20:84] : abi.encode(validUntil, validAfter)
+     * paymasterAndData[84:] : signature
+     */
+    function _validatePaymasterUserOp(
+        UserOperation calldata userOp,
+        bytes32 /*userOpHash*/,
+        uint256 requiredPreFund
+    ) internal override returns (bytes memory context, uint256 validationData) {
+        (requiredPreFund);
+
+        (
+            uint48 validUntil,
+            uint48 validAfter,
+            bytes calldata signature
+        ) = parsePaymasterAndData(userOp.paymasterAndData);
+        //ECDSA library supports both 64 and 65-byte long signatures.
+        // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA"
+        require(
+            signature.length == 64 || signature.length == 65,
+            "VerifyingPaymaster: invalid signature length in paymasterAndData"
+        );
+        bytes32 hash = ECDSA.toEthSignedMessageHash(
+            getHash(userOp, validUntil, validAfter)
+        );
+        senderNonce[userOp.getSender()]++;
+
+        //don't revert on signature failure: return SIG_VALIDATION_FAILED
+        if (verifyingSigner != ECDSA.recover(hash, signature)) {
+            return ("", _packValidationData(true, validUntil, validAfter));
+        }
+
+        //no need for other on-chain validation: entire UserOp should have been checked
+        // by the external service prior to signing it.
+        return ("", _packValidationData(false, validUntil, validAfter));
+    }
+
+    function parsePaymasterAndData(
+        bytes calldata paymasterAndData
+    )
+        public
+        pure
+        returns (uint48 validUntil, uint48 validAfter, bytes calldata signature)
+    {
+        (validUntil, validAfter) = abi.decode(
+            paymasterAndData[VALID_TIMESTAMP_OFFSET:SIGNATURE_OFFSET],
+            (uint48, uint48)
+        );
+        signature = paymasterAndData[SIGNATURE_OFFSET:];
+    }
+}
diff --git a/hardhat.config.js b/hardhat.config.js
new file mode 100644
index 0000000..09713f3
--- /dev/null
+++ b/hardhat.config.js
@@ -0,0 +1,19 @@
+require("@nomicfoundation/hardhat-toolbox");
+require("dotenv").config();
+require("hardhat-celo");
+
+/** @type import('hardhat/config').HardhatUserConfig */
+module.exports = {
+    solidity: "0.8.17",
+    networks: {
+        alfajores: {
+            accounts: [process.env.PAYMASTER_SIGNER_KEY],
+            url: "https://alfajores-forno.celo-testnet.org",
+        },
+    },
+    etherscan: {
+        apiKey: {
+            alfajores: process.env.CELOSCAN_KEY,
+        },
+    },
+};
diff --git a/package.json b/package.json
index b38b15b..341e691 100644
--- a/package.json
+++ b/package.json
@@ -15,11 +15,28 @@
         "@thirdweb-dev/sdk": "0.0.0-dev-2067f33-20230609035419",
         "@thirdweb-dev/wallets": "0.0.0-dev-2067f33-20230609035419",
         "dotenv": "^16.0.3",
-        "ethers": "^5"
+        "ethers": "^5",
+        "@account-abstraction/contracts": "^0.6.0",
+        "@openzeppelin/contracts": "^4.9.2"
     },
     "devDependencies": {
         "@types/node": "^18.11.17",
         "ts-node": "^10.9.1",
-        "typescript": "^4.9.4"
+        "typescript": "^4.9.4",
+        "typechain": "^8.1.0",
+        "@nomicfoundation/hardhat-chai-matchers": "^1.0.0",
+        "@nomicfoundation/hardhat-network-helpers": "^1.0.0",
+        "@nomicfoundation/hardhat-toolbox": "^2.0.2",
+        "@nomiclabs/hardhat-ethers": "^2.0.0",
+        "@nomiclabs/hardhat-etherscan": "^3.0.0",
+        "@typechain/ethers-v5": "^10.1.0",
+        "@typechain/hardhat": "^6.1.2",
+        "@types/chai": "^4.2.0",
+        "@types/mocha": ">=9.1.0",
+        "chai": "^4.2.0",
+        "hardhat": "^2.16.1",
+        "hardhat-celo": "^0.0.4",
+        "hardhat-gas-reporter": "^1.0.8",
+        "solidity-coverage": "^0.8.1"
     }
 }
diff --git a/scripts/deploy.js b/scripts/deploy.js
new file mode 100644
index 0000000..ccb1ffe
--- /dev/null
+++ b/scripts/deploy.js
@@ -0,0 +1,27 @@
+// We require the Hardhat Runtime Environment explicitly here. This is optional

[... diff too long, it was truncated ...]

GitHub
sha: a550cbd46f197911ae8a210e080aa0b4af11b451