0.3.6 • Published 6 months ago

metaplay v0.3.6

Weekly downloads
-
License
ISC
Repository
-
Last release
6 months ago

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:

NameShort nameIDDeployment AddressGas Fee %Spread - Fee %
Binance Smart Chain MainnetBNB56100.5
Polygon MainnetPolygon137100.5
Fantom OperaFantom250100.5
Arbitrum OneArbitrum421610xF73ab2d782bf6BA97ac4405D2CD4F1135da8dbd9100.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}`)
}
0.2.13

7 months ago

0.3.0

6 months ago

0.3.6

6 months ago

0.3.5

6 months ago

0.3.2

6 months ago

0.3.1

6 months ago

0.3.4

6 months ago

0.3.3

6 months ago

0.2.12

9 months ago

0.2.11

10 months ago

0.2.10

10 months ago

0.2.7

11 months ago

0.2.6

11 months ago

0.2.9

10 months ago

0.2.8

11 months ago

0.2.3

12 months ago

0.2.2

12 months ago

0.2.5

11 months ago

0.2.4

12 months ago

0.2.1

1 year ago

0.2.0

1 year ago

0.1.5

1 year ago

0.1.4

1 year ago

0.1.3

1 year ago

0.1.2

1 year ago

0.1.1

1 year ago

0.1.0

1 year ago