Normal execution
Sign, evaluate, and execute a TRANSFER or SWAP Action Plan through the ALLOW path.
This guide covers signing an Action Plan, evaluating it read-only, and executing it through the normal ALLOW path. It shows both TRANSFER and SWAP examples.
Load the deployment values
cd contracts
set -a
source .env
set +a
MANIFEST="$DEPLOYMENT_MANIFEST_PATH"
GRANTLINE="$(jq -r '.grantline.proxy' "$MANIFEST")"
EVALUATOR="$(jq -r '.modules.evaluator.proxy' "$MANIFEST")"
REGISTRY="$(jq -r '.modules.registry.proxy' "$MANIFEST")"
RPC="$XLAYER_TESTNET_RPC_URL"
AGENT="$AGENT_ADDRESS"TRANSFER example
Human-readable plan
{
"mandateId": "1",
"agent": "0xAgent",
"nonce": "1",
"deadline": "0",
"actions": [
{
"actionType": "TRANSFER",
"version": 1,
"asset": "native",
"to": "0xRecipient",
"amount": "1000000000000000000"
}
]
}Native asset is encoded as the zero address. The amount is 1 OKB in raw base units.
Encode the action parameters
import { encodeAbiParameters, parseEther, zeroAddress } from "viem";
const asset = zeroAddress;
const recipient = "0xRecipient" as `0x${string}`;
const parameters = encodeAbiParameters(
[{ type: "address" }, { type: "address" }, { type: "uint256" }],
[asset, recipient, parseEther("1")],
);
const actionPlan = {
mandateId: 1n,
agent: agentAddress,
nonce: 1n,
deadline: 0n,
actions: [
{
actionType: 0,
version: 1,
parameters,
},
],
} as const;For a token transfer, set asset to the token contract address and use the token's raw base units for amount.
Sign the EIP-712 message
const domain = {
name: "Grantline",
version: "1",
chainId: 1952,
verifyingContract: evaluatorAddress,
} as const;
const types = {
Action: [
{ name: "actionType", type: "uint8" },
{ name: "version", type: "uint8" },
{ name: "parameters", type: "bytes" },
],
ActionPlan: [
{ name: "mandateId", type: "uint256" },
{ name: "agent", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "actions", type: "Action[]" },
],
} as const;
const signature = await walletClient.signTypedData({
account: agentAddress,
domain,
types,
primaryType: "ActionPlan",
message: actionPlan,
});Evaluate before submitting
Use cast to evaluate the plan read-only:
cast call "$GRANTLINE" \
"evaluate((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)((uint8,uint8,uint256,uint256,uint256,uint256,uint256))" \
"($MANDATE_ID, $AGENT, 1, 0, ((0, 1, $TRANSFER_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC"The result fields are: decision, failureCode, failedActionIndex, nativeAmount, nativeUsdValue, nativeBalanceAfter, nativeBalanceUsdValue.
An ALLOW result (decision 0) means the plan can be executed.
Execute
Submit the transaction through the Grantline facade:
cast send "$GRANTLINE" \
"execute((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)(bytes32)" \
"($MANDATE_ID, $AGENT, 1, 0, ((0, 1, $TRANSFER_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC" \
--private-key "$AGENT_PRIVATE_KEY"Verify
Read the ActionPlanExecuted event:
cast logs --rpc-url "$RPC" --address "$GRANTLINE" \
'ActionPlanExecuted(bytes32,uint256,address,address,uint256)' \
--from-block 0Check that the nonce is now consumed:
cast call "$GRANTLINE" \
"getNonceState(uint256,uint256)(bool,bytes32)" \
"$MANDATE_ID" 1 \
--rpc-url "$RPC"The first field (used) should be true. The second field (reservation) should be the zero hash.
SWAP example
Human-readable plan
{
"mandateId": "1",
"agent": "0xAgent",
"nonce": "2",
"deadline": "1700000000",
"actions": [
{
"actionType": "SWAP",
"version": 1,
"swapAdapterId": "UNISWAP_V3",
"tokenIn": "0xTokenIn",
"amountIn": "1000000000000000000",
"tokenOut": "0xTokenOut",
"minAmountOut": "950000000000000000",
"hops": [
{
"pool": "0xPoolAddress",
"tokenIn": "0xTokenIn",
"tokenOut": "0xTokenOut"
}
]
}
]
}Encode SWAP parameters
const swapParameters = encodeAbiParameters(
[
{ type: "uint8" }, // swapAdapterId
{ type: "address" }, // tokenIn
{ type: "uint256" }, // amountIn
{ type: "address" }, // tokenOut
{ type: "uint256" }, // minAmountOut
{
type: "tuple[]",
components: [
{ name: "pool", type: "address" },
{ name: "tokenIn", type: "address" },
{ name: "tokenOut", type: "address" },
],
},
],
[
1, // SwapAdapterId.UNISWAP_V3
tokenInAddress,
parseEther("1"),
tokenOutAddress,
parseEther("0.95"),
[{ pool: poolAddress, tokenIn: tokenInAddress, tokenOut: tokenOutAddress }],
],
);The plan uses actionType: 1 (SWAP) and version: 1.
Evaluate and execute
The evaluate and execute calls follow the same pattern as TRANSFER. The evaluator validates the SWAP route through the configured adapter before returning ALLOW. If the adapter is not configured or the route is invalid, the evaluation returns DENY with SWAP_UNSUPPORTED or INVALID_SWAP_ROUTE.
cast send "$GRANTLINE" \
"execute((uint256,address,uint256,uint256,(uint8,uint8,bytes)[]),bytes)(bytes32)" \
"($MANDATE_ID, $AGENT, 2, 0, ((1, 1, $SWAP_PARAMS)))" \
"$SIGNATURE" \
--rpc-url "$RPC" \
--private-key "$AGENT_PRIVATE_KEY"Failed execution
If evaluation returns DENY, the executor reverts before calling the Vault. A submitted transaction reverts with EvaluationDenied and leaves no committed events.
If evaluation returns ALLOW but a downstream action fails (recipient rejects, token returns false, SWAP output below minimum), the complete transaction reverts. The nonce is not consumed. The receipt status is 0 with revert data.
See Decisions and failures for the full list of failure codes.
Last updated on