metaplay v0.3.6
MetaPlay
This is the ts implementation of the MetaPlay. It uses EIP2771 and some custom logic to allow users to pay for transactions with ERC20 tokens. This concept is also called meta transactions. A user can send a meta transaction to any receiver that has EIP2771 enabled.
At the same time, it adds the option to use a few feature that we call session keys. It allows another wallet to sign meta transactions for the user and enables a frontend DAPP to skip the tedious wallet signature. While approving the session signer, you can set the target contract that can be used in combination with the session signer.
This package helps to easily add the MetaPlay to existing projects. It is heavily used and tested in our own projects. The package helps to create the EIP712 signature from the contract interface using ethers.js. After that, it sends the signed request to the relay operators that take the request and send it to the target blockchain. They have zero options to change the request in any way.
Considerations
This package does not include any logic to manage the session signer private key, but it is exposed to the DAPP. This means, that the native approach is to generate a new session wallet every time the relay gets initialized. But you can also provider a privateKey
while creating the instance to reuse a previous signer.
Here you need to make sure the private key gets saved somewhere and is secure until it's used next. It's highly recommended using a reasonable deadline for the signer (can be extended later) and to encrypt the key while it's not used. DO NOT store the private key in a blank format. Whoever knows the key can use the user's funds together with the target contract.
Supported Blockchains:
Name | Short name | ID | Deployment Address | Gas Fee % | Spread - Fee % |
---|---|---|---|---|---|
Binance Smart Chain Mainnet | BNB | 56 | 10 | 0.5 | |
Polygon Mainnet | Polygon | 137 | 10 | 0.5 | |
Fantom Opera | Fantom | 250 | 10 | 0.5 | |
Arbitrum One | Arbitrum | 42161 | 0xF73ab2d782bf6BA97ac4405D2CD4F1135da8dbd9 | 10 | 0.5 |
Fees
The meta play charges a percentuale fee based on the tx fee used. For fee token other then WETH an additonal spread is charged for converting the token to ETH. The token value is based on on-chain chainlink feeds.
How to use
MetaPlayProvider Class
Creat a new MetaPlayProvider instance
While creating a new instance you have to specify the users address and the blockchain you want to use, using the network ID (see Supported Blockchains). You can also set a customRPC
to speed up some of the on-chain lookups. With privateKey
you can re-create a old session wallet. Use an object to pass the arguments.
Arguments:
- userAddress , as string
chainId , as number
optional:
- customRPC , as string - default: public RPC, might not be online at that time, best use custom one
- privateKey , as string - default: generate new session signer
After creating a new MetaPlayProvider class, you need to set a target
. A target is the contract you want to interact with. You can add it by either adding the contract ABI or with an already existing contract class. The metaProvider allows to add any number of targets at the same time.
const metaProvider = new MetaPlayProvider({ userAddress: '0x12c..4' })
const target = new ethers.Contract(targetAddress, targetAbi)
metaProvider.addNewTargetFromContract(target)
const exampleABI = ['function emitMessage(string memory message) public', 'mustReceiveEth(uint256 value) public payable']
const metaProvider = new MetaPlayProvider({ userAddress: '0x12c..4' })
metaProvider.addNewTargetFromAbi(targetAddress, exampleABI)
Approve new fee token
The metaProvider contract needs to be approved to collect the fee in the specific ERC20 token.
This can be done with the help of the package as well. It offers 2 diffrent approaches for this. You can either hand over the ethers wallet instance and the metaProvider class will send the approve tx automatically or you can request the calldata and send it using any provider you like.
check if already approved
Arguments:
- tokenAddress , as string
- minAmount , as ethers.BigNumber - default: ethers.constants.MaxUint256.div(10)
await metaProvider.isFeeTokenApproved(tokenAddress)
using buildin function
Arguments:
- userWallet , as ethers.Wallet
- tokenAddress , as string
- newAllowance , as BigNumber - default: ethers.constants.MaxUint256
await metaProvider.approveFeeToken(userWallet, tokenAddress, newAllowance)
using calldata + ethers
Arguments:
- tokenAddress , as string
- newAllowance , as BigNumber - default: ethers.constants.MaxUint256
const calldata = await metaProvider.approveFeeTokenCalldate(tokenAddress, newAllowance)
const tx = await userWallet.connect(provider).sendTransaction(calldata)
Approve new session signer
If you want to use session signers, the session signer needs to be approved. You can set the targets with targetAddresses as an array or let the package default to all not yet approved targets.
This can be done with the help of the package as well. It offers 2 diffrent approaches for this. You can either hand over the ethers wallet instance and the metaProvider class will send the approve tx automatically or you can request the calldata and send it using any provider you like.
check if signer is approved
Arguments:
- targetAddress , as string - default: if only one target is added, defaults to that one. Throws error otherwise if not set
await metaProvider.isApproved(targetAddress)
using buildin function
Arguments:
- userWallet , as ethers.Wallet
- validUntilTime , as number (unix) - default: Now() + one day
- targetAddresses , as string[] - default: all targets where signer is not approved
await testForwader.approveSigner(userWallet)
using calldata + ethers
Arguments:
- validUntilTime , as number (unix) - default: Now() + one day
- targetAddresses , as string[] - default: all targets where signer is not approved
const calldata = await metaProvider.approveSignerCalldata()
const tx = await userWallet.connect(provider).sendTransaction(calldata)
Create a new request
You mainly interact with the metaProvider using getNewRequest
. This function lets you create a new request. A request is a class on its own, that enables you to easily interact with the metaProvider. You have to specify one of the added targets by the address, if you have more then one target added.
If the function is payable and has to recive a specific amount of the native Token (eg. ETH on Arbitrum or FTM on Fantom), you can set that with msgValue
(BigNumber in wei). Each of the specific overwrite are set using the object notation.
Arguments:
- functionName , as string
parameters , as array
optional:
- msgValue , as BigNumber in wei - default 0
- feeToken , as string (address) - default wrapped native token address (eg WETH, WFTM, ...)
- nonce , as number - default next nonce
- validUntilTime , as number (unix) - default Now() + 120s
- gasLimit , as BigNumber - default estiamted at runntime
const requestOne = metaProvider.getNewRequest('emitMessage', ['Hello World'])
const requestTwo = metaProvider.getNewRequest('mustReceiveEth', [], { msgValue: ethers.BigNumber.from('1000') })
Request Class
The request class can be used to easily manage and interact with the metaProvider on a request level.
.valid()
Checks if the request is valid using the EIP2771 standart interface on the blockchain.
- Arguments: none
- Returns: boolean
.error()
Checks if an error was thrown why sending or processing the request and returns the error message.
- Arguments: none
- Returns: string
.estimatedFee()
Returns the fee amount in the specific fee token. By passing the number of decimals of the fee token, you can recive the fee as a decimal number. By not adding that, you can get the amount as BigNumber
- Arguments:
- decimals , as number - optional
- Returns:
- number (with decimals set)
- ethers.BigNumber (with decimals NOT set)
.send()
Sends the request to the metaProvider operator.
- Arguments: none
- Returns: { id: number; status: 'pending' | 'sending' | 'processed' | 'error' | 'rejected'; txHash?: string | undefined; errorReasonValidation?: string | undefined; errorReasonForwarder?: string | undefined; }
.status()
Fetch the status of the request from the metaProvider operator.
- Arguments: none
- Returns: { id: number; status: 'pending' | 'sending' | 'processed' | 'error' | 'rejected'; txHash?: string | undefined; errorReasonValidation?: string | undefined; errorReasonForwarder?: string | undefined; }
.wait()
After sending a request you can await the result. Optionally a specific status can be set.
- Arguments: 'sending' or 'processed' - default 'processed'
- Returns: { id: number; status: 'pending' | 'sending' | 'processed' | 'error' | 'rejected'; txHash?: string | undefined; errorReasonValidation?: string | undefined; errorReasonForwarder?: string | undefined; }
Complete Example
const usdcTokenAddress = '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8'
const exampleABI = ['function emitMessageWithValue(string memory message) public payable']
const metaProvider = new MetaPlayProvider({ userAddress: '0x12c..4', chainId: 42161 })
metaProvider.addNewTargetFromAbi(targetAddress, exampleABI)
const isUSDCApproved = await metaProvider.isFeeTokenApproved(usdcTokenAddress)
if (!isUSDCApproved) await metaProvider.approveFeeToken(userWallet, usdcTokenAddress, ethers.constants.MaxUint256)
// here we approve a new signer
// if metaProvider is created form privateKey that was used before, check with:
// await metaProvider.isApproved(targetAddress)
await testForwader.approveSigner(userWallet)
const request = await metaProvider.getNewRequest('emitMessageWithValue', ['Hello World'], { msgValue: ethers.BigNumber.from('1000'), feeToken: usdcTokenAddress })
const isValid = await request.valid()
if (!isValid) console.error('Request is not valid!')
const fee = await request.estimatedFee(6)
console.log(`Request will cost ${fee} USDC.`)
request.send()
const result = await request.wait()
if (result.status === 'processed') {
console.log(`Successfully send request. TxHash is ${result.txHash}`)
} else {
// alternative:
// errorMsg = result.errorReasonValidation || result.errorReasonForwarder
// one of those will always be set if the status is error or rejected
// here .error() is used as example:
const errorMsg = await request.error()
console.log(`Failed to send request, error message: ${errorMsg}`)
}
7 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
9 months ago
10 months ago
10 months ago
11 months ago
11 months ago
10 months ago
11 months ago
12 months ago
12 months ago
11 months ago
12 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago