Account Actions
This guide will explain Account Actions and how to implement them.
Account Actions are contracts that extend the Lens Protocol functionality by allowing Accounts to execute custom actions over or through other Accounts.
Account Action Interface
The Account Actions are defined by the IAccountAction interface, which basically requires two functions, one to configure the action and another to execute it.
interface IAccountAction { event Lens_AccountAction_Configured(address indexed account, bytes data);
event Lens_AccountAction_Executed(address indexed account, bytes data);
function configure( address account, bytes calldata data ) external returns (bytes memory);
function execute( address account, bytes calldata data ) external returns (bytes memory);}
Configuration
The configuration of the Action is done through the configure function, which purpose is to initialize any required state that the Action might require to work properly.
The function receives two parameters, the account address for which the Action is configured for and the data bytes that will be decoded into any extra required custom configuration parameters that the Action could require.
The configure function returns bytes that in case the Action requires to return some custom information to the caller.
This function could be called by anyone, it will depend on the implementation of the Action and its purpose who is allowed to invoke it.
For example, there might be Actions that do not require initialization at all, so the configure function implementation will be empty, while other Actions might require the caller to match the account parameter passed-in.
Every time the configure function is called, the Action should emit a Lens_AccountAction_Configured event matching the parameters of the call.
Execution
The Action execution is triggered by invoking the execute function.
The function receives two parameters, the account address where the Action is being executed on and the data bytes that will be decoded into any extra required custom execution parameters that the Action could require.
The execute function returns bytes that in case the Action requires to return some custom information to the caller.
Same as with the configure function, the execute function could be called by anyone, and it will depend on the implementation of the Action and its purpose who is allowed to invoke it.
For example, there might be Actions that can be executed by anyone, while other Actions might require the caller to match the account parameter passed-in, or to require some pre-condition that relates the caller with the account parameter in order to allow the execution.
Every time the execute function is called, the Action should emit a Lens_AccountAction_Executed event matching the parameters of the call.
Building an Account Action
Let's illustrate the process with an example. We will build a Tipping Action between Accounts.
First, implement the configure function. Given that ERC-20 tokens allow to do transfer without prior approval of the recipient, it does not make much sense to require configuration for this Action.
We want Accounts to use this Action instead of transferring tokens directly to the recipient, so then Apps can notify the recipient that someone tipped them.
We could implement the configure function with a simple return statement, but we will revert instead, so we don't even allow this function to be invoked successfully.
contract TippingAccountAction is IAccountAction {
function configure(address account, bytes calldata data) external pure override returns (bytes memory) { revert(); // Configuration not needed for tipping. }
Finally, implement the execute function. We use the data bytes to decode the ERC-20 token address and the tip amount to transfer, requiring the tip amount to be greater than zero.
contract TippingAccountAction is IAccountAction {
// . . .
function execute( address account, bytes calldata data ) external override returns (bytes memory) { (address erc20Token, uint256 tipAmount) = abi.decode(data, (address, uint256)); require(tipAmount > 0); }}
Next, we transfer the tip amount from the caller to the recipient Account:
contract TippingAccountAction is IAccountAction {
// . . .
function execute( address account, bytes calldata data ) external override returns (bytes memory) { (address erc20Token, uint256 tipAmount) = abi.decode(data, (address, uint256)); require(tipAmount > 0); IERC20(erc20Token).transferFrom(msg.sender, account, tipAmount); }}
And to finish this implementation, we emit the Lens_AccountAction_Executed event matching the parameters of the call and, given that we do not need to return anything to the caller, we just return empty bytes:
contract TippingAccountAction is IAccountAction {
// . . .
function execute( address account, bytes calldata data ) external override returns (bytes memory) { (address erc20Token, uint256 tipAmount) = abi.decode(data, (address, uint256)); require(tipAmount > 0); IERC20(erc20Token).transferFrom(msg.sender, account, tipAmount); emit Lens_AccountAction_Executed(account, data); return ""; }}
Now the TippingAccountAction is ready to be used. See the full code below:
contract TippingAccountAction is IAccountAction {
function configure(address account, bytes calldata data) external pure override returns (bytes memory) { revert(); // Configuration not needed for tipping. }
function execute( address account, bytes calldata data ) external override returns (bytes memory) { (address erc20Token, uint256 tipAmount) = abi.decode(data, (address, uint256)); require(tipAmount > 0); IERC20(erc20Token).transferFrom(msg.sender, account, tipAmount); emit Lens_AccountAction_Executed(account, data); return ""; }}