Skip to main content

Execute Relay Transactions

The LSP6 KeyManager standard enables anybody to execute a transaction on behalf of a Universal Profile, given they have a valid transaction which has been signed by a key that controls the Universal Profile.

Relayed execution enables use cases such as Transaction Relayer Services to be possible where users can send their transaction details to a third party to be executed, moving the gas cost burden away from the user who owns the Universal Profile.

For example, Alice can send an encoded transaction which updates the LSP3Profile picture on her Universal Profile to a second user, Bob, who executes the transaction and pays the gas cost of the transaction on behalf of Alice.

To execute the transaction, Bob needs to know:

  • the encoded ABI of the transaction that will get executed,
  • the transaction signature,
  • the nonce of the key that signed the transaction.

The transaction is then executed via the LSP6KeyManager function executeRelayCall.

Generate the signed transaction payload

This example shows how to prepare a transaction to be executed by a third party. This logic can be implemented client-side and then sent to a third-party application or service such as a Transaction Relay service to be executed.

Make sure you have the following dependencies installed before beginning this tutorial:

Install the dependencies
npm install web3 @lukso/lsp-smart-contracts @lukso/eip191-signer.js

Step 1 - Setup imports and constants

To encode a transaction, we need the address of the Universal Profile smart contract and the private key of a controller key with sufficient LSP6 permissions to execute the transaction.

Imports & Constants
import UniversalProfileContract from '@lukso/lsp-smart-contracts/artifacts/UniversalProfile.json';
import KeyManagerContract from '@lukso/lsp-smart-contracts/artifacts/LSP6KeyManager.json';
import { EIP191Signer } from '@lukso/eip191-signer.js';
import Web3 from 'web3';

// This is the version relative to the LSP6 standard, defined as the number 6.
import { LSP6_VERSION } from '@lukso/lsp-smart-contracts/constants';

const web3 = new Web3('https://rpc.testnet.lukso.network');
const universalProfileAddress = '0x...';
const msgValue = 0; // Amount of native tokens to be sent
const recipientAddress = '0x...';

// setup the Universal Profile controller account
const controllerPrivateKey = '0x...';
const controllerAccount = web3.eth.accounts.wallet.add(controllerPrivateKey);

Step 2 - Prepare the contact instances

We will get the contract instances for the Universal Profile and Key Manager for further use in the guide.

Contract instances
const universalProfile = new web3.eth.Contract(
UniversalProfileContract.abi,
universalProfileAddress
);

const keyManagerAddress = await universalProfile.methods.owner().call();
const keyManager = new web3.eth.Contract(
KeyManagerContract.abi,
keyManagerAddress
);

Step 3 - Prepare the relay call parameters

Get the nonce of the controller key from the KeyManager by instantiating the KeyManager smart contract instance and calling the getNonce function.

The channelId is used to prevent nonce conflicts when multiple apps send transactions to the same KeyManager at the same time. Read more about out of order execution here.

A validityTimestamp of 0 is used for simplicity in this guide.

Encode the ABI of the transaction you want to be executed. In this case, a LYX transfer to a recipient address.

tip

For more information about validity timestamps check How to sign relay transactions?

Prepare the relay call parameters
const channelId = 0;
const nonce = await keyManager.methods.getNonce(controllerAccount.address, channelId).call();

const validityTimestamps = 0; // no validity timestamp set

const abiPayload = universalProfile.methods
.execute(
0, // Operation type: CALL
recipientAddress,
msgValue,
'0x', // Data
)
.encodeABI();
ERC725X execute

You can find more information about the ERC725X execute call here.

Step 4 - Sign the transaction

Afterward, sign the transaction message from the controller key of the Universal Profile.

The message is constructed by signing the keyManagerAddress, keyManagerVersion, chainId, signer nonce, validityTimestamps, value and abiPayload.

ERC725X execute

For more information check: How to sign relay transactions?

