Skip to main content

Build the infrastructure for Orders

Overview

To implement automated Orders or Orders with price prediction, you should first build the infrastructure shared by both Order types.

This guide explains how to add such components as data structures, utils, deployment scripts, and so on. You'll also implement a transaction signing service using the x/warden module.

Prerequisites

Before you start, meet the prerequisites.

1. Create data structures

First, you need to create contracts defining data structures and interfaces for Orders. Store these files in the src/types directory.

  1. Create a library Types with the core data structures.

    Include data structures for signature requests, different Order types, and oracle/prediction price pairs. In the enum PriceCondition, implement the following price conditions:

     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 than the threshold
    }
  2. Create a library TypesV0 with the common execution data.

  3. Add the IExecutionV0 file with the execution data structure and an interface for executing Orders.

    Include an execute() function for executing Orders, isExecuted() for checking the execution status, and others.

2. Create helpers and utils

In the src/lib directory, create helper and utility libraries for Orders:

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

  2. Create an RLPEncode.sol library implementing RLP encoding for EIP-1559 transactions.

  3. Create a helper contract Create2.sol.

    The main deployment script will use this contract for deploying the Order infrastructure with the CREATE2 opcode.

3. Implement the registry

In the src directory, add a file Registry.sol implementing a registry for tracking transactions.

Include the register() and addTransaction() functions for registering Orders and storing transaction data.

4. Create mock precompiles

Mock precompiles are essential for end-to-end testing of the onchain Agent. In the mocks directory, build three contracts mocking Warden precompiles:

You can test mock precompiles in the test directory:

  1. Create a helper contract for testing the precompiles:

    contract PrecompileTestHelper {
    MockSlinkyPrecompile internal slinky;
    MockWardenPrecompile internal warden;

    function setUp() public {
    // Deploy and configure mocks
    slinky = new MockSlinkyPrecompile();
    warden = new MockWardenPrecompile();

    // Inject mock addresses
    vm.etch(ISLINKY_PRECOMPILE_ADDRESS, address(slinky).code);
    vm.etch(IWARDEN_PRECOMPILE_ADDRESS, address(warden).code);
    }

    // A price scenario helper
    function setupPriceScenario(
    string memory base,
    string memory quote,
    uint256 initialPrice,
    uint256 targetPrice
    ) internal {
    MockSlinkyPrecompile(ISLINKY_PRECOMPILE_ADDRESS)
    .setPrice(base, quote, initialPrice);

    // Simulate a price change
    skip(1 hours);
    MockSlinkyPrecompile(ISLINKY_PRECOMPILE_ADDRESS)
    .setPrice(base, quote, targetPrice);
    }
    }
  2. Create a scenario for testing the price feed:

    contract SlinkyTest is PrecompileTestHelper {
    function testPriceMovement() public {
    // Set up a price scenario
    setupPriceScenario("ETH", "USD", 3000e9, 3500e9);

    // Test the Order execution
    Types.OrderData memory orderData = createTestOrder(
    3200e9, // The threshold
    Types.PriceCondition.GTE
    );

    BasicOrder order = new BasicOrder(
    orderData,
    new CommonTypes.Coin[](0),
    address(this),
    address(registry)
    );

    assertTrue(order.canExecute());
    }

    function testPriceFeedErrors() public {
    vm.expectRevert("Price not set");
    slinky.getPrice("UNKNOWN", "PAIR");
    }
    }
  3. Test transaction signing:

    contract WardenTest is PrecompileTestHelper {
    function testSigningFlow() public {
    // Set up keys
    warden.addKey(1, true); // A valid key
    warden.addKey(2, false); // An invalid key

    // Test successful signing
    Types.SignRequestData memory goodRequest = createSignRequest(1);
    assertTrue(executeOrder(goodRequest));

    // Test failed signing
    Types.SignRequestData memory badRequest = createSignRequest(2);
    assertFalse(executeOrder(badRequest));
    }

    function testInvalidInputs() public {
    vm.expectRevert("Empty approve expression");
    Types.SignRequestData memory invalidRequest = createSignRequest(1);
    invalidRequest.expectedApproveExpression = "";
    executeOrder(invalidRequest);
    }
    }
  4. Use precompiles in scripts:

    contract CreateOrder is Script {
    function run(
    uint256 thresholdPrice,
    Types.PriceCondition priceCondition,
    Types.PricePair memory pricePair
    ) public {
    // Set up mock precompiles
    MockSlinkyPrecompile mSlinky = new MockSlinkyPrecompile();
    MockWardenPrecompile mWarden = new MockWardenPrecompile();

    // Configure the initial state
    vm.etch(ISLINKY_PRECOMPILE_ADDRESS, address(mSlinky).code);
    mSlinky.setPrice(pricePair.base, pricePair.quote, thresholdPrice);

    vm.etch(IWARDEN_PRECOMPILE_ADDRESS, address(mWarden).code);
    mWarden.addKey(1, true);

    // Create and verify an Order
    vm.broadcast();
    BasicOrder order = createOrder(/* params */);
    require(order.canExecute(), "Order cannot execute");
    }
    }

5. Implement transaction signing

In the src/orders directory, create an abstract contract AbstractOrderV0.sol for signing transactions. It'll be called by all types of Orders.

Your code should include a function createSignRequest() that creates signature requests for Keychains. Use the newSignRequest() function of the Warden precompile.

6. Implement Order creation

In the src/factories directory, create an OrderFactory contract for managing the creation and tracking of Orders.

Include the createOrder() and computeOrderAddress() functions for triggering Order deployment and computing the deterministic address of an Order.

Depending on the Order type selected by a user, OrderFactory should invoke either BasicOrderFactory or AdvancedOrderFactory and then emit an OrderCreated event. See the _createBasicOrder() and _createAdvancedOrder() functions.

7. Create deployment scripts

Finally, implement deployment scripts in the script directory:

  1. Implement the main script for deploying the Order infrastructure.

    It should handle the following tasks:

  2. Implement a script for creating Orders.

    It should handle the following tasks:

Next steps

Now you can implement automated Orders.