Skip to main content

Execute Relay Transactions

Disclaimer

This guide might contain outdated information and will be updated soon.

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 LSP25 standard, defined as the number 25.
import { LSP25_VERSION } from '@lukso/lsp-smart-contracts/constants';

const web3 = new Web3('https://rpc.testnet.lukso.network');
const universalProfileAddress = '0x...';
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

The validityTimestamp can take different forms. For more information about validity timestamps, see the following pages:

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 msgValue = 0; // Amount of native tokens to fund the UP with while calling

const abiPayload = universalProfile.methods
.execute(
0, // Operation type: CALL
recipientAddress,
web3.utils.toWei(3), // transfer 3 LYX to recipient
'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();

// prettier-ignore
let encodedMessage = web3.utils.encodePacked(
// MUST be number `25`
{ value: LSP25_VERSION, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000000019`
// e.g: `4201` for LUKSO Testnet
{ value: chainId, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000001069`
// e.g: nonce number 5 of the signer key X
// (the private key associated with the address of the controller that want to execute the payload)
{ value: nonce, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000000005`
// e.g: valid until 1st January 2025 at midnight (GMT).
// Timestamp = 1735689600
{ value: validityTimestamps, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000067748580`
// e.g: not funding the contract with any LYX (0)
{ value: msgValue, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000000000`
// e.g: execute(uint256,address,uint256,bytes)
// send 3 LYX to address `0xcafecafecafecafeafecafecafecafeafecafecafecafeafecafecafecafe`
{ value: abiPayload, type: 'bytes' }, // `0x44c028fe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cafecafecafecafecafecafecafecafecafecafe00000000000000000000000000000000000000000000000029a2241af62c000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000`
);

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 LSP25 standard, defined as the number 25.
import { LSP25_VERSION } from '@lukso/lsp-smart-contracts/constants';

const web3 = new Web3('https://rpc.testnet.lukso.network');
const universalProfileAddress = '0x...';
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 msgValue = 0; // Amount of native tokens to fund the UP with while calling

// send 3 LYX to recipient
const abiPayload = universalProfile.methods
.execute(
0, // Operation type: CALL
recipientAddress,
web3.utils.toWei(3), // transfer 3 LYX to recipient
'0x', // Data
)
.encodeABI();

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

// prettier-ignore
let encodedMessage = web3.utils.encodePacked(
// MUST be number `25`
{ value: LSP25_VERSION, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000000019`
// e.g: `4201` for LUKSO Testnet
{ value: chainId, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000001069`
// e.g: nonce nb 5
{ value: nonce, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000000005`
// e.g: valid until 1st January 2025 at midnight (GMT).
// Timestamp = 1735689600
{ value: validityTimestamps, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000067748580`
// e.g: not funding the contract with any LYX (0)
{ value: msgValue, type: 'uint256' }, // `0x0000000000000000000000000000000000000000000000000000000000000000`
// e.g: execute(uint256,address,uint256,bytes)
// send 3 LYX to address `0xcafecafecafecafeafecafecafecafeafecafecafecafeafecafecafecafe`
{ value: abiPayload, type: 'bytes' }, // `0x44c028fe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cafecafecafecafecafecafecafecafecafecafe00000000000000000000000000000000000000000000000029a2241af62c000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000`
);

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,
});