Sign the transaction
const chainId = await web3.eth.getChainId();

let encodedMessage = web3.utils.encodePacked(
{ value: LSP6_VERSION, type: 'uint256' },
{ value: chainId, type: 'uint256' },
{ value: nonce, type: 'uint256' },
{ value: validityTimestamps, type: 'uint256' },
{ value: msgValue, type: 'uint256' },
{ value: abiPayload, type: 'bytes' },
);

let eip191Signer = new EIP191Signer();

let { signature } = await eip191Signer.signDataWithIntendedValidator(
keyManagerAddress,
encodedMessage,
controllerPrivateKey,
);

Now the signature, abiPayload, nonce, validityTimestamps and keyManagerAddress can be sent to a third party to execute the transaction using executeRelayCall.

Execute via executeRelayCall

info

This example shows how a third party can execute a transaction on behalf of another user.

tip

For more information about relay execution check How to sign relay transactions?

To execute a signed transaction, ABI payload requires:

  • the KeyManager contract address
  • the transaction ABI payload
  • the nonce of the controller key which signed the transaction.
  • the validity timestamps for the execution of the relay call.
  • the signed transaction payload
note

To get the KeyManager address from the UniversalProfile address, call the owner function on the Universal Profile contract.

Send the transaction
const executeRelayCallTransaction = await keyManager.methods
.executeRelayCall(signature, nonce, validityTimestamps, abiPayload)
.send({
from: controllerAccount.address,
gasLimit: 300_000,
});
LSP6KeyManager executeRelayCall

You can find more information about the LSP6KeyManager executeRelayCall here.

Final code

Final code
import UniversalProfileContract from '@lukso/lsp-smart-contracts/artifacts/UniversalProfile.json';
import KeyManagerContract from '@lukso/lsp-smart-contracts/artifacts/LSP6KeyManager.json';
import { EIP191Signer } from '@lukso/eip191-signer.js';
import Web3 from 'web3';

// This is the version relative to the LSP6 standard, defined as the number 6.
import { LSP6_VERSION } from '@lukso/lsp-smart-contracts/constants';

const web3 = new Web3('https://rpc.testnet.lukso.network');
const universalProfileAddress = '0x...';
const msgValue = 0; // Amount of native tokens to be sent
const recipientAddress = '0x...';

// setup the Universal Profile controller account
const controllerPrivateKey = '0x...';
const controllerAccount = web3.eth.accounts.wallet.add(controllerPrivateKey);

const universalProfile = new web3.eth.Contract(
UniversalProfileContract.abi,
universalProfileAddress,
);

const keyManagerAddress = await universalProfile.methods.owner().call();
const keyManager = new web3.eth.Contract(
KeyManagerContract.abi,
keyManagerAddress,
);

const channelId = 0;
const nonce = await keyManager.methods
.getNonce(controllerAccount.address, channelId)
.call();

const validityTimestamps = 0; // no validity timestamp set

const abiPayload = universalProfile.methods
.execute(
0, // Operation type: CALL
recipientAddress,
msgValue,
'0x', // Data
)
.encodeABI();

const chainId = await web3.eth.getChainId();

let encodedMessage = web3.utils.encodePacked(
{ value: LSP6_VERSION, type: 'uint256' },
{ value: chainId, type: 'uint256' },
{ value: nonce, type: 'uint256' },
{ value: validityTimestamps, type: 'uint256' },
{ value: msgValue, type: 'uint256' },
{ value: abiPayload, type: 'bytes' },
);

let eip191Signer = new EIP191Signer();

let { signature } = await eip191Signer.signDataWithIntendedValidator(
keyManagerAddress,
encodedMessage,
controllerPrivateKey,
);

const executeRelayCallTransaction = await keyManager.methods
.executeRelayCall(signature, nonce, validityTimestamps, abiPayload)
.send({
from: controllerAccount.address,
gasLimit: 300_000,
});