Skip to content

Instantly share code, notes, and snippets.

@tolicodes
Last active March 4, 2022 19:42
Show Gist options
  • Save tolicodes/995b6eb5f6820d8928277411f9bef6b4 to your computer and use it in GitHub Desktop.
Save tolicodes/995b6eb5f6820d8928277411f9bef6b4 to your computer and use it in GitHub Desktop.
OpenGSN
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@opengsn/contracts/src/BaseRelayRecipient.sol";
// This is done so that OpenSea can cover Matic gas costs: https://docs.opensea.io/docs/polygon-basic-integration
// we need to figure out how to do this ourselves for the minting step
// https://docs.openzeppelin.com/learn/sending-gasless-transactions#what-is-a-meta-tx
import "./MetaTransactions.sol";
// We are using OpenGSN to do the transactions (gasless to the customer)
contract Collection is Ownable, IERC721, ERC721Enumerable, ERC2981, BaseRelayRecipient, ContextMixin, NativeMetaTransaction {
// string functionality
using Strings for uint256;
using Strings for uint32;
event ClaimIdWhitelisted(
string claimId,
address toAddress,
uint32 amount
);
event ClaimIdMinted(
string claimId,
address toAddress,
uint32 amount
);
uint256 private _nextTokenId = 0;
uint32 private _whitelistedTotal = 0;
string private _btURI;
// URI for collection metadata
string private _cURI;
// tracking whitelisting
mapping(string => uint32) whitelistedClaimIds; // track whitelist amount for claim
mapping(address => uint32) whitelistPerAddress; // track whitelisted amount per address
mapping(address => mapping(string => bool)) whitelistedAddressToClaimIdUsed; // ensures no dupes
// tracking minting
mapping(string => uint32) mintedClaimIds;
mapping(address => uint32) mintPerAddress;
// 0 means collection is NOT revealed, other numbers meen it is
uint256 public startingIndex = 0;
// the version of the recepient (the collection contract)
// should we same version number as Collection#versionPaymaster
string public override versionRecipient = "2.2.3+irelayrecipient";
// max amount of token
uint32 public immutable supplyCap;
// proves that metadata is valid (ex: like an MD5 hash of the metadata json)
string public metadataProofHash;
constructor(
// Name of collection (ex: Toli Collection)
string memory _name,
// Max amount of that token (ex: 10000)
uint32 _supplyCap,
// proves that metadata is valid (ex: like an MD5 hash of the metadata json)
string memory _metadataProofHash,
// URI for collection metadata (ex: https://xyz.com/metadata/x102129)
string memory __cURI,
// GSN Forwarder address for gasless txns
address _forwarder,
// Royalty recipient address
address _initialRoyaltyRecipient,
// Royalty percentage
uint96 _initialFraction
)
ERC721(_name, "")
{
supplyCap = _supplyCap;
metadataProofHash = _metadataProofHash;
_cURI = __cURI;
//
// Royalties
//
if (_initialFraction > 0) {
_setDefaultRoyalty(_initialRoyaltyRecipient, _initialFraction);
}
//
// OpenGSN
//
_setTrustedForwarder(_forwarder);
}
function contractURI() public view returns (string memory) {
return string(abi.encodePacked(_cURI, ".json"));
}
function whitelistClaimAmount(
address[] memory customerAddressesArr,
string memory claimId,
uint32 amount
) external onlyOwner {
if (whitelistedClaimIds[claimId] != 0) {
require(
whitelistedClaimIds[claimId] == amount,
string(
abi.encodePacked(
"collection already whitelisted for ",
whitelistedClaimIds[claimId].toString(),
" attempted amount of ",
amount.toString(),
" does not match"
)
)
);
} else {
require(
_whitelistedTotal + amount <= supplyCap,
"collection minting cap reached, the amount requested for whitelist would exceed the cap"
);
whitelistedClaimIds[claimId] = amount;
_whitelistedTotal = _whitelistedTotal + amount;
}
for (uint i = 0; i < customerAddressesArr.length; i++) {
address customerAddress = customerAddressesArr[i];
// only whitelist for this address if claim not already whitelisted for this address
if (!whitelistedAddressToClaimIdUsed[customerAddress][claimId]) {
whitelistedAddressToClaimIdUsed[customerAddress][claimId] = true;
emit ClaimIdWhitelisted(claimId, customerAddress, amount);
whitelistPerAddress[customerAddress] += amount;
}
}
}
function whitelistedClaimIdAmount(string memory claimId)
public
view
returns (uint32)
{
return whitelistedClaimIds[claimId];
}
function mintedClaimIdAmount(string memory claimId)
public
view
returns (uint32)
{
return mintedClaimIds[claimId];
}
function whitelistedAmount(address customerAddress) public view returns (uint32) {
return whitelistPerAddress[customerAddress];
}
function mintedAmount(address customerAddress) public view returns (uint32) {
return mintPerAddress[customerAddress];
}
function mintableAmount(address customerAddress) public view returns (uint32) {
uint32 _mintedAmount = mintedAmount(customerAddress);
uint32 _whitelistedAmount = whitelistedAmount(customerAddress);
if (_mintedAmount > _whitelistedAmount) {
return 0;
} else {
return whitelistedAmount(customerAddress) - mintedAmount(customerAddress);
}
}
function canMintAmount(address customerAddress, uint32 amount)
public
view
returns (bool)
{
return mintableAmount(customerAddress) >= amount;
}
function mint(uint32 amount, string memory claimId) external {
address customerAddress = _msgSender();
require(
canMintAmount(customerAddress, amount),
"requested mint amount exceeds requesting address's whitelisted amount"
);
require(
totalSupply() + amount <= supplyCap,
"collection minting cap reached"
);
require(
whitelistedClaimIds[claimId] != 0,
"Collection: claim ID has not been whitelisted"
);
require(
whitelistedClaimIds[claimId] == amount,
string(
abi.encodePacked(
"Collection: incorrect amount for claim ID, received ",
amount.toString(),
" but expected ",
whitelistedClaimIds[claimId].toString()
)
)
);
require(
mintedClaimIds[claimId] == 0,
"Collection: claim ID has already been minted"
);
// for tracking association to transaction, we emit this event
emit ClaimIdMinted(claimId, customerAddress, amount);
mintedClaimIds[claimId] = amount;
for (uint256 i = 0; i < amount; i++) {
_nextTokenId++; // this begins token IDs at 1
mintPerAddress[customerAddress]++;
_safeMint(customerAddress, _nextTokenId);
}
}
/**
* @dev Finalize starting index
*/
function reveal(string memory baseTokenURI) external onlyOwner {
require(!isRevealed(), "Collection: already revealed");
_btURI = baseTokenURI;
startingIndex = uint256(blockhash(block.number - 1)) % supplyCap;
// Prevent default sequence
if (startingIndex == 0) {
startingIndex = 1;
}
}
/**
* @dev Returns true if the metadata starting index has been revealed
*/
function isRevealed() public view returns (bool) {
return startingIndex > 0;
}
function walletOfOwner(address _owner)
external
view
returns (uint256[] memory)
{
uint256 ownerTokenCount = balanceOf(_owner);
uint256[] memory tokenIds = new uint256[](ownerTokenCount);
for (uint256 i; i < ownerTokenCount; i++) {
tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokenIds;
}
function tokenURI(uint256 tokenId)
public
view
virtual
override
returns (string memory)
{
require(
_exists(tokenId),
"ERC721Metadata: URI query for nonexistent token"
);
if (!isRevealed()) {
return
string(
abi.encodePacked(
_cURI,
"/",
tokenId.toString(),
"/placeholder.json"
)
);
}
// offset of 1 because token IDs start at 1, not 0
uint256 trueIndex = ((startingIndex + tokenId - 1) % supplyCap) + 1;
return
string(
abi.encodePacked(_btURI, "/", trueIndex.toString(), ".json")
);
}
//
// Needed for ERC2981 (Royalties standard)
//
function updateRoyalty(address recipient, uint96 fraction) external onlyOwner {
if (fraction > 0) {
_setDefaultRoyalty(recipient, fraction);
} else {
_deleteDefaultRoyalty();
}
_setDefaultRoyalty(recipient, fraction);
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721Enumerable, ERC2981) returns (bool) {
return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
}
/**
* Override isApprovedForAll to auto-approve OS's proxy contract
*/
function isApprovedForAll(address _owner, address _operator)
public
view
override(ERC721, IERC721)
returns (bool isOperator)
{
// if OpenSea's ERC721 Proxy Address is detected, auto-return true
if (_operator == address(0x58807baD0B376efc12F5AD86aAc70E78ed67deaE)) {
return true;
}
// otherwise, use the default ERC721.isApprovedForAll()
return ERC721.isApprovedForAll(_owner, _operator);
}
/**
* @dev Withdrawal implemented for safety reasons in case funds are
* accidentally sent to the contract
*/
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
payable(owner()).transfer(balance);
}
function _msgSender()
internal
view
override(Context, BaseRelayRecipient)
returns (address)
{
return BaseRelayRecipient._msgSender();
}
function _msgData()
internal
view
override(Context, BaseRelayRecipient)
returns (bytes memory)
{
return BaseRelayRecipient._msgData();
}
}
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { ethers } from "hardhat";
// eslint-disable-next-line camelcase, node/no-missing-import
import { NovelCollection, NovelCollection__factory } from "../typechain";
const COLLECTION_NAME = "Cats";
const MAX_QUANTITY = 1000;
const HASH_PROOF = "0x523abcde";
const CONTRACT_URI = "https://metadata-url.com/my-metadata";
describe("NovelCollection", function () {
// eslint-disable-next-line camelcase
let NovelCollectionFactory: NovelCollection__factory;
let novelCollection: NovelCollection;
let owner: SignerWithAddress;
// eslint-disable-next-line no-unused-vars
let creator: SignerWithAddress;
let user1: SignerWithAddress;
let user2: SignerWithAddress;
let user3: SignerWithAddress;
beforeEach(async () => {
NovelCollectionFactory = (await ethers.getContractFactory(
"NovelCollection",
)) as NovelCollection__factory;
novelCollection = await NovelCollectionFactory.deploy(
COLLECTION_NAME,
MAX_QUANTITY,
HASH_PROOF,
CONTRACT_URI,
ethers.constants.AddressZero,
ethers.constants.AddressZero,
0,
);
await novelCollection.deployed();
[owner, creator, user1, user2, user3] = await ethers.getSigners();
});
it("sets initialization parameters", async function () {
expect(await novelCollection.owner()).to.equal(owner.address);
expect(await novelCollection.name()).to.equal(COLLECTION_NAME);
expect(await novelCollection.supplyCap()).to.equal(MAX_QUANTITY);
expect(await novelCollection.metadataProofHash()).to.equal(HASH_PROOF);
});
describe("Token URI", function () {
it("returns a placeholder URI before reveal", async function () {
const claimId = `${Math.random()}`.slice(2);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
1,
);
await whitelistTx.wait();
const mintTx = await novelCollection
.connect(user1)
.mint(1, claimId);
await mintTx.wait();
expect(await novelCollection.isRevealed()).to.equal(false);
expect(await novelCollection.tokenURI(1)).to.contain(
"placeholder.json",
);
});
it("returns actual URI after reveal", async function () {
const claimId = `${Math.random()}`.slice(2);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
1,
);
await whitelistTx.wait();
const mintTx = await novelCollection
.connect(user1)
.mint(1, claimId);
await mintTx.wait();
expect(await novelCollection.isRevealed()).to.equal(false);
const revealTx = await novelCollection.reveal(
"https://realurl.com",
);
await revealTx.wait();
const startingIndex = await novelCollection.startingIndex();
const tokenIndex = 1;
const uri = await novelCollection.tokenURI(tokenIndex);
expect(uri).to.include("https://realurl.com/");
expect(uri).to.equal(
`https://realurl.com/${
(startingIndex.toNumber() + tokenIndex) % MAX_QUANTITY
}.json`,
);
expect(await novelCollection.isRevealed()).to.equal(true);
});
});
describe("Minting and Whitelist", function () {
it("allows the whitelisted user to mint", async function () {
expect(await novelCollection.balanceOf(user1.address)).to.equal(0);
const claimId = `${Math.random()}`.slice(2);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
1,
);
await whitelistTx.wait();
const mintTx = await novelCollection
.connect(user1)
.mint(1, claimId);
await mintTx.wait();
expect(await novelCollection.balanceOf(user1.address)).to.equal(1);
});
it("returns whitelist status", async function () {
expect(
await novelCollection.canMintAmount(user1.address, 1),
).to.equal(false);
expect(
await novelCollection.canMintAmount(user2.address, 1),
).to.equal(false);
expect(
await novelCollection.canMintAmount(user3.address, 1),
).to.equal(false);
const claimId = `${Math.random()}`.slice(2);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
3,
);
await whitelistTx.wait();
await expect(
novelCollection.connect(user1).mint(1, claimId),
).to.be.revertedWith(
"NovelCollection: incorrect amount for claim ID",
);
const mintTx = await novelCollection
.connect(user1)
.mint(3, claimId);
await mintTx.wait();
expect(
await novelCollection.canMintAmount(user1.address, 3),
).to.equal(false);
expect(
await novelCollection.canMintAmount(user1.address, 0),
).to.equal(true);
expect(
await novelCollection.canMintAmount(user2.address, 1),
).to.equal(false);
});
it("removes whitelist status after minting", async function () {
expect(
await novelCollection.canMintAmount(user1.address, 2),
).to.equal(false);
expect(
await novelCollection.canMintAmount(user2.address, 3),
).to.equal(false);
expect(
await novelCollection.canMintAmount(user3.address, 4),
).to.equal(false);
const claimId = `${Math.random()}`.slice(2);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
2,
);
await whitelistTx.wait();
const mint1Tx = await novelCollection
.connect(user1)
.mint(2, claimId);
await mint1Tx.wait();
expect(
await novelCollection.canMintAmount(user1.address, 1),
).to.equal(false);
});
it("succesful no-op if whitelist for claim ID already done previously", async function () {
expect(await novelCollection.balanceOf(user1.address)).to.equal(0);
const claimId = `${Math.random()}`.slice(2);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
1,
);
await whitelistTx.wait();
await expect(
novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
1,
),
).to.not.be.reverted;
});
it("fails to whitelist if claim ID used with a different amount previously", async function () {
expect(await novelCollection.balanceOf(user1.address)).to.equal(0);
const claimId = `${Math.random()}`.slice(2);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
1,
);
await whitelistTx.wait();
await expect(
novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
2,
),
).to.be.revertedWith(
"collection already whitelisted for 1 attempted amount of 2 does not match",
);
});
it("returns claimId status", async function () {
const claimId = `${Math.random()}`.slice(2);
expect(
await novelCollection.whitelistedClaimIdAmount(claimId),
).to.equal(0);
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
3,
);
await whitelistTx.wait();
expect(
await novelCollection.whitelistedClaimIdAmount(claimId),
).to.equal(3);
});
it("does not allow total cap to be exceeded", async function () {
novelCollection = await NovelCollectionFactory.deploy(
COLLECTION_NAME,
20,
HASH_PROOF,
CONTRACT_URI,
ethers.constants.AddressZero,
ethers.constants.AddressZero,
0,
);
await novelCollection.deployed();
const claimId = `${Math.random()}`.slice(2);
const whitelistTx1 = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
20,
);
await whitelistTx1.wait();
await expect(
novelCollection.whitelistClaimAmount(
[user2.address],
`${Math.random()}`.slice(2),
1,
),
).to.be.revertedWith(
"collection minting cap reached, the amount requested for whitelist would exceed the cap",
);
await expect(
novelCollection.connect(user1).mint(17, claimId),
).to.be.revertedWith(
"NovelCollection: incorrect amount for claim ID",
);
const mint1Tx = await novelCollection
.connect(user1)
.mint(20, claimId);
await mint1Tx.wait();
expect(await novelCollection.balanceOf(user1.address)).to.equal(20);
});
it("enforces whitelist limits", async function () {
const claimId = `${Math.random()}`.slice(2);
novelCollection = await NovelCollectionFactory.deploy(
COLLECTION_NAME,
20,
HASH_PROOF,
CONTRACT_URI,
ethers.constants.AddressZero,
ethers.constants.AddressZero,
0,
);
await novelCollection.deployed();
const whitelistTx = await novelCollection.whitelistClaimAmount(
[user1.address],
claimId,
2,
);
await whitelistTx.wait();
await expect(
novelCollection.connect(user1).mint(3, claimId),
).to.be.revertedWith(
"requested mint amount exceeds requesting address's whitelisted amount",
);
await expect(
novelCollection.connect(user2).mint(4, claimId),
).to.be.revertedWith(
"requested mint amount exceeds requesting address's whitelisted amount",
);
await expect(
novelCollection.connect(user3).mint(5, claimId),
).to.be.revertedWith(
"requested mint amount exceeds requesting address's whitelisted amount",
);
const mint1Tx = await novelCollection
.connect(user1)
.mint(2, claimId);
await mint1Tx.wait();
expect(await novelCollection.balanceOf(user1.address)).to.equal(2);
expect(
await novelCollection.canMintAmount(user1.address, 1),
).to.equal(false);
});
});
});
import { task } from "hardhat/config";
import { verifyCollection } from "./collection/verifyCollection";
task("verify-collection", "Verifies the collection on Polygon Scan")
.addParam("address", "path to the JSON for this collection")
.setAction(async ({ address }, hre) => {
await verifyCollection({
hre,
collectionAddress: address,
fetchFromFile: true,
});
});
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types";
import { AppEnvEnum } from "@novel/shared/utils/env";
import { NovelPaymaster__factory } from "../../typechain/factories/NovelPaymaster__factory";
import { getForwarderAddress, getRelayHubAddress } from "../getConstants";
import { getNovelPaymasterFactory, writePaymasterToFile } from "./getPaymaster";
export type PaymasterConstructorParams = {
relayHubAddress?: string;
gsnTrustedForwarderAddress?: string;
};
export interface IDeployParams extends PaymasterConstructorParams {
ethers?: HardhatEthersHelpers;
NovelPaymaster?: NovelPaymaster__factory;
env: AppEnvEnum;
}
export interface IDeployPaymasterOutput extends PaymasterConstructorParams {
paymasterAddress: string;
}
// Deploys Paymaster
export const deployPaymaster = async ({
env,
ethers,
// Paymaster funds transactions
// ethers interacts with ethereum provider
// hardhat makes web3 available without a browser
// also has a local blockchain network to test w/o
// testnet
NovelPaymaster,
// Relay hub is starting the transaction (asks the NovelPaymaster if the payment should
// go through (preRelayedCall), and then passes on the transaction to the forwarder
// The NovelPaymaster holds and hands out the gas needed for the transactions
relayHubAddress = getRelayHubAddress(),
gsnTrustedForwarderAddress = getForwarderAddress(),
}: IDeployParams) => {
console.debug("deployPaymaster - Constructor Params", {
relayHubAddress,
gsnTrustedForwarderAddress,
});
if (ethers) {
NovelPaymaster = (await getNovelPaymasterFactory(
ethers,
)) as NovelPaymaster__factory;
} else {
if (!NovelPaymaster) {
throw new Error("ethers or NovelPaymaster factory is required");
}
}
console.debug("deployPaymaster - Deploying Paymaster");
const paymaster = await NovelPaymaster!.deploy(
relayHubAddress,
gsnTrustedForwarderAddress,
);
console.debug("deployPaymaster - Waiting for Paymaster to be Deployed");
await paymaster.deployTransaction.wait(6); // ensures verification will work
const paymasterAddress = paymaster.address;
const deployOutput = {
paymasterAddress,
relayHubAddress,
gsnTrustedForwarderAddress,
};
// if the deploy is successful, we write the output to a file,
// so that we don't deploy multiple paymasters
// in production, this will be stored in a DB or secrets manager
writePaymasterToFile(env, deployOutput);
console.debug("deployPaymaster - Paymaster deployed", {
paymasterAddress,
});
return paymaster;
};
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types";
import { NovelPaymaster__factory } from "../../typechain/factories/NovelPaymaster__factory";
import { getPaymaster } from "./getPaymaster";
import { ethers as ethersUtils } from "ethers";
import { AppEnvEnum } from "@novel/shared/utils/env";
import { getGasParams } from "../getConstants";
interface IFillPaymasterParams {
env: AppEnvEnum;
ethers: HardhatEthersHelpers;
NovelPaymaster?: NovelPaymaster__factory;
isTestnet?: boolean;
ethToTransfer: string;
paymasterAddress?: string;
}
// Refills paymaster gas
export const fillPaymaster = async ({
env,
ethers,
// How much gas to put in, in eth
ethToTransfer,
paymasterAddress,
isTestnet = !!process.env.TESTNET,
}: IFillPaymasterParams) => {
const paymaster = await getPaymaster({
env,
ethers,
paymasterAddress,
});
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`;
paymasterAddress = paymaster.address;
const amountToSendInWei = ethersUtils.utils.parseUnits(
ethToTransfer,
"ether",
);
const amountToTransferAsHex = amountToSendInWei.toHexString();
console.debug(`fillPaymaster - sending to ${paymasterType} paymaster`, {
paymasterAddress,
amountToSendInWei,
amountToTransferAsHex,
});
// pulls from accounts (usually we have one, this just takes
// the last one)
const lastSigner = (await ethers.getSigners()).pop();
if (!lastSigner) throw new Error("no signers available");
const transferMoneyTxnParams = {
to: paymasterAddress,
value: amountToTransferAsHex,
// eslint-disable-next-line node/no-unsupported-features/es-syntax
...getGasParams(),
};
console.debug("fillPaymaster - sending Transaction - transferMoney", {
transferMoneyTxnParams,
});
const transferMoneyTxn = await lastSigner.sendTransaction(
transferMoneyTxnParams,
);
console.debug("fillPaymaster - Waiting for transferMoneyTxn to complete");
await transferMoneyTxn.wait();
console.debug(
`fillPaymaster - transferMoneyTxn sending ${ethToTransfer} MATIC to ${paymasterType} was successful!`,
);
return true;
};
const {
TESTNET,
GSN_TRUSTED_FORWARDER_ADDRESS_MUMBAI,
GSN_TRUSTED_FORWARDER_ADDRESS_POLYGON,
RELAY_HUB_ADDRESS_MUMBAI,
RELAY_HUB_ADDRESS_POLYGON,
ETH_RPC_URL,
ETH_TESTNET_RPC_URL,
ETH_WEBSOCKET_URL,
ETH_TESTNET_WEBSOCKET_URL,
ETH_PRIVATE_KEY,
POLYSCAN_API_KEY,
} = process.env;
export const getForwarderAddress = (): string => {
return TESTNET
? GSN_TRUSTED_FORWARDER_ADDRESS_MUMBAI!
: GSN_TRUSTED_FORWARDER_ADDRESS_POLYGON!;
};
export const getRelayHubAddress = (): string => {
return TESTNET ? RELAY_HUB_ADDRESS_MUMBAI! : RELAY_HUB_ADDRESS_POLYGON!;
};
export const getRpcUrl = (): string => {
return TESTNET ? ETH_TESTNET_RPC_URL! : ETH_RPC_URL!;
};
export const getWebsocketUrl = (): string => {
return TESTNET ? ETH_TESTNET_WEBSOCKET_URL! : ETH_WEBSOCKET_URL!;
};
export const getWalletAccounts = (): string[] => {
return ETH_PRIVATE_KEY ? [ETH_PRIVATE_KEY] : [];
};
export const getPolyscanApiKey = (): string => {
return POLYSCAN_API_KEY!;
};
export const getGasParams = () => ({
gasLimit: 10e6,
});
export async function getCurrentGasPricesInGwei() {
const response: {
safeLow: number;
standard: number;
fast: number;
fastest: number;
blockTime: number;
blockNumber: number;
// eslint-disable-next-line no-undef
} = await fetch("https://gasstation-mainnet.matic.network").then(
(response) => response.json(),
);
return response;
}
import { join } from "path";
import { existsSync, readFileSync, writeFileSync } from "fs";
import { NovelPaymaster__factory } from "../../typechain/factories/NovelPaymaster__factory";
import { IDeployPaymasterOutput } from "./deployPaymaster";
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types";
import { AppEnvEnum } from "@novel/shared/utils/env";
const { TESTNET } = process.env;
// get the path for paymaster details on local file system
// in prod, this will be in a DB or secrets manger (K8S)
export const getPaymasterJsonPath = (env: AppEnvEnum) => {
const fileName = `paymaster.${env}.${TESTNET ? "mumbai" : "polygon"}.json`;
console.log(`loading file ${fileName}`);
return join(
__dirname,
"..",
"..",
"..",
"shared",
"paymasterData",
fileName,
);
};
export const getNovelPaymasterFactory = (ethers?: HardhatEthersHelpers) => {
if (!ethers) {
throw new Error("ethers required to fetch factory from file");
}
return ethers!.getContractFactory(
"NovelPaymaster",
) as Promise<NovelPaymaster__factory>;
};
// Gets the paymaster ethers object from json file or passed in address
export const getPaymasterFromFile = async (
env: AppEnvEnum,
ethers?: HardhatEthersHelpers,
) => {
const path = getPaymasterJsonPath(env);
if (!existsSync(path)) {
throw new Error(
"Could not find paymaster.json - try running hardhat deploy-paymaster",
);
}
const json = readFileSync(path, "utf8");
const paymasterData = JSON.parse(json);
const paymasterAddress = paymasterData.paymasterAddress;
// get the paymaster contract ethers object
const paymaster = await getPaymasterByAddress({
ethers,
paymasterAddress,
});
return paymaster;
};
interface IGetPaymasterByAddressParams {
ethers?: HardhatEthersHelpers;
NovelPaymaster?: NovelPaymaster__factory;
paymasterAddress: string;
}
export const getPaymasterByAddress = async ({
ethers,
NovelPaymaster,
paymasterAddress,
}: IGetPaymasterByAddressParams) => {
NovelPaymaster =
NovelPaymaster ||
((await getNovelPaymasterFactory(ethers)) as NovelPaymaster__factory);
const paymaster = await NovelPaymaster!.attach(paymasterAddress);
return paymaster;
};
export const writePaymasterToFile = (
env: AppEnvEnum,
deployOutput: IDeployPaymasterOutput,
): void => {
const path = getPaymasterJsonPath(env);
writeFileSync(
path,
JSON.stringify(
{
paymasterAddress: deployOutput.paymasterAddress.toLowerCase(),
relayHubAddress: deployOutput.relayHubAddress!.toLowerCase(),
gsnTrustedForwarderAddress:
deployOutput.gsnTrustedForwarderAddress!.toLowerCase(),
},
null,
2,
),
);
console.debug(`Wrote paymaster JSON to ${path}`);
};
interface IGetPaymasterParams {
env: AppEnvEnum;
ethers?: HardhatEthersHelpers;
NovelPaymaster?: NovelPaymaster__factory;
paymasterAddress?: string;
}
export const getPaymaster = async ({
env,
ethers,
NovelPaymaster,
paymasterAddress,
}: IGetPaymasterParams) => {
let paymaster;
if (ethers) {
paymaster = await getPaymasterFromFile(env, ethers);
} else {
if (!paymasterAddress) {
throw new Error("ethers or paymasterAddress required");
}
paymaster = await getPaymasterByAddress({
NovelPaymaster,
paymasterAddress,
});
}
return paymaster;
};
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types";
import { AppEnvEnum } from "@novel/shared/utils/env";
import { getPaymaster } from "./getPaymaster";
import { ethers as ethersLib } from "ethers";
interface IGetPaymasterBalance {
env: AppEnvEnum;
ethers?: HardhatEthersHelpers;
paymasterAddress?: string;
isTestnet?: boolean;
}
// Get paymaster balance
export const getPaymasterBalance = async ({
env,
ethers,
paymasterAddress,
isTestnet = !!process.env.TESTNET,
}: IGetPaymasterBalance): Promise<string> => {
const paymaster = await getPaymaster({
env,
ethers,
paymasterAddress,
});
paymasterAddress = paymaster.address;
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`;
console.debug(
`getBalance - Checking ${paymasterType} ${paymasterAddress} paymaster balance`,
);
const balanceInMatic = ethersLib.utils.formatEther(
await paymaster.getRelayHubDeposit({
gasLimit: 1e6,
}),
);
console.debug(
`getBalance - ${paymasterType} paymaster MATIC Balance:`,
balanceInMatic,
);
return balanceInMatic;
};
import "../backend/src/env-config";
import "@nomiclabs/hardhat-waffle";
import "@nomiclabs/hardhat-etherscan";
import "@nomiclabs/hardhat-ethers";
import "hardhat-deploy-ethers";
import "@typechain/hardhat";
import "hardhat-gas-reporter";
import "solidity-coverage";
import path from "path";
import { subtask } from "hardhat/config";
import { TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD } from "hardhat/builtin-tasks/task-names";
import { getRpcUrl, getWalletAccounts } from "./scripts/getConstants";
const { POLYSCAN_API_KEY } = process.env;
// compilation must be complete before these scripts work
if (!process.env.COMPILING) {
// eslint-disable-next-line node/no-missing-require
require("./scripts/paymaster");
// eslint-disable-next-line node/no-missing-require
require("./scripts/collection");
}
// yanked straight from here: https://github.com/fvictorio/hardhat-examples/blob/master/custom-solc/hardhat.config.js
subtask(
TASK_COMPILE_SOLIDITY_GET_SOLC_BUILD,
async (args: { solcVersion: string }, hre, runSuper) => {
if (args.solcVersion === "0.8.4") {
const compilerPath = path.join(
__dirname,
"soljson-v0.8.4+commit.c7e474f2.js",
);
return {
compilerPath,
isSolcJs: true, // if you are using a native compiler, set this to false
version: args.solcVersion,
// this is used as extra information in the build-info files, but other than
// that is not important
longVersion: "0.8.4+commit.c7e474f2",
};
}
// we just use the default subtask if the version is not 0.8.5
return runSuper();
},
);
export default {
solidity: {
version: "0.8.4", // needs to stay fixed, set in Dockerfile
settings: {
optimizer: {
enabled: true,
runs: 2000,
},
},
},
networks: {
rpc: {
url: getRpcUrl(),
accounts: getWalletAccounts(),
},
},
gasReporter: {
enabled: process.env.ETH_REPORT_GAS !== undefined,
currency: "USD",
token: "MATIC",
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
gasPriceApi:
"https://api.polygonscan.com/api?module=proxy&action=eth_gasPrice",
},
rpc: {
// no limit for deploying
// money is no object
txfeecap: 0,
},
// support for verifying my contract
etherscan: {
apiKey: POLYSCAN_API_KEY,
},
};
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
// This is done so that OpenSea can cover Matic gas costs: https://docs.opensea.io/docs/polygon-basic-integration
// we need to figure out how to do this ourselves for the minting step
// https://docs.openzeppelin.com/learn/sending-gasless-transactions#what-is-a-meta-tx
/**
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/ContextMixin.sol
*/
abstract contract ContextMixin {
function msgSender()
internal
view
returns (address payable sender)
{
if (msg.sender == address(this)) {
bytes memory array = msg.data;
uint256 index = msg.data.length;
assembly {
// Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those.
sender := and(
mload(add(array, index)),
0xffffffffffffffffffffffffffffffffffffffff
)
}
} else {
sender = payable(msg.sender);
}
return sender;
}
}
/**
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/Initializable.sol
*/
contract Initializable {
bool inited = false;
modifier initializer() {
require(!inited, "already inited");
_;
inited = true;
}
}
/**
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/EIP712Base.sol
*/
contract EIP712Base is Initializable {
struct EIP712Domain {
string name;
string version;
address verifyingContract;
bytes32 salt;
}
string constant public ERC712_VERSION = "1";
bytes32 internal constant EIP712_DOMAIN_TYPEHASH = keccak256(
bytes(
"EIP712Domain(string name,string version,address verifyingContract,bytes32 salt)"
)
);
bytes32 internal domainSeperator;
// supposed to be called once while initializing.
// one of the contractsa that inherits this contract follows proxy pattern
// so it is not possible to do this in a constructor
function _initializeEIP712(
string memory name
)
internal
initializer
{
_setDomainSeperator(name);
}
function _setDomainSeperator(string memory name) internal {
domainSeperator = keccak256(
abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256(bytes(name)),
keccak256(bytes(ERC712_VERSION)),
address(this),
bytes32(getChainId())
)
);
}
function getDomainSeperator() public view returns (bytes32) {
return domainSeperator;
}
function getChainId() public view returns (uint256) {
uint256 id;
assembly {
id := chainid()
}
return id;
}
/**
* Accept message hash and returns hash message in EIP712 compatible form
* So that it can be used to recover signer from signature signed using EIP712 formatted data
* https://eips.ethereum.org/EIPS/eip-712
* "\\x19" makes the encoding deterministic
* "\\x01" is the version byte to make it compatible to EIP-191
*/
function toTypedMessageHash(bytes32 messageHash)
internal
view
returns (bytes32)
{
return
keccak256(
abi.encodePacked("\x19\x01", getDomainSeperator(), messageHash)
);
}
}
/**
* https://github.com/maticnetwork/pos-portal/blob/master/contracts/common/NativeMetaTransaction.sol
*/
contract NativeMetaTransaction is EIP712Base {
bytes32 private constant META_TRANSACTION_TYPEHASH = keccak256(
bytes(
"MetaTransaction(uint256 nonce,address from,bytes functionSignature)"
)
);
event MetaTransactionExecuted(
address userAddress,
address payable relayerAddress,
bytes functionSignature
);
mapping(address => uint256) nonces;
/*
* Meta transaction structure.
* No point of including value field here as if user is doing value transfer then he has the funds to pay for gas
* He should call the desired function directly in that case.
*/
struct MetaTransaction {
uint256 nonce;
address from;
bytes functionSignature;
}
function executeMetaTransaction(
address userAddress,
bytes memory functionSignature,
bytes32 sigR,
bytes32 sigS,
uint8 sigV
) public payable returns (bytes memory) {
MetaTransaction memory metaTx = MetaTransaction({
nonce: nonces[userAddress],
from: userAddress,
functionSignature: functionSignature
});
require(
verify(userAddress, metaTx, sigR, sigS, sigV),
"Signer and signature do not match"
);
// increase nonce for user (to avoid re-use)
nonces[userAddress] = nonces[userAddress] + 1;
emit MetaTransactionExecuted(
userAddress,
payable(msg.sender),
functionSignature
);
// Append userAddress and relayer address at the end to extract it from calling context
(bool success, bytes memory returnData) = address(this).call(
abi.encodePacked(functionSignature, userAddress)
);
require(success, "Function call not successful");
return returnData;
}
function hashMetaTransaction(MetaTransaction memory metaTx)
internal
pure
returns (bytes32)
{
return
keccak256(
abi.encode(
META_TRANSACTION_TYPEHASH,
metaTx.nonce,
metaTx.from,
keccak256(metaTx.functionSignature)
)
);
}
function getNonce(address user) public view returns (uint256 nonce) {
nonce = nonces[user];
}
function verify(
address signer,
MetaTransaction memory metaTx,
bytes32 sigR,
bytes32 sigS,
uint8 sigV
) internal view returns (bool) {
require(signer != address(0), "NativeMetaTransaction: INVALID_SIGNER");
return
signer ==
ecrecover(
toTypedMessageHash(hashMetaTransaction(metaTx)),
sigV,
sigR,
sigS
);
}
}
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
// SPDX-License-Identifier: MIT OR Apache-2.0
import "@opengsn/contracts/src/BasePaymaster.sol";
import "@opengsn/contracts/src/interfaces/IRelayHub.sol";
import "./Collection.sol";
contract Paymaster is BasePaymaster {
constructor(address _relayHubAddress, address _forwarderAddress) {
setRelayHub(IRelayHub(_relayHubAddress));
setTrustedForwarder(_forwarderAddress);
}
/**
* Withdrawal implemented for safety reasons in case funds are
* accidentally sent to the contract
*/
function withdraw() external onlyOwner {
uint256 balance = address(this).balance;
payable(owner()).transfer(balance);
withdrawRelayHubDepositTo(
getRelayHubDeposit(),
payable(owner())
);
}
mapping(address => bool) public targetWhitelist;
// this is how we mark a contract as one we will pay for
function enableContract(address target) external onlyOwner {
targetWhitelist[target] = true;
}
function isEnabledContract(address target) external view returns (bool) {
if (targetWhitelist[target]) {
return true;
} else {
return false;
}
}
function preRelayedCall(
GsnTypes.RelayRequest calldata relayRequest,
bytes calldata signature,
bytes calldata approvalData,
uint256 maxPossibleGas
) external virtual override returns (bytes memory context, bool) {
_verifyForwarder(relayRequest);
(signature, approvalData, maxPossibleGas);
require(
targetWhitelist[
relayRequest.request.to
],
string(
abi.encodePacked(
"target contract address ",
toString(relayRequest.request.to),
"is not whitelisted"
)
)
);
// Only work on mint function
Collection collection = Collection(
relayRequest.request.to
);
bytes4 methodSig = GsnUtils.getMethodSig(relayRequest.request.data);
require(
methodSig == collection.mint.selector,
string(
abi.encodePacked(
"attempted to call method with signature ",
methodSig,
" you may only call the mint function"
)
)
);
uint32 quantity = extractMintCountFromCall(relayRequest.request.data);
require(
collection.canMintAmount(relayRequest.request.from, quantity),
string(
abi.encodePacked(
"requested mint amount of ",
quantity,
" exceeds whitelisted balance"
)
)
);
// false as second arg means we don't cover the gas
return ("", false);
}
function postRelayedCall(
bytes calldata context,
bool success,
uint256 gasUseWithoutPost,
GsnTypes.RelayData calldata relayData
) external virtual override {
(context, success, gasUseWithoutPost, relayData);
}
function versionPaymaster()
external
view
virtual
override
returns (string memory)
{
return "2.2.3+paymaster";
}
function extractMintCountFromCall(bytes memory b)
internal
pure
returns (uint32)
{
uint32 number;
// if this doesn't work then try 6 instead of 4
for (uint8 i = 4; i < 8; i++) {
number = number + uint32(uint8(b[i]) * (2**(8 * (8 - (i + 1)))));
}
return number;
}
// below is just for logging address in case of error
function toString(address account) public pure returns(string memory) {
return toString(abi.encodePacked(account));
}
function toString(uint256 value) public pure returns(string memory) {
return toString(abi.encodePacked(value));
}
function toString(bytes32 value) public pure returns(string memory) {
return toString(abi.encodePacked(value));
}
function toString(bytes memory data) public pure returns(string memory) {
bytes memory alphabet = "0123456789abcdef";
bytes memory str = new bytes(2 + data.length * 2);
str[0] = "0";
str[1] = "x";
for (uint i = 0; i < data.length; i++) {
str[2+i*2] = alphabet[uint(uint8(data[i] >> 4))];
str[3+i*2] = alphabet[uint(uint8(data[i] & 0x0f))];
}
return string(str);
}
}
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { ethers } from "hardhat";
// eslint-disable-next-line camelcase, node/no-missing-import
import {
NovelPaymaster,
NovelPaymaster__factory,
NovelCollection__factory,
} from "../typechain";
const COLLECTION_NAME = "Cats";
const MAX_QUANTITY = 1000;
const HASH_PROOF = "0x523abcde";
const CONTRACT_URI = "https://metadata-url.com/my-metadata";
describe("NovelPaymaster", function () {
// eslint-disable-next-line camelcase
let NovelPaymasterFactory: NovelPaymaster__factory;
let novelPaymaster: NovelPaymaster;
// eslint-disable-next-line no-unused-vars
let owner: SignerWithAddress;
beforeEach(async () => {
NovelPaymasterFactory = (await ethers.getContractFactory(
"NovelPaymaster",
)) as NovelPaymaster__factory;
novelPaymaster = await NovelPaymasterFactory.deploy(
ethers.constants.AddressZero,
ethers.constants.AddressZero,
);
await novelPaymaster.deployed();
[owner] = await ethers.getSigners();
});
it("whitelisting in paymaster works", async function () {
const NovelCollectionFactory = (await ethers.getContractFactory(
"NovelCollection",
owner,
)) as NovelCollection__factory;
const novelCollection = await NovelCollectionFactory.deploy(
COLLECTION_NAME,
MAX_QUANTITY,
HASH_PROOF,
CONTRACT_URI,
ethers.constants.AddressZero,
ethers.constants.AddressZero,
0,
);
await novelCollection.deployed();
const isEnabledBefore = await novelPaymaster.isEnabledContract(
novelCollection.address,
);
expect(isEnabledBefore).to.equal(false);
await novelPaymaster.enableContract(novelCollection.address);
const isEnabledAfter = await novelPaymaster.isEnabledContract(
novelCollection.address,
);
expect(isEnabledAfter).to.equal(true);
});
});
// More info on GSN: https://docs.opengsn.org/#architecture
import { HardhatRuntimeEnvironment } from "hardhat/types/runtime";
import { task } from "hardhat/config";
import { deployPaymaster } from "./paymaster/deployPaymaster";
import { getPaymaster } from "./paymaster/getPaymaster";
import { verifyPaymaster } from "./paymaster/verifyPaymaster";
import { fillPaymaster } from "./paymaster/fillPaymaster";
import { withdrawFromPaymaster } from "./paymaster/withdrawFromPaymaster";
import { getPaymasterBalance } from "./paymaster/getPaymasterBalance";
task("deploy-paymaster", "Deploys the paymaster")
.addOptionalParam(
"verify",
"Should we verify the contract after deploying? (boolean)",
)
.addParam("env", "which environment to use for deploying the paymaster")
.setAction(async ({ verify: doVerify = true, env }, hre) => {
// check env value
if (!Object.values(AppEnvEnum).includes(env)) {
throw new Error(
`env value of ${env} does not match one of ${Object.values(
AppEnvEnum,
).join(", ")}`,
);
}
// run the deploy
const paymaster = await deployPaymaster({
ethers: hre.ethers,
env,
});
const paymasterAddress = paymaster.address;
// if we specify that we want to verify on Polygon scan,
// then do verification
if (!doVerify) {
return;
}
try {
await verifyPaymaster({
env,
hre,
paymasterAddress,
});
} catch (e) {}
});
task(
"verify-paymaster",
"Verify that a contract is whitelisted on the paymaster",
)
.addParam("env", "which environment to use for deploying the paymaster")
.setAction(async ({ env }, hre) => {
// check env value
if (!Object.values(AppEnvEnum).includes(env)) {
throw new Error(
`env value of ${env} does not match one of ${Object.values(
AppEnvEnum,
).join(", ")}`,
);
}
await verifyPaymaster({
env,
hre,
});
});
task("fill-paymaster", "Adds native token to paymaster")
.addParam(
"ethtotransfer",
"quantity in eth to add to the paymaster account - fractions allowed",
)
.addParam("env", "which environment to use for deploying the paymaster")
.setAction(
async ({ ethtotransfer, env }, hre: HardhatRuntimeEnvironment) => {
// check env value
if (!Object.values(AppEnvEnum).includes(env)) {
throw new Error(
`env value of ${env} does not match one of ${Object.values(
AppEnvEnum,
).join(", ")}`,
);
}
const ethToTransferAsFloat = parseFloat(ethtotransfer);
if (isNaN(ethToTransferAsFloat)) {
throw new Error("eth must be a number");
}
// $.02-ish at current pricing (minimum to run account)
if (ethToTransferAsFloat < 0.01)
throw new Error("Minimum amount is 0.01 ETH");
await fillPaymaster({
env,
ethers: hre.ethers,
ethToTransfer: ethtotransfer,
});
},
);
task("get-paymaster-balance", "Checks paymaster balance")
.addParam("env", "which environment to use for deploying the paymaster")
.setAction(async ({ env }, hre: HardhatRuntimeEnvironment) => {
// check env value
if (!Object.values(AppEnvEnum).includes(env)) {
throw new Error(
`env value of ${env} does not match one of ${Object.values(
AppEnvEnum,
).join(", ")}`,
);
}
await getPaymasterBalance({
env,
ethers: hre.ethers,
});
});
task("withdraw-from-paymaster", "Adds native token to paymaster")
.addParam("env", "which environment to use for deploying the paymaster")
.setAction(async ({ env }, hre: HardhatRuntimeEnvironment) => {
// check env value
if (!Object.values(AppEnvEnum).includes(env)) {
throw new Error(
`env value of ${env} does not match one of ${Object.values(
AppEnvEnum,
).join(", ")}`,
);
}
await withdrawFromPaymaster({
env,
ethers: hre.ethers,
});
});
task(
"check-collection-whitelisted",
"Check that a contract is whitelisted on the paymaster",
)
.addParam("address", "address of the contract")
.addParam("env", "which environment to use for deploying the paymaster")
.setAction(async ({ address, env }, hre: HardhatRuntimeEnvironment) => {
// check env value
if (!Object.values(AppEnvEnum).includes(env)) {
throw new Error(
`env value of ${env} does not match one of ${Object.values(
AppEnvEnum,
).join(", ")}`,
);
}
const paymaster = await getPaymaster({
env,
ethers: hre.ethers,
});
const isWhitelisted = await paymaster.isEnabledContract(address);
console.log("check-collection-whitelisted", {
isWhitelisted,
});
});
// Verifies contract with PolygonScan (adds green checkmark)
import { AppEnvEnum } from "@novel/shared/utils/env";
import { HardhatRuntimeEnvironment } from "hardhat/types/runtime";
import { getForwarderAddress, getRelayHubAddress } from "../getConstants";
import { getPaymaster } from "./getPaymaster";
interface IVerifyPaymasterParams {
env: AppEnvEnum;
hre: HardhatRuntimeEnvironment;
// the paymaster we want to verify on PolygonScan
paymasterAddress?: string;
relayHubAddress?: string;
gsnTrustedForwarderAddress?: string;
isTestnet?: boolean;
}
export const verifyPaymaster = async ({
env,
hre,
paymasterAddress,
relayHubAddress = getRelayHubAddress(),
gsnTrustedForwarderAddress = getForwarderAddress(),
isTestnet = !!process.env.TESTNET,
}: IVerifyPaymasterParams) => {
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`;
console.debug(
`verifyPaymaster - Verifying ${paymasterType} paymaster at`,
paymasterAddress,
);
const paymaster = await getPaymaster({
env,
ethers: hre.ethers,
paymasterAddress,
});
paymasterAddress = paymaster.address;
try {
// subtask provided by etherscan (built in)
await hre.run("verify:verify", {
address: paymasterAddress,
constructorArguments: [relayHubAddress, gsnTrustedForwarderAddress],
});
console.debug(
`verifyPaymaster - Verification of ${paymasterType} paymaster successful`,
);
} catch (e) {
const message = (e as Error).message;
// if already verified, that's fine, don't fail
// NOTE: annoyingly "similar match" is auto-generated from prior verified matching
// byte code (with different constructor arguments) and causes this to fail
// therefore it is impossible to achieve an "exact match"
// for a contract when there is previously verified matching byte code
if (message.includes("Reason: Already Verified")) {
console.debug(
`verifyPaymaster - Verification successful, ${paymasterType} paymaster already verified`,
);
} else {
console.warn(
`Could not verify ${paymasterType} paymaster`,
(e as Error).message,
);
throw e;
}
}
};
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types";
import { AppEnvEnum } from "@novel/shared/utils/env";
import { getPaymaster } from "./getPaymaster";
interface IWithdrawFromPaymasterParams {
env: AppEnvEnum;
ethers?: HardhatEthersHelpers;
paymasterAddress?: string;
isTestnet?: boolean;
}
// Withdraws from paymaster
export const withdrawFromPaymaster = async ({
env,
ethers,
// Address of the Paymaster
// If not specified, fills Paymaster from config file
paymasterAddress,
isTestnet = !!process.env.TESTNET,
}: IWithdrawFromPaymasterParams) => {
const paymaster = await getPaymaster({
env,
ethers,
paymasterAddress,
});
paymasterAddress = paymaster.address;
const paymasterType = `${env}:${isTestnet ? "testnet" : "mainnet"}`;
console.debug(
`withdrawFromPaymaster - withdrawing from ${paymasterType} paymaster`,
);
const withdrawTxn = await paymaster.withdraw();
console.debug("withdrawFromPaymaster - waiting on withdraw transaction");
await withdrawTxn.wait();
console.debug(
`withdrawFromPaymaster - withdraw from ${paymasterType} paymaster was successful!`,
);
return true;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment