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.

467 lines (373 loc) 12.7 kB
# Decorators Decorators are essential for OP_NET smart contracts. They define the ABI (Application Binary Interface) that allows external callers to interact with your contract methods. ## Overview OP_NET uses three main decorators: | Decorator | Purpose | |-----------|---------| | `@method()` | Defines input parameters for a contract method | | `@returns()` | Defines return values for a contract method | | `@emit()` | Specifies which event a method emits | ```typescript import { OP_NET, Calldata, BytesWriter, ABIDataTypes } from '@btc-vision/btc-runtime/runtime'; @final class MyContract extends OP_NET { public constructor() { super(); } @method({ name: 'recipient', type: ABIDataTypes.ADDRESS }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) @emit('Transferred') public transfer(calldata: Calldata): BytesWriter { const recipient = calldata.readAddress(); // ... implementation const writer = new BytesWriter(1); writer.writeBoolean(true); return writer; } } ``` ### Decorator Flow and ABI Generation ```mermaid --- config: theme: dark --- flowchart LR Start["Contract Source Code"] --> Parse["Compiler"] Parse --> Extract["Extract Decorators"] Extract --> Build["Build ABI Entry"] Build --> Gen["Generate Selector<br/>SHA256 -> u32"] Gen --> Output["abi.json"] ``` ## Solidity Comparison OP_NET decorators serve the same purpose as Solidity's function signatures but are more explicit: | Solidity | OP_NET | |----------|-------| | `function name() public view returns (string)` | `@method() @returns({ name: 'name', type: ABIDataTypes.STRING })` | | `function balanceOf(address owner) public view returns (uint256)` | `@method({ name: 'owner', type: ABIDataTypes.ADDRESS }) @returns({ name: 'balance', type: ABIDataTypes.UINT256 })` | | `function transfer(address to, uint256 amount) public` | `@method({ name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 })` | | `event Transfer(address from, address to, uint256 amount)` | `@emit('Transferred')` | ## ABIDataTypes The `ABIDataTypes` enum defines all supported parameter and return types: ### Numeric Types | Type | Description | Size | |------|-------------|------| | `ABIDataTypes.UINT8` | Unsigned 8-bit integer | 1 byte | | `ABIDataTypes.UINT16` | Unsigned 16-bit integer | 2 bytes | | `ABIDataTypes.UINT32` | Unsigned 32-bit integer | 4 bytes | | `ABIDataTypes.UINT64` | Unsigned 64-bit integer | 8 bytes | | `ABIDataTypes.UINT128` | Unsigned 128-bit integer | 16 bytes | | `ABIDataTypes.UINT256` | Unsigned 256-bit integer | 32 bytes | ### Address and Bytes Types | Type | Description | Size | |------|-------------|------| | `ABIDataTypes.ADDRESS` | OP_NET address | 32 bytes | | `ABIDataTypes.BYTES` | Variable-length bytes | Variable | | `ABIDataTypes.BYTES32` | Fixed 32-byte value | 32 bytes | ### Other Types | Type | Description | Size | |------|-------------|------| | `ABIDataTypes.BOOL` | Boolean value | 1 byte | | `ABIDataTypes.STRING` | UTF-8 string | Variable | ### Array Types | Type | Description | |------|-------------| | `ABIDataTypes.ADDRESS_ARRAY` | Array of addresses | | `ABIDataTypes.BYTES_ARRAY` | Array of byte arrays | | `ABIDataTypes.UINT256_ARRAY` | Array of u256 values | ## @method Decorator The `@method` decorator defines input parameters for a contract method. ### No Parameters ```typescript @method() @returns({ name: 'supply', type: ABIDataTypes.UINT256 }) public totalSupply(_: Calldata): BytesWriter { const writer = new BytesWriter(32); writer.writeU256(this._totalSupply.value); return writer; } ``` ### Single Parameter ```typescript @method({ name: 'owner', type: ABIDataTypes.ADDRESS }) @returns({ name: 'balance', type: ABIDataTypes.UINT256 }) public balanceOf(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const balance = this._balances.get(owner); const writer = new BytesWriter(32); writer.writeU256(balance); return writer; } ``` ### Multiple Parameters ```typescript @method( { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 }, ) @emit('Transferred') public transfer(calldata: Calldata): BytesWriter { const to = calldata.readAddress(); const amount = calldata.readU256(); this._transfer(Blockchain.tx.sender, to, amount); return new BytesWriter(0); } ``` ### Complex Parameters ```typescript @method( { name: 'owner', type: ABIDataTypes.ADDRESS }, { name: 'spender', type: ABIDataTypes.ADDRESS }, { name: 'value', type: ABIDataTypes.UINT256 }, { name: 'deadline', type: ABIDataTypes.UINT64 }, { name: 'signature', type: ABIDataTypes.BYTES }, ) @emit('Approved') public permit(calldata: Calldata): BytesWriter { const owner = calldata.readAddress(); const spender = calldata.readAddress(); const value = calldata.readU256(); const deadline = calldata.readU64(); const signature = calldata.readBytesWithLength(); // ... implementation return new BytesWriter(0); } ``` ### Named Method Override When your method name differs from the ABI name: ```typescript @method('name') // ABI will use 'name' as the method name @returns({ name: 'name', type: ABIDataTypes.STRING }) public fn_name(_: Calldata): BytesWriter { // Method is called 'fn_name' in code but 'name' in ABI const writer = new BytesWriter(this._name.value.length + 4); writer.writeString(this._name.value); return writer; } ``` ## @returns Decorator The `@returns` decorator defines return values for a contract method. ### Single Return Value ```typescript @method() @returns({ name: 'decimals', type: ABIDataTypes.UINT8 }) public decimals(_: Calldata): BytesWriter { const writer = new BytesWriter(1); writer.writeU8(this._decimals.value); return writer; } ``` ### Multiple Return Values ```typescript @method() @returns( { name: 'name', type: ABIDataTypes.STRING }, { name: 'symbol', type: ABIDataTypes.STRING }, { name: 'decimals', type: ABIDataTypes.UINT8 }, { name: 'totalSupply', type: ABIDataTypes.UINT256 }, ) public metadata(_: Calldata): BytesWriter { const writer = new BytesWriter(256); writer.writeString(this._name.value); writer.writeString(this._symbol.value); writer.writeU8(this._decimals.value); writer.writeU256(this._totalSupply.value); return writer; } ``` ### No Return Value Methods that only mutate state: ```typescript @method( { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 }, ) @emit('Transferred') public transfer(calldata: Calldata): BytesWriter { const to = calldata.readAddress(); const amount = calldata.readU256(); this._transfer(Blockchain.tx.sender, to, amount); return new BytesWriter(0); // Empty return } ``` ## @emit Decorator The `@emit` decorator specifies which event a method emits. This is used for ABI generation but doesn't automatically emit the event - you must call `this.emitEvent()` in your implementation. ```typescript @method( { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 }, ) @emit('Transferred') // Indicates this method emits Transferred event public transfer(calldata: Calldata): BytesWriter { const to = calldata.readAddress(); const amount = calldata.readU256(); const from = Blockchain.tx.sender; this._transfer(from, to, amount); // You must still emit the event manually this.emitEvent(new TransferredEvent(from, from, to, amount)); return new BytesWriter(0); } ``` ### How Decorators Work Together ```mermaid --- config: theme: dark --- flowchart LR Code["Method with Decorators"] --> Extract["Extract Metadata"] Extract --> GenSig["Generate Signature"] GenSig --> Selector["Selector: 0xABCD1234"] Selector --> ABI["ABI Entry"] ABI --> Call["External Call"] Call --> Match{"Match?"} Match -->|Yes| Execute["Execute Method"] Match -->|No| Next["Next Method"] Execute --> Return["Return Result"] ``` ## Complete Examples ### Simple Getter ```typescript @method() @returns({ name: 'owner', type: ABIDataTypes.ADDRESS }) public owner(_: Calldata): BytesWriter { const writer = new BytesWriter(32); writer.writeAddress(this._owner.value); return writer; } ``` ### Getter with Parameter ```typescript @method({ name: 'tokenId', type: ABIDataTypes.UINT256 }) @returns({ name: 'owner', type: ABIDataTypes.ADDRESS }) public ownerOf(calldata: Calldata): BytesWriter { const tokenId = calldata.readU256(); const owner = this._owners.get(tokenId); if (owner.isZero()) { throw new Revert('Token does not exist'); } const writer = new BytesWriter(32); writer.writeAddress(owner); return writer; } ``` ### State-Mutating Method ```typescript @method( { name: 'spender', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 }, ) @emit('Approved') public approve(calldata: Calldata): BytesWriter { const spender = calldata.readAddress(); const amount = calldata.readU256(); const owner = Blockchain.tx.sender; this._approve(owner, spender, amount); this.emitEvent(new ApprovedEvent(owner, spender, amount)); return new BytesWriter(0); } ``` ### Method with Bytes Input ```typescript @method(ABIDataTypes.BYTES) // Shorthand for { name: 'data', type: ABIDataTypes.BYTES } @returns({ name: 'valid', type: ABIDataTypes.BOOL }) public verifySignature(calldata: Calldata): BytesWriter { const signature = calldata.readBytesWithLength(); const message = new BytesWriter(32); message.writeString('Sign this message'); const messageHash = sha256(message.getBuffer()); const isValid = Blockchain.verifySignature( Blockchain.tx.origin, signature, messageHash, true ); const writer = new BytesWriter(1); writer.writeBoolean(isValid); return writer; } ``` ### Full Token Transfer ```typescript @method( { name: 'from', type: ABIDataTypes.ADDRESS }, { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 }, ) @emit('Transferred') public transferFrom(calldata: Calldata): BytesWriter { const from = calldata.readAddress(); const to = calldata.readAddress(); const amount = calldata.readU256(); const spender = Blockchain.tx.sender; // Check and update allowance const currentAllowance = this._allowances.get(from).get(spender); if (currentAllowance < amount) { throw new Revert('Insufficient allowance'); } // Deduct from allowance (unless unlimited) if (currentAllowance != u256.Max) { this._allowances.get(from).set(spender, SafeMath.sub(currentAllowance, amount)); } // Transfer this._transfer(from, to, amount); this.emitEvent(new TransferredEvent(spender, from, to, amount)); return new BytesWriter(0); } ``` ## Best Practices ### 1. Always Use Decorators for Public Methods ```typescript // Good - properly decorated @method({ name: 'amount', type: ABIDataTypes.UINT256 }) @emit('Burned') public burn(calldata: Calldata): BytesWriter { // ... return new BytesWriter(0); } // Bad - no decorators public burn(calldata: Calldata): BytesWriter { // Callers won't know the ABI return new BytesWriter(0); } ``` ### 2. Match Read Order with Parameter Order ```typescript @method( { name: 'to', type: ABIDataTypes.ADDRESS }, { name: 'amount', type: ABIDataTypes.UINT256 }, ) public transfer(calldata: Calldata): BytesWriter { // Read in same order as @method parameters const to = calldata.readAddress(); // First const amount = calldata.readU256(); // Second // ... } ``` ### 3. Use Descriptive Names ```typescript // Good - clear names @method({ name: 'recipient', type: ABIDataTypes.ADDRESS }) @returns({ name: 'success', type: ABIDataTypes.BOOL }) // Less clear @method({ name: 'a', type: ABIDataTypes.ADDRESS }) @returns({ name: 'r', type: ABIDataTypes.BOOL }) ``` ### 4. Group Related Returns ```typescript @method() @returns( { name: 'name', type: ABIDataTypes.STRING }, { name: 'symbol', type: ABIDataTypes.STRING }, { name: 'decimals', type: ABIDataTypes.UINT8 }, { name: 'totalSupply', type: ABIDataTypes.UINT256 }, { name: 'domainSeparator', type: ABIDataTypes.BYTES32 }, ) public metadata(_: Calldata): BytesWriter { // Single call returns all token metadata } ``` --- **Navigation:** - Previous: [Events](./events.md) - Next: [Security](./security.md)