diff --git a/README.md b/README.md
index c94afbc..94d31ae 100644
--- a/README.md
+++ b/README.md
@@ -1,325 +1,5 @@
-# BiUD — Bitcoin Username Domain (.sBTC)
+# BiUD
-
+A building identity platform on Stacks.
-```
-██████╗ ██╗██╗ ██╗██████╗
-██╔══██╗██║██║ ██║██╔══██╗
-██████╔╝██║██║ ██║██║ ██║
-██╔══██╗██║██║ ██║██║ ██║
-██████╔╝██║╚██████╔╝██████╔╝
-╚═════╝ ╚═╝ ╚═════╝ ╚═════╝
-```
-
-**Decentralized Username Registrar on Stacks (Bitcoin L2)**
-
-[](https://clarity-lang.org/)
-[](https://stacks.co/)
-[](LICENSE)
-
-
-
----
-
-## 🌟 Overview
-
-BiUD is a decentralized username system similar to ENS but built on **Stacks**, the leading Bitcoin L2. Users can register human-readable names with the `.sBTC` TLD:
-
-- `alice.sBTC`
-- `mybrand.sBTC`
-- `satoshi.sBTC`
-
-Names are registered, renewed, transferred, and resolved **entirely on-chain** using Clarity smart contracts.
-
----
-
-## ✨ Features
-
-| Feature | Description |
-|---------|-------------|
-| 📝 **Name Registration** | Register usernames with `.sBTC` TLD |
-| 🔄 **Renewals** | Extend ownership before expiry |
-| 🔀 **Transfers** | Transfer names to other principals |
-| 🎯 **Resolvers** | Pluggable resolution via traits |
-| 💎 **Premium Names** | Short names (≤4 chars) cost more |
-| 💰 **Fee Distribution** | Protocol fees to treasury + deployer |
-| ⏰ **Expiry System** | Auto-expiry with grace period |
-| 🔐 **Admin Governance** | Configurable fees and settings |
-
----
-
-## 📁 Project Structure
-
-```
-biud/
-├── Clarinet.toml # Clarinet configuration
-├── contracts/
-│ └── biud-username.clar # Main registry contract
-├── tests/
-│ └── biud-username_test.ts # Test suite
-├── settings/
-│ ├── Devnet.toml # Development network config
-│ ├── Testnet.toml # Testnet configuration
-│ └── Mainnet.toml # Mainnet configuration
-└── frontend/ # (Optional) Frontend UI
-```
-
----
-
-## 🚀 Quick Start
-
-### Prerequisites
-
-- [Clarinet](https://github.com/hirosystems/clarinet) >= 2.0
-- Node.js >= 18
-
-### Installation
-
-```bash
-# Clone the repository
-cd biud
-
-# Check the contract
-clarinet check
-
-# Run tests
-clarinet test
-
-# Start console for interactive development
-clarinet console
-```
-
-### Running Tests
-
-```bash
-# Run all tests
-clarinet test
-
-# Run with verbose output
-clarinet test --verbose
-
-# Run specific test file
-clarinet test tests/biud-username_test.ts
-```
-
----
-
-## 📖 Contract API
-
-### Core Functions
-
-#### `register-name`
-Register a new username with `.sBTC` TLD.
-
-```clarity
-(register-name (label (string-utf8 32)))
-```
-
-**Parameters:**
-- `label`: Username (lowercase a-z, 0-9, hyphen, max 32 chars)
-
-**Returns:** `{ name-id, full-name, expiry-height, fee-paid }`
-
-#### `renew-name`
-Extend the registration period for an existing name.
-
-```clarity
-(renew-name (label (string-utf8 32)))
-```
-
-#### `transfer-name`
-Transfer ownership to another principal.
-
-```clarity
-(transfer-name (label (string-utf8 32)) (new-owner principal))
-```
-
-#### `set-resolver`
-Set a resolver contract for custom name resolution.
-
-```clarity
-(set-resolver (label (string-utf8 32)) (resolver principal))
-```
-
-### Read-Only Functions
-
-| Function | Description |
-|----------|-------------|
-| `get-name` | Get full name record |
-| `is-available` | Check if name is available |
-| `get-owner` | Get owner of a name |
-| `get-expiry` | Get expiry block height |
-| `is-premium-name` | Check if name is premium |
-| `get-fee-config` | Get current fee configuration |
-| `get-registration-fee` | Calculate fee for a label |
-| `get-names-by-owner` | Get all names owned by principal |
-
-### Admin Functions
-
-| Function | Description |
-|----------|-------------|
-| `set-base-fee` | Update base registration fee |
-| `set-renew-fee` | Update renewal fee |
-| `set-premium-multiplier` | Update premium price multiplier |
-| `set-fee-recipient` | Update fee recipient address |
-| `set-premium-label` | Mark a label as premium |
-| `set-protocol-treasury` | Update protocol treasury |
-| `set-protocol-fee-percent` | Update protocol fee percentage |
-
----
-
-## 💎 Pricing
-
-### Standard Names (5+ characters)
-- **Registration:** 10 STX
-- **Renewal:** 5 STX
-
-### Premium Names (1-4 characters)
-- **Registration:** 50 STX (5x multiplier)
-- **Renewal:** 5 STX
-
-> Admin can mark any name as premium regardless of length.
-
----
-
-## ⏰ Registration Periods
-
-| Period | Blocks | Duration |
-|--------|--------|----------|
-| Registration | 52,560 | ~1 year |
-| Grace Period | 1,008 | ~7 days |
-
-During the grace period, only the original owner can renew the name.
-
----
-
-## 🔧 Fee Distribution
-
-Registration and renewal fees are split:
-
-- **90%** → Fee Recipient (deployer by default)
-- **10%** → Protocol Treasury
-
-> Configurable by admin via `set-protocol-fee-percent`.
-
----
-
-## 🔐 Error Codes
-
-| Code | Name | Description |
-|------|------|-------------|
-| 1001 | `ERR_NAME_TAKEN` | Name already registered |
-| 1002 | `ERR_NAME_EXPIRED` | Name has expired |
-| 1003 | `ERR_NOT_OWNER` | Caller is not the owner |
-| 1004 | `ERR_NOT_ADMIN` | Caller is not admin |
-| 1005 | `ERR_INVALID_LABEL` | Invalid label format |
-| 1006 | `ERR_PAYMENT_FAILED` | Fee transfer failed |
-| 1007 | `ERR_IN_GRACE_PERIOD` | Name is in grace period |
-| 1008 | `ERR_RESOLVER_INVALID` | Invalid resolver contract |
-| 1009 | `ERR_NAME_NOT_FOUND` | Name does not exist |
-| 1010 | `ERR_LABEL_TOO_LONG` | Label exceeds 32 characters |
-| 1011 | `ERR_LABEL_EMPTY` | Empty label provided |
-| 1012 | `ERR_INVALID_CHARACTER` | Label contains invalid character |
-| 1013 | `ERR_TRANSFER_TO_SELF` | Cannot transfer to self |
-| 1014 | `ERR_ZERO_FEE` | Fee cannot be zero |
-
----
-
-## 🎯 Resolver Trait
-
-External contracts can implement the resolver trait for custom resolution:
-
-```clarity
-(define-trait resolver-trait
- (
- (resolve ((string-utf8 32) principal)
- (response (optional (buff 64)) uint))
- )
-)
-```
-
-### Example Resolver
-
-```clarity
-(impl-trait .biud-username.resolver-trait)
-
-(define-public (resolve (label (string-utf8 32)) (owner principal))
- (ok (some 0x1234567890abcdef...))
-)
-```
-
----
-
-## 📡 Events
-
-The contract emits events for all major actions:
-
-- `NameRegistered` - New name registered
-- `NameRenewed` - Name renewed
-- `NameTransferred` - Ownership transferred
-- `ResolverSet` - Resolver contract updated
-- `FeeConfigUpdated` - Fee settings changed
-- `TreasuryUpdated` - Treasury settings changed
-- `PremiumLabelSet` - Premium label status changed
-
----
-
-## 🖥️ Frontend Integration
-
-See the [frontend/](frontend/) directory for a minimal UI outline. Key integrations:
-
-```typescript
-// Check availability
-const available = await callReadOnly('is-available', [stringUtf8(label)]);
-
-// Register name
-const result = await callPublic('register-name', [stringUtf8(label)]);
-
-// Get name info
-const nameInfo = await callReadOnly('get-name', [stringUtf8(label)]);
-```
-
----
-
-## 🔒 Security Considerations
-
-1. **Admin Key Security**: The deployer address has admin privileges. Secure this key.
-2. **Fee Configuration**: Only admin can modify fees to prevent manipulation.
-3. **Grace Period**: Protects users from losing names due to brief lapses.
-4. **Resolver Validation**: Resolvers must implement the trait correctly.
-
----
-
-## 🛣️ Roadmap
-
-- [x] Core registration system
-- [x] Premium name pricing
-- [x] Fee distribution
-- [x] Resolver trait
-- [ ] Subdomain support
-- [ ] NFT integration
-- [ ] Bulk registration
-- [ ] Auction system for premium names
-- [ ] Cross-chain resolution
-
----
-
-## 📄 License
-
-MIT License - see [LICENSE](LICENSE)
-
----
-
-## 🤝 Contributing
-
-Contributions welcome! Please read our contributing guidelines first.
-
----
-
-
-
-**Built on Stacks • Secured by Bitcoin**
-
-[Website](https://biud.example.com) • [Discord](https://discord.gg/biud) • [Twitter](https://twitter.com/biud)
-
-
+npm install
diff --git a/contracts/biud-username.clar b/contracts/biud-username.clar
index 27b99f4..cf4650c 100644
--- a/contracts/biud-username.clar
+++ b/contracts/biud-username.clar
@@ -233,6 +233,39 @@
)
)
+;; Validate subdomain label: supports sub.parent format
+(define-private (validate-subdomain-label (label (string-utf8 32)))
+ (let
+ (
+ (dot-index (index-of label "."))
+ )
+ (if (is-some dot-index)
+ ;; It's a subdomain
+ (let
+ (
+ (parent-end (unwrap-panic dot-index))
+ (sub-len (len label))
+ (parent (unwrap! (slice? label u0 parent-end) ERR_INVALID_LABEL))
+ (subdomain-start (+ parent-end u1))
+ (subdomain (unwrap! (slice? label subdomain-start sub-len) ERR_INVALID_LABEL))
+ )
+ ;; Check parent and subdomain not empty
+ (asserts! (> (len parent) u0) ERR_INVALID_LABEL)
+ (asserts! (> (len subdomain) u0) ERR_INVALID_LABEL)
+ ;; Validate parent and subdomain as valid labels
+ (try! (validate-label parent))
+ (try! (validate-label subdomain))
+ (ok { is-subdomain: true, parent: parent, subdomain: subdomain })
+ )
+ ;; Not a subdomain
+ (begin
+ (try! (validate-label label))
+ (ok { is-subdomain: false, parent: "", subdomain: "" })
+ )
+ )
+ )
+)
+
;; Generate the next name ID
(define-private (get-next-name-id)
(let
@@ -361,73 +394,110 @@
(let
(
;; Validate the label format
- (validation-result (try! (validate-label label)))
- ;; Generate the full name with TLD
- (full-name (unwrap! (as-max-len? (concat label u".sBTC") u64) ERR_INVALID_LABEL))
- ;; Check if name is premium
- (is-premium (check-is-premium label))
- ;; Calculate the registration fee
- (reg-fee (calculate-registration-fee is-premium))
- ;; Get existing registration if any
- (existing (map-get? name-registry { label: label }))
- ;; Generate new name ID
- (new-name-id (get-next-name-id))
- ;; Calculate expiry height
- (expiry (+ block-height REGISTRATION_PERIOD))
+ (validation-result (try! (validate-subdomain-label label)))
+ ;; Check parent ownership if subdomain
+ (is-subdomain (get is-subdomain validation-result))
)
- ;; Check if name is available
- (match existing
- name-record
- ;; Name exists - check if expired
- (begin
- (asserts! (is-name-expired (get expiry-height name-record)) ERR_NAME_TAKEN)
- ;; Remove from previous owner's list
- (try! (remove-name-from-owner (get owner name-record) (get name-id name-record)))
+ ;; If subdomain, verify parent ownership
+ (if is-subdomain
+ (let
+ (
+ (parent (get parent validation-result))
+ (parent-record (unwrap! (map-get? name-registry { label: parent }) ERR_NAME_NOT_FOUND))
+ )
+ (asserts! (is-eq (get owner parent-record) tx-sender) ERR_NOT_OWNER)
+ (asserts! (<= block-height (get expiry-height parent-record)) ERR_NAME_EXPIRED)
+ true
)
- ;; Name doesn't exist - proceed
true
)
-
- ;; Collect registration fee
- (asserts! (> reg-fee u0) ERR_ZERO_FEE)
- (try! (distribute-fees reg-fee))
-
- ;; Create the name record
- (map-set name-registry
- { label: label }
- {
+ ;; Generate the full name with TLD
+ (let
+ (
+ (full-name (if is-subdomain
+ (let
+ (
+ (parent (get parent validation-result))
+ (subdomain (get subdomain validation-result))
+ )
+ (unwrap! (as-max-len? (concat (concat subdomain ".") (concat parent ".sBTC")) u64) ERR_INVALID_LABEL)
+ )
+ (unwrap! (as-max-len? (concat label ".sBTC") u64) ERR_INVALID_LABEL)
+ ))
+ ;; Check if name is premium
+ (is-premium (check-is-premium label))
+ ;; Calculate the registration fee
+ (reg-fee (calculate-registration-fee is-premium))
+ ;; Get existing registration if any
+ (existing (map-get? name-registry { label: label }))
+ ;; Generate new name ID
+ (new-name-id (get-next-name-id))
+ ;; Calculate expiry height
+ (expiry (+ block-height REGISTRATION_PERIOD))
+ )
+ ;; Check if name is available
+ (match existing
+ name-record
+ ;; Name exists - check if expired
+ (begin
+ (asserts! (is-name-expired (get expiry-height name-record)) ERR_NAME_TAKEN)
+ ;; Remove from previous owner's list
+ (try! (remove-name-from-owner (get owner name-record) (get name-id name-record)))
+ )
+ ;; Name doesn't exist - proceed
+ true
+ )
+
+ ;; Collect registration fee
+ (asserts! (> reg-fee u0) ERR_ZERO_FEE)
+ (try! (distribute-fees reg-fee))
+
+ ;; Create the name record
+ (map-set name-registry
+ { label: label }
+ {
+ name-id: new-name-id,
+ full-name: full-name,
+ owner: tx-sender,
+ resolver: none,
+ expiry-height: expiry,
+ is-premium: is-premium,
+ created-at: block-height,
+ last-renewed: block-height
+ }
+ )
+
+ ;; Set reverse lookup
+ (map-set name-id-to-label
+ { name-id: new-name-id }
+ { label: label }
+ )
+
+ ;; Add to owner's name list
+ (try! (add-name-to-owner tx-sender new-name-id))
+
+ ;; Emit registration event
+ (emit-name-registered label full-name tx-sender new-name-id expiry reg-fee is-premium)
+
+ (ok {
name-id: new-name-id,
full-name: full-name,
- owner: tx-sender,
- resolver: none,
expiry-height: expiry,
- is-premium: is-premium,
- created-at: block-height,
- last-renewed: block-height
- }
- )
-
- ;; Set reverse lookup
- (map-set name-id-to-label
- { name-id: new-name-id }
- { label: label }
+ fee-paid: reg-fee
+ })
)
-
- ;; Add to owner's name list
- (try! (add-name-to-owner tx-sender new-name-id))
-
- ;; Emit registration event
- (emit-name-registered label full-name tx-sender new-name-id expiry reg-fee is-premium)
-
- (ok {
- name-id: new-name-id,
- full-name: full-name,
- expiry-height: expiry,
- fee-paid: reg-fee
- })
)
)
+;; Register multiple names in one transaction (up to 10)
+(define-private (register-name-wrapper (label (string-utf8 32)))
+ (register-name label)
+)
+
+(define-public (register-multiple-names (labels (list 10 (string-utf8 32))))
+ (ok (map register-name-wrapper labels))
+)
+
;; ════════════════════════════════════════════════════════════════════════════
;; PUBLIC FUNCTIONS - RENEWAL
;; ════════════════════════════════════════════════════════════════════════════
diff --git a/tests/biud-username_test.ts b/tests/biud-username_test.ts
index ad87fa8..974a514 100644
--- a/tests/biud-username_test.ts
+++ b/tests/biud-username_test.ts
@@ -785,6 +785,245 @@ describe("Event Emissions", () => {
});
});
+// ════════════════════════════════════════════════════════════════════════════
+// SUBDOMAIN TESTS
+// ════════════════════════════════════════════════════════════════════════════
+
+describe("Subdomain Support", () => {
+ it("should register a subdomain successfully", () => {
+ // Register parent
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("alice")], wallet1);
+
+ // Register subdomain
+ const result = simnet.callPublicFn(
+ contractName,
+ "register-name",
+ [Cl.stringUtf8("sub.alice")],
+ wallet1
+ );
+
+ expect(result.result).toBeOk(Cl.tuple({
+ "name-id": Cl.uint(2),
+ "full-name": Cl.stringUtf8("sub.alice.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000) // Same fee, not premium
+ }));
+ });
+
+ it("should fail to register subdomain if parent not owned", () => {
+ // Register parent by wallet1
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("alice")], wallet1);
+
+ // Try to register subdomain by wallet2
+ const result = simnet.callPublicFn(
+ contractName,
+ "register-name",
+ [Cl.stringUtf8("sub.alice")],
+ wallet2
+ );
+
+ expect(result.result).toBeErr(Cl.uint(1003)); // ERR_NOT_OWNER
+ });
+
+ it("should treat subdomain as non-premium", () => {
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("alice")], wallet1);
+
+ // Check premium status
+ expect(
+ simnet.callReadOnlyFn(contractName, "is-premium-name", [Cl.stringUtf8("sub.alice")], wallet1).result
+ ).toBeBool(false); // Longer than 4 chars
+ });
+
+ it("should allow renewal of subdomain", () => {
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("alice")], wallet1);
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("sub.alice")], wallet1);
+
+ const result = simnet.callPublicFn(
+ contractName,
+ "renew-name",
+ [Cl.stringUtf8("sub.alice")],
+ wallet1
+ );
+
+ expect(result.result).toBeOk(Cl.tuple({
+ "new-expiry-height": Cl.uint(simnet.blockHeight + 52560 + 52559),
+ "fee-paid": Cl.uint(5000000)
+ }));
+ });
+
+ it("should allow transfer of subdomain", () => {
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("alice")], wallet1);
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("sub.alice")], wallet1);
+
+ const result = simnet.callPublicFn(
+ contractName,
+ "transfer-name",
+ [Cl.stringUtf8("sub.alice"), Cl.principal(wallet2)],
+ wallet1
+ );
+
+ expect(result.result).toBeOk(Cl.bool(true));
+
+ // Verify owner changed
+ expect(
+ simnet.callReadOnlyFn(contractName, "get-owner", [Cl.stringUtf8("sub.alice")], wallet1).result
+ ).toBeSome(Cl.principal(wallet2));
+ });
+
+ it("should validate subdomain format", () => {
+ // Invalid: empty subdomain
+ let result = simnet.callPublicFn(
+ contractName,
+ "register-name",
+ [Cl.stringUtf8(".alice")], // starts with dot
+ wallet1
+ );
+ expect(result.result).toBeErr(Cl.uint(1005)); // ERR_INVALID_LABEL
+
+ // Invalid: empty parent
+ result = simnet.callPublicFn(
+ contractName,
+ "register-name",
+ [Cl.stringUtf8("sub.")], // ends with dot
+ wallet1
+ );
+ expect(result.result).toBeErr(Cl.uint(1005));
+
+ // Valid format but parent doesn't exist
+ result = simnet.callPublicFn(
+ contractName,
+ "register-name",
+ [Cl.stringUtf8("sub.nonexistent")],
+ wallet1
+ );
+ expect(result.result).toBeErr(Cl.uint(1009)); // ERR_NAME_NOT_FOUND
+ });
+});
+
+// ════════════════════════════════════════════════════════════════════════════
+// BULK REGISTRATION TESTS
+// ════════════════════════════════════════════════════════════════════════════
+
+describe("Bulk Registration", () => {
+ it("should register multiple names successfully", () => {
+ const labels = ["bulk1", "bulk2", "bulk3"];
+ const result = simnet.callPublicFn(
+ contractName,
+ "register-multiple-names",
+ [Cl.list(labels.map(l => Cl.stringUtf8(l)))],
+ wallet1
+ );
+
+ expect(result.result).toBeOk(Cl.list([
+ Cl.tuple({
+ "name-id": Cl.uint(1),
+ "full-name": Cl.stringUtf8("bulk1.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000)
+ }),
+ Cl.tuple({
+ "name-id": Cl.uint(2),
+ "full-name": Cl.stringUtf8("bulk2.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000)
+ }),
+ Cl.tuple({
+ "name-id": Cl.uint(3),
+ "full-name": Cl.stringUtf8("bulk3.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000)
+ })
+ ]));
+ });
+
+ it("should handle mixed successful and failed registrations", () => {
+ // Register one name first
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("exists")], wallet1);
+
+ const labels = ["newname", "exists", "another"];
+ const result = simnet.callPublicFn(
+ contractName,
+ "register-multiple-names",
+ [Cl.list(labels.map(l => Cl.stringUtf8(l)))],
+ wallet1
+ );
+
+ // The result should contain mixed ok/err
+ expect(result.result).toBeOk(Cl.list([
+ Cl.tuple({ // newname succeeds
+ "name-id": Cl.uint(2),
+ "full-name": Cl.stringUtf8("newname.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000)
+ }),
+ Cl.uint(1001), // exists fails with ERR_NAME_TAKEN
+ Cl.tuple({ // another succeeds
+ "name-id": Cl.uint(3),
+ "full-name": Cl.stringUtf8("another.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000)
+ })
+ ]));
+ });
+
+ it("should handle empty list", () => {
+ const result = simnet.callPublicFn(
+ contractName,
+ "register-multiple-names",
+ [Cl.list([])],
+ wallet1
+ );
+
+ expect(result.result).toBeOk(Cl.list([]));
+ });
+
+ it("should register subdomains in bulk", () => {
+ // Register parent
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("parent")], wallet1);
+
+ const labels = ["sub1.parent", "sub2.parent"];
+ const result = simnet.callPublicFn(
+ contractName,
+ "register-multiple-names",
+ [Cl.list(labels.map(l => Cl.stringUtf8(l)))],
+ wallet1
+ );
+
+ expect(result.result).toBeOk(Cl.list([
+ Cl.tuple({
+ "name-id": Cl.uint(2),
+ "full-name": Cl.stringUtf8("sub1.parent.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000)
+ }),
+ Cl.tuple({
+ "name-id": Cl.uint(3),
+ "full-name": Cl.stringUtf8("sub2.parent.sBTC"),
+ "expiry-height": Cl.uint(simnet.blockHeight + 52560),
+ "fee-paid": Cl.uint(10000000)
+ })
+ ]));
+ });
+
+ it("should fail bulk subdomain registration without parent ownership", () => {
+ // Register parent by wallet1
+ simnet.callPublicFn(contractName, "register-name", [Cl.stringUtf8("parent")], wallet1);
+
+ // Try to register subdomains by wallet2
+ const labels = ["sub.parent"];
+ const result = simnet.callPublicFn(
+ contractName,
+ "register-multiple-names",
+ [Cl.list(labels.map(l => Cl.stringUtf8(l)))],
+ wallet2
+ );
+
+ expect(result.result).toBeOk(Cl.list([
+ Cl.uint(1003) // ERR_NOT_OWNER
+ ]));
+ });
+});
+
// ════════════════════════════════════════════════════════════════════════════
// EDGE CASE TESTS
// ════════════════════════════════════════════════════════════════════════════