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.
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;
}
}---
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"]
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') |
The ABIDataTypes enum defines all supported parameter and return 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 |
| Type | Description | Size |
|---|---|---|
ABIDataTypes.ADDRESS |
OP_NET address | 32 bytes |
ABIDataTypes.BYTES |
Variable-length bytes | Variable |
ABIDataTypes.BYTES32 |
Fixed 32-byte value | 32 bytes |
| Type | Description | Size |
|---|---|---|
ABIDataTypes.BOOL |
Boolean value | 1 byte |
ABIDataTypes.STRING |
UTF-8 string | Variable |
| Type | Description |
|---|---|
ABIDataTypes.ADDRESS_ARRAY |
Array of addresses |
ABIDataTypes.BYTES_ARRAY |
Array of byte arrays |
ABIDataTypes.UINT256_ARRAY |
Array of u256 values |
The @method decorator defines input parameters for a contract method.
@method()
@returns({ name: 'supply', type: ABIDataTypes.UINT256 })
public totalSupply(_: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeU256(this._totalSupply.value);
return writer;
}@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;
}@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);
}@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);
}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;
}The @returns decorator defines return values for a contract method.
@method()
@returns({ name: 'decimals', type: ABIDataTypes.UINT8 })
public decimals(_: Calldata): BytesWriter {
const writer = new BytesWriter(1);
writer.writeU8(this._decimals.value);
return writer;
}@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;
}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
}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);
}---
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"]
@method()
@returns({ name: 'owner', type: ABIDataTypes.ADDRESS })
public owner(_: Calldata): BytesWriter {
const writer = new BytesWriter(32);
writer.writeAddress(this._owner.value);
return writer;
}@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;
}@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(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;
}@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);
}// 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);
}@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
// ...
}// 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 })@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: