Developers
Understanding Blueprint Macros
Context and Context Extensions

What is a Context?

In the Gadget SDK, a Context simply holds utilities a job may need that aren't direct inputs.

A Context can include various elements such as:

  • HTTP clients for making network requests
  • Access to a keystore
  • Smart contract bindings and wrappers
  • Database connections for data persistence
  • Loggers for recording application events
  • Configuration objects for environment-specific settings
  • Authentication tokens or credentials
  • Caches or in-memory data stores

The Context pattern is closely related to the Dependency Injection (DI) design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.

for more information on dependency injection, see:

Why do we need a Context?

The Context pattern offers several significant benefits in software design and development:

  1. Decoupling: By passing dependencies through a Context, we decouple the job implementation from the specific implementations of its dependencies. This makes the code more modular and easier to maintain.

  2. Testability: With a Context, it's easier to mock or stub dependencies during unit testing, allowing for more comprehensive and isolated tests.

  3. Flexibility: Contexts allow for easy swapping of implementations, which is particularly useful when adapting to different environments (e.g., development, staging, production).

  4. Separation of Concerns: The Context pattern helps separate the configuration and setup of dependencies from their usage, leading to cleaner, more focused code.

  5. Reusability: Jobs that rely on a Context can be more easily reused in different scenarios by simply providing a different Context.

Example: Uptime Service

An example service we can demonstrate contexts for is an uptime service or Is it down for everyone or just me?. The job the a user requests is a url and the function of the service is to check the uptime of this url. In order to check the uptime of the url, it is required to have access to an HTTP client for sending the request. Below, we've created a custom context that can be used to provide an HTTP client to a job.

// Define the Context struct with an HTTP client
#[derive(Debug, Clone)]
struct Context {
    http_client: reqwest::Client,
}
 
impl Context {
    // Constructor for creating a new Context
    fn new() -> Self {
        Self {
            http_client: reqwest::Client::new(),
        }
    }
 
    // Getter method for accessing the HTTP client
    fn http_client(&self) -> &reqwest::Client {
        &self.http_client
    }
}
 
/// Job to check if a website is down for everyone or just you.
#[job(id = 1, params(url), result(_))]
pub async fn down_for_everyone_or_just_me(
    ctx: &Context,  // The Context is passed as a parameter
    url: String,
) -> Result<String, reqwest::Error> {
    // Access the HTTP client from the Context
    let client = ctx.http_client();
    // Make a GET request to the specified URL
    let response = client.get(&url).send().await?;
    let status = response.status();
    // Return a message based on the HTTP status code
    Ok(match status {
        reqwest::StatusCode::OK => format!("It's just you! {url} looks up from here."),
        _ => format!("It's not just you! {url} looks down from here."),
    })
}

In this example, the Context struct encapsulates an HTTP client, which is then used by the down_for_everyone_or_just_me job. This design allows for easy testing and potential replacement of the HTTP client implementation without changing the job's code.

What is a Context Extension?

A Context Extension is a powerful feature in the Gadget SDK that allows you to add functionality to a Context without modifying its original structure. It's essentially a trait that can be implemented for your Context, providing additional methods and capabilities.

Context Extensions offer several advantages:

  1. Modularity: You can add new functionality without changing existing code.
  2. Separation of Concerns: Different aspects of the Context can be defined and implemented separately.
  3. Reusability: Extensions can be shared across different Context types.
  4. Flexibility: You can choose which extensions to implement based on your needs.

For more on extension methods and traits in Rust:

Built-in Context Extensions

The Gadget SDK provides several built-in Context Extensions that offer common functionality:

  1. KeystoreContext: Provides access to the GenericKeyStore (opens in a new tab), useful for managing cryptographic keys and secrets.
  2. EVMProviderContext: Offers access to the EVM (Ethereum Virtual Machine) Provider, which is an RPC Client for interacting with EVM-compatible blockchains.
  3. TangleClientContext: Provides access to the Tangle Client (Subxt), used for interacting with the Tangle network.
  4. ServicesContext: Allows access to the current Service instance properties, useful for service-specific operations.

for more documentation about the built-in extensions, please refer to the API Documentation (opens in a new tab).

These extensions can be easily added to your Context using derive macros, as shown in the following example.

Example: Using the KeystoreContext Extension

Here's an example of how to use the built-in KeystoreContext extension:

use std::convert::Infallible;
use gadget_sdk::ctx::KeystoreContext;
use gadget_sdk::config::StdGadgetConfiguration;
 
#[derive(Debug, Clone, KeystoreContext)] // Derive the KeystoreContext extension
struct Context {
    http_client: reqwest::Client,
    #[config]
    sdk_config: StdGadgetConfiguration,
}
 
#[job(id = 2, params(x), result(_))]
pub fn x_squared(ctx: &Context, x: u64) -> Result<u64, Infallible> {
    let keystore = ctx.keystore(); // Access the keystore using the extension method
    // Use the keystore here ...
    let x_squared = x.pow(2);
    Ok(x_squared)
}

By deriving KeystoreContext, we add the keystore() method to our Context, allowing easy access to the GenericKeyStore. This pattern can be applied to other built-in extensions as well.

How to create a custom Context Extension?

Context Extensions are just Rust traits that can be implemented for your Context struct. Here is an example of how you can create a custom Context Extension:

  1. Define the goal of the extension and the methods it should provide.
  2. Create a new trait with the required methods.
  3. Create a derive macro to automatically implement the trait for your Context struct (optional).
  4. Implement the trait for your Context struct, providing the required methods.

Creating a Custom Context Extension

Creating a custom Context Extension involves the following steps:

  1. Define the extension's purpose and required methods.
  2. Create a new trait with these methods.
  3. (Optional) Create a derive macro for automatic trait implementation.
  4. Implement the trait for your Context struct.

Here's an improved example of a custom HttpClientContext extension:

use reqwest::Client;
 
// Define the HttpClientContext trait
pub trait HttpClientContext {
    fn http_client(&self) -> Client;
}
 
// Implement the trait for our Context
impl HttpClientContext for Context {
    fn http_client(&self) -> Client {
        self.http_client.clone()
    }
}
 
// Example job using the custom extension
#[job(id = 3, params(url), result(_))]
pub async fn fetch_url(ctx: &Context, url: String) -> Result<String, reqwest::Error> {
    let client = ctx.http_client();
    let response = client.get(&url).send().await?;
    let body = response.text().await?;
    Ok(body)
}

Summary and Conclusion

In this guide, we've explored the concepts of Context and Context Extensions in the Gadget SDK:

  1. We learned that a Context encapsulates dependencies and environmental setup for jobs.
  2. We saw how Contexts promote decoupling, testability, and reusability in code.
  3. We examined built-in Context Extensions provided by the Gadget SDK.
  4. We created a custom Context Extension to add new functionality.

These patterns are fundamental to writing maintainable, testable, and flexible blueprints in the Gadget SDK ecosystem.