Skip to main content

Create helpers and utils

Overview

This article will guide you through building a foundation for the Agent executing automated Orders. You'll create helper libraries and contracts defining the core data structures and interfaces for managing Orders.

Directory

Store helper libraries and contracts in the /src directory.

1. Define data structures

First, create a library Types.sol with the core data structures:

Full code

You can find the full code on GitHub: /src/Types.sol

/src/Types.sol
library Types {
// Define swap parameters for Uniswap
struct SwapData {
uint256 amountIn; // The amount of input tokens
address[] path; // The trading path through Uniswap
address to; // The recipient address
uint256 deadline; // The transaction deadline
}

// A price condition for flexible trading
enum PriceCondition {
LTE, // Less than or equal to the threshold
GTE // Greater than or equal to the threshold
LT, // Less than the threshold
GT // Greater thatn the threshold
}

// The main order configuration
struct OrderData {
uint256 thresholdPrice;
PriceCondition priceCondition;
PricePair pricePair;
CreatorDefinedTxFields creatorDefinedTxFields;
SwapData swapData;
SignRequestData signRequestData;
}
}

2. Create an abstract Order

Now, add an abstract contract with functions required to create Orders of both types. Your code should include a function for creating signature requests by calling the Warden precompile.

Full code

You can find the full code on GitHub: /src/AbstractOrder.sol

/src/AbstractOrder.sol
abstract contract AbstractOrder {

function encodeUnsignedEIP1559(
uint256 nonce,
uint256 gas,
uint256 maxPriorityFeePerGas,
uint256 maxFeePerGas,
bytes[] calldata accessList,
Types.CreatorDefinedTxFields calldata creatorDefinedTxFields
)
public
pure
returns (bytes memory unsignedTx, bytes32 txHash)
{
uint256 txType = 2; // eip1559 tx type
bytes[] memory txArray = new bytes[](9);
txArray[0] = RLPEncode.encodeUint(creatorDefinedTxFields.chainId);
txArray[1] = RLPEncode.encodeUint(nonce);
txArray[2] = RLPEncode.encodeUint(maxPriorityFeePerGas);
txArray[3] = RLPEncode.encodeUint(maxFeePerGas);
txArray[4] = RLPEncode.encodeUint(gas);
txArray[5] = RLPEncode.encodeAddress(creatorDefinedTxFields.to);
txArray[6] = RLPEncode.encodeUint(creatorDefinedTxFields.value);
txArray[7] = RLPEncode.encodeBytes(creatorDefinedTxFields.data);
txArray[8] = RLPEncode.encodeList(accessList);
bytes memory unsignedTxEncoded = RLPEncode.encodeList(txArray);
unsignedTx = RLPEncode.concat(RLPEncode.encodeUint(txType), unsignedTxEncoded);
txHash = keccak256(unsignedTx);
}

function buildExecutionData(Types.CreatorDefinedTxFields calldata creatorDefinedTxFields)
public
view
returns (ExecutionData memory data)
{
data = ExecutionData({
caller: _keyAddress,
to: creatorDefinedTxFields.to,
chainId: creatorDefinedTxFields.chainId,
value: creatorDefinedTxFields.value,
data: creatorDefinedTxFields.data
});
}

// Create a new signature request by calling the Warden precompile
function createSignRequest(
Types.SignRequestData calldata signRequestData,
bytes calldata signRequestInput,
CommonTypes.Coin[] calldata maxKeychainFees
)
public
returns (bool)
{
return WARDEN_PRECOMPILE.newSignRequest(
signRequestData.keyId,
signRequestInput,
signRequestData.analyzers,
signRequestData.encryptionKey,
maxKeychainFees,
signRequestData.spaceNonce,
signRequestData.actionTimeoutHeight,
signRequestData.expectedApproveExpression,
signRequestData.expectedRejectExpression,
BroadcastType.Automatic
);
}
}

3. Implement the registry

In a file Registry.sol, implement a registry for tracking transactions:

Full code

You can find the full code on GitHub: /src/Registry.sol

/src/Registry.sol
contract Registry is ReentrancyGuard {
// Track Order creators
mapping(address executionAddress => address orderCreator) public executions;

// Store the transaction data
mapping(bytes32 txHash => bytes tx) public transactions;

// Register an Order with additional validation
function register(address execution) public {
if (execution == address(0)) {
revert InvalidExecutionAddress();
}
// Additional validation...

executions[execution] = msg.sender;
emit Registered(msg.sender, execution);
}
}

4. Implement string operations

To support EIP-1559 transactions, create a Strings.sol library implementing string operations:

Full code

You can find the full code on GitHub: /src/Strings.sol

/src/Strings.sol
library Strings {

// Parse a hexadecimal string and return the value as an `address`
function parseAddress(string memory input) internal pure returns (address) {
return parseAddress(input, 0, bytes(input).length);
}

// Parse a substring of `input` located between position `begin` (included) and `end` (excluded)
function parseAddress(string memory input, uint256 begin, uint256 end) internal pure returns (address) {
(bool success, address value) = tryParseAddress(input, begin, end);
if (!success) revert StringsInvalidAddressFormat();
return value;
}

// Add more functions for string operations...
}

5. Implement RLP encoding

To support EIP-1559 transactions, create an RLPEncode.sol library implementing RLP encoding:

Full code

You can find the full code on GitHub: /src/RLPEncode.sol

/src/RLPEncode.sol
library RLPEncode {
// Implement the RLP encoding for EIP-1559 transactions
function encodeTransaction(
Types.OrderData memory orderData,
uint256 nonce,
uint256 gas,
uint256 maxPriorityFeePerGas,
uint256 maxFeePerGas
) internal pure returns (bytes memory) {
bytes[] memory txArray = new bytes[](9);
// Transaction encoding...
return RLPEncode.encodeList(txArray);
}
}

6. Implement custom deployment

Create a helper contract used in the main deployment script. This contract allows deploying the infrastructure for Orders with the CREATE2 opcode.

Full code

You can find the full code on GitHub: /src/Create2.sol

/src/Create2.sol
contract Create2 {

function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address addr) {
if (creationCode.length == 0) {
revert Create2EmptyBytecode();
}

// solhint-disable-next-line
assembly {
addr := create2(callvalue(), add(creationCode, 0x20), mload(creationCode), salt)
}

if (addr == address(0)) {
revert Create2FailedDeployment();
}
}

function computeAddress(bytes32 salt, bytes32 creationCodeHash) external view returns (address addr) {
address contractAddress = address(this);

// solhint-disable-next-line
assembly {
let ptr := mload(0x40)

mstore(add(ptr, 0x40), creationCodeHash)
mstore(add(ptr, 0x20), salt)
mstore(ptr, contractAddress)
let start := add(ptr, 0x0b)
mstore8(start, 0xff)
addr := keccak256(start, 85)
}
}
}

Next steps

After building the foundation for your Agent, you can create mock precompiles.