Skip to content

Commit

Permalink
feat: add web3js paymaster dapp tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
Jovells committed Dec 22, 2024
1 parent ebf2753 commit 83a410a
Show file tree
Hide file tree
Showing 2 changed files with 339 additions and 0 deletions.
307 changes: 307 additions & 0 deletions content/tutorials/web3js-paymaster-tutorial/10.index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
---
title: Building a dApp with Stablecoin Gas Payments using Web3.js
description: Learn how to build a decentralized application that enables users to pay transaction gas fees using stablecoins instead of ETH on ZKSync.
---

Build a decentralized application that allows users to pay transaction gas fees using stablecoins instead of ETH on ZKSync.

## Prerequisites

- Node.js v14 or later
- Basic knowledge of React and TypeScript
- Understanding of Web3 concepts
- MetaMask wallet
- Basic understanding of smart contracts

## Project Setup

1. Create a new React TypeScript project:

```bash{copy}
npx create-react-app zksync-paymaster-demo --template typescript
cd zksync-paymaster-demo
```
2. Install required dependencies:
```bash{copy}
npm install web3 web3-plugin-zksync zksync-web3-contract-paymaster-plugin
```
## Smart Contract Setup
::callout{icon="i-ph-info-bold"}
First, let's set up our contract addresses and ABIs in a constants file. You'll need these for interacting with the smart contracts.
::
Store your contract addresses and ABIs in a constants file:
```typescript{copy}
export const MOCK_USDT_ADDRESS = "0xff68f7561562C1F24A317d939B46741F76c4Ef55";
export const MOCK_USDT_PAYMASTER_ADDRESS = "0xfA7Adc05E56893df8ecE1F63E4dB1db7767146f4";
export const STORE_CONTRACT_ADDRESS = "0xEc969112DB5440c954CB60B4Bbd1159673eeE4C3";
export const MOCK_USDT_ADDRESS = "0xff68f7561562C1F24A317d939B46741F76c4Ef55";
export const MOCK_USDT_PAYMASTER_ADDRESS = "0xfA7Adc05E56893df8ecE1F63E4dB1db7767146f4";
export const STORE_CONTRACT_ADDRESS = "0xEc969112DB5440c954CB60B4Bbd1159673eeE4C3";
// Store Contract ABI
export const STORE_CONTRACT_ABI = [
{
"inputs": [
{
"internalType": "uint256",
"name": "_productId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_quantity",
"type": "uint256"
}
],
"name": "purchaseProduct",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
];
// Stablecoin (MOCK USDT) ABI
export const STABLECOIN_ABI = [
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
];
```

## Paymaster Integration

### Understanding Paymasters in ZKSync

::callout{icon="i-ph-lightbulb-bold"}
Before diving into the implementation, it's important to understand what a Paymaster is and how it works in ZKSync.
::

A Paymaster is a smart contract in ZKSync that enables gas abstraction - allowing users to pay transaction fees with tokens other than ETH.
This is a powerful feature that improves user experience by:

- Letting users pay gas fees in ERC20 tokens
- Enabling sponsored transactions where a third party pays the gas
- Supporting more complex gas payment scenarios

