Skip to content

Commit

Permalink
docs: update & test paymaster frontend tutorial (#91)
Browse files Browse the repository at this point in the history
- fixes & updates the frontend paymaster tutorial
- adds a test
- adds synpress for metamask testing
  • Loading branch information
sarahschwartz authored Oct 16, 2024
1 parent 4016397 commit a8e7a6c
Show file tree
Hide file tree
Showing 57 changed files with 10,429 additions and 361 deletions.
27 changes: 19 additions & 8 deletions .github/workflows/playwright.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ on:

jobs:
test-tutorials:
timeout-minutes: 20
timeout-minutes: 12
runs-on: ubuntu-latest
strategy:
matrix:
tutorial:
- "tests/erc20-paymaster.spec.ts"
- "tests/how-to-test-contracts.spec.ts"
- "tests/daily-spend-limit.spec.ts"
- "tests/signing-txns-with-webauthn.spec.ts"
- "tests/native-aa-multisig.spec.ts"
- "erc20-paymaster"
- "how-to-test-contracts"
- "daily-spend-limit"
- "signing-txns-with-webauthn"
- "native-aa-multisig"
- "frontend-paymaster"

steps:
- uses: actions/checkout@v4
Expand All @@ -26,10 +27,20 @@ jobs:
run: bun playwright install chromium --with-deps
- name: Run Era Test Node
uses: dutterbutter/era-test-node-action@v1
- name: Create Metamask Cache
run: xvfb-run bun setup:mm
- name: Run test for ${{ matrix.tutorial }}
run: |
export TERM=xterm-256color
export COLUMNS=80
export LINES=24
bun test:github ${{ matrix.tutorial }}
xvfb-run bun test:headful tests/${{ matrix.tutorial }}.spec.ts
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.tutorial }}
path: |
${{ github.workspace }}/playwright-report/
retention-days: 10


1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ logs
tests-output
test-results
playwright-report
.cache-synpress

# Local env files
.env
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
.idea
public
**/*.md
tests-output
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions code/frontend-paymaster/contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
WALLET_PRIVATE_KEY=
ALLOWED_TOKEN=
114 changes: 114 additions & 0 deletions code/frontend-paymaster/contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.vscode

# hardhat artifacts
artifacts
cache

# zksync artifacts
artifacts-zk
cache-zk
deployments-zk

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port
1 change: 1 addition & 0 deletions code/frontend-paymaster/contracts/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legacy-peer-deps=true
18 changes: 18 additions & 0 deletions code/frontend-paymaster/contracts/contracts/Greeter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

contract Greeter {
string private greeting;

constructor(string memory _greeting) {
greeting = _greeting;
}

function greet() public view returns (string memory) {
return greeting;
}

function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
}
27 changes: 27 additions & 0 deletions code/frontend-paymaster/contracts/contracts/erc20/MyERC20Token.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";

/**
* @title MyERC20Token
* @dev This is a basic ERC20 token using the OpenZeppelin's ERC20PresetFixedSupply preset.
* You can edit the default values as needed.
*/
contract MyERC20Token is ERC20Burnable {

/**
* @dev Constructor to initialize the token with default values.
* You can edit these values as needed.
*/
constructor() ERC20("DefaultTokenName", "DTN") {
// Default initial supply of 1 million tokens (with 18 decimals)
uint256 initialSupply = 1_000_000 * (10 ** 18);

// The initial supply is minted to the deployer's address
_mint(msg.sender, initialSupply);
}

// Additional functions or overrides can be added here if needed.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol";
import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol";
import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";

import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/// @author Matter Labs
/// @notice This smart contract pays the gas fees for accounts with balance of a specific ERC20 token. It makes use of the approval-based flow paymaster.
contract ApprovalPaymaster is IPaymaster, Ownable {
uint256 constant PRICE_FOR_PAYING_FEES = 1;

address public allowedToken;

modifier onlyBootloader() {
require(
msg.sender == BOOTLOADER_FORMAL_ADDRESS,
"Only bootloader can call this method"
);
// Continue execution if called from the bootloader.
_;
}

constructor(address _erc20) {
allowedToken = _erc20;
}

function validateAndPayForPaymasterTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
)
external
payable
onlyBootloader
returns (bytes4 magic, bytes memory context)
{
// By default we consider the transaction as accepted.
magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
require(
_transaction.paymasterInput.length >= 4,
"The standard paymaster input must be at least 4 bytes long"
);

bytes4 paymasterInputSelector = bytes4(
_transaction.paymasterInput[0:4]
);
// Approval based flow
if (paymasterInputSelector == IPaymasterFlow.approvalBased.selector) {
// While the transaction data consists of address, uint256 and bytes data,
// the data is not needed for this paymaster
(address token, uint256 amount, bytes memory data) = abi.decode(
_transaction.paymasterInput[4:],
(address, uint256, bytes)
);

// Verify if token is the correct one
require(token == allowedToken, "Invalid token");

// We verify that the user has provided enough allowance
address userAddress = address(uint160(_transaction.from));

address thisAddress = address(this);

uint256 providedAllowance = IERC20(token).allowance(
userAddress,
thisAddress
);
require(
providedAllowance >= PRICE_FOR_PAYING_FEES,
"Min allowance too low"
);

// Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit,
// neither paymaster nor account are allowed to access this context variable.
uint256 requiredETH = _transaction.gasLimit *
_transaction.maxFeePerGas;

try
IERC20(token).transferFrom(userAddress, thisAddress, amount)
{} catch (bytes memory revertReason) {
// If the revert reason is empty or represented by just a function selector,
// we replace the error with a more user-friendly message
if (revertReason.length <= 4) {
revert("Failed to transferFrom from users' account");
} else {
assembly {
revert(add(0x20, revertReason), mload(revertReason))
}
}
}

// The bootloader never returns any data, so it can safely be ignored here.
(bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{
value: requiredETH
}("");
require(
success,
"Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough."
);
} else {
revert("Unsupported paymaster flow");
}
}

function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {}

function withdraw(address _to) external onlyOwner {
(bool success, ) = payable(_to).call{value: address(this).balance}("");
require(success, "Failed to withdraw funds from paymaster.");
}

receive() external payable {}
}
10 changes: 10 additions & 0 deletions code/frontend-paymaster/contracts/deploy/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { deployContract } from './utils';

// An example of a basic deploy script
// It will deploy a Greeter contract to selected network
// as well as verify it on Block Explorer if possible for the network
export default async function () {
const contractArtifactName = 'Greeter';
const constructorArguments = ['Hi there!'];
await deployContract(contractArtifactName, constructorArguments);
}
10 changes: 10 additions & 0 deletions code/frontend-paymaster/contracts/deploy/erc20/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { deployContract } from '../utils';

// This script is used to deploy an ERC20 token contract
// as well as verify it on Block Explorer if possible for the network

// Important: make sure to change contract name and symbol in contract source
// at contracts/erc20/MyERC20Token.sol
export default async function () {
await deployContract('MyERC20Token');
}
Loading

0 comments on commit a8e7a6c

Please sign in to comment.