Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 21 additions & 62 deletions docs/stylus/how-tos/exporting-abi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ interface IMyContract {

function setValue(uint256 new_value) external;

error Unauthorized(address caller);
error Unauthorized(address);
}
```

Expand All @@ -91,39 +91,16 @@ Generate JSON format ABI (requires `solc` installed):
cargo stylus export-abi --json > abi.json
```

Output:
The JSON output is produced by `solc`, so it includes `solc`'s header lines before the ABI array:

```json
[
{
"type": "function",
"name": "getValue",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "setValue",
"inputs": [
{
"name": "new_value",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
}
]
```
======= <stdin>:IMyContract =======
Contract JSON ABI
[{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"Unauthorized","type":"error"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"new_value","type":"uint256"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]
```

Strip the two header lines before feeding the array to front-end tooling.

## Writing ABI-compatible contracts

### Basic contract structure
Expand Down Expand Up @@ -281,14 +258,14 @@ impl Token {
}
Comment thread
anegg0 marked this conversation as resolved.
```

Generated interface includes errors:
Generated interface includes errors. Note that the exported Solidity drops the error parameter names, keeping only their types:

```solidity
interface IToken {
function transfer(address to, uint256 amount) external;

error InsufficientBalance(address account, uint256 requested, uint256 available);
error Unauthorized(address caller);
error InsufficientBalance(address, uint256, uint256);
error Unauthorized(address);
error InvalidAmount();
}
```
Expand Down Expand Up @@ -444,10 +421,10 @@ impl MyContract {
}
```

Export constructor signature:
Export constructor signature with the top-level `constructor` command (the constructor is not part of `export-abi` output):

```shell
cargo stylus export-abi constructor
cargo stylus constructor
```

Output:
Expand Down Expand Up @@ -478,29 +455,6 @@ constructor(address owner) payable

## Export configuration

### Custom license

Specify a custom SPDX license identifier:

```shell
cargo stylus export-abi --license=GPL-3.0
```

Output includes:

```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;
```

### Custom pragma

Specify a custom Solidity version pragma:

```shell
cargo stylus export-abi --pragma="pragma solidity ^0.8.20;"
```

### Rust features

Export ABI with specific Rust features enabled:
Expand Down Expand Up @@ -795,7 +749,7 @@ cp abi.json ../frontend/src/abis/
When adding or modifying constructors, regenerate and commit:

```shell
cargo stylus export-abi constructor > CONSTRUCTOR.txt
cargo stylus constructor > CONSTRUCTOR.txt
git add CONSTRUCTOR.txt
git commit -m "Update constructor signature"
```
Expand Down Expand Up @@ -958,11 +912,16 @@ impl AbiType for MyType {

Export ABIs for all contracts in a workspace:

The `--contract` value is a cargo package name. Repeat the flag to select several contracts; with no `--contract`, the workspace's default contracts are exported.

```shell
# Export specific contract
# Export specific contract by package name
cargo stylus export-abi --contract=my-token

# Export all contracts
# Select several contracts in one invocation
cargo stylus export-abi --contract=token --contract=staking

# Export each to its own file
for contract in token staking governance; do
cargo stylus export-abi --contract=$contract > interfaces/I${contract^}.sol
done
Expand Down
52 changes: 25 additions & 27 deletions docs/stylus/how-tos/trait-based-composition.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ use stylus_sdk::{
};

// Define traits for different functionality
#[public]
trait IErc20 {
fn name(&self) -> String;
fn symbol(&self) -> String;
Expand All @@ -72,6 +73,7 @@ trait IErc20 {
fn transfer(&mut self, to: Address, value: U256) -> bool;
}

#[public]
trait IOwnable {
fn owner(&self) -> Address;
fn transfer_ownership(&mut self, new_owner: Address) -> bool;
Expand Down Expand Up @@ -181,44 +183,41 @@ When using trait-based composition, you need to be careful about function select
</VanillaAdmonition>

<CustomDetails summary="Selector issue example: ERC-721">

```rust
// In Solidity, both these functions would have different selectors:
// function safeTransferFrom(address from, address to, uint256 tokenId)
// function safeTransferFrom(address from, address to, uint256 tokenId, bytes data)

// In Rust, we need to use different method names, but want the same selectors: #[public]
impl<T: Erc721Params> Erc721<T> {
// Use the #[selector] attribute to specify the correct Solidity-compatible name #[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from_with_data<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
from: Address,
to: Address,
token_id: U256,
data: Bytes,
) -> Result<(), Erc721Error> {
// Implementation
}
// In Solidity, these two functions share the name `safeTransferFrom` but have
// different selectors because their parameter lists differ:
// function safeTransferFrom(address from, address to, uint256 tokenId)
// function safeTransferFrom(address from, address to, uint256 tokenId, bytes data)

// Rust has no method overloading, so each variant needs a distinct Rust name.
// Use the #[selector(name = "...")] attribute to export a Solidity-compatible
// selector. Because the two variants take different parameters, both can keep
// the `safeTransferFrom` name on the ABI side without colliding.
#[public]
impl Erc721 {
#[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from(&mut self, from: Address, to: Address, token_id: U256) {
// Implementation here
}

// This method also needs the same selector name
#[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
pub fn safe_transfer_from_with_data(
&mut self,
from: Address,
to: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
// Implementation
data: Bytes,
) {
// Implementation here
}

}

```

</CustomDetails>

<VanillaAdmonition type="info" title="ABI generation and inheritance">
The Stylus SDK generates ABIs based on the methods that are available at the entrypoint contract.
When using trait-based composition, make sure that all methods you want exposed in the ABI are
properly included through the #[implements] attribute.
The Stylus SDK generates ABIs based on the methods that are available at the entrypoint contract. When using trait-based composition, make sure that all methods you want exposed in the ABI are properly included through the #[implements] attribute.
</VanillaAdmonition>

## Methods search order
Expand All @@ -235,4 +234,3 @@ In a typical composition chain:
- If not found there, it looks in the first trait specified in the inheritance list
- If still not found, it searches in the next trait in the list
- This continues until the method is found or all possibilities are exhausted
```
71 changes: 63 additions & 8 deletions docs/stylus/how-tos/using-constructors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ pub fn constructor(
self.balances.setter(deployer).set(initial_supply);

// Emit initialization event
log(self.vm(), TokenCreated {
self.vm().log(TokenCreated {
creator: deployer,
name: name.clone(),
symbol: symbol.clone(),
Expand All @@ -222,12 +222,30 @@ pub fn constructor(
Stylus uses trait-based composition instead of traditional inheritance. When implementing constructors with traits, each component typically has its own initialization logic:

```rust
// Define traits for different functionality
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use stylus_sdk::prelude::*;
use stylus_sdk::storage::{StorageAddress, StorageMap, StorageString, StorageU256};

sol! {
#[derive(Debug)]
error InvalidSupply();
}

#[derive(SolidityError, Debug)]
pub enum TokenError {
InvalidSupply(InvalidSupply),
}

// Define traits for different functionality. Traits exposed through
// #[implements(...)] must themselves be annotated with #[public].
#[public]
trait IErc20 {
fn balance_of(&self, account: Address) -> U256;
fn transfer(&mut self, to: Address, value: U256) -> bool;
}

#[public]
trait IOwnable {
fn owner(&self) -> Address;
fn transfer_ownership(&mut self, new_owner: Address) -> bool;
Expand Down Expand Up @@ -284,7 +302,7 @@ impl MyToken {

fn initialize_erc20(&mut self, initial_supply: U256) -> Result<(), TokenError> {
if initial_supply == U256::ZERO {
return Err(TokenError::InvalidSupply);
return Err(TokenError::InvalidSupply(InvalidSupply {}));
}

let deployer = self.vm().tx_origin();
Expand All @@ -293,6 +311,41 @@ impl MyToken {
Ok(())
}
}

// Each trait listed in #[implements(...)] must have a matching implementation.
#[public]
impl IErc20 for MyToken {
fn balance_of(&self, account: Address) -> U256 {
self.erc20.balances.get(account)
}

fn transfer(&mut self, to: Address, value: U256) -> bool {
let from = self.vm().msg_sender();
let from_balance = self.erc20.balances.get(from);
if from_balance < value {
return false;
}
self.erc20.balances.setter(from).set(from_balance - value);
let to_balance = self.erc20.balances.get(to);
self.erc20.balances.setter(to).set(to_balance + value);
true
}
}

#[public]
impl IOwnable for MyToken {
fn owner(&self) -> Address {
self.ownable.owner.get()
}

fn transfer_ownership(&mut self, new_owner: Address) -> bool {
if self.vm().msg_sender() != self.ownable.owner.get() {
return false;
}
self.ownable.owner.set(new_owner);
true
}
}
```

<VanillaAdmonition type="info" title="Trait-based composition in Stylus">
Expand Down Expand Up @@ -363,12 +416,14 @@ mod tests {

Deploy your contract with constructor arguments using `cargo stylus deploy`:

```bash
# Deploy with constructor parameters
```shell
# Deploy with constructor parameters.
# The constructor is annotated #[payable], so use --constructor-value to send ETH to it.
cargo stylus deploy \
--private-key-path ~/.arbitrum/key \
--endpoint https://sepolia-rollup.arbitrum.io/rpc \
--constructor-args "MyToken" "MTK" 1000000
--constructor-args "MyToken" "MTK" 1000000 \
--constructor-value 0
```

### Constructor argument encoding
Expand All @@ -382,7 +437,7 @@ For complex types:
- **Addresses**: Provide as hex strings with 0x prefix
- **Arrays**: Use JSON array syntax

```bash
```shell
# Example with multiple argument types
cargo stylus deploy \
--constructor-args "TokenName" "TKN" 1000000 "0x742d35Cc6635C0532925a3b8D95B5C1b0ea3C28F"
Expand Down Expand Up @@ -463,7 +518,7 @@ pub fn constructor(&mut self, params: ConstructorParams) -> Result<(), Error> {
self.balances.setter(deployer).set(params.initial_supply);

// 6. Emit events last
log(self.vm(), ContractInitialized { /* ... */ });
self.vm().log(ContractInitialized { /* ... */ });

Ok(())
}
Expand Down
Loading