Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Wasm build flags, scoped to the wasm target only (native `cargo test` is
# unaffected):
# * link-arg=--import-undefined — emit CosmWasm host fns (db_read,
# addr_validate, ...) as wasm imports instead of failing on undefined
# symbols. Needed because rustc 1.96/LLVM 22 no longer marks them itself.
# * target-feature=-... — disable post-MVP wasm extensions that
# recent rustc enables by default but the CosmWasm VM rejects
# (bulk-memory, reference-types, multivalue). Keeps the artifact loadable
# on-chain. The Docker optimizer applies the same restriction.
[target.wasm32-unknown-unknown]
rustflags = [
"-C", "link-arg=--import-undefined",
"-C", "target-feature=-bulk-memory,-reference-types,-multivalue",
]

[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown -p safhandle"
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ artifacts/*.wasm
!deployment/.env.*.example
deployment/state/

# Editor and OS
# Editor, OS, and local agent tooling
.DS_Store
.idea/
.vscode/
*.swp
*.swo
.claude/

# Node (doc tooling)
node_modules/

.info
24 changes: 20 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0] - 2026-07-07

First public **v1** release: short-name registry only.

### Changed

- **Phone linking is now gated behind the `phone` Cargo feature (off by default).** The v1 wasm registers short names only; all phone messages, queries, state, and the `phone_link_fee_usaf` config field are compiled out unless built with `--features phone`.
- Phone numbers (phone build) are stored as bare digits, no leading `+`.

### Added

- Hardened name validation: dedicated `EmailNotAllowed` error for inputs containing `@`, plus rejection of non-ASCII / homoglyph input and stray dots outside the `.saf` suffix.
- Documentation aligned with the implemented v1 contract (phone content marked Phase 2 throughout).

## [0.2.0]

### Added

- Initial open-source documentation and repository scaffold.
- Contract API specification for short names and phone linking.
- Fee model: 50 SAF (name), 100 SAF (phone), governance-updatable, dev module wallet sink.
- Network configuration examples for Safrochain testnet and mainnet.
- CosmWasm contract implementation: name registry, exact-fee enforcement, dev-module-wallet fee routing, governance config, and `migrate`.
- `cw-multi-test` integration tests and JSON schema generation.
- Fee model: 50 SAF (name), 100 SAF (phone, phone build), governance-updatable, dev module wallet sink.
- Network configuration examples for Safrochain testnet and mainnet; testnet deployment.
- CI workflows for documentation, link checks, and license validation.
28 changes: 28 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[workspace]
members = ["contracts/*"]
resolver = "2"

[workspace.package]
edition = "2021"
version = "1.0.0"
license = "MIT"
repository = "https://github.com/Safrochain-Org/safhandle-contract"

[workspace.dependencies]
cosmwasm-std = "2.1"
cosmwasm-schema = "2.1"
cw-storage-plus = "2.0"
cw2 = "2.0"
thiserror = "1.0"
cw-multi-test = "2.1"

# Release profile tuned for CosmWasm wasm artifacts.
[profile.release]
opt-level = "z"
debug = false
rpath = false
lto = true
overflow-checks = true
codegen-units = 1
panic = "abort"
incremental = false
47 changes: 27 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@

---

> **Status: Specification / Documentation phase.** CosmWasm source code and deployment artifacts will be added in a follow-up milestone. This repository defines the on-chain registry design and open-source project structure.
> **Status: v1 implemented and deployed to testnet.** The CosmWasm contract is built, covered by `cw-multi-test` integration tests, and live on `safro-testnet-1`. **v1 registers short names only** — phone linking is a Phase 2 feature compiled out of the shipped wasm (Cargo feature `phone`, off by default).

**SafHandle** lets users send SAF to short names like `john.saf` or phone numbers like `+243899123456` instead of long `addr_safro1...` addresses. This repository contains the **CosmWasm smart contract** specification and deployment documentation.
**SafHandle** lets users send SAF to short names like `john.saf` instead of long `addr_safro1...` addresses. This repository contains the **CosmWasm smart contract**, its tests, and deployment documentation. Phone-number handles are planned for Phase 2.

> **Read the full explanation:** [docs/HOW_IT_WORKS.md](./docs/HOW_IT_WORKS.md)

Expand All @@ -30,7 +30,7 @@
| Long, error-prone wallet addresses | Human-readable short names (`john`, `john.saf`) |
| Copy-paste friction in P2P payments | Resolve a name or phone to `addr_safro` in one query |
| No standard name registry on Safrochain | Canonical on-chain registry any wallet or dApp can query |
| Phone-based payments off-chain only | On-chain phone link now; off-chain verification in Phase 2 |
| Phone-based payments off-chain only | On-chain phone links planned for Phase 2 (see [PHONE_LINKING.md](./docs/PHONE_LINKING.md)) |

## How it works

Expand Down Expand Up @@ -58,10 +58,10 @@ sequenceDiagram

## Fees

| Action | Default fee | Routing |
| --- | --- | --- |
| Register short name | **50 SAF** | Dev module wallet |
| Link phone number | **100 SAF** | Dev module wallet |
| Action | Default fee | Routing | Availability |
| --- | --- | --- | --- |
| Register short name | **50 SAF** | Dev module wallet | v1 |
| Link phone number | **100 SAF** | Dev module wallet | Phase 2 |

Fees are **governance-updatable**. See [docs/FEES_AND_GOVERNANCE.md](./docs/FEES_AND_GOVERNANCE.md).

Expand All @@ -83,30 +83,37 @@ Fees are **governance-updatable**. See [docs/FEES_AND_GOVERNANCE.md](./docs/FEES
| [SECURITY_MODEL.md](./docs/SECURITY_MODEL.md) | Threat model |
| [ROADMAP.md](./docs/ROADMAP.md) | Implementation phases |

## Planned repository layout
## Repository layout

```text
contracts/safhandle/
├── Cargo.toml
├── src/
│ ├── contract.rs
│ ├── msg.rs
│ ├── state.rs
│ ├── error.rs
│ ├── fees.rs
│ └── names.rs
│ ├── contract.rs # entry points: instantiate / execute / query / migrate
│ ├── msg.rs # InstantiateMsg / ExecuteMsg / QueryMsg / MigrateMsg
│ ├── state.rs # Config, NameRecord, storage Maps/Items
│ ├── error.rs # ContractError variants
│ ├── validation.rs # name normalization + hardened validation
│ ├── helpers.rs # fee assertion helper
│ ├── lib.rs
│ └── bin/schema.rs # JSON schema generator
├── tests/
└── artifacts/
└── checksums.txt
deployment/
├── deploy.sh
├── migrate.sh
└── lib/
│ └── integration.rs # cw-multi-test coverage
└── schema/ # generated JSON schema (gitignored)
scripts/
└── deploy-testnet.sh # store + instantiate on testnet
artifacts/
├── safhandle.wasm # optimized build (gitignored)
└── checksums.txt
config/
├── testnet.json
└── mainnet.json
```

Phone linking code (state, messages, queries, validation) also lives in `src/`
but is compiled out unless built with `--features phone`. See
[PHONE_LINKING.md](./docs/PHONE_LINKING.md).

## Network configuration

| Network | Chain ID | Bech32 prefix | Denom |
Expand Down
1 change: 1 addition & 0 deletions artifacts/checksums.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
8940f19df97f60482b48a8b707a4e7e6ed2f045bf9f5e342924304914f9fa97a safhandle.wasm
1 change: 0 additions & 1 deletion config/mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"role": "production",
"safhandle": {
"nameRegistrationFeeUsaf": "50000000",
"phoneLinkFeeUsaf": "100000000",
"devModuleWallet": "",
"governanceModule": "gov"
}
Expand Down
7 changes: 3 additions & 4 deletions config/testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"rpc": "https://rpc.testnet.safrochain.com",
"rest": "https://rest.testnet.safrochain.com",
"ws": "wss://rpc.testnet.safrochain.com/websocket",
"explorer": "https://explorer.testnet.safrochain.com",
"contractAddress": "",
"explorer": "https://explorer.safrochain.com",
"contractAddress": "addr_safro1j6n2q333gy80pmpd6avss32y4nhd8deayv8m9x4uazt6zkdczk9sxjfun6",
"gasPrice": "0.025usaf",
"gasPriceSteps": {
"low": 0.01,
Expand All @@ -22,8 +22,7 @@
"role": "testing",
"safhandle": {
"nameRegistrationFeeUsaf": "50000000",
"phoneLinkFeeUsaf": "100000000",
"devModuleWallet": "",
"devModuleWallet": "addr_safro1qyzdpg5v9xepwn0jx65uxhjrq07t8v8w93qq7d",
"governanceModule": "gov"
}
}
30 changes: 30 additions & 0 deletions contracts/safhandle/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "safhandle"
description = "SafHandle CosmWasm registry — short names and phone links on Safrochain"
edition.workspace = true
version.workspace = true
license.workspace = true
repository.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]

[features]
# Disable the exported entry points so the crate can be used as a library
# dependency (e.g. by a future verifier contract) without symbol clashes.
library = []
# Phone linking is deferred to Phase 2. The v1 wasm is built WITHOUT this
# feature, so all phone messages, queries, state, and validation are compiled
# out entirely. Build with `--features phone` to re-enable the Phase 2 path.
phone = []

[dependencies]
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-storage-plus = { workspace = true }
cw2 = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
cw-multi-test = { workspace = true }
anyhow = "1"
114 changes: 114 additions & 0 deletions contracts/safhandle/schema/raw/execute.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"description": "Register a short name for the sender. Funds: exactly the name fee.",
"type": "object",
"required": [
"register_name"
],
"properties": {
"register_name": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Transfer name ownership. Owner-only.",
"type": "object",
"required": [
"transfer_name"
],
"properties": {
"transfer_name": {
"type": "object",
"required": [
"name",
"new_owner"
],
"properties": {
"name": {
"type": "string"
},
"new_owner": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Release a name back to the pool. Owner-only.",
"type": "object",
"required": [
"release_name"
],
"properties": {
"release_name": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Governance-only partial config update.",
"type": "object",
"required": [
"update_config"
],
"properties": {
"update_config": {
"type": "object",
"properties": {
"dev_module_wallet": {
"type": [
"string",
"null"
]
},
"name_registration_fee_usaf": {
"anyOf": [
{
"$ref": "#/definitions/Uint128"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
}
32 changes: 32 additions & 0 deletions contracts/safhandle/schema/raw/instantiate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"dev_module_wallet",
"governance_admin",
"name_registration_fee_usaf",
"native_denom"
],
"properties": {
"dev_module_wallet": {
"type": "string"
},
"governance_admin": {
"type": "string"
},
"name_registration_fee_usaf": {
"$ref": "#/definitions/Uint128"
},
"native_denom": {
"type": "string"
}
},
"additionalProperties": false,
"definitions": {
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
}
Loading
Loading