Infernet
SDK
Consumers
Subscription

SubscriptionConsumer

Technical reference

The SubscriptionConsumer is a simple, easy-to-inherit interface that developers can use in their smart contracts to create recurring requests for off-chain output, delivered via callback.

It is best used when your contract needs:

  1. Recurring, time-based subscriptions fulfilled by Infernet nodes at a fixed interval
  2. To expose dynamic inputs by overriding getContainerInputs()

Using this consumer

Install dependencies

Before getting started, ensure you have installed the Infernet SDK in your Solidity smart contract project. You can follow the installation instructions found in the introduction.

Inherit SubscriptionConsumer.sol

In your smart contract, you must inherit the SubscriptionConsumer.sol abstract contract found in infernet/core/consumers/Subscription.sol:

import {SubscriptionConsumer} from "infernet/core/consumers/Subscription.sol";
 
contract MyContract is SubscriptionConsumer {}

Initialize the consumer

Once inherited, you must provide the address to the Infernet registry contract (see: architecture: coordinator if unfamiliar) to the SubscriptonConsumer constructor:

import {SubscriptionConsumer} from "infernet/core/consumers/Subscription.sol";
 
contract MyContract is SubscriptionConsumer {
    constructor(address registry) SubscriptionConsumer(registry) {}
}

Create a new subscription

By default, the SubscriptionConsumer exposes two functions used to create and cancel subscriptions:

  1. _createComputeSubscription() (technical reference)
  2. _cancelComputeSubscription() (technical refernece)

We can use the _createComputeSubscription() function to run our off-chain test-model ML container workflow once every hour, for a week, with responses from a single node.

import {SubscriptionConsumer} from "infernet/core/consumers/Subscription.sol";
 
contract MyContract is SubscriptionConsumer {
    constructor(address registry) SubscriptionConsumer(registry) {}
 
    function createHourlySubscription() external {
        _createComputeSubscription(
            "test-model", // Container ID
            7 * 24, // Running for a week
            1 hours, // Hourly subscription
            1, // Only 1 responding node
            false, // Eager response
            // No payment
            address(0),
            0,
            address(0),
            // No proof verification
            address(0)
        );
    }
}

Notice, that unlike in the CallbackConsumer, we do not explicitly specify inputs when creating a subscription. This is because the SubscriptionConsumer allows us to expose dynamic inputs via a view function.

Expose dynamic inputs

By default, the SubscriptionConsumer exposes a getContainerInputs() (technical reference) function that we can override to broadcast our inputs dynamically to off-chain nodes.

For more details about the architecture behind how this works, reference Coordinator and Infernet nodes.

For our constrained example, we will simply broadcast the abi.encode'd timestamp multiplied by two:

import {SubscriptionConsumer} from "infernet/core/consumers/Subscription.sol";
 
contract MyContract is SubscriptionConsumer {
    constructor(address registry) SubscriptionConsumer(registry) {}
 
    function getContainerInputs(
        uint32 subscriptionId,
        uint32 interval,
        uint32 timestamp,
        address caller
    ) external view override returns (bytes memory) {
        return abi.encode(timestamp * 2);
    }
 
    function createHourlySubscription() external {
        _createComputeSubscription(
            "test-model", // Container ID
            7 * 24, // Running for a week
            1 hours, // Hourly subscription
            1, // Only 1 responding node
            false, // Eager response
            // No payment
            address(0),
            0,
            address(0),
            // No proof verification
            address(0)
        );
    }
}

Receive container output via callback

By default, the SubscriptionConsumer exposes a _receiveCompute() function (technical reference) which is called every time your smart contract receives a callback response from an Infernet node. This is where you should consume your response outputs, do proof verification, or store response data.

In our example, we will simply push our received output from our test workflow above to an outputs array for future consumption.

import {SubscriptionConsumer} from "infernet/core/consumers/Subscription.sol";
 
contract MyContract is SubscriptionConsumer {
    bytes[] public outputs;
 
    constructor(address registry) SubscriptionConsumer(registry) {}
 
    function getContainerInputs(
        uint32 subscriptionId,
        uint32 interval,
        uint32 timestamp,
        address caller
    ) external view override returns (bytes memory) {
        return abi.encode(timestamp * 2);
    }
 
    function _receiveCompute(
        uint32 subscriptionId,
        uint32 interval,
        uint16 redundancy,
        address node,
        bytes calldata input,
        bytes calldata output,
        bytes calldata proof,
        bytes32 containerId,
        uint256 index
    ) internal override {
        // We simply track `output` for future consumption in our callback
        outputs.push(output);
    }
 
    function createHourlySubscription() external {
        _createComputeSubscription(
            "test-model", // Container ID
            7 * 24, // Running for a week
            1 hours, // Hourly subscription
            1, // Only 1 responding node
            false, // Eager response
            // No payment
            address(0),
            0,
            address(0),
            // No proof verification
            address(0)
        );
    }
}

Test your implementation

That's all it takes to get started with the SubscriptionConsumer! You're now ready to test your implementation with mock data (see: our testing best-practices) and deploy your contracts.

Sane defaults

With great power comes great responsibility — Uncle Ben to Spider-Man

While the SubcriptionConsumer provides unlimited configuration flexibility, using it to its limits requires understanding the SDK's architecture and considering the limits of both your own application and the smart contract execution environment where your contracts will live.

As sane defaults, we recommend:

  • Setting period to be at least long enough for an Infernet node to run your container + reasonably respond on-chain, waiting the appropriate confirmation or finality window