Example with the Execution API
This is a hands-on tutorial that walks through building a simple refuel automation using a JS script. Perfect for getting familiar with core API types and execution flows.
While the Execution Layer provides the foundational security and access control for automations, the Orchestration Layer enhances it with scheduling capabilities. For complex automation scenarios that go beyond the standard scheduling API, developers can directly utilize the core execution endpoints. Let's explore this flexibility through an example.
A Simple Cross Chain Refuel BOT
An automation built using Console Automation APIs that helps keep ETH balances topped up across different chains.
Users can subscribe by providing:
A
refuelAddress
to monitorminAmount
of ETH to maintainList of
chains
to watch
The automation checks if the refuelAddress
drops below minAmount
of ETH on any of the specified chains. If it does, it automatically bridges funds to keep the balances above the minimum threshold
Creating the Automation
To create an automation, the executor must first configure the automation parameters.
// Define the EIP-712 message parameters
const msgParams = {
domain: {
chainId: 42161
},
message: {
timestamp: 1,
executor: "0xAE75B29ADe678372D77A8B41225654138a7E6ff1",
inputTokens: ["0x0000000000000000000000000000000000000000"],
hopAddresses: ["0x3a23F943181408EAC424116Af7b7790c94Cb97a5"],
feeInBPS: 0,
feeToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
feeReceiver: "0xAE75B29ADe678372D77A8B41225654138a7E6ff1",
limitPerExecution: true,
clientId: "auto-refuel"
},
primaryType: "RegisterExecutor",
types: {
EIP712Domain: [
{ name: "chainId", type: "uint256" },
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "verifyingContract", type: "address" }
],
RegisterExecutor: [
{ name: "timestamp", type: "uint256" },
{ name: "executor", type: "address" },
{ name: "inputTokens", type: "address[]" },
{ name: "hopAddresses", type: "address[]" },
{ name: "feeInBPS", type: "uint256" },
{ name: "feeToken", type: "address" },
{ name: "feeReceiver", type: "address" },
{ name: "limitPerExecution", type: "bool" },
{ name: "clientId", type: "string" }
]
}
};
Once we have the automation parameters defined, we can create a helper to sign the parameters, as this signature is required for automation creation.
const provider = new providers.JsonRpcProvider(RPC_URL);
const signer = new Wallet(EXECUTOR_PRIVATE_KEY, provider);
async function signAutomationRegistrationMessage(domain, types, message) {
try {
// Create a wallet instance using the private key
const wallet = new ethers.Wallet(privateKey);
// Sign the message according to EIP-712
const signature = await wallet._signTypedData(domain, types, message);
console.log("Signature:", signature);
return signature; // Return the generated signature
} catch (err) {
console.error("Error signing message:", err);
throw err;
}
}
Here, RPC_URL
and EXECUTOR_PRIVATE_KEY
must be configured with appropriate values.
Once the signature is ready, we can simply call the POST /executor
endpoint to create the automation for the executor.
const SVC_BASE_URL = "https://gtw.brahma.fi";
// Function to execute the API call
async function executeApiCall() {
try {
// Generate signature
const signature = await signMessage(
msgParams.domain,
msgParams.types,
msgParams.message
);
// Prepare the data for the POST request
const data = {
config: {
feeInBPS: msgParams.message.feeInBPS,
feeReceiver: msgParams.message.feeReceiver,
feeToken: msgParams.message.feeToken,
limitPerExecution: msgParams.message.limitPerExecution,
inputTokens: msgParams.message.inputTokens,
hopAddresses: msgParams.message.hopAddresses
},
executor: msgParams.message.executor,
signature: signature, // Use the generated signature
chainId: msgParams.domain.chainId,
timestamp: msgParams.message.timestamp,
executorMetadata: {
id: msgParams.message.clientId,
name: "auto-refuel",
logo: "https://smolrefuel.com/logo.png",
metadata: {}
}
};
// Make the POST request using Axios
const response = await axios.post(
`${SVC_BASE_URL}/v1/automations/executor`,
data,
{
headers: {
"Content-Type": "application/json"
}
}
);
console.log("Create Executor response:", response.data);
} catch (error) {
console.error("Error creating Executor response:", error);
}
}
Fetching subscribers of automation
Once the automation is created, Console users will be able to subscribe to the automation from console's Automation Marketplace. The automation executors can fetch a list of all subscribers and their metadata from the API, to perform further operations.
First, the executor's registryID must be fetched in order to fetch its subscribers.
const fetchExecutorByAddress = async (address, chainId) => {
try {
const resp = await Axios.get(
`${SVC_BASE_URL}/v1/automations/executor/${address}/${chainId}`
);
return resp.data.data;
} catch (err) {
console.log("err: failed to call fetchExecutorByAddress");
throw "failed to fetch executor by address";
}
};
Post fetching, we can also perform further cleanup to filter out the active subscriptions.
const fetchActiveSubscriptions = async (registryID) => {
try {
const response = await Axios.get(
`${SVC_BASE_URL}/v1/automations/executor/${registryID}/subscriptions`
);
const subscriptions = response.data.data;
let activeSubscriptions = [];
for (let subscription of subscriptions) {
// filter our if not currently active
if (subscription.status != CANCELLED_STATUS) {
activeSubscriptions.push(subscription);
}
}
return activeSubscriptions;
} catch (err) {
console.log("err: failed to get subscriptions");
throw "failed to get executor subscriptions";
}
};
Checking if the automation should be executed for a subscriber
The automation only has to be executed, if the refuelAddress
has a balance lower than the minAmount
threshold on any given chain ID from chains
. We define a function to perform a check to see if a target address has a low balance-
const canExecute = async (targetChainID, target, threshold) => {
const provider = new providers.JsonRpcProvider(RPC_URL);
const bal = await provider.getBalance(target);
logger.info(
logMessage(
"CanExecute Check",
`Current balance: ${bal.toString()}, Threshold: ${threshold.toString()}`
)
);
if (bal.gt(threshold)) {
return false;
}
return true;
};
Here, RPC_URL
needs to be defined
Now we can go through all the subscribers and check if the automation is to be performed.
const metadata = await fetchExecutorByAddress(EXECUTOR_ADDRESS, CHAIN_ID);
let subscriptions = await fetchActiveSubscriptions(metadata.id);
logger.info(
logMessage("Found Subscriptions", JSON.stringify(subscriptions, "", 4))
);
for (let subscription of subscriptions) {
const metadata = subscription.metadata;
let refuel = false;
let targetChainID = 0;
let subaccount = subscription.subAccountAddress;
// Check if refuel is needed for any of the target chains
for (let targetChain of metadata.chains) {
if (
await canExecute(targetChain, metadata.refuelAddress, metadata.minAmount)
) {
refuel = true;
targetChainID = targetChain;
break;
}
}
}
Here, EXECUTOR_ADDRESS
and CHAIN_ID
must be defined.
Executing calldata to bridge funds to refuelAddress
on required chains from subscriber
refuelAddress
on required chains from subscriberBefore proceeding with the actual execution, we first need to define some helpers.
Signing execution digest
const buildExecutionDigestSignature = async ( chainId, to, value, data, operation, account, nonce, wallet, executor ) => { const types = { ExecutionParams: [ { name: "operation", type: "uint8" }, { name: "to", type: "address" }, { name: "account", type: "address" }, { name: "executor", type: "address" }, { name: "gasToken", type: "address" }, { name: "refundReceiver", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "safeTxGas", type: "uint256" }, { name: "baseGas", type: "uint256" }, { name: "gasPrice", type: "uint256" }, { name: "data", type: "bytes" } ] }; const executorPluginDomain = { name: "ExecutorPlugin", version: "1.0", chainId: chainId, verifyingContract: EXECUTOR_PLUGIN_ADDRESS }; const executionParamsData = { to, value, data, operation, account, executor: executor, nonce, gasToken: ethers.constants.AddressZero, refundReceiver: ethers.constants.AddressZero, safeTxGas: 0, baseGas: 0, gasPrice: 0 }; const signature = await wallet._signTypedData( executorPluginDomain, types, executionParamsData ); return signature; }; const signCallData = async ( account, executor, executable, wallet, chainId, rpc ) => { const provider = new ethers.providers.JsonRpcProvider(rpc); const executorPluginContract = new ethers.Contract( EXECUTOR_PLUGIN_ADDRESS, EXECUTOR_PLUGIN_ABI, provider ); /// Generate signature for execution digest const executorSignature = await buildExecutionDigestSignature( chainId, executable.to, executable.value, executable.data, executable.callType, account, ( await executorPluginContract.executorNonce(account, executor) ).toString(), wallet, executor ); return executorSignature; };
Here,
EXECUTOR_PLUGIN_ADDRESS
andEXECUTOR_PLUGIN_ABI
must be assigned to the appropriate values.Helpers to get bridge routes from socket
async function getQuote( fromChainId, fromTokenAddress, toChainId, toTokenAddress, fromAmount, senderAddress, recipient, uniqueRoutesPerBridge, sort, singleTxOnly ) { const response = await fetch( `https://api.socket.tech/v2/quote?fromChainId=${fromChainId}&fromTokenAddress=${fromTokenAddress}&toChainId=${toChainId}&toTokenAddress=${toTokenAddress}&fromAmount=${fromAmount}&userAddress=${senderAddress}&recipient=${recipient}&bridgeWithGas=false&sort=${sort}&singleTxOnly=${singleTxOnly}&isContractCall=true`, { method: "GET", headers: { "API-KEY": API_KEY, Accept: "application/json", "Content-Type": "application/json" } } ); const json = await response.json(); return json; } async function getRouteTransactionData(route) { const response = await fetch("https://api.socket.tech/v2/build-tx", { method: "POST", headers: { "API-KEY": API_KEY, Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ route: route }) }); const json = await response.json(); return json; }
Here,
API_KEY
must be assigned to the executor's socket API key.Helper to use the
/execute
endpoint to execute a task on subscriberconst executeTask = async ({ chainId, executable, subaccount, executorSignature, executor }) => { try { const { data: relayerResponse } = await Axios.post( `${SVC_BASE_URL}/v1/automations/tasks/execute/${chainId}`, { task: { executable, subaccount, executorSignature, executor } }, { timeout: 50 * 1000 } ); return relayerResponse; } catch (err) { console.log( "failed to execute task ", JSON.stringify(err.response.data, "", 4) ); throw "failed to execute task"; } };
Now that we have all the required helpers, we can combine them for the complete automation execution logic.
for (let subscription of subscriptions) {
const metadata = subscription.metadata;
let refuel = false;
let targetChainID = 0;
let subaccount = subscription.subAccountAddress;
// Check if refuel is needed for any of the target chains
for (let targetChain of metadata.chains) {
if (
await canExecute(targetChain, metadata.refuelAddress, metadata.minAmount)
) {
refuel = true;
targetChainID = targetChain;
break;
}
}
if (refuel) {
let amount = metadata.amount;
logger.info(
logMessage(
"withdrawing amount",
utils.formatEther(BigNumber.from(amount))
)
);
let executable = {};
logger.info(logMessage("Generating Bridging Quote", ""));
// Get quote for bridging
const quote = await getQuote(
CHAIN_ID,
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
targetChainID,
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
amount,
subaccount,
metadata.refuelAddress,
true,
"time",
true
);
const route = quote.result.routes[0];
logger.info(logMessage("Quote", JSON.stringify(route)));
// Get transaction data for the route
const apiReturnData = await getRouteTransactionData(route);
logger.info(logMessage("Call Data", JSON.stringify(apiReturnData, "", 2)));
// Prepare executable transaction data
executable = {
callType: 0,
to: apiReturnData.result.txTarget,
value: BigNumber.from(apiReturnData.result.value).toString(),
data: apiReturnData.result.txData
};
// Sign the call data
const executorSignature = await signCallData(
subaccount,
EXECUTOR_ADDRESS,
executable,
signer,
CHAIN_ID,
RPC_URL
);
logger.info(logMessage("Executor Signature", executorSignature));
// Execute the task
const taskExecResponse = await executeTask({
chainId: CHAIN_ID,
executable,
subaccount,
executorSignature,
executor: EXECUTOR_ADDRESS
});
logger.info(
logMessage("Executed Task", JSON.stringify(taskExecResponse, "", 4))
);
// wait before next run
logger.info(logMessage("Waiting for intent finalization", "~2 mins"));
await delay(60 * second);
} else {
await delay(5 * second);
}
}
Here, EXECUTOR_ADDRESS
, CHAIN_ID
and RPC_URL
must be configured with appropriate values.
Automation Demo
Wrapping up
Reference for the APIs used in this example can be found here.
The complete script for this example automation can be found here.
Last updated