Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ Usage: compactc.bin <flag> ... <source-pathname> <target-directory-pathname>

## Set up the project

> ### Requirements
>
> - [node](https://nodejs.org/)
> - [yarn](https://yarnpkg.com/getting-started/install)
> - [compact](https://docs.midnight.network/develop/tutorial/building/#midnight-compact-compiler)

Clone the repository:

```bash
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* xref:index.adoc[Overview]
* xref:extensibility.adoc[Extensibility]
161 changes: 161 additions & 0 deletions docs/modules/ROOT/pages/extensibility.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Extensibility

## The Module/Contract Pattern

We use the term *modular composition by delegation* to describe the practice of having contracts call into module-defined circuits to implement behavior. Rather than inheriting or overriding functionality, a contract delegates responsibility to the module by explicitly invoking its exported circuits.

The idea is that there are two types of compact files: modules and contracts. To minimize risk, boilerplate, and avoid naming clashes, we follow these rules:

### Modules

Modules expose functionality through three circuit types:

1. `internal`: private helpers → used to break up logic within the module.
2. `public`: composable building blocks → intended for contracts to use in complex flows (`_mint`, `_burn`).
3. `external`: standalone circuits → safe to expose as-is (`transfer`, `approve`).

Modules must:

- Export only `public` and `external` circuits.
- Prefix `public` circuits with `_` (e.g., `FungibleToken._mint`).
- Avoid `_` prefix for `external` circuits (e.g., `FungibleToken.transfer`).
- Avoid defining or calling constructors or `initialize()` directly.
- Optionally define an `initialize()` circuit for internal setup—but execution must be delegated to the contract.

**Note**: Compact files must contain only one top-level module and all logic must be defined *inside* the module declaration.

### Contracts

Contracts compose behavior by explicitly invoking the relevant circuits from imported modules. Therefore, contracts:

- Can import from modules.
- Should add prefix to imports (`import "FungibleToken" prefix FungibleToken_;`).
- Should re-expose external module circuits through wrapper circuits to control naming and layering. Avoid raw re-exports to prevent name clashes.
- Should implement constructor that calls `initialize` from imported modules.
- Must not call initializers outside of the constructor.

This pattern balances modularity with local control, avoids tight coupling, and works within Compact’s language constraints. As Compact matures, this pattern will likely evolve as well.

### Example contract implementing modules

```ts
/** FungibleTokenMintablePausableOwnableContract */
pragma language_version >= 0.15.0;

import CompactStandardLibrary;

import FungibleToken prefix FungibleToken_;
import Pausable prefix Pausable_;
import Ownable prefix Ownable_;

constructor(
_name: Maybe<Opaque<"string">>,
_symbol: Maybe<Opaque<"string">>,
_decimals:Uint<8>,
_owner: Either<ZswapCoinPublicKey, ContractAddress>
) {
FungibleToken_initialize(_name, _symbol, _decimals);
Ownable_initialize(_owner);
}

/** IFungibleTokenMetadata */

export circuit name(): Maybe<Opaque<"string">> {
return FungibleToken_name();
}

export circuit symbol(): Maybe<Opaque<"string">> {
return FungibleToken_symbol();
}

export circuit decimals(): Uint<8> {
return FungibleToken_decimals();
}

/** IFungibleToken */

export circuit totalSupply(): Uint<128> {
return FungibleToken_totalSupply();
}

export circuit balanceOf(
account: Either<ZswapCoinPublicKey, ContractAddress>
): Uint<128> {
return FungibleToken_balanceOf(account);
}

export circuit allowance(
owner: Either<ZswapCoinPublicKey, ContractAddress>,
spender: Either<ZswapCoinPublicKey, ContractAddress>
): Uint<128> {
return FungibleToken_allowance(owner, spender);
}

export circuit transfer(
to: Either<ZswapCoinPublicKey, ContractAddress>,
value: Uint<128>
): Boolean {
Pausable_assertNotPaused();
return FungibleToken_transfer(to, value);
}

export circuit transferFrom(
from: Either<ZswapCoinPublicKey, ContractAddress>,
to: Either<ZswapCoinPublicKey, ContractAddress>,
value: Uint<128>
): Boolean {
Pausable_assertNotPaused();
return FungibleToken_transferFrom(from, to, value);
}

export circuit approve(
spender: Either<ZswapCoinPublicKey, ContractAddress>,
value: Uint<128>
): Boolean {
Pausable_assertNotPaused();
return FungibleToken_approve(spender, value);
}

/** IMintable */

export circuit mint(
account: Either<ZswapCoinPublicKey, ContractAddress>,
value: Uint<128>
): [] {
Pausable_assertNotPaused();
Ownable_assertOnlyOwner();
return FungibleToken__mint(account, value);
}

/** IPausable */

export circuit isPaused(): Boolean {
return Pausable_isPaused();
}

export circuit pause(): [] {
Ownable_assertOnlyOwner();
return Pausable__pause();
}

export circuit unpause(): [] {
Ownable_assertOnlyOwner();
return Pausable__unpause();
}

/** IOwnable */

export circuit owner(): Either<ZswapCoinPublicKey, ContractAddress> {
return Ownable_owner();
}

export circuit transferOwnership(
newOwner: Either<ZswapCoinPublicKey, ContractAddress>
): [] {
return Ownable_transferOwnership(newOwner);
}

export circuit renounceOwnership(): [] {
return Ownable_renounceOwnership();
}
```