Skip to content

Latest commit

 

History

History
466 lines (373 loc) · 12.4 KB

File metadata and controls

466 lines (373 loc) · 12.4 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
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

---
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"]
Loading

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

@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

@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

@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

@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:

@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

@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

@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:

@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.

@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

---
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"]
Loading

Complete Examples

Simple Getter

@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

@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

@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

@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

@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

// 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

@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

// 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

@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: