Liquidation bot for Zest protocol

Introduction

In this tutorial, we'll guide you through building a liquidation bot for Zest, a lending protocol built on Stacks. We'll focus on how to connect to Ortege's API using WebSockets and the REST syntax to receive real-time data. Specifically, we'll use the newly added stacks_contract_events cube to monitor contract events related to Zest's protocol. We'll show you how to filter by contract_id and function_name to target the events you're interested in.


Table of Contents

  1. Prerequisites

  2. Understanding WebSockets with REST Syntax

  3. API Configuration

  4. The stacks_contract_events Cube

  5. Setting Up the Subscription Query

  6. Python Implementation

  7. JavaScript Implementation

  8. Conclusion


Prerequisites

  • Basic understanding of WebSockets and REST APIs

  • Familiarity with Python or JavaScript

  • Access to Ortege's API (Ensure you have your JWT token)

  • Familiarity with Stacks and Zest protocol concepts


Understanding WebSockets with REST Syntax

WebSockets provide a persistent, full-duplex communication channel between a client and a server. By sending REST-style queries over WebSockets, you can receive real-time updates without the overhead of repeated HTTP requests.

This method allows you to:

  • Maintain a persistent connection for real-time data.

  • Use familiar REST query syntax.

  • Reduce latency and improve efficiency.


API Configuration

Below is the WebSocket endpoint you'll use:

WEBSOCKET_URL = "wss://api-staging.ortege.ai/cubejs-api/v1/load/"

The stacks_contract_events Cube

We've added a new cube called stacks_contract_events to facilitate access to contract event data from the Stacks blockchain. This cube allows you to monitor events emitted by smart contracts, which is essential for building a liquidation bot that responds to specific actions like withdraw, supply, or borrow in the Zest protocol.

Cube Definition:

cubes:
  - name: stacks_contract_events
    sql_table: contract_call_events
    data_source: stacks

    measures:
      - name: asset_amount
        sql: asset_amount
        type: sum

    dimensions:
      - name: contract_id
        sql: contract_id
        type: string

      - name: sender_address
        sql: sender_address
        type: string

      - name: block_number
        sql: block_number
        type: number

      - name: function_name
        sql: function_name
        type: string
      
      - name: event_type
        sql: event_type
        type: string
      
      - name: tx_date
        sql: tx_date
        type: time

      - name: asset_event_type
        sql: asset_event_type
        type: string

      - name: asset_recipient
        sql: asset_recipient
        type: string

Key Dimensions and Measures:

  • Measures:

    • asset_amount: The sum of the asset amounts involved in the events.

  • Dimensions:

    • contract_id: The ID of the smart contract.

    • function_name: The name of the function called (withdraw, supply, borrow).

    • tx_date: The date of the transaction.

    • Other dimensions provide additional context like sender_address and block_number.


Setting Up the Subscription Query

We'll create a REST-style subscription query to monitor events from the Zest protocol's contract and specific functions.

Target Contract ID:

SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.borrow-helper-v1-3

Target Function Names:

  • withdraw

  • supply

  • borrow

REST Query Payload:

{
  "queryType": "subscribe",
  "query": {
    "measures": ["stacks_contract_events.asset_amount"],
    "timeDimensions": [],
    "dimensions": [
      "stacks_contract_events.contract_id",
      "stacks_contract_events.function_name",
      "stacks_contract_events.sender_address",
      "stacks_contract_events.tx_date"
    ],
    "filters": [
      {
        "dimension": "stacks_contract_events.contract_id",
        "operator": "equals",
        "values": ["SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.borrow-helper-v1-3"]
      },
      {
        "dimension": "stacks_contract_events.function_name",
        "operator": "equals",
        "values": ["withdraw", "supply", "borrow"]
      }
    ]
  }
}

Explanation:

  • Measures:

    • We're summing the asset_amount to monitor the total assets involved.

  • Dimensions:

    • contract_id, function_name, sender_address, tx_date provide context for each event.

  • Filters:

    • Contract ID Filter: Ensures we only receive events from the Zest protocol's contract.

    • Function Name Filter: Limits events to the specified functions (withdraw, supply, borrow).


Python Implementation

Installation

Install the required Python packages:

pip install websocket-client

Code Example

import json
import threading
import time
import uuid
from websocket import WebSocketApp

# Configuration Parameters
WEBSOCKET_URL = "wss://api-staging.ortege.ai/cubejs-api/v1/load/"
jwt_token = "YOUR_JWT_TOKEN"  # Replace with your actual JWT token

def example_websocket_query():
    """
    Constructs the REST-style subscription query to be sent over WebSocket.
    """
    query_payload = {
        "queryType": "subscribe",
        "query": {
            "measures": ["stacks_contract_events.asset_amount"],
            "timeDimensions": [],
            "dimensions": [
                "stacks_contract_events.contract_id",
                "stacks_contract_events.function_name",
                "stacks_contract_events.sender_address",
                "stacks_contract_events.tx_date"
            ],
            "filters": [
                {
                    "dimension": "stacks_contract_events.contract_id",
                    "operator": "equals",
                    "values": ["SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.borrow-helper-v1-3"]
                },
                {
                    "dimension": "stacks_contract_events.function_name",
                    "operator": "equals",
                    "values": ["withdraw", "supply", "borrow"]
                }
            ]
        }
    }
    return query_payload

def on_message(ws, message):
    """
    Callback when a message is received from the WebSocket.
    """
    print("\n--- WebSocket Message Received ---")
    try:
        parsed_message = json.loads(message)
        print(json.dumps(parsed_message, indent=2))
        # Add your liquidation logic here
        # For example:
        # for event in parsed_message.get('data', []):
        #     process_event(event)
    except json.JSONDecodeError:
        print(message)

def on_error(ws, error):
    """
    Callback when an error occurs on the WebSocket.
    """
    print(f"\nWebSocket Error: {error}")

def on_close(ws, close_status_code, close_msg):
    """
    Callback when the WebSocket connection is closed.
    """
    print("\nWebSocket Connection Closed")

def on_open(ws):
    """
    Callback when the WebSocket connection is opened.
    Sends the subscription query.
    """
    print("\nWebSocket Connection Opened")

    # Generate a unique requestId using UUID4
    request_id = str(uuid.uuid4())

    # Prepare the subscription message
    query_payload = example_websocket_query()
    query_payload['requestId'] = request_id

    # Send the subscription message
    ws.send(json.dumps(query_payload))
    print(f"WebSocket Subscription Query Sent with requestId: {request_id}")

def websocket_api_query():
    """
    Executes a WebSocket API subscription query against Ortegr's API.
    """
    headers = {
        "Authorization": f"Bearer {jwt_token}"
    }

    ws = WebSocketApp(
        WEBSOCKET_URL,
        header=headers,
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )

    # Run WebSocket in a separate thread to prevent blocking
    wst = threading.Thread(target=ws.run_forever, kwargs={"ping_interval": 60, "ping_timeout": 10})
    wst.daemon = True
    wst.start()
    print("WebSocket thread started.")

    # Keep the main thread alive to listen to incoming messages
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        ws.close()
        print("\nWebSocket Connection Interrupted and Closed")

def main():
    try:
        # Execute the WebSocket API subscription query
        print("\n--- Initiating WebSocket API Subscription ---")
        websocket_api_query()

    except Exception as e:
        print(f"\nWebSocket API Error: {str(e)}")

if __name__ == "__main__":
    main()

Explanation

  • Headers: Include your JWT token in the WebSocket headers.

  • Subscription Message: Send a REST-style query as a JSON payload over the WebSocket connection.

  • Callbacks: Implement on_message, on_error, on_close, and on_open to handle WebSocket events.

  • requestId: A unique identifier for the query, useful for matching responses.

  • Liquidation Logic: Insert your bot's liquidation logic inside the on_message callback where indicated.

Running the Script

  1. Replace "YOUR_JWT_TOKEN" with your actual JWT token.

  2. Run the script:

    python your_script_name.py

JavaScript Implementation

Installation

Install the required Node.js packages:

npm install websocket uuid

Code Example

const WebSocketClient = require('websocket').w3cwebsocket;
const uuid = require('uuid');

// Configuration Parameters
const WEBSOCKET_URL = "wss://api-staging.ortege.ai/cubejs-api/v1/load/";
const jwt_token = "YOUR_JWT_TOKEN"; // Replace with your actual JWT token

function exampleWebSocketQuery() {
  return {
    "queryType": "subscribe",
    "query": {
      "measures": ["stacks_contract_events.asset_amount"],
      "timeDimensions": [],
      "dimensions": [
        "stacks_contract_events.contract_id",
        "stacks_contract_events.function_name",
        "stacks_contract_events.sender_address",
        "stacks_contract_events.tx_date"
      ],
      "filters": [
        {
          "dimension": "stacks_contract_events.contract_id",
          "operator": "equals",
          "values": ["SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N.borrow-helper-v1-3"]
        },
        {
          "dimension": "stacks_contract_events.function_name",
          "operator": "equals",
          "values": ["withdraw", "supply", "borrow"]
        }
      ]
    }
  };
}

function onMessage(message) {
  console.log("\n--- WebSocket Message Received ---");
  try {
    const parsedMessage = JSON.parse(message.data);
    console.log(JSON.stringify(parsedMessage, null, 2));
    // Add your liquidation logic here
    // For example:
    // parsedMessage.data.forEach(event => processEvent(event));
  } catch (error) {
    console.log(message.data);
  }
}

function onError(error) {
  console.error(`\nWebSocket Error: ${error.message}`);
}

function onClose() {
  console.log("\nWebSocket Connection Closed");
}

function onOpen() {
  console.log("\nWebSocket Connection Opened");

  const requestId = uuid.v4();

  const queryPayload = exampleWebSocketQuery();
  queryPayload['requestId'] = requestId;

  ws.send(JSON.stringify(queryPayload));
  console.log(`WebSocket Subscription Query Sent with requestId: ${requestId}`);
}

// WebSocket Connection
const ws = new WebSocketClient(WEBSOCKET_URL, null, null, { 'Authorization': `Bearer ${jwt_token}` });

ws.onopen = onOpen;
ws.onmessage = onMessage;
ws.onerror = onError;
ws.onclose = onClose;

Explanation

  • Headers: Include your JWT token in the WebSocket headers.

  • Subscription Message: Send a REST-style query as a JSON payload.

  • Callbacks: Implement onopen, onmessage, onerror, and onclose to handle WebSocket events.

  • UUID: Use the uuid package to generate a unique requestId.

  • Liquidation Logic: Insert your bot's logic inside the onMessage function.

Running the Script

  1. Replace "YOUR_JWT_TOKEN" with your actual JWT token.

  2. Run the script:

    node your_script_name.js

Conclusion

By integrating the stacks_contract_events cube into your WebSocket subscription, you've tailored your liquidation bot to monitor specific contract events from the Zest protocol. This allows your bot to react promptly to critical events like withdraw, supply, or borrow actions, enhancing its effectiveness.

Next Steps:

  • Implement Logic: Add your liquidation logic inside the on_message (Python) or onMessage (JavaScript) callback.

  • Data Processing: Parse the incoming data to extract necessary information for your bot's operations.

  • Error Handling: Enhance error handling for robustness in production environments.

  • Security: Ensure your JWT token is stored securely.


Note: Always ensure you comply with all relevant laws and regulations when building and deploying bots or automated systems.

Last updated