@btc-vision/btc-runtime
Version:
Bitcoin L1 Smart Contract Runtime for OP_NET. Build decentralized applications on Bitcoin using AssemblyScript and WebAssembly. Fully audited.
653 lines (522 loc) • 16.7 kB
Markdown
# Events
Events in OP_NET allow contracts to emit signals that can be observed by off-chain systems. They're essential for tracking state changes, building user interfaces, and indexing blockchain data.
## Overview
Events are:
- **Write-only** - Contracts can emit but not read events
- **Indexed** - Off-chain systems can filter and search events
- **Size-limited** - Maximum 352 bytes per event
- **Lightweight** - Less overhead than storage writes
## Creating Events
### Event Class Hierarchy
```mermaid
classDiagram
class NetEvent {
<<abstract>>
+eventType: string
#data: BytesWriter
+length: u32
+getEventData() Uint8Array
}
class TransferredEvent {
+constructor(operator, from, to, amount)
eventType = 'Transferred'
}
class TransferredSingleEvent {
+constructor(operator, from, to, id, value)
eventType = 'TransferredSingle'
}
class TransferredBatchEvent {
+constructor(operator, from, to, ids, values)
eventType = 'TransferredBatch'
}
class ApprovedEvent {
+constructor(owner, spender, amount)
eventType = 'Approved'
}
class ApprovedForAllEvent {
+constructor(account, operator, approved)
eventType = 'ApprovedForAll'
}
class MintedEvent {
+constructor(to, amount)
eventType = 'Minted'
}
class BurnedEvent {
+constructor(from, amount)
eventType = 'Burned'
}
class URIEvent {
+constructor(value, id)
eventType = 'URI'
}
class CustomEvent {
+constructor(...)
}
NetEvent <|-- TransferredEvent
NetEvent <|-- TransferredSingleEvent
NetEvent <|-- TransferredBatchEvent
NetEvent <|-- ApprovedEvent
NetEvent <|-- ApprovedForAllEvent
NetEvent <|-- MintedEvent
NetEvent <|-- BurnedEvent
NetEvent <|-- URIEvent
NetEvent <|-- CustomEvent
```
### Using Predefined Events
OP_NET provides common events out of the box:
```typescript
import {
TransferredEvent,
TransferredSingleEvent,
TransferredBatchEvent,
ApprovedEvent,
ApprovedForAllEvent,
MintedEvent,
BurnedEvent,
URIEvent,
} from '@btc-vision/btc-runtime/runtime';
// Emit a transfer event (operator is the caller initiating the transfer)
// Event type: 'Transferred'
this.emitEvent(new TransferredEvent(operator, from, to, amount));
// Emit a single token transfer (ERC1155-style)
// Event type: 'TransferredSingle'
this.emitEvent(new TransferredSingleEvent(operator, from, to, tokenId, amount));
// Emit a batch token transfer (ERC1155-style, max 3 items)
// Event type: 'TransferredBatch'
this.emitEvent(new TransferredBatchEvent(operator, from, to, tokenIds, amounts));
// Emit an approval event
// Event type: 'Approved'
this.emitEvent(new ApprovedEvent(owner, spender, amount));
// Emit an operator approval event
// Event type: 'ApprovedForAll'
this.emitEvent(new ApprovedForAllEvent(account, operator, true));
// Emit a mint event
// Event type: 'Minted'
this.emitEvent(new MintedEvent(to, amount));
// Emit a burn event
// Event type: 'Burned'
this.emitEvent(new BurnedEvent(from, amount));
// Emit a URI event (max 200 bytes for URI)
// Event type: 'URI'
this.emitEvent(new URIEvent('https://example.com/token/1', tokenId));
```
### Custom Events
Create custom events by extending `NetEvent`:
```typescript
import { NetEvent, BytesWriter, Address, ADDRESS_BYTE_LENGTH, U256_BYTE_LENGTH } from '@btc-vision/btc-runtime/runtime';
import { u256 } from '@btc-vision/as-bignum/assembly';
@final
export class StakeEvent extends NetEvent {
public constructor(
staker: Address,
amount: u256,
duration: u64
) {
// Create BytesWriter with appropriate size
const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH + U256_BYTE_LENGTH + 8);
data.writeAddress(staker);
data.writeU256(amount);
data.writeU64(duration);
super('Stake', data);
}
}
```
**Solidity Comparison:**
```solidity
// Solidity
event Stake(address indexed staker, uint256 amount, uint64 duration);
```
```typescript
// OP_NET
@final
export class StakeEvent extends NetEvent {
public constructor(staker: Address, amount: u256, duration: u64) {
const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH + U256_BYTE_LENGTH + 8);
data.writeAddress(staker);
data.writeU256(amount);
data.writeU64(duration);
super('Stake', data);
}
}
```
### Emitting Events
```typescript
// In your contract method
@method()
public stake(calldata: Calldata): BytesWriter {
const amount = calldata.readU256();
const duration = calldata.readU64();
// ... staking logic ...
// Emit event
this.emitEvent(new StakeEvent(
Blockchain.tx.sender,
amount,
duration
));
return new BytesWriter(0);
}
```
### Event Emission Flow
```mermaid
sequenceDiagram
participant Contract
participant EventClass as Event Class Instance
participant BytesWriter
participant Blockchain
participant EventLog as Event Log (Off-chain)
Contract->>EventClass: new TransferEvent(from, to, amount)
activate EventClass
Note over EventClass: Constructor builds data
EventClass->>BytesWriter: new BytesWriter(size)
EventClass->>BytesWriter: writeAddress(from)
EventClass->>BytesWriter: writeAddress(to)
EventClass->>BytesWriter: writeU256(amount)
EventClass->>EventClass: super('Transfer', data)
Note over EventClass: Validates size <= 352 bytes
EventClass-->>Contract: Return event instance
deactivate EventClass
Contract->>Contract: this.emitEvent(event)
activate Contract
Contract->>Blockchain: Blockchain.emit(event)
activate Blockchain
Blockchain->>EventClass: event.getEventData()
EventClass-->>Blockchain: Return encoded bytes
Note over Blockchain: Format: [4B typeLen][type][4B dataLen][data]
Blockchain->>EventLog: Store in transaction receipt
EventLog-->>Blockchain: Event logged
Blockchain-->>Contract: Success
deactivate Blockchain
deactivate Contract
Note over EventLog: Event available for<br/>off-chain indexing
```
## Event Structure
Every event has:
| Component | Description |
|-----------|-------------|
| **Event Type** | String identifier (e.g., "Transferred", "Approved", "Minted", "Burned") |
| **Data** | Encoded event parameters |
| **Contract** | Address of emitting contract (automatic) |
### Encoding
Events are encoded using `BytesWriter`:
```typescript
@final
export class MyEvent extends NetEvent {
public constructor(
value1: u256,
value2: Address,
value3: bool
) {
// Calculate size: 32 (u256) + 32 (Address) + 1 (bool) = 65 bytes
const data: BytesWriter = new BytesWriter(65);
data.writeU256(value1); // 32 bytes
data.writeAddress(value2); // 32 bytes
data.writeBoolean(value3); // 1 byte
super('MyEvent', data); // Event type name and data
}
}
```
### Event Creation Flow
```mermaid
---
config:
theme: dark
---
flowchart LR
Start["Create Event"] --> Init["Create BytesWriter with size"]
Init --> Write["Write data to BytesWriter"]
Write --> Check{"Size <= 352 bytes?"}
Check -->|Yes| Super["Call super(eventType, data)"]
Check -->|No| Revert["Constructor throws Revert"]
Super --> Emit["emitEvent(event)"]
Emit --> Index["Indexed Off-chain"]
```
## Size Limit (352 Bytes)
**CRITICAL:** Events cannot exceed 352 bytes of encoded data.
```typescript
// Calculate your event size
// Address: 32 bytes
// u256: 32 bytes
// u64: 8 bytes
// bool: 1 byte
// string: length + content
@final
export class LargeEvent extends NetEvent {
public constructor(
addr1: Address, addr2: Address, addr3: Address,
amount1: u256, amount2: u256, amount3: u256,
amount4: u256, amount5: u256, amount6: u256,
timestamp: u64
) {
// Calculate: 3 addresses (96) + 6 u256 (192) + 1 u64 (8) = 296 bytes - OK!
const data: BytesWriter = new BytesWriter(296);
data.writeAddress(addr1); // 32 bytes
data.writeAddress(addr2); // 32 bytes
data.writeAddress(addr3); // 32 bytes
data.writeU256(amount1); // 32 bytes
data.writeU256(amount2); // 32 bytes
data.writeU256(amount3); // 32 bytes
data.writeU256(amount4); // 32 bytes
data.writeU256(amount5); // 32 bytes
data.writeU256(amount6); // 32 bytes
data.writeU64(timestamp); // 8 bytes
super('LargeEvent', data);
}
}
```
### What If You Exceed the Limit?
```typescript
// This will fail at runtime with "Event data length exceeds maximum length."
@final
export class TooLargeEvent extends NetEvent {
public constructor(values: u256[]) {
// Writing 11 x u256 = 352 bytes
// This is the absolute maximum!
const data: BytesWriter = new BytesWriter(352);
for (let i = 0; i < 11; i++) {
data.writeU256(values[i]);
}
super('TooLarge', data); // Will throw if data exceeds 352 bytes
}
}
```
**Solutions:**
1. Split into multiple events
2. Only include essential data
3. Use shorter encodings where possible
## Predefined Events
### TransferredEvent
```typescript
// Emitted on token transfers
// Event type: 'Transferred'
new TransferredEvent(operator: Address, from: Address, to: Address, amount: u256)
// Fields:
// - operator: address initiating the transfer
// - from: sender address
// - to: recipient address
// - amount: tokens transferred
```
### TransferredSingleEvent
```typescript
// Emitted on single token transfers (ERC1155-style)
// Event type: 'TransferredSingle'
new TransferredSingleEvent(operator: Address, from: Address, to: Address, id: u256, value: u256)
// Fields:
// - operator: address initiating the transfer
// - from: sender address
// - to: recipient address
// - id: token ID
// - value: amount transferred
```
### TransferredBatchEvent
```typescript
// Emitted on batch token transfers (ERC1155-style)
// Event type: 'TransferredBatch'
// Limited to 3 items due to 352-byte event size limit
new TransferredBatchEvent(operator: Address, from: Address, to: Address, ids: u256[], values: u256[])
// Fields:
// - operator: address initiating the transfer
// - from: sender address
// - to: recipient address
// - ids: array of token IDs (max 3)
// - values: array of amounts (max 3)
```
### ApprovedEvent
```typescript
// Emitted on approval changes
// Event type: 'Approved'
new ApprovedEvent(owner: Address, spender: Address, amount: u256)
// Fields:
// - owner: token owner
// - spender: approved spender
// - amount: approved amount
```
### ApprovedForAllEvent
```typescript
// Emitted on operator approval changes
// Event type: 'ApprovedForAll'
new ApprovedForAllEvent(account: Address, operator: Address, approved: boolean)
// Fields:
// - account: token owner granting approval
// - operator: address being approved/revoked
// - approved: true if approved, false if revoked
```
### MintedEvent
```typescript
// Emitted when tokens are minted
// Event type: 'Minted'
new MintedEvent(to: Address, amount: u256)
// Fields:
// - to: recipient of minted tokens
// - amount: tokens minted
```
### BurnedEvent
```typescript
// Emitted when tokens are burned
// Event type: 'Burned'
new BurnedEvent(from: Address, amount: u256)
// Fields:
// - from: address tokens burned from
// - amount: tokens burned
```
### URIEvent
```typescript
// Emitted when token URI is updated
// Event type: 'URI'
// URI length is limited to 200 bytes
new URIEvent(value: string, id: u256)
// Fields:
// - value: URI string (max 200 bytes)
// - id: token ID
```
## Solidity Comparison
### Event Declaration
```solidity
// Solidity
event Transfer(address indexed from, address indexed to, uint256 value);
```
```typescript
// OP_NET - uses predefined TransferredEvent with operator field
// Event type: 'Transferred'
import { TransferredEvent } from '@btc-vision/btc-runtime/runtime';
// Or create a custom transfer event without operator:
@final
export class TransferEvent extends NetEvent {
public constructor(from: Address, to: Address, value: u256) {
const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 2 + U256_BYTE_LENGTH);
data.writeAddress(from);
data.writeAddress(to);
data.writeU256(value);
super('Transfer', data);
}
}
```
### Emitting Events
```solidity
// Solidity
emit Transfer(from, to, amount);
```
```typescript
// OP_NET - using predefined TransferredEvent (includes operator)
this.emitEvent(new TransferredEvent(Blockchain.tx.sender, from, to, amount));
// Or using custom TransferEvent (without operator)
this.emitEvent(new TransferEvent(from, to, amount));
```
### Indexed Parameters
```solidity
// Solidity: indexed parameters for filtering
event Transfer(address indexed from, address indexed to, uint256 value);
// OP_NET: All parameters can be filtered by off-chain indexers
// (no explicit "indexed" keyword needed)
```
## Best Practices
### 1. Event for Every State Change
```typescript
@method()
@emit('Transferred') // Decorator documents which event this method emits
public transfer(calldata: Calldata): BytesWriter {
const to = calldata.readAddress();
const amount = calldata.readU256();
const from = Blockchain.tx.sender;
// _transfer internally emits TransferredEvent via createTransferredEvent
this._transfer(from, to, amount);
return new BytesWriter(0);
}
// For custom events in your own methods:
@method()
@emit('Staked')
public stake(calldata: Calldata): BytesWriter {
const amount = calldata.readU256();
// Update state first
this._stakes.set(Blockchain.tx.sender, amount);
// Then emit event
this.emitEvent(new StakedEvent(Blockchain.tx.sender, amount));
return new BytesWriter(0);
}
```
### 2. Meaningful Event Names
```typescript
// Good: Descriptive names
@final class TokenStaked extends NetEvent { /* ... */ }
@final class RewardsClaimed extends NetEvent { /* ... */ }
@final class PoolCreated extends NetEvent { /* ... */ }
// Bad: Generic names
@final class Event1 extends NetEvent { /* ... */ }
@final class DataChanged extends NetEvent { /* ... */ }
```
### 3. Include Context
```typescript
// Good: Include relevant context
@final
class SwapExecuted extends NetEvent {
public constructor(
user: Address,
tokenIn: Address,
tokenOut: Address,
amountIn: u256,
amountOut: u256,
timestamp: u64
) {
const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 3 + U256_BYTE_LENGTH * 2 + 8);
data.writeAddress(user);
data.writeAddress(tokenIn);
data.writeAddress(tokenOut);
data.writeU256(amountIn);
data.writeU256(amountOut);
data.writeU64(timestamp);
super('SwapExecuted', data);
}
}
// Bad: Missing context
@final
class Swap extends NetEvent {
public constructor(amount: u256) {
const data: BytesWriter = new BytesWriter(U256_BYTE_LENGTH);
data.writeU256(amount);
super('Swap', data);
}
}
```
### 4. Check Size Before Deployment
```typescript
// Test your event sizes - event.length returns the data size
function testEventSize(): void {
const event = new MyEvent(/* max size parameters */);
// The event validates size in the constructor
// If it exceeds 352 bytes, it will throw a Revert error
assert(event.length <= 352, 'Event exceeds size limit');
}
```
## Multiple Events
You can emit multiple events in a single transaction:
```typescript
@method()
@emit('Burned')
@emit('Minted')
@emit('FeeCollected')
public complexOperation(calldata: Calldata): BytesWriter {
// ... logic ...
// Emit multiple events using predefined events
this.emitEvent(new BurnedEvent(from, burnAmount));
this.emitEvent(new MintedEvent(to, mintAmount));
this.emitEvent(new FeeCollectedEvent(feeRecipient, feeAmount));
return new BytesWriter(0);
}
```
## Listening to Events (Off-chain)
Events are indexed and can be queried:
```typescript
// Off-chain code (not AssemblyScript)
const events = await indexer.getEvents({
contract: tokenAddress,
eventType: 'Transferred', // Use the event type string from the event class
fromBlock: 100000,
toBlock: 'latest',
filter: {
from: userAddress
}
});
```
---
**Navigation:**
- Previous: [Pointers](./pointers.md)
- Next: [Security](./security.md)