In this tutorial we are using a custom made paymaster which accepts a custom token as referenced above:
`MOCK_USDT_PAYMASTER_ADDRESS` and `MOCK_USDT_ADDRESS`. The paymaster contract address must be supplied to the `zksync-web3-contract-paymaster-plugin`.
A comprehensive guide on creating a custom ERC20 paymaster can be found in the [ZKSync ERC20 Paymaster Tutorial](https://code.ZKSync.io/tutorials/erc20-paymaster).

1. Create a custom hook for Paymaster interactions
2. Set up Web3 and plugin initialization
3. Implement contract interaction methods

### Implementation

1. Create a custom hook for Paymaster interactions:

```typescript{copy}
import { useState, useEffect, useCallback } from 'react';
import Web3 from 'web3';
import { getPaymasterParams, types, Web3ZKSyncL2, ZKSyncPlugin } from 'web3-plugin-zksync';
import ZKSyncContractPaymasterPlugin from "zksync-web3-contract-paymaster-plugin";
import { MOCK_USDT_ADDRESS } from './constants';
const usePaymasterAsync = (contractAddress: string, contractAbi: any, _paymasterAddress?: string) => {
// Note: _paymasterAddress is optional - if not provided, the plugin will use testnet paymaster
const [web3, setWeb3] = useState<Web3>();
const [plugin, setPlugin] = useState<ZKSyncContractPaymasterPlugin>();
// Initialize Web3 and register the required plugins
useEffect(() => {
const initializeWeb3 = async () => {
if (typeof window.ethereum !== 'undefined') {
// Create Web3 instance
const web3 = new Web3(window.ethereum);
// Initialize ZKSync L2 provider for Sepolia testnet
const l2 = Web3ZKSyncL2.initWithDefaultProvider(types.Network.Sepolia);
// Create instance of the Paymaster plugin
const plugin = new ZKSyncContractPaymasterPlugin(window.ethereum);
// Register both plugins with Web3
web3.registerPlugin(plugin);
web3.registerPlugin(new ZKSyncPlugin(l2));
setPlugin(plugin);
setWeb3(web3);
}
};
initializeWeb3();
}, []);
// This function handles contract interactions with Paymaster integration
const writeContractWithPaymaster = useCallback(async (
{ functionName, args }: { functionName: string; args: any[] }
) => {
if (!web3 || !plugin) return;
return await plugin.write(contractAddress, contractAbi, {
methodName: functionName,
args: args,
from: await web3.eth.getAccounts().then(a => a[0]),
customData: {
// Gas per pubdata is a ZKSync-specific parameter
gasPerPubdata: 50000,
// Configure Paymaster parameters for gas abstraction
paymasterParams: getPaymasterParams(_paymasterAddress, {
type: "ApprovalBased",
minimalAllowance: 10,
token: MOCK_USDT_ADDRESS,
innerInput: new Uint8Array(),
})
}
});
}, [web3, plugin, contractAddress]);
return { writeContractWithPaymaster };
};
```
The `usePaymasterAsync` hook does several important things:
• Initializes Web3 with ZKSync plugins
• Provides a function to interact with any smart contract while using the Paymaster
• Handles the configuration of Paymaster parameters for gas fee abstraction
2. Implement the purchase function with Paymaster support:
This example shows how to use the Paymaster hook with any smart contract.
In this case, we're using a marketplace contract that takes a
product ID and quantity, but you can adapt this pattern for any contract function:
```typescript{copy}
const buyProductWithPayMaster = async (product: Product, itemQty: number) => {
try {
// IMPORTANT: First approve the spending of tokens
// This is required for any ERC20 token interaction
const approvalTx = await approveWithPaymasterAsync(
{
functionName: 'approve',
args: [STORE_CONTRACT_ADDRESS, product.price]
}
);
// Wait for approval transaction to be confirmed
await approvalTx.wait();
// Then make the actual contract call
// This could be any function on any contract
const tx = await buyWithPaymasterAsync(
{
functionName: 'purchaseProduct', // Your contract function name
args: [product.id, itemQty] // Your function arguments
}
);
return { tx, product };
} catch (error) {
console.error('Error buying product with Paymaster', error);
throw error;
}
};
// Example of how this is used in a component
const Products = ({ account, web3 }) => {
// Initialize the Paymaster hooks for both token and marketplace contracts
const { writeContractWithPaymaster: approveWithPaymasterAsync } = usePaymasterAsync(
MOCK_USDT_ADDRESS, // Token contract
STABLECOIN_ABI,
MOCK_USDT_PAYMASTER_ADDRESS
);
const { writeContractWithPaymaster: buyWithPaymasterAsync } = usePaymasterAsync(
STORE_CONTRACT_ADDRESS, // Your contract
STORE_CONTRACT_ABI,
MOCK_USDT_PAYMASTER_ADDRESS
);
// Function to handle the purchase
async function buy(product: Product, quantity: number) {
if (!web3 || !account) {
toast.error('Wallet not connected');
return;
}
const id = toast.loading(`Purchasing ${product.name}... and paying gas fees with MOCK_USDT`);
try {
const val = await buyProductWithPayMaster(product, quantity);
if (!val) return;
const { tx } = val;
toast.success(`Purchase successful. Gas paid with MOCK_USDT`, { id });
} catch (error) {
toast.error('Error purchasing product: ' + error, { id });
}
}
return (
// Your UI components
);
};
```

::callout{icon="i-ph-list-checks-bold"}
Here are the key points to remember when implementing Paymaster functionality:
::

1. **Token Approval**: Before any ERC20 token transaction, you must approve the spending amount. This is standard for all ERC20 tokens.

2. **Contract Interaction**: The example shows a purchase function, but you can use this pattern with any smart contract function. Just modify:
- The contract address
- The contract ABI
- The function name
- The function arguments

3. **Gas Payment**: The Paymaster handles converting your token payment into gas fees automatically.

## Conclusion

This tutorial demonstrated how to build a dApp that leverages ZKSync's Paymaster feature to enable stablecoin gas payments. The key components are:

- Integration with ZKSync's L2 network using the Paymaster plugin
- Smart contract interactions for token approvals and purchases
- User interface for seamless interaction

::callout{icon="i-ph-arrow-right-bold"}
You can view a live demo of this implementation at [Web3js ZKSync Plugin Demo](https://web3js-ZKSync-plugin.vercel.app/).
::

::alert{type="success"}
Congratulations! You've built a dApp that uses ZKSync's Paymaster feature to enable stablecoin gas payments.
::
32 changes: 32 additions & 0 deletions content/tutorials/web3js-paymaster-tutorial/_dir.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
title: Building a ZKSync dApp with Stablecoin Gas Payments using Web3.js
authors:
- name: Jovells
image: https://avatars.githubusercontent.com/u/100000000?v=4
twitter: https://twitter.com/jovells
github: https://github.com/jovells
tags:
- web3.js
- paymaster
- gas-abstraction
- stablecoin
- react
- typescript
level: intermediate
duration: 45 minutes
github_repo: https://github.com/Jovells/ZKSync-web3js-paymaster-demo
summary: |
Learn how to build a dApp that allows users to pay transaction gas fees using stablecoins instead of ETH on ZKSync.
This tutorial uses Web3.js and ZKSync's Paymaster feature to implement gas abstraction.
description: |
This comprehensive guide walks through creating a decentralized application that leverages ZKSync's Paymaster feature
for stablecoin gas payments. You'll learn how to integrate Web3.js with ZKSync plugins, implement smart contract
interactions, and create a user interface for seamless gas abstraction.
what_you_will_learn:
- How to integrate Web3.js with ZKSync plugins
- How to implement smart contract interactions
- How to create a user interface for seamless gas abstraction
updated: 2024-12-21
tools:
- Web3.js
- React
- MetaMask

0 comments on commit 83a410a

Please sign in to comment.