# Solidity on-chain Reactivity Tutorial

This tutorial builds a Solidity handler contract that subscribes to ERC-20 `Transfer` events and counts them on-chain. The subscription is owned by the handler contract itself, so the contract pays gas for each reactive transaction.

Use on-chain reactivity when the reaction should be part of chain execution. For client-side subscriptions over WebSocket, use [off-chain reactivity](/developer/reactivity/reactivity-offchain.md).

## How This Example Works

The contract:

1. Inherits from `SomniaEventHandler`.
2. Builds a non-wildcard `SubscriptionFilter` for one token's `Transfer` logs.
3. Calls `SomniaExtensions.subscribe(address(this), filter, options)` from the constructor.
4. Receives matching callbacks through `_onEvent`.
5. Can unsubscribe later through `stop()`.

On-chain wildcard subscriptions are not allowed. At least one of `eventTopics`, `origin`, or `emitter` must be set.

## Prerequisites

You'll need:

* Node.js 20+
* A Foundry project
* A Somnia RPC endpoint (see [Network info](/developer/network-info.md))
* A deployer account with enough SOMI or STT to fund the handler contract with at least 32 native tokens, plus gas for deployment and callback execution
* An ERC-20 token address to watch (see [Smart contracts](/developer/smart-contracts.md))

Install the Solidity package:

```bash
npm install @somnia-chain/reactivity-contracts
```

## Step 1: Write the Handler

Create `src/TransferCounter.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import {
    SomniaEventHandler
} from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
import {
    SomniaExtensions
} from "@somnia-chain/reactivity-contracts/contracts/interfaces/SomniaExtensions.sol";

contract TransferCounter is SomniaEventHandler {
    bytes32 private constant TRANSFER_TOPIC =
        keccak256("Transfer(address,address,uint256)");

    event TransferObserved(address indexed from, address indexed to, uint256 value);

    address public immutable owner;
    address public immutable token;
    uint256 public subscriptionId;
    uint256 public transferCount;
    mapping(address recipient => uint256 totalReceived) public receivedBy;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor(address token_, uint64 gasLimit) payable {
        require(token_ != address(0), "Token is zero address");

        owner = msg.sender;
        token = token_;

        SomniaExtensions.SubscriptionFilter memory filter =
            SomniaExtensions.SubscriptionFilter({
                eventTopics: [
                    TRANSFER_TOPIC,
                    bytes32(0),
                    bytes32(0),
                    bytes32(0)
                ],
                origin: address(0),
                emitter: token_
            });

        SomniaExtensions.SubscriptionOptions memory options =
            SomniaExtensions.SubscriptionOptions({
                priorityFeePerGas: 1,
                maxFeePerGas: 0,
                gasLimit: gasLimit
            });

        subscriptionId = SomniaExtensions.subscribe(
            address(this),
            filter,
            options
        );
    }

    function _onEvent(
        address emitter,
        bytes32[] calldata eventTopics,
        bytes calldata data
    ) internal override {
        require(emitter == token, "Unexpected emitter");
        require(eventTopics.length >= 3, "Missing Transfer topics");

        address from = address(uint160(uint256(eventTopics[1])));
        address to = address(uint160(uint256(eventTopics[2])));
        uint256 value = abi.decode(data, (uint256));

        transferCount += 1;
        receivedBy[to] += value;

        emit TransferObserved(from, to, value);
    }

    function stop() external onlyOwner {
        require(subscriptionId != 0, "No active subscription");
        SomniaExtensions.unsubscribe(subscriptionId);
        subscriptionId = 0;
    }

    receive() external payable {}
}
```

`SomniaEventHandler` verifies that only the reactivity precompile can call the external `onEvent` entry point. Your contract only implements `_onEvent`.

## Step 2: Deploy and Fund the Handler

The subscription owner must hold at least 32 SOMI when the subscription is created. Because this constructor creates the subscription, send the funding with the deploy transaction.

```bash
export RPC_URL=<your Somnia RPC endpoint>
export PK=<your funded private key>
export TOKEN=<erc20 token address to watch>

forge create \
  --rpc-url "$RPC_URL" \
  --private-key "$PK" \
  --broadcast \
  --value 33ether \
  src/TransferCounter.sol:TransferCounter \
  --constructor-args "$TOKEN" 2000000
```

`2000000` is the handler gas limit. Increase it if your `_onEvent` logic does more work. Somnia storage operations can require more gas than the same pattern on Ethereum, so leave margin. See the on-chain Reactivity [development advice](/developer/reactivity/reactivity-onchain.md#development-advice) for more gas-limit guidance.

## Step 3: Inspect the Subscription

Set the deployed address:

```bash
export COUNTER=<address printed by forge create>
```

Read the subscription ID:

```bash
cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)'
```

Fetch subscription details from the node:

```bash
SUB_ID=$(cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)' | awk '{print $1}')

cast rpc --rpc-url "$RPC_URL" \
  somnia_reactivityGetSubscriptionInfo "$(cast to-hex "$SUB_ID")"
```

You should see the `Transfer` event topic and the token address in the stored filter.

## Step 4: Trigger a Matching Transfer

Send or otherwise trigger a transfer on the watched token:

```bash
cast send --rpc-url "$RPC_URL" --private-key "$PK" \
  "$TOKEN" 'transfer(address,uint256)' <recipient> <amount>
```

After the transfer lands, the chain checks the log against active subscriptions. If it matches, validators include a synthetic transaction that calls the handler.

Read the counter:

```bash
cast call --rpc-url "$RPC_URL" "$COUNTER" 'transferCount()(uint256)'
```

Read the tracked amount for a recipient:

```bash
cast call --rpc-url "$RPC_URL" "$COUNTER" \
  'receivedBy(address)(uint256)' <recipient>
```

## Step 5: Stop the Subscription

When you no longer want the contract to react, unsubscribe:

```bash
cast send --rpc-url "$RPC_URL" --private-key "$PK" \
  "$COUNTER" 'stop()'
```

After `stop()` succeeds, the old subscription ID should return an empty result from `somnia_reactivityGetSubscriptionInfo`.

## Notes

* The handler runs in a separate reactive transaction after the triggering transaction has executed.
* `msg.sender` inside `onEvent` is the reactivity precompile at `0x0100`.
* `tx.origin` inside the handler is the subscription owner.
* The owner pays for every handler invocation.
* `caller`, `isGuaranteed`, and `isCoalesced` are reserved fields on the raw precompile struct. `SomniaExtensions` sets them to their required defaults.
* Avoid feedback loops where the handler emits an event that also matches its own subscription. See the on-chain Reactivity [development advice](/developer/reactivity/reactivity-onchain.md#development-advice) before subscribing to events your handler might emit.

For the full API reference and failure modes, see [On-chain Reactivity](/developer/reactivity/reactivity-onchain.md).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.somnia.network/developer/reactivity/tutorials/solidity-on-chain-reactivity-tutorial.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
