UNPKG

@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
# 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)