βοΈBack end
In order to be able to relay transaction we need a centralized back-end able to relay the transactions on behalf of users.
Example of relaying a transaction
Reading a player structure (simple GET API endpoit)
The role of the back-end in a wallet-less dApp
In the previous section (Build a gas-less transaction), we learned how to use Moonbeam's callPermit in order to relay a transaction and make it gassless. In order to go one step further and removing the need to sign a transaction, the transaction is going to be generated by the mirror wallet and dispatched by the contract's owner (or any authorized address defined in a particular modifier).
The backend is going to generate the data to sign and execute it on behalf of the user, following for instance a web2 POST call.
Example of a simple Express POST function
app.post('/doTx', async (req, res) => {
let infos = await req.body
let sendTxn = await prepareCall(infos)
if (await sendTxn == true) {
await res.status(200).json({ 'ok': true, 'message': 'Tx successfully sent!' })
}
else {
await res.status(200).json({ 'ok': false, 'message': 'There was a problem during the tx' })
}
})
Example of a function that encodes, sign, relay/executes a transaction
// dependencies
let ethers = require('ethers');
let {
signTypedData,
SignTypedDataVersion
} = require('@metamask/eth-sig-util');
async function prepareCall(userData) {
let dataFct
let messageData
let finalCall
let formattedSignature = {}
// first we use the real EOA address received in the post
let wallet = userData.userWallet
// we then get its mirror's wallet private key
let key = await getPvKey(wallet) // here you need to create a function able to get the mirror wallet private key using your own security standard
let userSigner = new ethers.Wallet(await key, provider); // internal wallet signer
let publicAddy = await userSigner.getAddress() // internal wallet address
let currentNonce = await getNonces(publicAddy) // useful function made by Moonbeam to get the current Nonces for an address (see helpers below)
// here we're encoding the function using the required arguments
let iface = new ethers.utils.Interface(yourABI);
dataFct = iface.encodeFunctionData("yourSolidityFunction",
[
arg1,
arg2,
arg3
])
userData.encoded = dataFct
// function to generate compliant message for callPermit
const createPermitMessageData = async function() {
const message = {
from: publicAddy,
to: yourContractAddress,
value: 0,
data: userData.encoded,
gaslimit: 300000,
nonce: currentNonce.toNumber(),
deadline: Date.now() + (5 * 60 * 1000),
};
const typedData = {
types: {
EIP712Domain: [{
name: 'name',
type: 'string'
}, {
name: 'version',
type: 'string'
}, {
name: 'chainId',
type: 'uint256'
}, {
name: 'verifyingContract',
type: 'address'
}, ],
CallPermit: [{
name: 'from',
type: 'address'
}, {
name: 'to',
type: 'address'
}, {
name: 'value',
type: 'uint256'
}, {
name: 'data',
type: 'bytes'
}, {
name: 'gaslimit',
type: 'uint64'
}, {
name: 'nonce',
type: 'uint256'
}, {
name: 'deadline',
type: 'uint256'
}, ],
},
primaryType: 'CallPermit',
domain: {
name: 'Call Permit Precompile',
version: '1',
chainId: 1284,
verifyingContract: '0x000000000000000000000000000000000000080a',
},
message: message,
};
return {
typedData,
message,
};
};
try {
messageData = await createPermitMessageData();
} catch (errMd) {
console.log(errMd)
}
// sign the message
let signature
try {
signature = signTypedData({
privateKey: Buffer.from(key, 'hex'),
data: await messageData.typedData,
version: SignTypedDataVersion.V4,
});
console.log(`Signature successful with hash: ${signature}`);
} catch (signErr) {
console.log(signErr, 'signErr')
}
try {
// validate the message
let {
r,
s,
v
} = await getSignatureParameters(signature); // see helpers below
formattedSignature = {
r: r,
s: s,
v: v,
};
} catch (sigError) {
console.log(sigError)
}
try {
finalCall = await {
'message': await messageData.message,
'signature': await formattedSignature,
'config': await userData
}
} catch (err) {
console.log(err)
}
try {
let deployerKey = "yourPrivateKey" // only for development purpose, don't do that on production
let deployerSigner = new ethers.Wallet(deployerKey, provider);
const preCompileContract = new ethers.Contract(callPermitAddress, callPermitABI, deployerSigner)
let tx = await preCompileContract.dispatch(
finalCall.message.from,
finalCall.message.to,
finalCall.message.value,
finalCall.message.data,
finalCall.message.gaslimit,
finalCall.message.deadline,
finalCall.signature.v,
finalCall.signature.r,
finalCall.signature.s)
let result = await tx.wait()
await console.log('Tx succesfull!')
return true
} catch (err) {
console.log(err)
return false
}
}
// useful helpers made by Moonbeam team
async function getSignatureParameters(signature) {
if (!ethers.utils.isHexString(signature)) {
throw new Error(
'Given value "'.concat(signature, '" is not a valid hex string.')
);
}
var r = signature.slice(0, 66);
var s = "0x".concat(signature.slice(66, 130));
var v = "0x".concat(signature.slice(130, 132));
v = ethers.BigNumber.from(v).toNumber();
if (![27, 28].includes(v)) v += 27;
return {
r: r,
s: s,
v: v
};
};
async function getNonces(wallet) {
const preCompileContract = new ethers.Contract(callPermitAddress, callPermitABI, provider)
let nonce
try {
nonce = await preCompileContract.nonces(wallet);
} catch (nonceErr) {
console.log(nonceErr, 'nonce error')
}
return await nonce
}
Last updated