diff --git a/README.md b/README.md
index 2758469..240403d 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Bitcoin Cash for Assigned Names and Numbers (BitCANN) Smart Contracts
-> ⚠️ Important Notice: The contracts have not undergone extensive auditing by third parties. Users should be aware of potential risks, including the possibility of losing domain ownership or funds during auctions. Exercise caution and consider these risks before use.**
+> ⚠️ Important Notice: The contracts have not undergone extensive auditing by third parties. Users should be aware of potential risks, including the possibility of losing name ownership or funds during auctions. Exercise caution and consider these risks before use.**
## Installation
@@ -26,26 +26,26 @@ npm install @bitcann/contracts
import { BitCANNArtifacts } from '@bitcann/contracts';
// Access contract artifacts
-const { Registry, Auction, Domain } = BitCANNArtifacts;
+const { Registry, Auction, Name } = BitCANNArtifacts;
```
---
# Documentation
-BitCANN - **Bitcoin Cash for Assigned Names and Numbers** – is a decentralized domain name and identity system built on the Bitcoin Cash Blockchain.
+BitCANN - **Bitcoin Cash for Assigned Names and Numbers** – is a decentralized name and identity system built on the Bitcoin Cash Blockchain.
-- Decentralized Domain Names like `.sat` and `.bch` and more.
-- Add Records, RPA Pay Codes, Add Currency Addresses, Text Records, Custom Records, Social, Email, and more.
+- Decentralized Names like `.sat` and `.bch` and more.
+- Add Records, RPA Pay Codes, Currency Addresses, Text Records, Custom Records, Social, Email, and more.
- No Renewals or Expiry*
-- NFT Domain ownership, enabling secondary market trading.
+- NFT ownership, enabling secondary market trading
- Easy lookups
- Sign-In using your Identity
- Plugin for other contract systems
- Earn by protecting the system by:
- Burning illegal registration attempts
- Identifying and burning registration conflicts
- - Proving domain violations
+ - Proving name violations
## Table of Contents
1. [Contracts](#contracts)
@@ -53,18 +53,18 @@ BitCANN - **Bitcoin Cash for Assigned Names and Numbers** – is a decentralized
- [Operational Contracts](#operational-contracts)
- [Auction](#auction)
- [Bid](#bid)
- - [DomainFactory](#domainfactory)
+ - [Factory](#factory)
- [Guard Contracts](#guard-contracts)
- - [AuctionNameEnforcer](#auctionnameenforcer)
- - [DomainOwnershipGuard](#domainownershipguard)
- - [AuctionConflictResolver](#auctionconflictresolver)
- - [Domain](#domain)
+ - [NameEnforcer](#nameenforcer)
+ - [OwnershipGuard](#ownershipguard)
+ - [ConflictResolver](#conflictresolver)
+ - [Name](#name)
- [Accumulator](#accumulator)
2. [Cashtokens](#cashtokens)
- [RegistrationNFTs](#registrationnfts)
- [AuctionNFT](#auctionnft)
- [AuthorizedThreadNFTs](#authorizedthreadnfts)
- - [DomainNFTs](#domainnfts)
+ - [NameNFTs](#namenfts)
3. [TLDs](#tlds)
4. [Genesis](#genesis)
5. [Dual Decay Mechanism](#dual-decay-mechanism)
@@ -73,13 +73,13 @@ BitCANN - **Bitcoin Cash for Assigned Names and Numbers** – is a decentralized
- [Can a bid be cancelled?](#can-a-bid-be-cancelled)
- [How is any TLD assigned?](#how-is-any-tld-assigned)
- [Who earns from the auction sales?](#who-earns-from-the-auction-sales)
- - [Can anyone renounce ownership of a domain?](#can-anyone-renounce-ownership-of-a-domain)
+ - [Can anyone renounce ownership of a name?](#can-anyone-renounce-ownership-of-a-name)
- [What occurs during a ownership renouncement event?](#what-occurs-during-a-ownership-renouncement-event)
- [How does ownership transfer work?](#how-does-ownership-transfer-work)
- [How to records managed?](#how-to-records-managed)
- [No Renewal or Expiry?](#no-renewal-or-expiry)
- [Why use text-based ownership instead of hash-based ownership?](#why-use-text-based-ownership-instead-of-hash-based-ownership)
- - [How do I know I or someone else owns a domain?](#how-do-i-know-i-or-someone-else-owns-a-domain)
+ - [How do I know I or someone else owns a name?](#how-do-i-know-i-or-someone-else-owns-a-name)
- [What if the incentive system is not 100% effective?](#what-if-the-incentive-system-is-not-100-effective)
- [What if an invalid name is registered?](#what-if-an-invalid-name-is-registered)
@@ -91,11 +91,11 @@ The architecture is built around a series of smart contracts, categorized into t
- **Registry Contract**: [Registry.cash](#registry)
-- **Operational Contracts**: [Auction.cash](#auction), [Bid.cash](#bid), [DomainFactory.cash](#domainfactory)
+- **Operational Contracts**: [Auction.cash](#auction), [Bid.cash](#bid), [Factory.cash](#factory)
-- **Guard Contracts**: [AuctionNameEnforcer.cash](#auctionnameenforcer), [DomainOwnershipGuard.cash](#domainownershipguard), [AuctionConflictResolver.cash](#auctionconflictresolver)
+- **Guard Contracts**: [NameEnforcer.cash](#nameenforcer), [OwnershipGuard.cash](#ownershipguard), [ConflictResolver.cash](#conflictresolver)
-- **Domain Contract**: [Domain.cash](#domain)
+- **Name Contract**: [Name.cash](#name)
- **Accumulator Contract**: [Accumulator.cash](#accumulator)
@@ -103,10 +103,10 @@ The architecture is built around a series of smart contracts, categorized into t
### Registry
The Registry contract functions as the control and storage hub. Operational, Guard, and Accumulator contracts must execute their transactions in conjunction with the Registry contract.
-This contract holds [RegistrationNFTs](#registrationnfts), [AuctionNFTs](#auctionnft), and [AuthorizedThreadNFTs](#authorizedthreadnfts).
+This contract holds [RegistrationNFTs](#registrationnfts), [AuctionNFTs](#auctionnft), and [AuthorizedThreadNFTs](#authorizedthreadnfts) and [NameNFTs](#namenfts).
Constructor:
-- `domainCategory`: The category of the domain. All the NFTs in the system belong to this category.
+- `nameCategory`: The category of the name. All the NFTs in the system belong to this category.
Transaction Structure:
| # | Inputs | Outputs |
@@ -123,7 +123,7 @@ Transaction Structure:
The Auction contract lets anyone start a new auction.
Each auction requires:
- A minimum starting bid of at least `minStartingBid` BCH.
- - It runs for at least `minWaitTime`(check [DomainFactory](#domainfactory) contract). The timer resets with a new bid.
+ - It runs for at least `minWaitTime`(check [Factory](#factory) contract). The timer resets with a new bid.
Constructor:
- `minStartingBid`: The minimum starting bid of the auction.
@@ -131,7 +131,7 @@ Constructor:
Transaction Structure:
Parameters:
-- `name`: The name of the domain. This does not include the TLD. [TLDs](#tlds)
+- `name`: The name. This does not include the TLD. [TLDs](#tlds)
| # | Inputs | Outputs |
|---|--------|---------|
@@ -139,13 +139,12 @@ Parameters:
| 1 | Any UTXO from self | Back to self |
| 2 | [RegistrationNFTs](#registrationnfts) Counter NFT | [RegistrationNFTs](#registrationnfts) Counter NFT, with nftCommitment incremented by 1 and tokenAmount decreased by NewRegistrationID |
| 3 | Funding UTXO from bidder | [AuctionNFT](#auctionnft) |
-| 4 | | OP_RETURN revealing the name |
-| 5 | | Optional change in BCH |
+| 4 | | Optional change in BCH |
#### Bid
-The Bid contract allows anyone to bid on an active auction by allowing restricted manipulation of auctionNFT. It updates the `satoshisValue` and the `pkh` in the `nftCommitment`. The only condition is that the new Bid amount must be at least `minBidIncreasePercentage` higher. Even if the auction is passed the `minWaitTime` and the winning bid has not claimed the domain's ownership, it's still possible to continue bidding which will reset the timer to atleast `minWaitTime`.
+The Bid contract allows anyone to bid on an active auction by allowing restricted manipulation of auctionNFT. It updates the `satoshisValue` and the `pkh` in the `nftCommitment`. The only condition is that the new Bid amount must be at least `minBidIncreasePercentage` higher. Even if the auction is passed the `minWaitTime` and the winning bid has not claimed the name's ownership, it's still possible to continue bidding which will reset the timer to atleast `minWaitTime`.
Constructor:
- `minBidIncreasePercentage`: The minimum percentage increase in the new bid amount.
@@ -160,39 +159,38 @@ Transaction Structure:
| 4 | | Optional change to new bidder |
-#### DomainFactory
+#### Factory
-The DomainFactory burns the auctionNFT and issues 3 new NFTs [DomainNFTs](#domainnfts). It verifies that the actionNFT input is at least `minWaitTime` old. It also attaches the tokenAmount from auctionNFT to the authorized contract's thread.
+The Factory burns the auctionNFT and issues 3 new NFTs [NameNFTs](#namenfts). It verifies that the actionNFT input is at least `minWaitTime` old. It also attaches the tokenAmount from auctionNFT to the authorized contract's thread.
Constructor:
-- `domainContractBytecode`: The partial bytecode of the domain contract.
-- `minWaitTime`: The minimum wait time after which the domain can be claimed by the bidder.
+- `nameContractBytecode`: The partial bytecode of the name contract.
+- `minWaitTime`: The minimum wait time after which the name can be claimed by the bidder.
- `maxPlatformFeePercentage`: The maximum fee percentage that can be charged by the platform.
+- `tld`: The TLD of the name.
Transaction Structure:
| # | Inputs | Outputs |
|---|--------|---------|
| 0 | [AuthorizedThreadNFT](#authorizedthreadnfts) NFT with authorized contract's locking bytecode as commitment from [Registry Contract](#registry) | [AuthorizedThreadNFT](#authorizedthreadnfts) back to [Registry Contract](#registry) + tokenAmount from auctionNFT input|
| 1 | Any UTXO from self | Back to self |
-| 2 | [RegistrationNFT](#registrationnfts) Domain Minting NFT | [RegistrationNFT](#registrationnfts) Domain Minting NFT back to registry contract |
-| 3 | [AuctionNFT](#auctionnft) | [DomainNFT](#domainnfts) External Auth NFT |
-| 4 | Pure BCH from bidder | [DomainNFT](#domainnfts) Internal Auth NFT |
-| 5 | | [DomainNFT](#domainnfts) Ownership NFT |
+| 2 | [RegistrationNFT](#registrationnfts) Name Minting NFT | [RegistrationNFT](#registrationnfts) Name Minting NFT back to registry contract |
+| 3 | [AuctionNFT](#auctionnft) | [NameNFT](#namenfts) External Auth NFT |
+| 4 | Pure BCH from bidder | [NameNFT](#namenfts) Internal Auth NFT |
+| 5 | | [NameNFT](#namenfts) Ownership NFT |
| 6 | | Pure BCH back to Bidder |
| 7 | | Platform fee and rest to miners |
### Guard Contracts
-These contracts serve the purpose of incentivizing the enforcement of the rules. For example, if someone were to start an auction for a domain that is already owned then the [DomainOwnershipGuard](#domainownershipguard) contract will allow anyone to provide proof of ownership of the domain using [External Auth DomainNFT](#domainnfts) and penalize the illegal auction by burning the auctionNFT and giving the funds to the proof provider.
+These contracts serve the purpose of incentivizing the enforcement of the rules. For example, if someone were to start an auction for a name that is already owned then the [OwnershipGuard](#ownershipguard) contract will allow anyone to provide proof of ownership of the name using [External Auth NameNFT](#namenfts) and penalize the illegal auction by burning the auctionNFT and giving the funds to the proof provider.
Similarly, other contracts also provide a way to penalize anyone who attempts to break the rules of the system.
-#### AuctionNameEnforcer
+#### NameEnforcer
-The AuctionNameEnforcer contract allows anyone to prove that the running auction has an invalid domain name. By providing proof (index of the invalid character) they burn the auctionNFT, taking away the entire amount as a reward.
-
-> **INFO:** The nature of this architecture is that it allows for more types of restrictions. These rules can be modified to allow for more or fewer restrictions.
+The NameEnforcer contract allows anyone to prove that the running auction has an invalid name. By providing proof (index of the invalid character) they burn the auctionNFT, taking away the entire amount as a reward.
Rules:
- The name must consist of only these characters
@@ -211,28 +209,29 @@ Parameters:
| 1 | Any UTXO from self | Back to self |
| 2 | [AuctionNFT](#auctionnft) | Reward output |
-> **Important**: Applications must verify that domain name follows the rules before starting an auction. Failing to do so will result in the user losing their bid amount.
+> **Important**: Applications must verify that name follows the rules before starting an auction. Failing to do so will result in the user losing their bid amount.
-#### DomainOwnershipGuard
+#### OwnershipGuard
-This prevents registrations for domains that have already been registered and have owners. Anyone can provide proof of valid ownership([External Auth DomainNFT](#domainnfts)) and burn the auctionNFT and claim the funds as a reward.
+This prevents registrations for names that have already been registered and have owners. Anyone can provide proof of valid ownership([External Auth NameNFT](#namenfts)) and burn the auctionNFT and claim the funds as a reward.
Constructor:
-- `domainContractBytecode`: The partial bytecode of the domain contract.
+- `nameContractBytecode`: The partial bytecode of the name contract.
+- `tld`: The TLD of the name.
Transaction Structure:
| # | Inputs | Outputs |
|---|--------|---------|
| 0 | [AuthorizedThreadNFT](#authorizedthreadnfts) NFT with authorized contract's locking bytecode as commitment from [Registry Contract](#registry) | [AuthorizedThreadNFT](#authorizedthreadnfts) back to [Registry Contract](#registry) + tokenAmount from auctionNFT input|
| 1 | Any UTXO from self | Back to self |
-| 2 | [DomainNFT](#domainnfts) External Auth NFT | [DomainNFT](#domainnfts) External Auth NFT back to the Domain Contract |
+| 2 | [NameNFT](#namenfts) External Auth NFT | [NameNFT](#namenfts) External Auth NFT back to the Name Contract |
| 3 | [AuctionNFT](#auctionnft) | Reward output |
-> **Important**: Applications must verify the presence of External Auth NFT in the Domain Contract before creating a new auction. Failing to do so will result in the user losing their bid amount.
+> **Important**: Applications must verify the presence of External Auth NFT in the Name Contract before creating a new auction. Failing to do so will result in the user losing their bid amount.
-#### AuctionConflictResolver
+#### ConflictResolver
-If two registration auctions exist for the same domain name, the one with the higher registrationID i.e the tokenAmount is invalid. (Since registration is a single-threaded operation such scenarios are unlikely to occur willingly.)
+If two registration auctions exist for the same name, the one with the higher registrationID i.e the tokenAmount is invalid. (Since registration is a single-threaded operation such scenarios are unlikely to occur willingly.)
This contract allows anyone to prove that an auction is invalid and burn the invalid auctionNFT in the process and taking away the funds as a reward for keeping the system in check.
@@ -246,55 +245,55 @@ Transaction Structure:
> **Important**: Applications must verify that an auctionNFT with the same name doesn't already exist in the registry contract before creating a new auction. Failing to do so will result in the user losing their bid amount. BCH's UTXO-based system has no concept of 'Contract Storage' to confirm the existence of an ongoing auction.
-### Domain
+### Name
-The Domain contract allows the owner to perform a few operations after [DomainNFTs](#domainnfts) are issued from [DomainFactory](#domainfactory). There exists a unique domain contract for each unique domain name.
+The Name contract allows the owner to perform a few operations after [NameNFTs](#namenfts) are issued from [Factory](#factory). There exists a unique name contract for each unique name.
Constructor:
-- `inactivityExpiryTime`: The time after which the domain is considered abandoned.
-- `name`: The name of the domain.
-- `domainCategory`: The category of the domain.
+- `inactivityExpiryTime`: The time after which the name is considered abandoned.
+- `fullName`: The full name. (This includes the TLD)
+- `nameCategory`: The category of the name.
-There are 3 functions in each Domain Contract:
+There are 3 functions in each Name Contract:
- **useAuth**: This can be used to perform a variety of actions.
For example:
- - Prove the the ownership of the domain by other contracts.
- - Perform any actions in conjunction with other contracts. (A Lease Contract)
+ - Prove the ownership of the name by other contracts.
+ - Perform any actions in conjunction with other contracts. (E.g. A Lease Contract)
- Add records and invalidate multiple records in a single transaction.
Transaction Structure:
| # | Inputs | Outputs |
|---|--------|---------|
-| x | [DomainNFTs](#domainnfts) Internal/External Auth NFT from self | Back to self |
-| x+1 (optional) | [OwnershipNFT](#domainnfts) from owner | [OwnershipNFT](#domainnfts) as output |
+| x | [NameNFTs](#namenfts) Internal/External Auth NFT from self | Back to self |
+| x+1 (optional) | [OwnershipNFT](#namenfts) from owner | [OwnershipNFT](#namenfts) as output |
| x+2 | | OP_RETURN containing record data or removal hash |
-- **burn**: This allows the owner of the domain to renounce ownership OR if the domain has been inactive for > `inactivityExpiryTime` then anyone can burn the domain allowing for a new auction.
+- **burn**: This allows the owner of the name to renounce ownership OR if the name has been inactive for > `inactivityExpiryTime` then anyone can burn the name allowing for a new auction.
Transaction Structure:
| # | Inputs | Outputs |
|---|--------|---------|
-| 0 | [DomainNFTs](#domainnfts) Internal Auth NFT | BCH change output |
-| 1 | [DomainNFTs](#domainnfts) External Auth NFT | |
-| 2 | Pure BCH or [DomainNFTs](#domainnfts) Domain ownership NFT from owner | |
+| 0 | [NameNFTs](#namenfts) Internal Auth NFT | BCH change output |
+| 1 | [NameNFTs](#namenfts) External Auth NFT | |
+| 2 | Pure BCH or [NameNFTs](#namenfts) Name ownership NFT from owner | |
-- **resolveOwnerConflict**: Ideally, this function will never be triggered as no one would want to keep the free money on the table by not triggering the transaction that earns them money. Having said that, it's important to have a safeguard for such an unforceable future where these incentive system are unable to catch a registration conflict or burn two competing auctionNFTs for the same name at the same time period resulting in more than 1 owner for a domain. The owner with the lowest registrationID must be the only owner for a domain. To help enforce this rule, this function will allow anyone to burn both the Auth NFTs of the NEW invalid owner.
+- **resolveOwnerConflict**: Ideally, this function will never be triggered as no one would want to keep the free money on the table by not triggering the transaction that earns them money. Having said that, it's important to have a safeguard for such an unforceable future where these incentive system are unable to catch a registration conflict or burn two competing auctionNFTs for the same name at the same time period resulting in more than 1 owner for a name. The owner with the lowest registrationID must be the only owner for a name. To help enforce this rule, this function will allow anyone to burn both the Auth NFTs of the NEW invalid owner.
Transaction Structure:
| # | Inputs | Outputs |
|---|--------|---------|
-| 0 | Valid External Auth [DomainNFT](#domainnfts) | Valid External Auth [DomainNFT](#domainnfts) back to self |
-| 1 | Valid Internal Auth [DomainNFT](#domainnfts) | Valid Internal Auth [DomainNFT](#domainnfts) back to self |
-| 2 | Invalid External Auth [DomainNFT](#domainnfts) | BCH change output |
-| 3 | Invalid Internal Auth [DomainNFT](#domainnfts) | |
+| 0 | Valid External Auth [NameNFT](#namenfts) | Valid External Auth [NameNFT](#namenfts) back to self |
+| 1 | Valid Internal Auth [NameNFT](#namenfts) | Valid Internal Auth [NameNFT](#namenfts) back to self |
+| 2 | Invalid External Auth [NameNFT](#namenfts) | BCH change output |
+| 3 | Invalid Internal Auth [NameNFT](#namenfts) | |
| 4 | BCH input from anyone | |
### Accumulator
-Once enough auctions have happened, there might come a time when the counterNFT's tokenAmount is not enough to create new Auction NFT. Since the amount would be accumulating in the thread NFTs, this contract can be used to transfer them back to the CounterNFT to keep the system functioning smoothly.
+Once enough auctions have happened, there will come a time when the counterNFT's tokenAmount is not enough to create new Auction NFT. Since the amount would be accumulating in the thread NFTs, this contract can be used to transfer them back to the CounterNFT to keep the system functioning smoothly.
Transaction Structure:
| # | Inputs | Outputs |
@@ -313,16 +312,16 @@ The contracts talk to each other through cashtokens. There are 4 types in this s
- [RegistrationNFTs](#registrationnfts)
- [AuctionNFT](#auctionnft)
- [AuthorizedThreadNFTs](#authorizedthreadnfts)
-- [DomainNFTs](#domainnfts)
+- [NameNFTs](#namenfts)
#### RegistrationNFTs
A pair of minting NFTs that exist as UTXOs within the [Registry.cash](#registry) contract, consisting of:
- **CounterNFT**: This minting hybrid NFT has nftCommitment that starts from 0 and increments by 1 with each new registration. It is also initialized with the maximum possible token amount of `9223372036854775807` that interacts with [Auction.cash](#auction) to facilitate the creation of new auction NFTs. Based on the value of the new registrationID from it's own commitment, the new minted AuctionNFT gets the exact tokenAmount. [FAQ](#what-if-the-tokenamount-in-the-counternft-runs-out)
- - `category`: domainCategory
+ - `category`: nameCategory
- `commitment`: registrationID < 8 bytes >
- `tokenAmount`: Keeps reducing with each new registration.
- - **DomainMintingNFT**: A minting NFT that works with [DomainFactory.cash](#domainfactory) to issue new Domain NFTs. This has no nftCommitment or tokenAmount.
- - `category`: domainCategory
+ - **NameMintingNFT**: A minting NFT that works with [Factory.cash](#factory) to issue new Name NFTs. This has no nftCommitment or tokenAmount.
+ - `category`: nameCategory
#### AuctionNFT
A mutable hybrid NFT created for each new auction that remains within [Registry.cash](#registry), containing comprehensive auction information through the following attributes:
@@ -330,14 +329,14 @@ A mutable hybrid NFT created for each new auction that remains within [Registry.
- `tokenAmount`: This represents the registrationID
- `capability`: Mutable
- `satoshis`: The latest bid amount
- - `category`: The designated domainCategory
+ - `category`: The designated nameCategory
A new bid simply updates the `pkh` in the `nftCommitment` and updates the `satoshisValue` to the new amount.
#### AuthorizedThreadNFTs
-Each authorized contract's lockingbytecode(Excluding [Domain.cash](#domain)) is added to an immutable NFT commitment and sent to the [Registry.cash](#registry) at the time of genesis. These immutable NFTs stay with `Registry.cash` forever. Any interaction with the registry must include one of these thread NFTs to create a transaction.
+Each authorized contract's lockingbytecode(Excluding [Name.cash](#name)) is added to an immutable NFT commitment and sent to the [Registry.cash](#registry) at the time of genesis. These immutable NFTs stay with `Registry.cash` forever. Any interaction with the registry must include one of these thread NFTs to create a transaction.
Structure:
- - `category`: domainCategory
+ - `category`: nameCategory
- `commitment`: lockingbytecode of authorized contract <35 bytes>
The Registry Contract has a designated number of threads for authorized contracts:
@@ -346,52 +345,50 @@ x = number of threads [The exact value can be anything. It must be decided at th
- Auction: ~x threads
- Bid: ~x threads
-- DomainFactory: ~x threads
-- AuctionNameEnforcer: ~x threads
-- DomainOwnershipGuard: ~x threads
-- AuctionConflictResolver: ~x threads
+- Factory: ~x threads
+- NameEnforcer: ~x threads
+- OwnershipGuard: ~x threads
+- ConflictResolver: ~x threads
- Accumulator: ~x threads
-#### DomainNFTs
+#### NameNFTs
A set of 3 immutable NFTs minted when an auction ends:
- - **OwnershipNFT**: This NFT proves ownership of a specific domain.
- - `category`: domainCategory
+ - **OwnershipNFT**: This NFT proves ownership of a specific name.
+ - `category`: nameCategory
- `commitment`: registrationID < 8 bytes > + name < bytes >
- - **InternalAuthNFT**: A specialized authorization NFT that resides within the Domain contract and must be used together with the OwnershipNFT to enable the owner's interaction with [Domain.cash](#domain).
- - `category`: domainCategory
+ - **InternalAuthNFT**: A specialized authorization NFT that resides within the Name contract and must be used together with the OwnershipNFT to enable the owner's interaction with [Name.cash](#name).
+ - `category`: nameCategory
- `commitment`: registrationID < 8 bytes >
- - **ExternalAuthNFT**: A specialized authorization NFT that resides within the Domain Contract but can be attached to any transaction, particularly utilized by [DomainOwnershipGuard.cash](#domainownershipguard) to prove existing domain ownership and enforce penalties on illegal auction attempts.
- - `category`: domainCategory
+ - **ExternalAuthNFT**: A specialized authorization NFT that resides within the Name Contract but can be attached to any transaction, particularly utilized by [OwnershipGuard.cash](#ownershipguard) to prove existing name ownership and enforce penalties on illegal auction attempts.
+ - `category`: nameCategory
-If the domain has been inactive for > `inactivityExpiryTime` then the domain is considered abandoned and anyone can prove the inactivity and burn the Internal and External Auth NFTs to make the domain available for auction.
+If the name has been inactive for > `inactivityExpiryTime` then the name is considered abandoned and anyone can prove the inactivity and burn the Internal and External Auth NFTs to make the name available for auction.
## TLDs
-Top Level Domains (TLDs) like `.bch` and `.sat` do not exist within the contract system directly as a `value`. The names as part of the commitment in any of the NFTs in the system do not have the TLD in them. Instead, it exists in the AuthChain. This is done to allow bigger names and reduce the contract size and complexity.
-
-During the genesis phase, the Registry.cash contract is initialized with the `domainCategory`. The `authHead` for this category must include the symbol and name as the TLD, making it accessible to all applications. This entry will be the first and only one in the `authChain`. After this step, the `authHead` must be permanently removed by creating an OP_RETURN output as the first output.
+Top Level Domains (TLDs) like `.bch` and `.sat` exist in the contracts as constructor parameters, adding them to the authchain is not required.
## Genesis
To ensure the system operates as expected, the following steps must be followed :
-- Mint a new hybrid token with an NFT commitment set to 0 (8 bytes) and the maximum possible token amount of `9223372036854775807`, the tokenCategory of this NFT will be `domainCategory`.
-- Using the `tokenCategory` i.e domainCategory, create the locking bytecode for `Registry.cash`.
-- Mint a mintingNFT i.e `DomainMintingNFT` and send it to the `Registry.cash`
+- Mint a new hybrid token with an NFT commitment set to 0 (8 bytes) and the maximum possible token amount of `9223372036854775807`, the tokenCategory of this NFT will be `nameCategory`.
+- Using the `tokenCategory` i.e nameCategory, create the locking bytecode for `Registry.cash`.
+- Mint a mintingNFT i.e `NameMintingNFT` and send it to the `Registry.cash`
- Determine the following parameters and generate the locking bytecode of all the other authorized contracts:
- `inactivityExpiryTime`
- `minWaitTime`
- `maxPlatformFeePercentage`
- `minBidIncreasePercentage`
- `minStartingBid`
- - `domainContractBytecode`
+ - `nameContractBytecode`
+ - `tld`
- Create multiple threadNFTs for each authorized contract, commitment of each threadNFT must be the lockingbytecode of the authorized contract and the capability must be immutable.
- Send the threadNFTs to the `Registry.cash`
-- Remove the authhead after adding information(Name and Symbol) about the domain in the authchain.
## Dual Decay Mechanism
@@ -468,32 +465,30 @@ Even if the minimum duration has passed without the highest bidder claiming the
No.
#### How is any TLD assigned?
-Top Level Domains (TLDs) such as .bch and .sat are not directly stored as values within the contract system or as commitments in the NFTs. Instead, they are represented in the AuthChain, which allows for larger names and reduces contract complexity. Also, BCMR being a widely adopted standard makes it easier for applications to display the relevant information.
-
-During the genesis phase, the [Registry.cash](#registry) contract is initialized with a token category. The authHead for this category must include the symbol and name as the TLD, ensuring accessibility to all applications. This entry is the first and only one in the authChain. Subsequently, the authHead is permanently removed by creating an OP_RETURN output as the first output.
+Top Level Domains (TLDs) such as .bch and/or .sat exist in the contracts as constructor parameters.
This implies that while anyone can claim any TLD, the community will naturally gravitate towards and adopt the most popular and widely used ones.
#### Who earns from the auction sales?
-Since this is an open protocol, the platform facilitating the interaction can attach their own address to get a percentage of the fee. The percentage of the fee is set in the contract parameters of the [DomainFactory](#domainfactory) contract. They can choose to get any percentage less than `maxPlatformFeePercentage`. Remaining funds are sent to the miners.
+Since this is an open protocol, the platform facilitating the interaction can attach their own address to get a percentage of the fee. The percentage of the fee is set in the contract parameters of the [Factory](#factory) contract. They can choose to get any percentage less than `maxPlatformFeePercentage`. Remaining funds are sent to the miners.
-#### Can anyone renounce ownership of a domain?
-Yes, The owner must call the `burn` function of their respective Domain contract. The function will burn the Internal Auth NFT and the External Auth NFT allowing anyone to initiate a new auction for the domain. This action will allow anyone to initiate a new auction for the domain and claim for themselves.
+#### Can anyone renounce ownership of a name?
+Yes, The owner must call the `burn` function of their respective Name contract. The function will burn the Internal Auth NFT and the External Auth NFT allowing anyone to initiate a new auction for the name. This action will allow anyone to initiate a new auction for the name and claim for themselves.
#### What occurs during a ownership renouncement event?
-Each domain contract has an inbuilt function that allows the owner to renounce ownership by burning the internalAuthNFT and externalAuthNFT along with the ownershipNFT.
+Each name contract has an inbuilt function that allows the owner to renounce ownership by burning the internalAuthNFT and externalAuthNFT along with the ownershipNFT.
-This action will allow anyone to initiate a new auction for the domain and claim for themselves.
+This action will allow anyone to initiate a new auction for the name and claim for themselves.
#### How does ownership transfer work?
-Ownership transfer is simply transferring the ownershipDomainNFT to the new owner.
+Ownership transfer is simply transferring the ownershipNameNFT to the new owner.
#### How to records managed?
Record management is done by following [SORTS](https://github.com/BitCANN/sorts) standard
#### No Renewal or Expiry?
-The protocol uses an activity-based maintenance system to ensure domain upkeep:
+The protocol uses an activity-based maintenance system to ensure name upkeep:
Owners are required to engage in at least one activity within a specified timeframe, known as `inactivityExpiryTime`, which is determined during the genesis process.
@@ -510,7 +505,7 @@ Marketplaces would need to depend on indexers to display names accurately.
Creating a registry of hashes is relatively easy, which undermines the perceived privacy of a hash-based system.
-#### How do I know I or someone else owns a domain?
+#### How do I know I or someone else owns a name?
Upon the conclusion of the auction and the successful claiming of the name, three distinct NFTs are generated, each serving a unique purpose. Let us explore their roles in more detail.
**Ownership NFT:**
@@ -519,16 +514,16 @@ The Ownership NFT, along with the Internal Auth NFT, serves as definitive proof
**Internal Auth NFT:**
-The Internal Auth NFT, along with the Ownership NFT, is used to prove ownership to the domain contract. The internalAuthNFT is minted to the domain contract that is responsible to control the name and contains the same registrationID as the ownershipNFT.
-To interact with the domain contract, one must provide ownershipNFT and use the internalAuthNFT that have the same registrationID.
+The Internal Auth NFT, along with the Ownership NFT, is used to prove ownership to the name contract. The internalAuthNFT is minted to the name contract that is responsible to control the name and contains the same registrationID as the ownershipNFT.
+To interact with the name contract, one must provide ownershipNFT and use the internalAuthNFT that have the same registrationID.
**External Auth NFT:**
-The External Auth NFT is used to prove that a domain contract is already owner by someone. It is minted to the domain contract that is responsible to control the name and does not contain anything in its commitment.
+The External Auth NFT is used to prove that a name contract is already owner by someone. It is minted to the name contract that is responsible to control the name and does not contain anything in its commitment.
#### What if the incentive system is not 100% effective?
-In rare instances, a name may be claimed by two different bidders in separate auctions. The legitimate owner will be the bidder with the lower registrationID. Each domain contract includes a built-in function that allows anyone to present two competing pairs of internal and external auth NFTs and burn the one with the higher registrationID.
+In rare instances, a name may be claimed by two different bidders in separate auctions. The legitimate owner will be the bidder with the lower registrationID. Each name contract includes a built-in function that allows anyone to present two competing pairs of internal and external auth NFTs and burn the one with the higher registrationID.
As mentioned earlier, it will become impossible for the party with the mismatched registrationID in their ownershipNFT and internalAuthNFT to use the name, rendering the ownershipNFT issued to the party with the higher registrationID ineffective.
diff --git a/architecture.png b/architecture.png
index e90e4b3..0e50802 100644
Binary files a/architecture.png and b/architecture.png differ
diff --git a/contracts/Accumulator.cash b/contracts/Accumulator.cash
index 7a31511..b173d04 100644
--- a/contracts/Accumulator.cash
+++ b/contracts/Accumulator.cash
@@ -1,8 +1,8 @@
-pragma cashscript 0.11.2;
+pragma cashscript 0.11.4;
contract Accumulator() {
/**
- * Once enough auctions have happened, there might come a time when the counterNFT's tokenAmount is not enough.
+ * Once enough auctions have happened, there will come a time when the counterNFT's tokenAmount is not enough.
* Since the amount would be accumulating in the thread NFTs, this function can be used to transfer them back to the
* Counter NFT to keep the system functioning smoothly.
*
@@ -21,49 +21,55 @@ contract Accumulator() {
* - Output4: Change BCH
*/
function call(){
- require(tx.inputs.length == 5);
- require(tx.outputs.length == 5);
+ require(tx.inputs.length == 5, "Transaction: must have exactly 5 inputs");
+ require(tx.outputs.length == 5, "Transaction: must have exactly 5 outputs");
// This contract can only be used at input1 and it should return the input1 back to itself.
- require(this.activeInputIndex == 1);
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
+ require(this.activeInputIndex == 1, "Input 1: accumulator contract UTXO must be at this index");
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
// Restriction on output category is important as minting NFT is used in this transaction.
- require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);
+ require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");
// This contract can only be used with the 'lockingbytecode' used in the 0th input.
// Note: This contract can be used with any contract that fulfills these conditions, and that is fine
// because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
// be manipulating their own utxos.
bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
- require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
- require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);
+
+ // Enforce input 2 and 3 are from the registry
+ require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: locking bytecode does not match registry input's locking bytecode");
+ require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, "Input 3: locking bytecode does not match registry input's locking bytecode");
- require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);
- require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode);
+ // Enforce output 2 and 3 are returning to the registry
+ require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, "Output 2: locking bytecode does not match registry input's locking bytecode");
+ require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode, "Output 3: locking bytecode does not match registry input's locking bytecode");
- require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory);
- require(tx.outputs[3].tokenCategory == tx.inputs[3].tokenCategory);
+ // Enforce NFT transfer preserves token categories
+ require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory, "Output 2: token category does not match input 2");
+ require(tx.outputs[3].tokenCategory == tx.inputs[3].tokenCategory, "Output 3: token category does not match input 3");
+ // authorizedThreadNFTs are immutable — must match registry input category
bytes registryInputCategory = tx.inputs[0].tokenCategory;
+ require(tx.inputs[3].tokenCategory == registryInputCategory, "Input 3: token category does not match registry (immutable NFT check)");
- // authorizedThreadNFTs are immutable
- require(tx.inputs[3].tokenCategory == registryInputCategory);
-
+ // Split counter token category and capability
bytes counterCategory, bytes counterCapability = tx.inputs[2].tokenCategory.split(32);
- require(counterCategory == registryInputCategory);
- require(counterCapability == 0x02); // Minting
+ require(counterCategory == registryInputCategory, "Input 2: token category prefix does not match registry");
+ // Minting
+ require(counterCapability == 0x02, "Input 2: counter capability must be minting capability (0x02)");
// Locking bytecode of the authorized contract is 35 bytes long.
- require(tx.inputs[3].nftCommitment.length == 35);
+ require(tx.inputs[3].nftCommitment.length == 35, "Input 3: NFT commitment length must be 35 bytes (authorized contract locking bytecode)");
// Since the nftCommitment of counterNFT is registrationID so it must not be null
- // as the DomainMintingNFT has no nftCommitment nor tokenAmount
- require(tx.inputs[2].nftCommitment != 0x);
- require(tx.inputs[2].tokenAmount > 0); // Ensure that the counter minting NFT is used.
- require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount + tx.inputs[3].tokenAmount);
+ // as the NameMintingNFT has no nftCommitment nor tokenAmount
+ require(tx.inputs[2].nftCommitment != 0x, "Input 2: counter NFT must have a non-empty commitment (registration ID)");
+ // Ensure that the counter minting NFT is used.
+ require(tx.inputs[2].tokenAmount > 0, "Input 2: counter NFT must have token amount greater than 0");
+ require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount + tx.inputs[3].tokenAmount, "Output 2: token amount must equal input 2 + input 3 amounts (accumulation)");
// Pure BCH input and output.
- require(tx.inputs[4].tokenCategory == 0x);
- require(tx.outputs[4].tokenCategory == 0x);
+ require(tx.inputs[4].tokenCategory == 0x, "Input 4: must be pure BCH (no token category)");
+ require(tx.outputs[4].tokenCategory == 0x, "Output 4: must be pure BCH (no token category)");
}
}
\ No newline at end of file
diff --git a/contracts/Auction.cash b/contracts/Auction.cash
index 8956f30..78d4344 100644
--- a/contracts/Auction.cash
+++ b/contracts/Auction.cash
@@ -1,11 +1,8 @@
-pragma cashscript 0.11.2;
+pragma cashscript 0.11.4;
-/**
- * @param minStartingBid The minimum starting bid for the auction.
- */
-contract Auction(int minStartingBid) {
+contract Auction() {
/**
- * Starts a new domain registration auction.
+ * Starts a new name registration auction.
* @param name The name being registered.
*
* The function creates a new auction with:
@@ -28,73 +25,95 @@ contract Auction(int minStartingBid) {
* - Output1: Input1 back to this contract without any change.
* - Output2: Minting CounterNFT going back to the Registry contract.
* - Output3: auctionNFT to the Registry contract.
- * - Output4: OP_RETURN output containing the name.
- * - Output5: Optional change in BCH.
+ * - Output4: Optional change in BCH.
*/
function call(bytes name) {
- require(tx.inputs.length == 4);
- require(tx.outputs.length <= 6);
+ require(tx.inputs.length == 4, "Transaction: must have exactly 4 inputs");
+ require(tx.outputs.length <= 5, "Transaction: must have at most 5 outputs");
// This contract can only be used at input1 and it should return the input1 back to itself.
- require(this.activeInputIndex == 1);
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
- // Ensure that the domainCategory in not minted here.
- require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);
+ require(this.activeInputIndex == 1, "Input 1: auction contract UTXO must be at this index");
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
+ // Ensure that no tokenCategory is minted here.
+ require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");
// This contract can only be used with the 'lockingbytecode' used in the 0th input.
// Note: This contract can be used with any contract that fulfills these conditions, and that is fine
// because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
// be manipulating their own utxos.
bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
- require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
- require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);
- require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode);
+ require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: locking bytecode does not match registry input's locking bytecode");
+ require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, "Output 2: locking bytecode does not match registry input's locking bytecode");
+ require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode, "Output 3: locking bytecode does not match registry input's locking bytecode");
// Registration ID increases by 1 with each transaction.
- int prevRegistrationId = int(tx.inputs[2].nftCommitment.reverse());
- int nextRegistrationId = int(tx.outputs[2].nftCommitment.reverse());
- require(nextRegistrationId == prevRegistrationId + 1);
+ int prevRegistrationId = int(tx.inputs[2].nftCommitment);
+ int currentRegistrationId = int(tx.outputs[2].nftCommitment);
+ require(currentRegistrationId == prevRegistrationId + 1, "Output 2: registration ID must increase by 1");
// Reduce the tokenAmount in the counterNFT as some amount is going to auctionNFT
- require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount - nextRegistrationId);
+ require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount - currentRegistrationId, "Output 2: counter NFT token amount must decrease by currentRegistrationId");
// tokenAmount in the auctionNFT is the registrationId.
- require(tx.outputs[3].tokenAmount == nextRegistrationId);
+ require(tx.outputs[3].tokenAmount == currentRegistrationId, "Output 3: auction NFT token amount must equal currentRegistrationId");
- // Every auction begins with a min base value of at least minStartingBid satoshis.
- require(tx.outputs[3].value >= minStartingBid);
+ // Dual Decay mechanism, auction price decays linearly with the step.
+ // To facilitate higher precisions and since decimals do not exist in VM, we multiply
+ // it by 1e6 (1000000) and call the units as points.
+
+ // TODO: make this 1000000 (0.01 BCH)
+ int constant minStartingBid = 10000;
+ // 1. Decay points (0.0003% per step)
+ int decayPoints = minStartingBid * currentRegistrationId * 3;
+ // 2. Get auction price points
+ int currentPricePoints = minStartingBid * 1e6;
+ // 3. Subtract price points by decay points to get the current auction price.
+ int currentAuctionPrice = (currentPricePoints - decayPoints) / 1e6;
+
+ // Set the minimum auction price to 20000 satoshis.
+ currentAuctionPrice = max(currentAuctionPrice, 20000);
+
+ // Every auction begins with a min base value of at least currentAuctionPrice satoshis.
+ require(tx.outputs[3].value >= currentAuctionPrice, "Output 3: auction price must be at least minimum calculated price");
// Funding UTXO/ Bid UTXO
- require(tx.inputs[3].tokenCategory == 0x);
+ require(tx.inputs[3].tokenCategory == 0x, "Input 3: funding UTXO must be pure BCH");
- // Ensure that the funding happens from a P2PKH UTXO.
- require(tx.inputs[3].lockingBytecode.length == 25);
+ // Ensure that the funding happens from a P2PKH UTXO because there will be no way to know the locking bytecode as
+ // name can be of any length.
+ require(tx.inputs[3].lockingBytecode.length == 25, "Input 3: locking bytecode must be 25 bytes (P2PKH)");
- // Extract the PKH from the lockingBytecode of the Funding UTXO.
- // + name > 20 bytes
- bytes pkh = tx.inputs[3].lockingBytecode.split(3)[1].split(20)[0];
- require(tx.outputs[3].nftCommitment == pkh + name);
+ bytes pkhLockingBytecodeHead, bytes pkhLockingBytecodeBody = tx.inputs[3].lockingBytecode.split(3);
+ // OP_DUP OP_HASH160 Push 20-byte
+ require(pkhLockingBytecodeHead == 0x76a914, "Input 3: locking bytecode must start with OP_DUP OP_HASH160 (0x76a914)");
+ bytes pkh, bytes pkhLockingBytecodeTail = pkhLockingBytecodeBody.split(20);
+ // OP_EQUALVERIFY OP_CHECKSIG
+ require(pkhLockingBytecodeTail == 0x88ac, "Input 3: locking bytecode must end with OP_EQUALVERIFY OP_CHECKSIG (0x88ac)");
+ require(tx.outputs[3].nftCommitment == pkh + name, "Output 3: NFT commitment must match bidder PKH + name");
+
+ // Ensure that the name is not too long, as of 2025 upgrade, the nftcommitment is 40 bytes.
+ // 20 bytes pkh + 16 bytes name + 4 bytes TLD
+ require(name.length <= 16, "Name: length must be at most 16 characters");
// CounterNFT should keep the same category and capability.
- require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory);
+ require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory, "Output 2: counter NFT token category must match input 2");
// All the token categories in the transaction should be the same.
bytes registryInputCategory = tx.inputs[0].tokenCategory;
- // CounterNFT should be minting and of the 'domainCategory' i.e registryInputCategory
+ // CounterNFT should be minting and of the 'nameCategory' i.e registryInputCategory
bytes counterCategory, bytes counterCapability = tx.outputs[2].tokenCategory.split(32);
- require(counterCategory == registryInputCategory);
- require(counterCapability == 0x02); // Minting
+ require(counterCategory == registryInputCategory, "Output 2: counter NFT token category prefix must match registry");
+ // Minting
+ require(counterCapability == 0x02, "Output 2: counter NFT capability must be minting (0x02)");
- // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory
+ // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory
bytes auctionCategory, bytes auctionCapability = tx.outputs[3].tokenCategory.split(32);
- require(auctionCategory == registryInputCategory);
- require(auctionCapability == 0x01); // Mutable
-
- // Enforce an OP_RETURN output that contains the name.
- require(tx.outputs[4].lockingBytecode == new LockingBytecodeNullData([name]));
+ require(auctionCategory == registryInputCategory, "Output 3: auction NFT token category prefix must match registry");
+ // Mutable
+ require(auctionCapability == 0x01, "Output 3: auction NFT capability must be mutable (0x01)");
- if (tx.outputs.length == 6) {
+ if (tx.outputs.length == 5) {
// If any change, then it must be pure BCH.
- require(tx.outputs[5].tokenCategory == 0x);
+ require(tx.outputs[4].tokenCategory == 0x, "Output 4: change must be pure BCH (no token category)");
}
}
}
\ No newline at end of file
diff --git a/contracts/Bid.cash b/contracts/Bid.cash
index 64db089..6d42117 100644
--- a/contracts/Bid.cash
+++ b/contracts/Bid.cash
@@ -1,14 +1,12 @@
-pragma cashscript 0.11.2;
+pragma cashscript 0.11.4;
-/**
- * @param minBidIncreasePercentage The minimum percentage increase required for a new bid over the previous bid.
- */
-contract Bid(int minBidIncreasePercentage) {
+
+contract Bid() {
/**
- * Places a new bid on an active domain registration auction.
+ * Places a new bid on an active name registration auction.
*
* The function allows placing a new bid with:
- * - A minimum `minBidIncreasePercentage` increase over the previous bid.
+ * - A minimum 5% increase over the previous bid.
* - The previous bidder receives their bid amount back in the same transaction.
* - A successful bid updates the auctionNFT by updating the PKH in the nftCommitment and satoshiValue.
* capability: Mutable
@@ -31,56 +29,63 @@ contract Bid(int minBidIncreasePercentage) {
* - Output4: Optional change in BCH to the new bidder.
*/
function call() {
- require(tx.inputs.length == 4);
- require(tx.outputs.length <= 5);
+ require(tx.inputs.length == 4, "Transaction: must have exactly 4 inputs");
+ require(tx.outputs.length <= 5, "Transaction: must have at most 5 outputs");
// This contract can only be used at input1 and it should return the input1 back to itself.
- require(this.activeInputIndex == 1);
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
+ require(this.activeInputIndex == 1, "Input 1: bid contract UTXO must be at this index");
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
+ // Ensure that no tokenCategory is minted here.
+ require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");
// This contract can only be used with the 'lockingbytecode' used in the 0th input.
// Note: This contract can be used with any contract that fulfills these conditions, and that is fine
// because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
// be manipulating their own utxos.
bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
- require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
- require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);
+ require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: auction NFT locking bytecode does not match registry input's locking bytecode");
+ require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, "Output 2: auction NFT locking bytecode does not match registry input's locking bytecode");
// AuctionNFT should keep the same category and capability.
- require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory);
+ require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory, "Output 2: auction NFT token category must match input 2");
bytes registryInputCategory = tx.inputs[0].tokenCategory;
// The second part of the pair changes with each new bid, hence it's marked as mutable.
// Enforcing the structure of the pair results in predictable behavior.
bytes auctionCategory, bytes auctionCapability = tx.outputs[2].tokenCategory.split(32);
- require(auctionCategory == registryInputCategory);
- require(auctionCapability == 0x01); // Mutable
+ require(auctionCategory == registryInputCategory, "Output 2: auction NFT token category prefix must match registry");
+ require(auctionCapability == 0x01, "Output 2: auction NFT capability must be mutable (0x01)");
- // Ensure that the funding happens from a P2PKH UTXO.
- require(tx.inputs[3].lockingBytecode.length == 25);
+ // Ensure that the funding happens from a P2PKH UTXO, need to do a payout to present bidder and the information is stored in commitment.
+ require(tx.inputs[3].lockingBytecode.length == 25, "Input 3: locking bytecode must be 25 bytes (P2PKH)");
+ bytes pkhLockingBytecodeHead, bytes pkhLockingBytecodeBody = tx.inputs[3].lockingBytecode.split(3);
+ // OP_DUP OP_HASH160 Push 20-byte
+ require(pkhLockingBytecodeHead == 0x76a914, "Input 3: locking bytecode must start with OP_DUP OP_HASH160 (0x76a914)");
+ bytes pkh, bytes pkhLockingBytecodeTail = pkhLockingBytecodeBody.split(20);
+ // OP_EQUALVERIFY OP_CHECKSIG
+ require(pkhLockingBytecodeTail == 0x88ac, "Input 3: locking bytecode must end with OP_EQUALVERIFY OP_CHECKSIG (0x88ac)");
bytes20 previousPKH, bytes name = tx.inputs[2].nftCommitment.split(20);
- // Extract the PKH from the lockingBytecode of the Funding UTXO.
- // + name > 20 bytes
- bytes pkh = tx.inputs[3].lockingBytecode.split(3)[1].split(20)[0];
-
+
// AuctionNFT should have updated PKH in it's commitment.
- require(tx.outputs[2].nftCommitment == pkh + name);
+ require(tx.outputs[2].nftCommitment == pkh + name, "Output 2: auction NFT commitment must match new bidder PKH + name");
// Since tokenAmount is registrationID, make sure that it's not changing.
- require(tx.inputs[2].tokenAmount == tx.outputs[2].tokenAmount);
+ require(tx.inputs[2].tokenAmount == tx.outputs[2].tokenAmount, "Output 2: auction NFT token amount must match input 2");
- // Ensure that the bid amount is greater than or equal to the previous bid amount + minBidIncreasePercentage.
- require(tx.outputs[2].value * 100 >= tx.inputs[2].value * (100 + minBidIncreasePercentage));
+ // Ensure that the bid amount is greater than or equal to the previous bid amount + 5%.
+ require(tx.outputs[2].value * 100 >= tx.inputs[2].value * 105, "Output 2: bid amount must be at least 5 percentage higher");
// Locking bytecode of the previous bidder.
- require(tx.outputs[3].lockingBytecode == new LockingBytecodeP2PKH(previousPKH));
+ require(tx.outputs[3].lockingBytecode == new LockingBytecodeP2PKH(previousPKH), "Output 3: previous bidder locking bytecode must match previous PKH");
// The amount being sent back to the previous bidder.
- require(tx.outputs[3].value == tx.inputs[2].value);
+ require(tx.outputs[3].value == tx.inputs[2].value, "Output 3: previous bidder refund amount must match previous bid amount");
+ // Funding UTXO/ Bid UTXO
+ require(tx.inputs[3].tokenCategory == 0x, "Input 3: funding UTXO must be pure BCH");
if (tx.outputs.length == 5) {
// If any change, then it must be pure BCH.
- require(tx.outputs[4].tokenCategory == 0x);
+ require(tx.outputs[4].tokenCategory == 0x, "Output 4: change must be pure BCH (no token category)");
}
}
}
\ No newline at end of file
diff --git a/contracts/AuctionConflictResolver.cash b/contracts/ConflictResolver.cash
similarity index 62%
rename from contracts/AuctionConflictResolver.cash
rename to contracts/ConflictResolver.cash
index 6215a76..af9836c 100644
--- a/contracts/AuctionConflictResolver.cash
+++ b/contracts/ConflictResolver.cash
@@ -1,6 +1,6 @@
-pragma cashscript 0.11.2;
+pragma cashscript 0.11.4;
-contract AuctionConflictResolver() {
+contract ConflictResolver() {
/**
* Resolves a conflict between two competing registration auctions for the same name.
*
@@ -24,42 +24,45 @@ contract AuctionConflictResolver() {
* - Output3: BCH change/reward to caller.
*/
function call() {
- require(tx.inputs.length == 4);
- require(tx.outputs.length == 4);
+ require(tx.inputs.length == 4, "Transaction: must have exactly 4 inputs");
+ require(tx.outputs.length == 4, "Transaction: must have exactly 4 outputs");
// This contract can only be used at input1 and it should return the input1 back to itself.
- require(this.activeInputIndex == 1);
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
+ require(this.activeInputIndex == 1, "Input 1: conflict resolver contract UTXO must be at this index");
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
+ // Ensure that no tokenCategory is minted here.
+ require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");
// This contract can only be used with the 'lockingbytecode' used in the 0th input.
// Note: This contract can be used with any contract that fulfills these conditions, and that is fine
// because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
// be manipulating their own utxos.
bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
- require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
- require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);
- require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);
+ require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: valid auction locking bytecode does not match registry input's locking bytecode");
+ require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, "Input 3: invalid auction locking bytecode does not match registry input's locking bytecode");
+ require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, "Output 2: valid auction locking bytecode does not match registry input's locking bytecode");
// All the token categories in the transaction should be the same.
bytes registryInputCategory = tx.inputs[0].tokenCategory;
// auctionNFT should be mutable
bytes auctionCategory, bytes auctionCapability = tx.inputs[2].tokenCategory.split(32);
- require(auctionCategory == registryInputCategory);
- require(auctionCapability == 0x01); // Mutable
+ require(auctionCategory == registryInputCategory, "Input 2: valid auction token category prefix must match registry");
+ // Valid auction capability should be mutable.
+ require(auctionCapability == 0x01, "Input 2: valid auction capability must be mutable (0x01)");
// Invalid and valid auctionNFTs both should have the same category and capability.
- require(tx.inputs[2].tokenCategory == tx.inputs[3].tokenCategory);
+ require(tx.inputs[2].tokenCategory == tx.inputs[3].tokenCategory, "Input 2 and 3: auction token categories must match");
// Both auctionNFTs should also have the same 'name'
- require(tx.inputs[2].nftCommitment.split(20)[1] == tx.inputs[3].nftCommitment.split(20)[1]);
+ require(tx.inputs[2].nftCommitment.split(20)[1] == tx.inputs[3].nftCommitment.split(20)[1], "Input 2 and 3: auction names must match");
// The valid auctionNFT will have a lower registrationID
- require(tx.inputs[2].tokenAmount < tx.inputs[3].tokenAmount);
+ require(tx.inputs[2].tokenAmount < tx.inputs[3].tokenAmount, "Input 2: valid auction registration ID must be lower than input 3");
// tokenAmount from the invalid auctionNFT goes to the authorizedThreadNFT to be accumulated later
// and merged back with the CounterNFT using the `Accumulator` Contract
- require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount);
+ require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount, "Output 0: token amount must equal input 0 + input 3 amounts (accumulation)");
// Attach any output to take away the funds as reward
- require(tx.outputs[3].tokenCategory == 0x);
+ require(tx.outputs[3].tokenCategory == 0x, "Output 3: reward must be pure BCH (no token category)");
}
}
\ No newline at end of file
diff --git a/contracts/Domain.cash b/contracts/Domain.cash
deleted file mode 100644
index 185bdab..0000000
--- a/contracts/Domain.cash
+++ /dev/null
@@ -1,167 +0,0 @@
-pragma cashscript 0.11.2;
-
-/**
- * @param inactivityExpiryTime The time period after which the domain is considered inactive.
- * @param name The name of the domain.
- * @param domainCategory The category of the domain.
- */
-contract Domain(
- int inactivityExpiryTime,
- bytes name,
- bytes domainCategory
- ) {
-
- /**
- * This function can be used to perform a variety of actions.
- *
- * For example:
- * - It can be used to prove the the ownership of the domain by other contracts.
- * - This function allows the owner to perform any actions in conjunction with other contracts.
- * - This function can be used to add records and invalidate multiple records in a single transaction.
- *
- * Records are created using OP_RETURN outputs. To add a record, include the record data directly in the OP_RETURN output.
- * To invalidate a record, prefix "RMV" followed by the hash of the record content in the OP_RETURN output. This will signal
- * the library/indexers to exclude the record from the valid records.
- *
- * @inputs
- * - Inputx: Internal/External Auth NFT
- * - Inputx+1 (optional): Domain ownership NFT from the owner
- *
- * @outputs
- * - Outputx: Internal/External Auth NFT returned to this contract
- * - Outputx+1 (optional): Domain NFT returned
- *
- */
- function useAuth(int authID) {
- // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.
- require(tx.version == 2);
-
- // The activeInputIndex can be anything as long as the utxo properties are preserved and comes back to the
- // contract without alteration.
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
- require(tx.inputs[this.activeInputIndex].tokenCategory == domainCategory);
- require(tx.outputs[this.activeInputIndex].tokenCategory == domainCategory);
- require(tx.inputs[this.activeInputIndex].nftCommitment == tx.outputs[this.activeInputIndex].nftCommitment);
-
- if(authID == 1) {
- // The next input from the InternalAuthNFT must be the ownershipNFT.
- require(tx.inputs[this.activeInputIndex + 1].tokenCategory == domainCategory);
- bytes registrationId, bytes nameFromOwnerNFT = tx.inputs[this.activeInputIndex + 1].nftCommitment.split(8);
- require(nameFromOwnerNFT == name);
- require(tx.inputs[this.activeInputIndex].nftCommitment == registrationId);
- } else {
- // One known use of ExternalAuthNFT in the `DomainOwnershipGuard` contract. ExternalAuthNFT is
- // used to prove that an owner exists.
- require(tx.inputs[this.activeInputIndex].nftCommitment == 0x);
- }
- }
-
- /**
- * If the incentive system fails, i.e `DomainOwnershipGuard` or `AuctionConflictResolver` fails to prevent a
- * a owner conflict. When this happens there will be > 1 owner for this domain.
- * The owner with the lowest registrationID must be the only owner for this domain.
- * To help enforce this rule, this function will allow anyone to burn both the Auth NFTs of the NEW owner.
- *
- * @inputs
- * - Input0: Valid External Auth NFT from self
- * - Input1: Valid Internal Auth NFT from self
- * - Input2: Invalid External Auth NFT from self
- * - Input3: Invalid Internal Auth NFT from self
- * - Input4: BCH input from anyone
- *
- * @outputs
- * - Output0: Valid External Auth NFT back to self
- * - Output1: Valid Internal Auth NFT back to self
- * - Output3: BCH change output
- */
- function resolveOwnerConflict(){
- // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.
- require(tx.version == 2);
-
- require(tx.inputs.length == 5);
- require(tx.outputs.length == 3);
-
- // Pure BCH input and output to fund the transaction
- require(tx.inputs[4].tokenCategory == 0x);
- require(tx.outputs[2].tokenCategory == 0x);
-
- bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
- require(tx.inputs[0].lockingBytecode == selfLockingBytecode);
- require(tx.inputs[1].lockingBytecode == selfLockingBytecode);
- require(tx.inputs[2].lockingBytecode == selfLockingBytecode);
- require(tx.inputs[3].lockingBytecode == selfLockingBytecode);
-
- require(tx.outputs[0].lockingBytecode == selfLockingBytecode);
- require(tx.outputs[1].lockingBytecode == selfLockingBytecode);
-
- // External Auth NFTs
- require(tx.inputs[0].nftCommitment == 0x);
- require(tx.inputs[2].nftCommitment == 0x);
-
- // Commitments of Valid Auth NFts back to self
- require(tx.outputs[0].nftCommitment == 0x);
- require(tx.outputs[1].nftCommitment == tx.inputs[1].nftCommitment);
-
- // Ensure that all the token inputs and outputs have domainCategory
- require(tx.inputs[0].tokenCategory == domainCategory);
- require(tx.inputs[1].tokenCategory == domainCategory);
- require(tx.inputs[2].tokenCategory == domainCategory);
- require(tx.inputs[3].tokenCategory == domainCategory);
-
- require(tx.outputs[0].tokenCategory == domainCategory);
- require(tx.outputs[1].tokenCategory == domainCategory);
-
- // Compare the registrationID
- require(int(tx.inputs[1].nftCommitment.reverse()) < int(tx.inputs[3].nftCommitment.reverse()));
- }
-
- /**
- * Allows the domain owner or anyone to burn the InternalAuthNFT and externalAuthNFT making this domain available
- * for auction.
- *
- * - Owner can burn the AuthNFTs anytime.
- * - External party can burn the AuthNFTs when the internalAuth NFT has not been used for more than `inactivityExpiryTime`.
- *
- * @inputs
- * - Input0: External Auth NFT
- * - Input1: Internal Auth NFT
- * - Input2: Pure BCH or Domain ownership NFT from the owner
- *
- * @outputs
- * - Output0: BCH change
- *
- */
- function burn() {
- // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.
- // Need version 2 enforcement for relative timelocks.
- require(tx.version == 2);
-
- require(tx.inputs.length == 3);
- require(tx.outputs.length == 1);
-
- // If an external party is attempting to burn the authNFTs
- if (tx.inputs[2].tokenCategory == 0x) {
- // If pure BCH input, then allow anyone to burn given the time limit has passed.
- require(tx.inputs[1].sequenceNumber == inactivityExpiryTime);
- } else {
- // If domain ownership NFT input, then allow the owner to burn anytime.
- require(tx.inputs[2].tokenCategory == domainCategory);
- // Make sure that the registrationID in the domainOwnershipNFT and the internalAuthNFT are the same.
- require(tx.inputs[2].nftCommitment.split(8)[0] == tx.inputs[0].nftCommitment);
- }
-
- bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
- require(tx.inputs[0].lockingBytecode == selfLockingBytecode);
- require(tx.inputs[1].lockingBytecode == selfLockingBytecode);
-
- // ExternalAuthNFT
- require(tx.inputs[0].nftCommitment == 0x);
- // Both InternalAuthNFT and externalAuthNFT are immutable and have the same tokenCategory
- require(tx.inputs[0].tokenCategory == tx.inputs[1].tokenCategory);
- require(tx.inputs[0].tokenCategory == domainCategory);
- require(tx.inputs[1].tokenCategory == domainCategory);
-
- // Return the BCH as change.
- require(tx.outputs[0].tokenCategory == 0x);
- }
-}
diff --git a/contracts/DomainFactory.cash b/contracts/DomainFactory.cash
deleted file mode 100644
index 0e9de61..0000000
--- a/contracts/DomainFactory.cash
+++ /dev/null
@@ -1,150 +0,0 @@
-pragma cashscript 0.11.2;
-
-/**
- * @param domainContractBytecode - Partial bytecode of the domain contract
- * @param minWaitTime - Minimum wait time to consider an auction ended
- * @param maxPlatformFeePercentage - Maximum platform fee percentage
- */
-contract DomainFactory(
- bytes domainContractBytecode,
- int minWaitTime,
- int maxPlatformFeePercentage
-) {
- /**
- * This function finalizes a domain registration auction by:
- * - Verifying the auction has ended and the winner's bid is valid
- * - Issuing an immutable externalAuthNFT to the Domain Contract
- * - Issuing an immutable internalAuthNFT to the Domain Contract
- * - Issuing an immutable domain NFT to the auction winner
- * - Distributing auction fees between the platform and miners
- * - Burning the auctionNFT
- * - Pure BCH input from bidder is used to prevent miners from taking away the funds from any or all transactions in the future.
- * Out of many possible ways, this method will be suitable to easily implement by applications.
- *
- *
- * @inputs
- * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract
- * - Input1: Any input from this contract
- * - Input2: DomainMintingNFT from the Registry Contract
- * - Input3: auctionNFT from the Registry Contract
- * - Input4: Pure BCH from bidder
- *
- * @outputs
- * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.
- * - Output1: Input1 back to this contract without any change
- * - Output2: DomainMintingNFT back to the Registry contract
- * - Output3: External Auth NFT to the domain contract
- * - Output4: Internal Auth NFT to the domain contract
- * - Output5: Domain NFT to the auction winner
- * - Output6: Pure BCH back to the bidder
- * - Output7: Platform fee
- *
- */
- function call(){
- require(tx.inputs.length == 5);
- require(tx.outputs.length == 8);
-
- // This contract can only be used at input1 and it should return to itself.
- require(this.activeInputIndex == 1);
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
- // Ensure that the domainCategory in not minted here.
- require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);
- // Strict value checks to ensure the platform and miner get fee.
- require(tx.inputs[this.activeInputIndex].value == tx.outputs[this.activeInputIndex].value);
-
- // This contract can only be used with the 'lockingbytecode' used in the 0th input.
- // Note: This contract can be used with any contract that fulfills these conditions, and that is fine
- // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
- // be manipulating their own utxos.
- bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
- require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
- require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);
- require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);
-
- // All the token categories in the transaction should be the same.
- bytes registryInputCategory = tx.inputs[0].tokenCategory;
- require(tx.outputs[3].tokenCategory == registryInputCategory);
- require(tx.outputs[4].tokenCategory == registryInputCategory);
- require(tx.outputs[5].tokenCategory == registryInputCategory);
-
- // DomainMintingNFT should be minting and of the 'domainCategory' i.e registryInputCategory
- bytes domainMintingCategory, bytes domainMintingCapability = tx.inputs[2].tokenCategory.split(32);
- require(domainMintingCategory == registryInputCategory);
- require(domainMintingCapability == 0x02); // Mutable
- // DomainMintingNFT should keep the same category and capability
- require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory);
-
- // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory
- bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);
- require(auctionCategory == registryInputCategory);
- require(auctionCapability == 0x01); // Mutable
-
- // Enforce strict restrictions on DomainMintingNFT
- require(tx.inputs[2].nftCommitment == tx.outputs[2].nftCommitment);
- // DomainMintingNFT has no nftCommitment
- require(tx.outputs[2].nftCommitment == 0x);
- // DomainMintingNFT has no tokenAmount
- require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount);
- require(tx.outputs[2].tokenAmount == 0);
-
- // Strict value check
- require(tx.outputs[2].value == tx.inputs[2].value);
-
- // Enforcing the relative timelock, the auctionNFT must be atleast `minWaitTime` old
- // to be considered ended.
- require(tx.inputs[3].sequenceNumber == minWaitTime);
-
- // Extract the PKH and name from the auctionNFT
- bytes20 bidderPKH, bytes name = tx.inputs[3].nftCommitment.split(20);
-
- // Get the name length to generate the complete bytecode of the domain contract
- int nameLength = name.length;
- // category + name + bytecode.
- // Note: `inactivityExpiryTime` in the domain is already added to the domainContractBytecode in the constructor.
- bytes domainBytecode = 0x20 + registryInputCategory + bytes(nameLength) + name + domainContractBytecode;
- bytes32 scriptHash = hash256(domainBytecode);
- bytes35 domainLockingBytecode = new LockingBytecodeP2SH32(scriptHash);
-
- // ExternalAuthNFT goes to the domain contract
- require(tx.outputs[3].lockingBytecode == domainLockingBytecode);
- // InternalAuthNFT goes to the domain contract
- require(tx.outputs[4].lockingBytecode == domainLockingBytecode);
-
- // ExternalAuthNFT does not have any commitment
- require(tx.outputs[3].nftCommitment == 0x);
- // Strict value check
- require(tx.outputs[3].value == 1000);
-
- // InternalAuthNFT has registrationID as the commitment so it can be used to authenticate
- // along with the ownershipNFT
- bytes8 registrationId = bytes8(tx.inputs[3].tokenAmount).reverse();
- require(tx.outputs[4].nftCommitment == registrationId);
- // Strict value check
- require(tx.outputs[4].value == 1000);
-
- // Send the domain ownership NFT to the bidder
- require(tx.outputs[5].nftCommitment == registrationId + name);
- require(tx.outputs[5].lockingBytecode == new LockingBytecodeP2PKH(bidderPKH));
- require(tx.outputs[5].value == 1000);
-
- // Ensure that the bidder receiving the domain ownership NFT is also receiving the pure BCH back
- require(tx.inputs[4].lockingBytecode == tx.outputs[5].lockingBytecode);
- require(tx.inputs[4].lockingBytecode == tx.outputs[6].lockingBytecode);
- // Ensure that the value of input from bidder is the same and goes back to the bidder
- require(tx.inputs[4].value == tx.outputs[6].value);
-
- // Ensure that input and output to the bidder does not have any tokenCategory
- require(tx.inputs[4].tokenCategory == 0x);
- require(tx.outputs[6].tokenCategory == 0x);
-
- // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later
- // and merged back with the CounterNFT using the `Accumulator` Contract
- require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount);
-
- // Output can be added by anyone (Mainly platforms)
- require(tx.outputs[7].tokenCategory == 0x);
- // Enforce that the other piece of the fee goes to the miners.
- require(tx.outputs[7].value <= (tx.inputs[3].value / 100) * maxPlatformFeePercentage);
- }
-
-}
\ No newline at end of file
diff --git a/contracts/DomainOwnershipGuard.cash b/contracts/DomainOwnershipGuard.cash
deleted file mode 100644
index b9b09d0..0000000
--- a/contracts/DomainOwnershipGuard.cash
+++ /dev/null
@@ -1,77 +0,0 @@
-pragma cashscript 0.11.2;
-
-/**
- * @param domainContractBytecode The the partial bytecode of the domain contract that has an Owner..
- */
-contract DomainOwnershipGuard(bytes domainContractBytecode) {
- /**
- * If the domain being auctioned already has an `externalAuthNFT` with the same category, then the auction is invalid.
- * Because it means that an owner still exists. If it is known that the domain has been abandoned for > `inactivityExpiryTime`
- * then one must use the `burn` method of the domain.cash to burn the internalAuthNFT and externalAuthNFT making the
- * domain to be available for auction.
- *
- * Penalizes invalid domain registrations by allowing anyone to burn the auctionNFT and claim the funds as a reward.
- *
- * @inputs
- * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract
- * - Input1: Any input from this contract
- * - Input2: External Auth NFT from the Domain Contract
- * - Input3: auctionNFT from Registry Contract
- *
- * @outputs
- * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.
- * - Output1: Input1 back to this contract without any change
- * - Output2: External Auth NFT back to the Domain Contract
- * - Output3: BCH change/reward to caller
- */
- function call(){
- require(tx.inputs.length == 4);
- require(tx.outputs.length == 4);
-
- // This contract can only be used at input1 and it should return the input1 back to itself.
- require(this.activeInputIndex == 1);
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
-
- // This contract can only be used with the 'lockingbytecode' used in the 0th input.
- // Note: This contract can be used with any contract that fulfills these conditions, and that is fine
- // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
- // be manipulating their own utxos.
- bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
- require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);
-
- bytes registryInputCategory = tx.inputs[0].tokenCategory;
- require(tx.inputs[2].tokenCategory == registryInputCategory);
- require(tx.outputs[2].tokenCategory == registryInputCategory);
-
- // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory
- bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);
- require(auctionCategory == registryInputCategory);
- require(auctionCapability == 0x01); // Mutable
-
- // nftCommiment of the externalAuthNFT must stay the same
- require(tx.inputs[2].nftCommitment == tx.outputs[2].nftCommitment);
- // Ensure that the externalAuth NFT is used and not the internalAuth NFT.
- require(tx.inputs[2].nftCommitment == 0x);
-
- // Get the name of the domain from the auctionNFT
- bytes name = tx.inputs[3].nftCommitment.split(20)[1];
- // Get the name length to generate the complete bytecode of the domain contract
- int nameLength = name.length;
- // category + name + bytecode.
- // Note: `inactivityExpiryTime` in the domain is already added to the domainContractBytecode in the constructor.
- bytes domainBytecode = 0x20 + registryInputCategory + bytes(nameLength) + name + domainContractBytecode;
- bytes32 scriptHash = hash256(domainBytecode);
- bytes35 domainLockingBytecode = new LockingBytecodeP2SH32(scriptHash);
-
- // Ensure that the externalAuthNFT is coming from the correct Domain Contract
- require(tx.inputs[2].lockingBytecode == domainLockingBytecode);
- require(tx.outputs[2].lockingBytecode == domainLockingBytecode);
-
- // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later
- // and merged back with the CounterNFT using the `Accumulator` Contract
- require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount);
-
- // Reward Output
- require(tx.outputs[3].tokenCategory == 0x);
- }
-}
\ No newline at end of file
diff --git a/contracts/Factory.cash b/contracts/Factory.cash
new file mode 100644
index 0000000..2705c25
--- /dev/null
+++ b/contracts/Factory.cash
@@ -0,0 +1,148 @@
+pragma cashscript 0.11.4;
+
+/**
+ * @param nameContractBytecode - Partial bytecode of the name contract
+ * @param creatorIncentivePKH - PKH of the creator incentive
+ */
+contract Factory(
+ bytes nameContractBytecode,
+ bytes20 creatorIncentivePKH,
+) {
+ /**
+ * This function finalizes a name registration auction by:
+ * - Verifying the auction has ended and the winner's bid is valid
+ * - Issuing an immutable externalAuthNFT to the Name Contract
+ * - Issuing an immutable internalAuthNFT to the Name Contract
+ * - Issuing an immutable name NFT to the auction winner
+ * - Distributing auction fees between the platform and miners
+ * - Burning the auctionNFT
+ *
+ * @inputs
+ * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract
+ * - Input1: Any input from this contract
+ * - Input2: NameMintingNFT from the Registry Contract
+ * - Input3: auctionNFT from the Registry Contract
+ *
+ * @outputs
+ * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.
+ * - Output1: Input1 back to this contract without any change
+ * - Output2: NameMintingNFT back to the Registry contract
+ * - Output3: External Auth NFT to the name contract
+ * - Output4: Internal Auth NFT to the name contract
+ * - Output5: Name NFT to the auction winner
+ * - Output6: Platform fee [Reduces and the not included]
+ *
+ */
+ function call(){
+ require(tx.inputs.length == 4, "Transaction: must have exactly 4 inputs");
+ require(tx.outputs.length <= 7, "Transaction: must have at most 7 outputs");
+
+ // This contract can only be used at input1 and it should return to itself.
+ require(this.activeInputIndex == 1, "Input 1: factory contract UTXO must be at this index");
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
+ // Ensure that no tokenCategory is minted here.
+ require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");
+ // Strict value checks to ensure the platform and miner get fee.
+ require(tx.inputs[this.activeInputIndex].value == tx.outputs[this.activeInputIndex].value, "Input 1: satoshi value must match output 1");
+
+ // This contract can only be used with the 'lockingbytecode' used in the 0th input.
+ // Note: This contract can be used with any contract that fulfills these conditions, and that is fine
+ // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
+ // be manipulating their own utxos.
+ bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
+ require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: name minting NFT locking bytecode does not match registry input's locking bytecode");
+ require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, "Input 3: auction NFT locking bytecode does not match registry input's locking bytecode");
+ require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, "Output 2: name minting NFT locking bytecode does not match registry input's locking bytecode");
+
+ // All the token categories in the transaction should be the same.
+ bytes registryInputCategory = tx.inputs[0].tokenCategory;
+ require(tx.outputs[3].tokenCategory == registryInputCategory, "Output 3: external auth NFT token category prefix must match registry");
+ require(tx.outputs[4].tokenCategory == registryInputCategory, "Output 4: internal auth NFT token category prefix must match registry");
+ require(tx.outputs[5].tokenCategory == registryInputCategory, "Output 5: name ownership NFT token category prefix must match registry");
+
+ // NameMintingNFT should be minting and of the 'nameCategory' i.e registryInputCategory
+ bytes nameMintingCategory, bytes nameMintingCapability = tx.inputs[2].tokenCategory.split(32);
+ require(nameMintingCategory == registryInputCategory, "Input 2: name minting NFT token category prefix must match registry");
+ // Minting
+ require(nameMintingCapability == 0x02, "Input 2: name minting NFT capability must be minting (0x02)");
+ // NameMintingNFT should keep the same category and capability
+ require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory, "Output 2: name minting NFT token category must match input 2");
+
+ // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory
+ bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);
+ require(auctionCategory == registryInputCategory, "Input 3: auction NFT token category prefix must match registry");
+ // Mutable
+ require(auctionCapability == 0x01, "Input 3: auction NFT capability must be mutable (0x01)");
+
+ // NameMintingNFT has no nftCommitment
+ require(tx.outputs[2].nftCommitment == 0x, "Output 2: name minting NFT must have empty commitment");
+ require(tx.inputs[2].nftCommitment == 0x, "Input 2: name minting NFT must have empty commitment");
+
+ // NameMintingNFT has no tokenAmount
+ require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount, "Output 2: name minting NFT token amount must match input 2");
+ require(tx.outputs[2].tokenAmount == 0, "Output 2: name minting NFT token amount must be 0");
+
+ // Strict value check
+ require(tx.outputs[2].value == tx.inputs[2].value, "Output 2: name minting NFT satoshi value must match input 2");
+
+ // Enforcing the relative timelock, the auctionNFT must be atleast 4194306 old
+ // to be considered ended.
+ // 4194306 is sequence number in time, 2*512 seconds
+
+ // TODO: Make this 3 hours
+ require(tx.inputs[3].sequenceNumber == 4194306, "Input 3: auction NFT sequence number must equal 4194306");
+
+ // Extract the PKH and name from the auctionNFT
+ bytes20 bidderPKH, bytes name = tx.inputs[3].nftCommitment.split(20);
+
+ // Get the name length to generate the complete bytecode of the name contract
+ int nameLength = name.length;
+ // category + name + bytecode.
+ // Note: `inactivityExpiryTime` in the name is already added to the nameContractBytecode in the constructor.
+ bytes constant tld = bytes('.bch');
+ bytes nameBytecode = 0x20 + registryInputCategory + bytes(tld.length) + tld + bytes(nameLength) + name + nameContractBytecode;
+ bytes32 scriptHash = hash256(nameBytecode);
+ bytes35 nameLockingBytecode = new LockingBytecodeP2SH32(scriptHash);
+
+ // ExternalAuthNFT goes to the name contract
+ require(tx.outputs[3].lockingBytecode == nameLockingBytecode, "Output 3: external auth NFT locking bytecode must match name contract");
+ // InternalAuthNFT goes to the name contract
+ require(tx.outputs[4].lockingBytecode == nameLockingBytecode, "Output 4: internal auth NFT locking bytecode must match name contract");
+
+ // ExternalAuthNFT does not have any commitment
+ require(tx.outputs[3].nftCommitment == 0x, "Output 3: external auth NFT must have empty commitment");
+ // Strict value check
+ require(tx.outputs[3].value == 1000, "Output 3: external auth NFT satoshi value must be 1000");
+
+ // InternalAuthNFT has registrationID as the commitment so it can be used to authenticate
+ // along with the ownershipNFT
+ int registrationId = int(tx.outputs[4].nftCommitment);
+ require(tx.inputs[3].tokenAmount == registrationId, "Output 4: internal auth NFT commitment must match registration ID");
+ // Strict value check
+ require(tx.outputs[4].value == 1000, "Output 4: internal auth NFT satoshi value must be 1000");
+
+ // Send the name ownership NFT to the bidder
+ require(tx.outputs[5].nftCommitment == tx.outputs[4].nftCommitment + name, "Output 5: name ownership NFT commitment must match registration ID + name");
+ require(tx.outputs[5].lockingBytecode == new LockingBytecodeP2PKH(bidderPKH), "Output 5: name ownership NFT locking bytecode must match bidder PKH");
+ require(tx.outputs[5].value == 1000, "Output 5: name ownership NFT satoshi value must be 1000");
+
+ // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later
+ // and merged back with the CounterNFT using the `Accumulator` Contract
+ require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount, "Output 0: token amount must equal input 0 + input 3 amounts (accumulation)");
+
+ // Dual Decay mechanism, creator incentive decays linearly with the step.
+
+ // Initial minimal fee is 5000; 1000 * 3 (nft outputs) + 2000 Miner Fee
+ int minimalDeduction = tx.inputs[3].value - 5000;
+ int creatorIncentive = (minimalDeduction * (1e5 - tx.inputs[3].tokenAmount) / 1e5);
+
+ // If incentive is > 20000 satoshis, then it goes to the creator, else it goes to the miners.
+ if(creatorIncentive > 20000) {
+ require(tx.outputs[6].tokenCategory == 0x, "Output 6: creator incentive must be pure BCH (no token category)");
+ // Enforce that the other piece of the fee goes to the miners.
+ require(tx.outputs[6].value == creatorIncentive, "Output 6: creator incentive satoshi value must match calculated incentive");
+ require(tx.outputs[6].lockingBytecode == new LockingBytecodeP2PKH(creatorIncentivePKH), "Output 6: creator incentive locking bytecode must match creator PKH");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/contracts/Name.cash b/contracts/Name.cash
new file mode 100644
index 0000000..82a5a68
--- /dev/null
+++ b/contracts/Name.cash
@@ -0,0 +1,225 @@
+pragma cashscript 0.11.4;
+
+/**
+ * @param name The name of the name.
+ * @param tld The TLD of the name.
+ * @param nameCategory The category of the name.
+ */
+contract Name(
+ bytes name,
+ bytes tld,
+ bytes nameCategory
+ ) {
+
+ /**
+ * This function can be used to perform a variety of actions.
+ *
+ * For example:
+ * - It can be used to prove the the ownership of the name by other contracts.
+ * - This function allows the owner to perform any actions in conjunction with other contracts.
+ * - This function can be used to add records and invalidate multiple records in a single transaction.
+ *
+ * Records are created using OP_RETURN outputs. To add a record, include the record data directly in the OP_RETURN output.
+ * To invalidate a record, include the hash of the record content in the OP_RETURN output. This will signal
+ * the library/indexers to exclude the record from the valid records.
+ *
+ * @inputs
+ * - Inputx: Internal/External Auth NFT
+ * - Inputx+1 (optional): Name ownership NFT from the owner
+ *
+ * @outputs
+ * - Outputx: Internal/External Auth NFT returned to this contract
+ * - Outputx+1 (optional): Name NFT returned
+ *
+ */
+ function useAuth(int authID) {
+ // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.
+ require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");
+
+ // The activeInputIndex can be anything as long as the utxo properties are preserved and comes back to the
+ // contract without alteration.
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Auth input: locking bytecode must match active output");
+ require(tx.inputs[this.activeInputIndex].tokenCategory == nameCategory, "Auth input: token category must match name category");
+ require(tx.outputs[this.activeInputIndex].tokenCategory == nameCategory, "Auth output: token category must match name category");
+ require(tx.inputs[this.activeInputIndex].nftCommitment == tx.outputs[this.activeInputIndex].nftCommitment, "Auth input: NFT commitment must match active output");
+
+ if(authID == 1) {
+ // The next input from the InternalAuthNFT must be the ownershipNFT.
+ require(tx.inputs[this.activeInputIndex + 1].tokenCategory == nameCategory, "Ownership input: ownership NFT token category must match name category");
+ require(tx.outputs[this.activeInputIndex + 1].tokenCategory == tx.inputs[this.activeInputIndex + 1].tokenCategory, "Ownership output: token category must match active input");
+
+ bytes registrationId, bytes nameFromOwnerNFT = tx.inputs[this.activeInputIndex + 1].nftCommitment.split(8);
+ require(nameFromOwnerNFT == name + tld, "Ownership input: ownership NFT name must match contract name + TLD");
+ require(tx.inputs[this.activeInputIndex].nftCommitment == registrationId, "Auth input: internal auth NFT commitment must match ownership NFT registration ID");
+ require(tx.outputs[this.activeInputIndex + 1].nftCommitment == tx.inputs[this.activeInputIndex + 1].nftCommitment, "Ownership output: NFT commitment must match active input");
+
+ // Note only the commitment and category are enforced on the next output index and not the locking bytecode.
+ } else {
+ // One known use of ExternalAuthNFT in the `OwnershipGuard` contract. ExternalAuthNFT is
+ // used to prove that an owner exists.
+ require(tx.inputs[this.activeInputIndex].nftCommitment == 0x, "Auth input: external auth NFT must have empty commitment");
+ }
+ }
+
+ /**
+ * If an invalid name is registered, this function allows anyone to burn the NFTs
+ * @inputs
+ * - Input0: External Auth NFT from self
+ * - Input1: Internal Auth NFT from self
+ * - Input2: BCH input from anyone
+ *
+ * @outputs
+ * - Output0: BCH change output
+ */
+ function penaliseInvalidName(int characterNumber) {
+ // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.
+ require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");
+
+ require(tx.inputs.length == 3, "Transaction: must have exactly 3 inputs");
+ require(tx.outputs.length == 1, "Transaction: must have exactly 1 output");
+
+ bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
+ require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: external auth NFT locking bytecode must match name contract");
+ require(tx.inputs[1].lockingBytecode == selfLockingBytecode, "Input 1: internal auth NFT locking bytecode must match name contract");
+
+ require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: external auth NFT token category must match name category");
+ require(tx.inputs[1].tokenCategory == nameCategory, "Input 1: internal auth NFT token category must match name category");
+
+ // External Auth NFT
+ require(tx.inputs[0].nftCommitment == 0x, "Input 0: external auth NFT must have empty commitment");
+
+ // Internal Auth NFT
+ // First 8 bytes are the registrationID and the rest is the name.
+ int sliceEndIndex = tx.inputs[1].nftCommitment.length - tld.length;
+ bytes nameFromNFT = tx.inputs[1].nftCommitment.slice(8, sliceEndIndex);
+
+ bytes characterSplitBytes = nameFromNFT.split(characterNumber)[0];
+ characterNumber = characterNumber - 1;
+ bytes character = characterSplitBytes.split(characterNumber)[1];
+ int charVal = int(character);
+
+ // Character is not a hyphen.
+ require(charVal != 45, "Character is a hyphen");
+ // Character is not from a-z.
+ require(!within(charVal, 97, 123), "Character is lowercase letter");
+ // Character is not from A-Z.
+ require(!within(charVal, 65, 91), "Character is uppercase letter");
+ // Character is not from 0-9.
+ require(!within(charVal, 48, 58), "Character is digit");
+
+ // Pure BCH, ensures burn
+ require(tx.outputs[0].tokenCategory == 0x, "Output 0: change must be pure BCH (no token category)");
+ }
+
+ /**
+ * If the incentive system fails, i.e `OwnershipGuard` or `AuctionConflictResolver` fails to prevent a
+ * a owner conflict. When this happens there will be > 1 owner for this name.
+ * The owner with the lowest registrationID must be the only owner for this name.
+ * To help enforce this rule, this function will allow anyone to burn both the Auth NFTs of the NEW owner.
+ *
+ * @inputs
+ * - Input0: Valid External Auth NFT from self
+ * - Input1: Valid Internal Auth NFT from self
+ * - Input2: Invalid External Auth NFT from self
+ * - Input3: Invalid Internal Auth NFT from self
+ * - Input4: BCH input from anyone
+ *
+ * @outputs
+ * - Output0: Valid External Auth NFT back to self
+ * - Output1: Valid Internal Auth NFT back to self
+ * - Output2: BCH change output
+ */
+ function resolveOwnerConflict(){
+ // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.
+ require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");
+
+ require(tx.inputs.length == 5, "Transaction: must have exactly 5 inputs");
+ require(tx.outputs.length <= 3, "Transaction: must have at most 3 outputs");
+
+ // Pure BCH input and output to fund the transaction
+ require(tx.inputs[4].tokenCategory == 0x, "Input 4: funding input must be pure BCH (no token category)");
+ if (tx.outputs.length == 3) {
+ require(tx.outputs[2].tokenCategory == 0x, "Output 2: change must be pure BCH (no token category)");
+ }
+
+ bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
+ require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: valid external auth NFT locking bytecode must match name contract");
+ require(tx.inputs[1].lockingBytecode == selfLockingBytecode, "Input 1: valid internal auth NFT locking bytecode must match name contract");
+ require(tx.inputs[2].lockingBytecode == selfLockingBytecode, "Input 2: invalid external auth NFT locking bytecode must match name contract");
+ require(tx.inputs[3].lockingBytecode == selfLockingBytecode, "Input 3: invalid internal auth NFT locking bytecode must match name contract");
+
+ require(tx.outputs[0].lockingBytecode == selfLockingBytecode, "Output 0: valid external auth NFT locking bytecode must match name contract");
+ require(tx.outputs[1].lockingBytecode == selfLockingBytecode, "Output 1: valid internal auth NFT locking bytecode must match name contract");
+
+ // External Auth NFTs
+ require(tx.inputs[0].nftCommitment == 0x, "Input 0: valid external auth NFT must have empty commitment");
+ require(tx.inputs[2].nftCommitment == 0x, "Input 2: invalid external auth NFT must have empty commitment");
+
+ // Commitments of Valid Auth NFts back to self
+ require(tx.outputs[0].nftCommitment == 0x, "Output 0: valid external auth NFT must have empty commitment");
+ require(tx.outputs[1].nftCommitment == tx.inputs[1].nftCommitment, "Output 1: valid internal auth NFT commitment must match input 1");
+
+ // Ensure that all the token inputs and outputs have nameCategory
+ require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: valid external auth NFT token category must match name category");
+ require(tx.inputs[1].tokenCategory == nameCategory, "Input 1: valid internal auth NFT token category must match name category");
+ require(tx.inputs[2].tokenCategory == nameCategory, "Input 2: invalid external auth NFT token category must match name category");
+ require(tx.inputs[3].tokenCategory == nameCategory, "Input 3: invalid internal auth NFT token category must match name category");
+
+ require(tx.outputs[0].tokenCategory == nameCategory, "Output 0: valid external auth NFT token category must match name category");
+ require(tx.outputs[1].tokenCategory == nameCategory, "Output 1: valid internal auth NFT token category must match name category");
+
+ // Compare the registrationID
+ require(int(tx.inputs[1].nftCommitment) < int(tx.inputs[3].nftCommitment), "Input 1: valid internal auth NFT registration ID must be lower than input 3");
+ }
+
+ /**
+ * Allows the name owner or anyone to burn the InternalAuthNFT and externalAuthNFT making this name available
+ * for auction.
+ *
+ * - Owner can burn the AuthNFTs anytime.
+ * - External party can burn the AuthNFTs when the internalAuth NFT has not been used for more than `inactivityExpiryTime`.
+ *
+ * @inputs
+ * - Input0: External Auth NFT
+ * - Input1: Internal Auth NFT
+ * - Input2: Pure BCH or Name ownership NFT from the owner
+ *
+ * @outputs
+ * - Output0: BCH change
+ *
+ */
+ function burn() {
+ // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.
+ // Need version 2 enforcement for relative timelocks.
+ require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");
+
+ require(tx.inputs.length == 3, "Transaction: must have exactly 3 inputs");
+ require(tx.outputs.length == 1, "Transaction: must have exactly 1 output");
+
+ // If an external party is attempting to burn the authNFTs
+ if (tx.inputs[2].tokenCategory == 0x) {
+ // If pure BCH input, then allow anyone to burn given the time limit has passed.
+ // 4194305 is sequence number in time, 1*512 seconds
+
+ // TODO: Make this 2 years
+ require(tx.inputs[1].sequenceNumber == 4194305, "Input 1: internal auth NFT sequence number must equal inactivity expiry time");
+ } else {
+ // If name ownership NFT input, then allow the owner to burn anytime.
+ require(tx.inputs[2].tokenCategory == nameCategory, "Input 2: name ownership NFT token category must match name category");
+ // Make sure that the registrationID in the nameOwnershipNFT and the internalAuthNFT are the same.
+ require(tx.inputs[2].nftCommitment.split(8)[0] == tx.inputs[1].nftCommitment, "Input 2: name ownership NFT registration ID must match input 1 commitment");
+ }
+
+ bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
+ require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: external auth NFT locking bytecode must match name contract");
+ require(tx.inputs[1].lockingBytecode == selfLockingBytecode, "Input 1: internal auth NFT locking bytecode must match name contract");
+
+ // ExternalAuthNFT
+ require(tx.inputs[0].nftCommitment == 0x, "Input 0: external auth NFT must have empty commitment");
+ require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: external auth NFT token category must match name category");
+ require(tx.inputs[1].tokenCategory == nameCategory, "Input 1: internal auth NFT token category must match name category");
+
+ // Return the BCH as change.
+ require(tx.outputs[0].tokenCategory == 0x, "Output 0: change must be pure BCH (no token category)");
+ }
+}
diff --git a/contracts/AuctionNameEnforcer.cash b/contracts/NameEnforcer.cash
similarity index 56%
rename from contracts/AuctionNameEnforcer.cash
rename to contracts/NameEnforcer.cash
index 0965cb6..ebb385b 100644
--- a/contracts/AuctionNameEnforcer.cash
+++ b/contracts/NameEnforcer.cash
@@ -1,8 +1,8 @@
-pragma cashscript 0.11.2;
+pragma cashscript 0.11.4;
-contract AuctionNameEnforcer() {
+contract NameEnforcer() {
/**
- * Proves that a domain name contains invalid characters, burns the auctionNFT, and takes away the funds as a reward.
+ * Proves that a name contains invalid characters, burns the auctionNFT, and takes away the funds as a reward.
* During the entire auction, this can be called at any time by anyone.
*
* Rules:
@@ -25,24 +25,27 @@ contract AuctionNameEnforcer() {
*
*/
function call(int characterNumber) {
- require(tx.inputs.length == 3);
- require(tx.outputs.length == 3);
+ require(tx.inputs.length == 3, "Transaction: must have exactly 3 inputs");
+ require(tx.outputs.length == 3, "Transaction: must have exactly 3 outputs");
// This contract can only be used at input1 and it should return the input1 back to itself.
- require(this.activeInputIndex == 1);
- require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);
+ require(this.activeInputIndex == 1, "Input 1: name enforcer contract UTXO must be at this index");
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
+ // Ensure that no tokenCategory is minted here.
+ require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");
// Lock this contract to only be used with the registry type contract.
bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
- require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);
+ require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, "Input 2: locking bytecode does not match registry input's locking bytecode");
// All the token categories in the transaction should be the same.
bytes registryInputCategory = tx.inputs[0].tokenCategory;
- // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory
+ // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory
bytes auctionCategory, bytes auctionCapability = tx.inputs[2].tokenCategory.split(32);
- require(auctionCategory == registryInputCategory);
- require(auctionCapability == 0x01); // Mutable
+ require(auctionCategory == registryInputCategory, "Input 2: auction token category does not match registry");
+ // Auction capability should be mutable.
+ require(auctionCapability == 0x01, "Input 2: auction capability must be mutable (0x01)");
bytes name = tx.inputs[2].nftCommitment.split(20)[1];
bytes characterSplitBytes = name.split(characterNumber)[0];
@@ -51,19 +54,19 @@ contract AuctionNameEnforcer() {
int charVal = int(character);
// Character is not a hyphen.
- require(charVal != 45);
+ require(charVal != 45, "Character is a hyphen");
// Character is not from a-z.
- require(!within(charVal, 97, 123));
+ require(!within(charVal, 97, 123), "Character is lowercase letter");
// Character is not from A-Z.
- require(!within(charVal, 65, 91));
+ require(!within(charVal, 65, 91), "Character is uppercase letter");
// Character is not from 0-9.
- require(!within(charVal, 48, 58));
+ require(!within(charVal, 48, 58), "Character is a digit");
// tokenAmount from the invalid auctionNFT goes to the authorizedThreadNFT to be accumulated later
// and merged back with the CounterNFT using the `Accumulator` Contract
- require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[2].tokenAmount);
+ require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[2].tokenAmount, "Output 0: token amount must equal input 0 + input 2 amounts (accumulation)");
// Pure BCH.
- require(tx.outputs[2].tokenCategory == 0x);
+ require(tx.outputs[2].tokenCategory == 0x, "Output 2: reward must be pure BCH (no token category)");
}
}
\ No newline at end of file
diff --git a/contracts/OwnershipGuard.cash b/contracts/OwnershipGuard.cash
new file mode 100644
index 0000000..8bd006e
--- /dev/null
+++ b/contracts/OwnershipGuard.cash
@@ -0,0 +1,81 @@
+pragma cashscript 0.11.4;
+
+/**
+ * @param nameContractBytecode The the partial bytecode of the name contract that has an Owner.
+ */
+contract OwnershipGuard(bytes nameContractBytecode) {
+ /**
+ * If the name being auctioned already has an `externalAuthNFT` with the same category, then the auction is invalid.
+ * Because it means that an owner still exists. If it is known that the name has been abandoned for > `inactivityExpiryTime`
+ * then one must use the `burn` method of the name.cash to burn the internalAuthNFT and externalAuthNFT making the
+ * name to be available for auction.
+ *
+ * Penalizes invalid name registrations by allowing anyone to burn the auctionNFT and claim the funds as a reward.
+ *
+ * @inputs
+ * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract
+ * - Input1: Any input from this contract
+ * - Input2: External Auth NFT from the Name Contract
+ * - Input3: auctionNFT from Registry Contract
+ *
+ * @outputs
+ * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.
+ * - Output1: Input1 back to this contract without any change
+ * - Output2: External Auth NFT back to the Name Contract
+ * - Output3: BCH change/reward to caller
+ */
+ function call(){
+ require(tx.inputs.length == 4, "Transaction: must have exactly 4 inputs");
+ require(tx.outputs.length == 4, "Transaction: must have exactly 4 outputs");
+
+ // This contract can only be used at input1 and it should return the input1 back to itself.
+ require(this.activeInputIndex == 1, "Input 1: ownership guard contract UTXO must be at this index");
+ require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Input 1: locking bytecode must match output 1");
+ // Ensure that no tokenCategory is minted here.
+ require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, "Output 1: must not have any token category (pure BCH only)");
+
+ // This contract can only be used with the 'lockingbytecode' used in the 0th input.
+ // Note: This contract can be used with any contract that fulfills these conditions, and that is fine
+ // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will
+ // be manipulating their own utxos.
+ bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;
+ require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, "Input 3: auction NFT locking bytecode does not match registry input's locking bytecode");
+
+ bytes registryInputCategory = tx.inputs[0].tokenCategory;
+ require(tx.inputs[2].tokenCategory == registryInputCategory, "Input 2: external auth NFT token category prefix must match registry");
+ require(tx.outputs[2].tokenCategory == registryInputCategory, "Output 2: external auth NFT token category prefix must match registry");
+
+ // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory
+ bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);
+ require(auctionCategory == registryInputCategory, "Input 3: auction NFT token category prefix must match registry");
+ // Mutable
+ require(auctionCapability == 0x01, "Input 3: auction NFT capability must be mutable (0x01)");
+
+ // nftCommiment of the externalAuthNFT must stay the same
+ require(tx.inputs[2].nftCommitment == tx.outputs[2].nftCommitment, "Output 2: external auth NFT commitment must match input 2");
+ // Ensure that the externalAuth NFT is used and not the internalAuth NFT.
+ require(tx.inputs[2].nftCommitment == 0x, "Input 2: external auth NFT must have empty commitment");
+
+ // Get the name of the name from the auctionNFT
+ bytes name = tx.inputs[3].nftCommitment.split(20)[1];
+ // Get the name length to generate the complete bytecode of the name contract
+ int nameLength = name.length;
+ // category + name + bytecode.
+ // Note: `inactivityExpiryTime` in the name is already added to the nameContractBytecode in the constructor.
+ bytes constant tld = bytes('.bch');
+ bytes nameBytecode = 0x20 + registryInputCategory + bytes(tld.length) + tld + bytes(nameLength) + name + nameContractBytecode;
+ bytes32 scriptHash = hash256(nameBytecode);
+ bytes35 nameLockingBytecode = new LockingBytecodeP2SH32(scriptHash);
+
+ // Ensure that the externalAuthNFT is coming from the correct Name Contract
+ require(tx.inputs[2].lockingBytecode == nameLockingBytecode, "Input 2: external auth NFT locking bytecode must match name contract");
+ require(tx.outputs[2].lockingBytecode == nameLockingBytecode, "Output 2: external auth NFT locking bytecode must match name contract");
+
+ // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later
+ // and merged back with the CounterNFT using the `Accumulator` Contract
+ require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount, "Output 0: token amount must equal input 0 + input 3 amounts (accumulation)");
+
+ // Reward Output
+ require(tx.outputs[3].tokenCategory == 0x, "Output 3: reward must be pure BCH (no token category)");
+ }
+}
\ No newline at end of file
diff --git a/contracts/Registry.cash b/contracts/Registry.cash
index 58fdc35..dff18b4 100644
--- a/contracts/Registry.cash
+++ b/contracts/Registry.cash
@@ -1,18 +1,18 @@
-pragma cashscript 0.11.2;
+pragma cashscript 0.11.4;
/**
- * @param domainCategory - The category of the domain NFTs that are authorized to be registered. [In reverse order]
+ * @param nameCategory - The category of the name NFTs that are authorized to be registered. [In reverse order]
*
* The Registry has two minting NFTs:
* 1. CounterMintingNFT, has tokenAmount and nftCommitment.
- * 2. DomainMintingNFT, does not have any tokenAmount or nftCommitment.
+ * 2. NameMintingNFT, does not have any tokenAmount or nftCommitment.
*/
-contract Registry(bytes domainCategory) {
+contract Registry(bytes nameCategory) {
/**
* The Registry contract serves as both a source and storage for authorized NFTs.
* It holds: RegistrationNFTs, AuctionNFTs, and AuthorizedThreadNFTs
*
- * AuthorizedThreadNFTs are NFTs with immutable capability that share the same category as domainCategory.
+ * AuthorizedThreadNFTs are NFTs with immutable capability that share the same category as nameCategory.
* These NFTs contain the lockingBytecode of authorized contracts.
* Multiple copies of these NFTs enable parallel processing through multiple threads.
*
@@ -24,15 +24,15 @@ contract Registry(bytes domainCategory) {
* for a given action. To use the code in these authorized contracts, a random UTXO is used and
* sent back to itself to be used again in future.
*
- * All the utxos, except for the DomainNFTs (InternalAuth, ExternalAuth and DomainOwnershipNFT),
+ * All the utxos, except for the NameNFTs (InternalAuth, ExternalAuth and NameOwnershipNFT),
* stay with the Registry contract.
*
* @note Authorized contracts and their thread counts:
* - Auction: [1 thread] (Single-threaded registration)
* - Bid: [~x threads]
- * - DomainFactory: [~x threads]
+ * - NameFactory: [~x threads]
* - AuctionNameEnforcer: [~x threads]
- * - DomainOwnershipGuard: [~x threads]
+ * - NameOwnershipGuard: [~x threads]
* - AuctionConflictResolver: [~x threads]
* - Accumulator: [~x threads]
*
@@ -48,30 +48,32 @@ contract Registry(bytes domainCategory) {
// 1. Since the registry contract is static, version check is required to prevent from any vulnerabilities
// caused due to future versions.
// 2. BitCANN uses relative timelocks, need to enforce version 2.
- require(tx.version == 2);
+ require(tx.version == 2, "Registry: transaction version must be 2 (relative timelocks required)");
// Registry Contract
bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
// authorizedThreadNFT must stay with the Registry Contract.
- require(tx.inputs[0].lockingBytecode == selfLockingBytecode);
- require(tx.outputs[0].lockingBytecode == selfLockingBytecode);
+ require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: locking bytecode must match registry contract");
+ require(tx.outputs[0].lockingBytecode == selfLockingBytecode, "Output 0: locking bytecode must match registry contract");
- // Immutable NFTs of domainCategory in Registry Contract will always be authorizedThreadNFTs
- // Mutable NFTs of domainCategory in Registry Contract will always be auctionNFTs
- // Minting NFTs of domainCategory in Registry Contract will always be counterMintingNFT or DomainMintingNFT
- require(tx.inputs[0].tokenCategory == domainCategory);
- require(tx.outputs[0].tokenCategory == domainCategory);
+ // Immutable NFTs of nameCategory in Registry Contract will always be authorizedThreadNFTs
+ // Mutable NFTs of nameCategory in Registry Contract will always be auctionNFTs
+ // Minting NFTs of nameCategory in Registry Contract will always be counterMintingNFT or NameMintingNFT
+ require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: token category must match name category");
+ require(tx.outputs[0].tokenCategory == nameCategory, "Output 0: token category must match name category");
// Keeping the value same to not influence any satoshi movement in authorized contracts
- require(tx.outputs[0].value == tx.inputs[0].value);
+ require(tx.outputs[0].value == tx.inputs[0].value, "Output 0: satoshi value must match input 0");
// The commitment that has the lockingbytecode of the authorized contract should never change.
- require(tx.outputs[0].nftCommitment == tx.inputs[0].nftCommitment);
+ // It is possible that in other contracts a minting NFT of the nameCategory is used, in that case
+ // it becomes possible to change the nft commitment of 0th output.
+ require(tx.outputs[0].nftCommitment == tx.inputs[0].nftCommitment, "Output 0: NFT commitment must match input 0");
// Not checking the tokenAmount as it changes.
// Authorized Contract
// Expect the NFT commitment that contains the lockingBytecode of the authorized contract.
- require(tx.inputs[1].lockingBytecode == tx.inputs[0].nftCommitment);
+ require(tx.inputs[1].lockingBytecode == tx.inputs[0].nftCommitment, "Input 1: locking bytecode must match authorized contract from input 0 NFT commitment");
// With these prerequisites met, we just need to make sure that all the contracts that are deployed are written
// and initialized properly, and they expect this structure and handle the inputs and outputs as expected.
}
diff --git a/lib/compiled/Accumulator.ts b/lib/compiled/Accumulator.ts
index 9badcea..88d5e84 100644
--- a/lib/compiled/Accumulator.ts
+++ b/lib/compiled/Accumulator.ts
@@ -8,97 +8,117 @@ export default {
},
],
'bytecode': 'OP_TXINPUTCOUNT OP_5 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_5 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY OP_3 OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_UTXOTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_2 OP_EQUALVERIFY OP_3 OP_UTXOTOKENCOMMITMENT OP_SIZE OP_NIP 23 OP_NUMEQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUAL OP_NOT OP_VERIFY OP_2 OP_UTXOTOKENAMOUNT OP_0 OP_GREATERTHAN OP_VERIFY OP_2 OP_OUTPUTTOKENAMOUNT OP_2 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_4 OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_4 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL',
- 'source': "pragma cashscript 0.11.2;\n\ncontract Accumulator() {\n /**\n * Once enough auctions have happened, there might come a time when the counterNFT's tokenAmount is not enough.\n * Since the amount would be accumulating in the thread NFTs, this function can be used to transfer them back to the\n * Counter NFT to keep the system functioning smoothly.\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract\n * - Input2: Minting CounterNFT + tokenAmount from Registry Contract\n * - Input3: authorizedThreadNFT with tokenAmount from Registry Contract\n * - Input4: Pure BCH\n * \n * @outputs\n * - Output0: Registry Contract's thread NFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Minting CounterNFT back to the Registry contract + tokenAmount\n * - Output3: authorizedThreadNFT without tokenAmount back to the Registry contract\n * - Output4: Change BCH\n */\n function call(){\n require(tx.inputs.length == 5);\n require(tx.outputs.length == 5);\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1);\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n // Restriction on output category is important as minting NFT is used in this transaction.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);\n\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);\n require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode);\n\n require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory);\n require(tx.outputs[3].tokenCategory == tx.inputs[3].tokenCategory);\n\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n \n // authorizedThreadNFTs are immutable\n require(tx.inputs[3].tokenCategory == registryInputCategory);\n \n bytes counterCategory, bytes counterCapability = tx.inputs[2].tokenCategory.split(32);\n require(counterCategory == registryInputCategory);\n require(counterCapability == 0x02); // Minting\n\n // Locking bytecode of the authorized contract is 35 bytes long.\n require(tx.inputs[3].nftCommitment.length == 35);\n\n // Since the nftCommitment of counterNFT is registrationID so it must not be null\n // as the DomainMintingNFT has no nftCommitment nor tokenAmount\n require(tx.inputs[2].nftCommitment != 0x);\n require(tx.inputs[2].tokenAmount > 0); // Ensure that the counter minting NFT is used.\n require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount + tx.inputs[3].tokenAmount);\n\n // Pure BCH input and output.\n require(tx.inputs[4].tokenCategory == 0x);\n require(tx.outputs[4].tokenCategory == 0x);\n }\n}",
+ 'source': "pragma cashscript 0.11.4;\n\ncontract Accumulator() {\n /**\n * Once enough auctions have happened, there will come a time when the counterNFT's tokenAmount is not enough.\n * Since the amount would be accumulating in the thread NFTs, this function can be used to transfer them back to the\n * Counter NFT to keep the system functioning smoothly.\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract\n * - Input2: Minting CounterNFT + tokenAmount from Registry Contract\n * - Input3: authorizedThreadNFT with tokenAmount from Registry Contract\n * - Input4: Pure BCH\n * \n * @outputs\n * - Output0: Registry Contract's thread NFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Minting CounterNFT back to the Registry contract + tokenAmount\n * - Output3: authorizedThreadNFT without tokenAmount back to the Registry contract\n * - Output4: Change BCH\n */\n function call(){\n require(tx.inputs.length == 5, \"Transaction: must have exactly 5 inputs\");\n require(tx.outputs.length == 5, \"Transaction: must have exactly 5 outputs\");\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1, \"Input 1: accumulator contract UTXO must be at this index\");\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, \"Input 1: locking bytecode must match output 1\");\n // Restriction on output category is important as minting NFT is used in this transaction.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, \"Output 1: must not have any token category (pure BCH only)\");\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n \n // Enforce input 2 and 3 are from the registry\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, \"Input 2: locking bytecode does not match registry input's locking bytecode\");\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, \"Input 3: locking bytecode does not match registry input's locking bytecode\");\n\n // Enforce output 2 and 3 are returning to the registry\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, \"Output 2: locking bytecode does not match registry input's locking bytecode\");\n require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode, \"Output 3: locking bytecode does not match registry input's locking bytecode\");\n\n // Enforce NFT transfer preserves token categories\n require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory, \"Output 2: token category does not match input 2\");\n require(tx.outputs[3].tokenCategory == tx.inputs[3].tokenCategory, \"Output 3: token category does not match input 3\");\n\n // authorizedThreadNFTs are immutable — must match registry input category\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n require(tx.inputs[3].tokenCategory == registryInputCategory, \"Input 3: token category does not match registry (immutable NFT check)\");\n \n // Split counter token category and capability\n bytes counterCategory, bytes counterCapability = tx.inputs[2].tokenCategory.split(32);\n require(counterCategory == registryInputCategory, \"Input 2: token category prefix does not match registry\");\n // Minting\n require(counterCapability == 0x02, \"Input 2: counter capability must be minting capability (0x02)\");\n\n // Locking bytecode of the authorized contract is 35 bytes long.\n require(tx.inputs[3].nftCommitment.length == 35, \"Input 3: NFT commitment length must be 35 bytes (authorized contract locking bytecode)\");\n\n // Since the nftCommitment of counterNFT is registrationID so it must not be null\n // as the NameMintingNFT has no nftCommitment nor tokenAmount\n require(tx.inputs[2].nftCommitment != 0x, \"Input 2: counter NFT must have a non-empty commitment (registration ID)\");\n // Ensure that the counter minting NFT is used.\n require(tx.inputs[2].tokenAmount > 0, \"Input 2: counter NFT must have token amount greater than 0\");\n require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount + tx.inputs[3].tokenAmount, \"Output 2: token amount must equal input 2 + input 3 amounts (accumulation)\");\n\n // Pure BCH input and output.\n require(tx.inputs[4].tokenCategory == 0x, \"Input 4: must be pure BCH (no token category)\");\n require(tx.outputs[4].tokenCategory == 0x, \"Output 4: must be pure BCH (no token category)\");\n }\n}",
'debug': {
'bytecode': 'c3559dc4559dc0519dc0c7c0cd88c0d1008800c752c7788853c7788852cd788853cd8852d152ce8853d153ce8800ce53ce788852ce01207f7c7b88528853cf827701239d52cf0087916952d000a06952d352d053d0939d54ce008854d10087',
- 'sourceMap': '24:12:24:28;:32::33;:4::35:1;25:12:25:29:0;:33::34;:4::36:1;28:12:28:33:0;:37::38;:4::40:1;29:22:29:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;31:23:31:44:0;:12::59:1;:63::65:0;:4::67:1;37:51:37:52:0;:41::69:1;38:22:38:23:0;:12::40:1;:44::72:0;:4::74:1;39:22:39:23:0;:12::40:1;:44::72:0;:4::74:1;41:23:41:24:0;:12::41:1;:45::73:0;:4::75:1;42:23:42:24:0;:12::41:1;:4::75;44:23:44:24:0;:12::39:1;:53::54:0;:43::69:1;:4::71;45:23:45:24:0;:12::39:1;:53::54:0;:43::69:1;:4::71;47:44:47:45:0;:34::60:1;50:22:50:23:0;:12::38:1;:42::63:0;:4::65:1;52:63:52:64:0;:53::79:1;:86::88:0;:53::89:1;53:12:53:27:0;:31::52;:4::54:1;54:33:54:37:0;:4::39:1;57:22:57:23:0;:12::38:1;:::45;;:49::51:0;:4::53:1;61:22:61:23:0;:12::38:1;:42::44:0;:12:::1;;:4::46;62:22:62:23:0;:12::36:1;:39::40:0;:12:::1;:4::42;63:23:63:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::94;66:22:66:23:0;:12::38:1;:42::44:0;:4::46:1;67:23:67:24:0;:12::39:1;:43::45:0;:4::47:1',
+ 'sourceMap': '24:12:24:28;:32::33;:4::78:1;25:12:25:29:0;:33::34;:4::80:1;28:12:28:33:0;:37::38;:4::100:1;29:22:29:43:0;:12::60:1;:75::96:0;:64::113:1;:4::164;31:23:31:44:0;:12::59:1;:63::65:0;:4::129:1;37:51:37:52:0;:41::69:1;40:22:40:23:0;:12::40:1;:44::72:0;:4::152:1;41:22:41:23:0;:12::40:1;:44::72:0;:4::152:1;44:23:44:24:0;:12::41:1;:45::73:0;:4::154:1;45:23:45:24:0;:12::41:1;:4::154;48:23:48:24:0;:12::39:1;:53::54:0;:43::69:1;:4::122;49:23:49:24:0;:12::39:1;:53::54:0;:43::69:1;:4::122;52:44:52:45:0;:34::60:1;53:22:53:23:0;:12::38:1;:42::63:0;:4::138:1;56:63:56:64:0;:53::79:1;:86::88:0;:53::89:1;57:12:57:27:0;:31::52;:4::112:1;59:33:59:37:0;:4::104:1;62:22:62:23:0;:12::38:1;:::45;;:49::51:0;:4::143:1;66:22:66:23:0;:12::38:1;:42::44:0;:12:::1;;:4::121;68:22:68:23:0;:12::36:1;:39::40:0;:12:::1;:4::104;69:23:69:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::172;72:22:72:23:0;:12::38:1;:42::44:0;:4::95:1;73:23:73:24:0;:12::39:1;:43::45:0;:4::97:1',
'logs': [],
'requires': [
{
'ip': 2,
'line': 24,
+ 'message': 'Transaction: must have exactly 5 inputs',
},
{
'ip': 5,
'line': 25,
+ 'message': 'Transaction: must have exactly 5 outputs',
},
{
'ip': 8,
'line': 28,
+ 'message': 'Input 1: accumulator contract UTXO must be at this index',
},
{
'ip': 13,
'line': 29,
+ 'message': 'Input 1: locking bytecode must match output 1',
},
{
'ip': 17,
'line': 31,
+ 'message': 'Output 1: must not have any token category (pure BCH only)',
},
{
'ip': 23,
- 'line': 38,
+ 'line': 40,
+ 'message': "Input 2: locking bytecode does not match registry input's locking bytecode",
},
{
'ip': 27,
- 'line': 39,
+ 'line': 41,
+ 'message': "Input 3: locking bytecode does not match registry input's locking bytecode",
},
{
'ip': 31,
- 'line': 41,
+ 'line': 44,
+ 'message': "Output 2: locking bytecode does not match registry input's locking bytecode",
},
{
'ip': 34,
- 'line': 42,
+ 'line': 45,
+ 'message': "Output 3: locking bytecode does not match registry input's locking bytecode",
},
{
'ip': 39,
- 'line': 44,
+ 'line': 48,
+ 'message': 'Output 2: token category does not match input 2',
},
{
'ip': 44,
- 'line': 45,
+ 'line': 49,
+ 'message': 'Output 3: token category does not match input 3',
},
{
'ip': 50,
- 'line': 50,
+ 'line': 53,
+ 'message': 'Input 3: token category does not match registry (immutable NFT check)',
},
{
'ip': 57,
- 'line': 53,
+ 'line': 57,
+ 'message': 'Input 2: token category prefix does not match registry',
},
{
'ip': 59,
- 'line': 54,
+ 'line': 59,
+ 'message': 'Input 2: counter capability must be minting capability (0x02)',
},
{
'ip': 65,
- 'line': 57,
+ 'line': 62,
+ 'message': 'Input 3: NFT commitment length must be 35 bytes (authorized contract locking bytecode)',
},
{
'ip': 71,
- 'line': 61,
+ 'line': 66,
+ 'message': 'Input 2: counter NFT must have a non-empty commitment (registration ID)',
},
{
'ip': 76,
- 'line': 62,
+ 'line': 68,
+ 'message': 'Input 2: counter NFT must have token amount greater than 0',
},
{
'ip': 84,
- 'line': 63,
+ 'line': 69,
+ 'message': 'Output 2: token amount must equal input 2 + input 3 amounts (accumulation)',
},
{
'ip': 88,
- 'line': 66,
+ 'line': 72,
+ 'message': 'Input 4: must be pure BCH (no token category)',
},
{
'ip': 93,
- 'line': 67,
+ 'line': 73,
+ 'message': 'Output 4: must be pure BCH (no token category)',
},
],
},
'compiler': {
'name': 'cashc',
- 'version': '0.11.2',
+ 'version': '0.11.4',
},
- 'updatedAt': '2025-07-15T19:31:10.154Z',
+ 'updatedAt': '2025-08-10T17:47:21.270Z',
};
diff --git a/lib/compiled/Auction.ts b/lib/compiled/Auction.ts
index cca4253..d9a09f2 100644
--- a/lib/compiled/Auction.ts
+++ b/lib/compiled/Auction.ts
@@ -1,11 +1,6 @@
export default {
'contractName': 'Auction',
- 'constructorInputs': [
- {
- 'name': 'minStartingBid',
- 'type': 'int',
- },
- ],
+ 'constructorInputs': [],
'abi': [
{
'name': 'call',
@@ -17,106 +12,138 @@ export default {
],
},
],
- 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_6 OP_LESSTHANOREQUAL OP_VERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_REVERSEBYTES OP_BIN2NUM OP_2 OP_OUTPUTTOKENCOMMITMENT OP_REVERSEBYTES OP_BIN2NUM OP_DUP OP_ROT OP_1ADD OP_NUMEQUALVERIFY OP_2 OP_OUTPUTTOKENAMOUNT OP_2 OP_UTXOTOKENAMOUNT OP_2 OP_PICK OP_SUB OP_NUMEQUALVERIFY OP_3 OP_OUTPUTTOKENAMOUNT OP_NUMEQUALVERIFY OP_3 OP_OUTPUTVALUE OP_LESSTHANOREQUAL OP_VERIFY OP_3 OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_SIZE OP_NIP 19 OP_NUMEQUALVERIFY OP_3 OP_UTXOBYTECODE OP_3 OP_SPLIT OP_NIP 14 OP_SPLIT OP_DROP OP_3 OP_OUTPUTTOKENCOMMITMENT OP_SWAP OP_2 OP_PICK OP_CAT OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_2 OP_EQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_4 OP_OUTPUTBYTECODE 6a OP_ROT OP_SIZE OP_DUP 4b OP_GREATERTHAN OP_IF 4c OP_SWAP OP_CAT OP_ENDIF OP_SWAP OP_CAT OP_CAT OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_6 OP_NUMEQUAL OP_IF OP_5 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_1',
- 'source': "pragma cashscript 0.11.2;\n\n/**\n * @param minStartingBid The minimum starting bid for the auction.\n */\ncontract Auction(int minStartingBid) {\n /**\n * Starts a new domain registration auction.\n * @param name The name being registered.\n * \n * The function creates a new auction with:\n * - Starting bid >= `minStartingBid` BCH.\n * - A successful registration initiation results in an auctionNFT representing the auction state:\n * - capability: (Mutable)\n * - category: registryInputCategory\n * - tokenAmount: (Represents the registrationId)\n * - satoshiValue: (Represents the bid amount)\n * - commitment: bidder's PKH (20 bytes) + name (bytes)\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: Minting CounterNFT from Registry contract (Increases the registrationId by 1 in the output).\n * - Input3: Funding UTXO.\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Minting CounterNFT going back to the Registry contract.\n * - Output3: auctionNFT to the Registry contract.\n * - Output4: OP_RETURN output containing the name.\n * - Output5: Optional change in BCH.\n */\n function call(bytes name) {\n require(tx.inputs.length == 4);\n require(tx.outputs.length <= 6);\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1);\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n // Ensure that the domainCategory in not minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);\n require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode);\n\n // Registration ID increases by 1 with each transaction.\n int prevRegistrationId = int(tx.inputs[2].nftCommitment.reverse());\n int nextRegistrationId = int(tx.outputs[2].nftCommitment.reverse());\n require(nextRegistrationId == prevRegistrationId + 1);\n\n // Reduce the tokenAmount in the counterNFT as some amount is going to auctionNFT\n require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount - nextRegistrationId);\n // tokenAmount in the auctionNFT is the registrationId.\n require(tx.outputs[3].tokenAmount == nextRegistrationId);\n\n // Every auction begins with a min base value of at least minStartingBid satoshis.\n require(tx.outputs[3].value >= minStartingBid);\n // Funding UTXO/ Bid UTXO\n require(tx.inputs[3].tokenCategory == 0x);\n\n // Ensure that the funding happens from a P2PKH UTXO.\n require(tx.inputs[3].lockingBytecode.length == 25);\n\n // Extract the PKH from the lockingBytecode of the Funding UTXO.\n // + name > 20 bytes\n bytes pkh = tx.inputs[3].lockingBytecode.split(3)[1].split(20)[0];\n require(tx.outputs[3].nftCommitment == pkh + name);\n\n // CounterNFT should keep the same category and capability.\n require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory);\n \n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n \n // CounterNFT should be minting and of the 'domainCategory' i.e registryInputCategory\n bytes counterCategory, bytes counterCapability = tx.outputs[2].tokenCategory.split(32);\n require(counterCategory == registryInputCategory);\n require(counterCapability == 0x02); // Minting\n\n // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.outputs[3].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory);\n require(auctionCapability == 0x01); // Mutable\n\n // Enforce an OP_RETURN output that contains the name.\n require(tx.outputs[4].lockingBytecode == new LockingBytecodeNullData([name]));\n\n if (tx.outputs.length == 6) {\n // If any change, then it must be pure BCH.\n require(tx.outputs[5].tokenCategory == 0x);\n }\n }\n}",
+ 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_5 OP_LESSTHANOREQUAL OP_VERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_BIN2NUM OP_2 OP_OUTPUTTOKENCOMMITMENT OP_BIN2NUM OP_DUP OP_ROT OP_1ADD OP_NUMEQUALVERIFY OP_2 OP_OUTPUTTOKENAMOUNT OP_2 OP_UTXOTOKENAMOUNT OP_2 OP_PICK OP_SUB OP_NUMEQUALVERIFY OP_3 OP_OUTPUTTOKENAMOUNT OP_OVER OP_NUMEQUALVERIFY 1027 OP_DUP OP_ROT OP_MUL OP_3 OP_MUL OP_SWAP 40420f OP_MUL OP_SWAP OP_SUB 40420f OP_DIV OP_DUP 204e OP_MAX OP_3 OP_OUTPUTVALUE OP_LESSTHANOREQUAL OP_VERIFY OP_3 OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_SIZE OP_NIP 19 OP_NUMEQUALVERIFY OP_3 OP_UTXOBYTECODE OP_3 OP_SPLIT OP_SWAP 76a914 OP_EQUALVERIFY 14 OP_SPLIT 88ac OP_EQUALVERIFY OP_3 OP_OUTPUTTOKENCOMMITMENT OP_SWAP OP_3 OP_PICK OP_CAT OP_EQUALVERIFY OP_SWAP OP_SIZE OP_NIP OP_16 OP_LESSTHANOREQUAL OP_VERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_2 OP_EQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_5 OP_NUMEQUAL OP_IF OP_4 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_DROP OP_1',
+ 'source': "pragma cashscript 0.11.4;\n\ncontract Auction() {\n /**\n * Starts a new name registration auction.\n * @param name The name being registered.\n * \n * The function creates a new auction with:\n * - Starting bid >= `minStartingBid` BCH.\n * - A successful registration initiation results in an auctionNFT representing the auction state:\n * - capability: (Mutable)\n * - category: registryInputCategory\n * - tokenAmount: (Represents the registrationId)\n * - satoshiValue: (Represents the bid amount)\n * - commitment: bidder's PKH (20 bytes) + name (bytes)\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: Minting CounterNFT from Registry contract (Increases the registrationId by 1 in the output).\n * - Input3: Funding UTXO.\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Minting CounterNFT going back to the Registry contract.\n * - Output3: auctionNFT to the Registry contract.\n * - Output4: Optional change in BCH.\n */\n function call(bytes name) {\n require(tx.inputs.length == 4, \"Transaction: must have exactly 4 inputs\");\n require(tx.outputs.length <= 5, \"Transaction: must have at most 5 outputs\");\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1, \"Input 1: auction contract UTXO must be at this index\");\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, \"Input 1: locking bytecode must match output 1\");\n // Ensure that no tokenCategory is minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, \"Output 1: must not have any token category (pure BCH only)\");\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, \"Input 2: locking bytecode does not match registry input's locking bytecode\");\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, \"Output 2: locking bytecode does not match registry input's locking bytecode\");\n require(tx.outputs[3].lockingBytecode == registryInputLockingBytecode, \"Output 3: locking bytecode does not match registry input's locking bytecode\");\n\n // Registration ID increases by 1 with each transaction.\n int prevRegistrationId = int(tx.inputs[2].nftCommitment);\n int currentRegistrationId = int(tx.outputs[2].nftCommitment);\n require(currentRegistrationId == prevRegistrationId + 1, \"Output 2: registration ID must increase by 1\");\n\n // Reduce the tokenAmount in the counterNFT as some amount is going to auctionNFT\n require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount - currentRegistrationId, \"Output 2: counter NFT token amount must decrease by currentRegistrationId\");\n // tokenAmount in the auctionNFT is the registrationId.\n require(tx.outputs[3].tokenAmount == currentRegistrationId, \"Output 3: auction NFT token amount must equal currentRegistrationId\");\n\n // Dual Decay mechanism, auction price decays linearly with the step.\n // To facilitate higher precisions and since decimals do not exist in VM, we multiply\n // it by 1e6 (1000000) and call the units as points.\n\n // TODO: make this 1000000 (0.01 BCH)\n int constant minStartingBid = 10000;\n // 1. Decay points (0.0003% per step)\n int decayPoints = minStartingBid * currentRegistrationId * 3;\n // 2. Get auction price points\n int currentPricePoints = minStartingBid * 1e6;\n // 3. Subtract price points by decay points to get the current auction price.\n int currentAuctionPrice = (currentPricePoints - decayPoints) / 1e6;\n\n // Set the minimum auction price to 20000 satoshis.\n currentAuctionPrice = max(currentAuctionPrice, 20000);\n\n // Every auction begins with a min base value of at least currentAuctionPrice satoshis.\n require(tx.outputs[3].value >= currentAuctionPrice, \"Output 3: auction price must be at least minimum calculated price\");\n // Funding UTXO/ Bid UTXO\n require(tx.inputs[3].tokenCategory == 0x, \"Input 3: funding UTXO must be pure BCH\");\n\n // Ensure that the funding happens from a P2PKH UTXO because there will be no way to know the locking bytecode as \n // name can be of any length.\n require(tx.inputs[3].lockingBytecode.length == 25, \"Input 3: locking bytecode must be 25 bytes (P2PKH)\");\n\n bytes pkhLockingBytecodeHead, bytes pkhLockingBytecodeBody = tx.inputs[3].lockingBytecode.split(3);\n // OP_DUP OP_HASH160 Push 20-byte\n require(pkhLockingBytecodeHead == 0x76a914, \"Input 3: locking bytecode must start with OP_DUP OP_HASH160 (0x76a914)\");\n bytes pkh, bytes pkhLockingBytecodeTail = pkhLockingBytecodeBody.split(20);\n // OP_EQUALVERIFY OP_CHECKSIG\n require(pkhLockingBytecodeTail == 0x88ac, \"Input 3: locking bytecode must end with OP_EQUALVERIFY OP_CHECKSIG (0x88ac)\");\n require(tx.outputs[3].nftCommitment == pkh + name, \"Output 3: NFT commitment must match bidder PKH + name\");\n\n // Ensure that the name is not too long, as of 2025 upgrade, the nftcommitment is 40 bytes.\n // 20 bytes pkh + 16 bytes name + 4 bytes TLD\n require(name.length <= 16, \"Name: length must be at most 16 characters\");\n\n // CounterNFT should keep the same category and capability.\n require(tx.outputs[2].tokenCategory == tx.inputs[2].tokenCategory, \"Output 2: counter NFT token category must match input 2\");\n \n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n \n // CounterNFT should be minting and of the 'nameCategory' i.e registryInputCategory\n bytes counterCategory, bytes counterCapability = tx.outputs[2].tokenCategory.split(32);\n require(counterCategory == registryInputCategory, \"Output 2: counter NFT token category prefix must match registry\");\n // Minting\n require(counterCapability == 0x02, \"Output 2: counter NFT capability must be minting (0x02)\");\n\n // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.outputs[3].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory, \"Output 3: auction NFT token category prefix must match registry\");\n // Mutable\n require(auctionCapability == 0x01, \"Output 3: auction NFT capability must be mutable (0x01)\");\n\n if (tx.outputs.length == 5) {\n // If any change, then it must be pure BCH.\n require(tx.outputs[4].tokenCategory == 0x, \"Output 4: change must be pure BCH (no token category)\");\n }\n }\n}",
'debug': {
- 'bytecode': 'c3549dc456a169c0519dc0c7c0cd88c0d1008800c752c7788852cd788853cd8852cfbc8152d2bc81767b8b9d52d352d05279949d53d39d53cca16953ce008853c7827701199d53c7537f7701147f7553d27c52797e8852d152ce8800ce52d101207f7c527988528853d101207f7c7b88518854cd016a7b8276014ba063014c7c7e687c7e7e88c4569c6355d100886851',
- 'sourceMap': '35:12:35:28;:32::33;:4::35:1;36:12:36:29:0;:33::34;:12:::1;:4::36;39:12:39:33:0;:37::38;:4::40:1;40:22:40:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;42:23:42:44:0;:12::59:1;:63::65:0;:4::67:1;48:51:48:52:0;:41::69:1;49:22:49:23:0;:12::40:1;:44::72:0;:4::74:1;50:23:50:24:0;:12::41:1;:45::73:0;:4::75:1;51:23:51:24:0;:12::41:1;:4::75;54:43:54:44:0;:33::59:1;:::69;:29::70;55:44:55:45:0;:33::60:1;:::70;:29::71;56:12:56:30:0;:34::52;:::56:1;:4::58;59:23:59:24:0;:12::37:1;:51::52:0;:41::65:1;:68::86:0;;:41:::1;:4::88;61:23:61:24:0;:12::37:1;:4::61;64:23:64:24:0;:12::31:1;:::49;:4::51;66:22:66:23:0;:12::38:1;:42::44:0;:4::46:1;69:22:69:23:0;:12::40:1;:::47;;:51::53:0;:4::55:1;73:26:73:27:0;:16::44:1;:51::52:0;:16::53:1;:::56;:63::65:0;:16::66:1;:::69;74:23:74:24:0;:12::39:1;:43::46:0;:49::53;;:43:::1;:4::55;77:23:77:24:0;:12::39:1;:53::54:0;:43::69:1;:4::71;80:44:80:45:0;:34::60:1;83:64:83:65:0;:53::80:1;:87::89:0;:53::90:1;84:12:84:27:0;:31::52;;:4::54:1;85:33:85:37:0;:4::39:1;88:64:88:65:0;:53::80:1;:87::89:0;:53::90:1;89:12:89:27:0;:31::52;:4::54:1;90:33:90:37:0;:4::39:1;93:23:93:24:0;:12::41:1;:45::80:0;:74::78;::::1;;;;;;;;;;;;:4::82;95:8:95:25:0;:29::30;:8:::1;:32:98:5:0;97:25:97:26;:14::41:1;:45::47:0;:6::49:1;95:32:98:5;34:2:99:3',
+ 'bytecode': 'c3549dc455a169c0519dc0c7c0cd88c0d1008800c752c7788852cd788853cd8852cf8152d281767b8b9d52d352d05279949d53d3789d021027767b9553957c0340420f957c940340420f967602204ea453cca16953ce008853c7827701199d53c7537f7c0376a9148801147f0288ac8853d27c53797e887c827760a16952d152ce8800ce52d101207f7c527988528853d101207f7c7b885188c4559c6354d10088687551',
+ 'sourceMap': '31:12:31:28;:32::33;:4::78:1;32:12:32:29:0;:33::34;:12:::1;:4::80;35:12:35:33:0;:37::38;:4::96:1;36:22:36:43:0;:12::60:1;:75::96:0;:64::113:1;:4::164;38:23:38:44:0;:12::59:1;:63::65:0;:4::129:1;44:51:44:52:0;:41::69:1;45:22:45:23:0;:12::40:1;:44::72:0;:4::152:1;46:23:46:24:0;:12::41:1;:45::73:0;:4::154:1;47:23:47:24:0;:12::41:1;:4::154;50:43:50:44:0;:33::59:1;:29::60;51:47:51:48:0;:36::63:1;:32::64;52:12:52:33:0;:37::55;:::59:1;:4::109;55:23:55:24:0;:12::37:1;:51::52:0;:41::65:1;:68::89:0;;:41:::1;:4::168;57:23:57:24:0;:12::37:1;:41::62:0;:4::135:1;64:34:64:39:0;66:22:66:36;:39::60;:22:::1;:63::64:0;:22:::1;68:29:68:43:0;:46::49;:29:::1;70:52:70:63:0;:31:::1;:67::70:0;:30:::1;73::73:49:0;:51::56;:26::57:1;76:23:76:24:0;:12::31:1;:::54;:4::125;78:22:78:23:0;:12::38:1;:42::44:0;:4::88:1;82:22:82:23:0;:12::40:1;:::47;;:51::53:0;:4::109:1;84:75:84:76:0;:65::93:1;:100::101:0;:65::102:1;86:12:86:34:0;:38::46;:4::122:1;87:75:87:77:0;:46::78:1;89:38:89:44:0;:4::125:1;90:23:90:24:0;:12::39:1;:43::46:0;:49::53;;:43:::1;:4::112;94:12:94:16:0;:::23:1;;:27::29:0;:12:::1;:4::77;97:23:97:24:0;:12::39:1;:53::54:0;:43::69:1;:4::130;100:44:100:45:0;:34::60:1;103:64:103:65:0;:53::80:1;:87::89:0;:53::90:1;104:12:104:27:0;:31::52;;:4::121:1;106:33:106:37:0;:4::98:1;109:64:109:65:0;:53::80:1;:87::89:0;:53::90:1;110:12:110:27:0;:31::52;:4::121:1;112:33:112:37:0;:4::98:1;114:8:114:25:0;:29::30;:8:::1;:32:117:5:0;116:25:116:26;:14::41:1;:45::47:0;:6::106:1;114:32:117:5;30:2:118:3;',
'logs': [],
'requires': [
{
- 'ip': 3,
+ 'ip': 2,
+ 'line': 31,
+ 'message': 'Transaction: must have exactly 4 inputs',
+ },
+ {
+ 'ip': 6,
+ 'line': 32,
+ 'message': 'Transaction: must have at most 5 outputs',
+ },
+ {
+ 'ip': 9,
'line': 35,
+ 'message': 'Input 1: auction contract UTXO must be at this index',
},
{
- 'ip': 7,
+ 'ip': 14,
'line': 36,
+ 'message': 'Input 1: locking bytecode must match output 1',
},
{
- 'ip': 10,
- 'line': 39,
+ 'ip': 18,
+ 'line': 38,
+ 'message': 'Output 1: must not have any token category (pure BCH only)',
},
{
- 'ip': 15,
- 'line': 40,
+ 'ip': 24,
+ 'line': 45,
+ 'message': "Input 2: locking bytecode does not match registry input's locking bytecode",
},
{
- 'ip': 19,
- 'line': 42,
+ 'ip': 28,
+ 'line': 46,
+ 'message': "Output 2: locking bytecode does not match registry input's locking bytecode",
},
{
- 'ip': 25,
- 'line': 49,
+ 'ip': 31,
+ 'line': 47,
+ 'message': "Output 3: locking bytecode does not match registry input's locking bytecode",
},
{
- 'ip': 29,
- 'line': 50,
+ 'ip': 41,
+ 'line': 52,
+ 'message': 'Output 2: registration ID must increase by 1',
},
{
- 'ip': 32,
- 'line': 51,
+ 'ip': 49,
+ 'line': 55,
+ 'message': 'Output 2: counter NFT token amount must decrease by currentRegistrationId',
},
{
- 'ip': 44,
- 'line': 56,
+ 'ip': 53,
+ 'line': 57,
+ 'message': 'Output 3: auction NFT token amount must equal currentRegistrationId',
},
{
- 'ip': 52,
- 'line': 59,
+ 'ip': 73,
+ 'line': 76,
+ 'message': 'Output 3: auction price must be at least minimum calculated price',
},
{
- 'ip': 55,
- 'line': 61,
+ 'ip': 77,
+ 'line': 78,
+ 'message': 'Input 3: funding UTXO must be pure BCH',
},
{
- 'ip': 59,
- 'line': 64,
+ 'ip': 83,
+ 'line': 82,
+ 'message': 'Input 3: locking bytecode must be 25 bytes (P2PKH)',
},
{
- 'ip': 63,
- 'line': 66,
+ 'ip': 90,
+ 'line': 86,
+ 'message': 'Input 3: locking bytecode must start with OP_DUP OP_HASH160 (0x76a914)',
},
{
- 'ip': 69,
- 'line': 69,
+ 'ip': 94,
+ 'line': 89,
+ 'message': 'Input 3: locking bytecode must end with OP_EQUALVERIFY OP_CHECKSIG (0x88ac)',
},
{
- 'ip': 84,
- 'line': 74,
+ 'ip': 101,
+ 'line': 90,
+ 'message': 'Output 3: NFT commitment must match bidder PKH + name',
},
{
- 'ip': 89,
- 'line': 77,
+ 'ip': 107,
+ 'line': 94,
+ 'message': 'Name: length must be at most 16 characters',
},
{
- 'ip': 99,
- 'line': 84,
+ 'ip': 112,
+ 'line': 97,
+ 'message': 'Output 2: counter NFT token category must match input 2',
},
{
- 'ip': 101,
- 'line': 85,
+ 'ip': 122,
+ 'line': 104,
+ 'message': 'Output 2: counter NFT token category prefix must match registry',
},
{
- 'ip': 108,
- 'line': 89,
+ 'ip': 124,
+ 'line': 106,
+ 'message': 'Output 2: counter NFT capability must be minting (0x02)',
},
{
- 'ip': 110,
- 'line': 90,
+ 'ip': 131,
+ 'line': 110,
+ 'message': 'Output 3: auction NFT token category prefix must match registry',
},
{
- 'ip': 127,
- 'line': 93,
+ 'ip': 133,
+ 'line': 112,
+ 'message': 'Output 3: auction NFT capability must be mutable (0x01)',
},
{
- 'ip': 135,
- 'line': 97,
+ 'ip': 141,
+ 'line': 116,
+ 'message': 'Output 4: change must be pure BCH (no token category)',
},
],
},
'compiler': {
'name': 'cashc',
- 'version': '0.11.2',
+ 'version': '0.11.4',
},
- 'updatedAt': '2025-07-15T19:31:07.927Z',
+ 'updatedAt': '2025-08-10T17:47:18.633Z',
};
diff --git a/lib/compiled/AuctionConflictResolver.ts b/lib/compiled/AuctionConflictResolver.ts
deleted file mode 100644
index 3b9d29c..0000000
--- a/lib/compiled/AuctionConflictResolver.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-export default {
- 'contractName': 'AuctionConflictResolver',
- 'constructorInputs': [],
- 'abi': [
- {
- 'name': 'call',
- 'inputs': [],
- },
- ],
- 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_3 OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_3 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_EQUALVERIFY OP_2 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_LESSTHAN OP_VERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL',
- 'source': "pragma cashscript 0.11.2;\n\ncontract AuctionConflictResolver() {\n /**\n * Resolves a conflict between two competing registration auctions for the same name.\n * \n * RULE:\n * - If any new auction is created when an auction already exists, then the new auction is open for penalization.\n *\n * Anyone can provide proof of an active auction's existence and take away the funds from the \"new\" invalid auction\n * as a form of reward for keeping the system secure and predictable.\n * Therefore, it's the responsibility of the application to check for any running auctions for the same name.\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: Valid auctionNFT from Registry Contract.\n * - Input3: Invalid auctionNFT from Registry Contract.\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Valid auctionNFT back to Registry Contract.\n * - Output3: BCH change/reward to caller.\n */\n function call() {\n require(tx.inputs.length == 4);\n require(tx.outputs.length == 4);\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1);\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);\n\n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n\n // auctionNFT should be mutable\n bytes auctionCategory, bytes auctionCapability = tx.inputs[2].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory);\n require(auctionCapability == 0x01); // Mutable\n\n // Invalid and valid auctionNFTs both should have the same category and capability.\n require(tx.inputs[2].tokenCategory == tx.inputs[3].tokenCategory);\n // Both auctionNFTs should also have the same 'name'\n require(tx.inputs[2].nftCommitment.split(20)[1] == tx.inputs[3].nftCommitment.split(20)[1]);\n // The valid auctionNFT will have a lower registrationID\n require(tx.inputs[2].tokenAmount < tx.inputs[3].tokenAmount);\n\n // tokenAmount from the invalid auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount);\n\n // Attach any output to take away the funds as reward\n require(tx.outputs[3].tokenCategory == 0x);\n }\n}",
- 'debug': {
- 'bytecode': 'c3549dc4549dc0519dc0c7c0cd8800c752c7788853c7788852cd8800ce52ce01207f7c7b88518852ce53ce8852cf01147f7753cf01147f778852d053d09f6900d300d053d0939d53d10087',
- 'sourceMap': '27:12:27:28;:32::33;:4::35:1;28:12:28:29:0;:33::34;:4::36:1;31:12:31:33:0;:37::38;:4::40:1;32:22:32:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;38:51:38:52:0;:41::69:1;39:22:39:23:0;:12::40:1;:44::72:0;:4::74:1;40:22:40:23:0;:12::40:1;:44::72:0;:4::74:1;41:23:41:24:0;:12::41:1;:4::75;44:44:44:45:0;:34::60:1;47:63:47:64:0;:53::79:1;:86::88:0;:53::89:1;48:12:48:27:0;:31::52;:4::54:1;49:33:49:37:0;:4::39:1;52:22:52:23:0;:12::38:1;:52::53:0;:42::68:1;:4::70;54:22:54:23:0;:12::38:1;:45::47:0;:12::48:1;:::51;:65::66:0;:55::81:1;:88::90:0;:55::91:1;:::94;:4::96;56:22:56:23:0;:12::36:1;:49::50:0;:39::63:1;:12;:4::65;60:23:60:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::94;63:23:63:24:0;:12::39:1;:43::45:0;:4::47:1',
- 'logs': [],
- 'requires': [
- {
- 'ip': 2,
- 'line': 27,
- },
- {
- 'ip': 5,
- 'line': 28,
- },
- {
- 'ip': 8,
- 'line': 31,
- },
- {
- 'ip': 13,
- 'line': 32,
- },
- {
- 'ip': 19,
- 'line': 39,
- },
- {
- 'ip': 23,
- 'line': 40,
- },
- {
- 'ip': 26,
- 'line': 41,
- },
- {
- 'ip': 35,
- 'line': 48,
- },
- {
- 'ip': 37,
- 'line': 49,
- },
- {
- 'ip': 42,
- 'line': 52,
- },
- {
- 'ip': 53,
- 'line': 54,
- },
- {
- 'ip': 59,
- 'line': 56,
- },
- {
- 'ip': 67,
- 'line': 60,
- },
- {
- 'ip': 72,
- 'line': 63,
- },
- ],
- },
- 'compiler': {
- 'name': 'cashc',
- 'version': '0.11.2',
- },
- 'updatedAt': '2025-07-15T19:31:09.830Z',
-};
diff --git a/lib/compiled/AuctionNameEnforcer.ts b/lib/compiled/AuctionNameEnforcer.ts
deleted file mode 100644
index 4eb28c5..0000000
--- a/lib/compiled/AuctionNameEnforcer.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-export default {
- 'contractName': 'AuctionNameEnforcer',
- 'constructorInputs': [],
- 'abi': [
- {
- 'name': 'call',
- 'inputs': [
- {
- 'name': 'characterNumber',
- 'type': 'int',
- },
- ],
- },
- ],
- 'bytecode': 'OP_TXINPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_OVER OP_SPLIT OP_DROP OP_OVER OP_1SUB OP_SPLIT OP_NIP OP_BIN2NUM OP_DUP 2d OP_NUMNOTEQUAL OP_VERIFY OP_DUP 61 7b OP_WITHIN OP_NOT OP_VERIFY OP_DUP 41 5b OP_WITHIN OP_NOT OP_VERIFY 30 3a OP_WITHIN OP_NOT OP_VERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_2 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NIP',
- 'source': "pragma cashscript 0.11.2;\n\ncontract AuctionNameEnforcer() {\n /**\n * Proves that a domain name contains invalid characters, burns the auctionNFT, and takes away the funds as a reward.\n * During the entire auction, this can be called at any time by anyone.\n * \n * Rules:\n * 1. The name must consist of only these characters:\n * - Letters (a-z or A-Z)\n * - Numbers (0-9)\n * - Hyphens (-)\n *\n * @param characterNumber - Number of the character in the name that is invalid (starting from 1)\n *\n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: auctionNFT from Registry Contract.\n *\n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Reward to caller.\n *\n */\n function call(int characterNumber) {\n require(tx.inputs.length == 3);\n require(tx.outputs.length == 3);\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1);\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n\n // Lock this contract to only be used with the registry type contract.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);\n\n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n\n // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.inputs[2].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory);\n require(auctionCapability == 0x01); // Mutable\n\n bytes name = tx.inputs[2].nftCommitment.split(20)[1];\n bytes characterSplitBytes = name.split(characterNumber)[0];\n characterNumber = characterNumber - 1;\n bytes character = characterSplitBytes.split(characterNumber)[1];\n int charVal = int(character);\n\n // Character is not a hyphen.\n require(charVal != 45); \n // Character is not from a-z.\n require(!within(charVal, 97, 123));\n // Character is not from A-Z.\n require(!within(charVal, 65, 91));\n // Character is not from 0-9.\n require(!within(charVal, 48, 58));\n\n // tokenAmount from the invalid auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[2].tokenAmount);\n\n // Pure BCH.\n require(tx.outputs[2].tokenCategory == 0x);\n }\n}",
- 'debug': {
- 'bytecode': 'c3539dc4539dc0519dc0c7c0cd8800c752c78800ce52ce01207f7c7b88518852cf01147f77787f75788c7f778176012d9e69760161017ba59169760141015ba591690130013aa5916900d300d052d0939d52d1008777',
- 'sourceMap': '28:12:28:28;:32::33;:4::35:1;29:12:29:29:0;:33::34;:4::36:1;32:12:32:33:0;:37::38;:4::40:1;33:22:33:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;36:51:36:52:0;:41::69:1;37:22:37:23:0;:12::40:1;:4::74;40:44:40:45:0;:34::60:1;43:63:43:64:0;:53::79:1;:86::88:0;:53::89:1;44:12:44:27:0;:31::52;:4::54:1;45:33:45:37:0;:4::39:1;47:27:47:28:0;:17::43:1;:50::52:0;:17::53:1;:::56;48:43:48:58:0;:32::59:1;:::62;49:22:49:37:0;:::41:1;50::50:64;:::67;51:18:51:32;54:12:54:19:0;:23::25;:12:::1;:4::27;56:20:56::0;:29::31;:33::36;:13::37:1;:12;:4::39;58:20:58:27:0;:29::31;:33::35;:13::36:1;:12;:4::38;60:29:60:31:0;:33::35;:13::36:1;:12;:4::38;64:23:64:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::94;67:23:67:24:0;:12::39:1;:43::45:0;:4::47:1;27:2:68:3',
- 'logs': [],
- 'requires': [
- {
- 'ip': 2,
- 'line': 28,
- },
- {
- 'ip': 5,
- 'line': 29,
- },
- {
- 'ip': 8,
- 'line': 32,
- },
- {
- 'ip': 13,
- 'line': 33,
- },
- {
- 'ip': 18,
- 'line': 37,
- },
- {
- 'ip': 27,
- 'line': 44,
- },
- {
- 'ip': 29,
- 'line': 45,
- },
- {
- 'ip': 46,
- 'line': 54,
- },
- {
- 'ip': 52,
- 'line': 56,
- },
- {
- 'ip': 58,
- 'line': 58,
- },
- {
- 'ip': 63,
- 'line': 60,
- },
- {
- 'ip': 71,
- 'line': 64,
- },
- {
- 'ip': 76,
- 'line': 67,
- },
- ],
- },
- 'compiler': {
- 'name': 'cashc',
- 'version': '0.11.2',
- },
- 'updatedAt': '2025-07-15T19:31:09.230Z',
-};
diff --git a/lib/compiled/Bid.ts b/lib/compiled/Bid.ts
index 214f17b..e2d9e74 100644
--- a/lib/compiled/Bid.ts
+++ b/lib/compiled/Bid.ts
@@ -1,93 +1,124 @@
export default {
'contractName': 'Bid',
- 'constructorInputs': [
- {
- 'name': 'minBidIncreasePercentage',
- 'type': 'int',
- },
- ],
+ 'constructorInputs': [],
'abi': [
{
'name': 'call',
'inputs': [],
},
],
- 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_5 OP_LESSTHANOREQUAL OP_VERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_SIZE OP_NIP 19 OP_NUMEQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_3 OP_UTXOBYTECODE OP_3 OP_SPLIT OP_NIP 14 OP_SPLIT OP_DROP OP_2 OP_OUTPUTTOKENCOMMITMENT OP_SWAP OP_ROT OP_CAT OP_EQUALVERIFY OP_2 OP_UTXOTOKENAMOUNT OP_2 OP_OUTPUTTOKENAMOUNT OP_NUMEQUALVERIFY OP_2 OP_OUTPUTVALUE 64 OP_MUL OP_2 OP_UTXOVALUE 64 OP_4 OP_ROLL OP_ADD OP_MUL OP_GREATERTHANOREQUAL OP_VERIFY OP_3 OP_OUTPUTBYTECODE 76a914 OP_ROT OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_3 OP_OUTPUTVALUE OP_2 OP_UTXOVALUE OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_5 OP_NUMEQUAL OP_IF OP_4 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_1',
- 'source': "pragma cashscript 0.11.2;\n\n/**\n * @param minBidIncreasePercentage The minimum percentage increase required for a new bid over the previous bid.\n */\ncontract Bid(int minBidIncreasePercentage) {\n /**\n * Places a new bid on an active domain registration auction.\n * \n * The function allows placing a new bid with:\n * - A minimum `minBidIncreasePercentage` increase over the previous bid.\n * - The previous bidder receives their bid amount back in the same transaction.\n * - A successful bid updates the auctionNFT by updating the PKH in the nftCommitment and satoshiValue.\n * capability: Mutable\n * category: registryInputCategory\n * tokenAmount: Represents the registrationId\n * satoshiValue: Represents the bid amount\n * commitment: new Bidder's PKH (20 bytes) + name (bytes)\n *\n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: auctionNFT from the Registry contract.\n * - Input3: Funding UTXO from the new bidder.\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Updated auctionNFT back to the Registry contract.\n * - Output3: Previous bid amount to the previous bidder.\n * - Output4: Optional change in BCH to the new bidder.\n */\n function call() {\n require(tx.inputs.length == 4);\n require(tx.outputs.length <= 5);\n \n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1);\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode); \n\n // AuctionNFT should keep the same category and capability.\n require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory);\n\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n // The second part of the pair changes with each new bid, hence it's marked as mutable.\n // Enforcing the structure of the pair results in predictable behavior.\n bytes auctionCategory, bytes auctionCapability = tx.outputs[2].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory);\n require(auctionCapability == 0x01); // Mutable\n\n // Ensure that the funding happens from a P2PKH UTXO.\n require(tx.inputs[3].lockingBytecode.length == 25);\n\n bytes20 previousPKH, bytes name = tx.inputs[2].nftCommitment.split(20);\n // Extract the PKH from the lockingBytecode of the Funding UTXO.\n // + name > 20 bytes\n bytes pkh = tx.inputs[3].lockingBytecode.split(3)[1].split(20)[0];\n \n // AuctionNFT should have updated PKH in it's commitment.\n require(tx.outputs[2].nftCommitment == pkh + name);\n\n // Since tokenAmount is registrationID, make sure that it's not changing.\n require(tx.inputs[2].tokenAmount == tx.outputs[2].tokenAmount);\n\n // Ensure that the bid amount is greater than or equal to the previous bid amount + minBidIncreasePercentage.\n require(tx.outputs[2].value * 100 >= tx.inputs[2].value * (100 + minBidIncreasePercentage));\n\n // Locking bytecode of the previous bidder.\n require(tx.outputs[3].lockingBytecode == new LockingBytecodeP2PKH(previousPKH));\n // The amount being sent back to the previous bidder.\n require(tx.outputs[3].value == tx.inputs[2].value);\n\n if (tx.outputs.length == 5) {\n // If any change, then it must be pure BCH.\n require(tx.outputs[4].tokenCategory == 0x);\n }\n }\n}",
+ 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_5 OP_LESSTHANOREQUAL OP_VERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_SIZE OP_NIP 19 OP_NUMEQUALVERIFY OP_3 OP_UTXOBYTECODE OP_3 OP_SPLIT OP_SWAP 76a914 OP_EQUALVERIFY 14 OP_SPLIT 88ac OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_2 OP_OUTPUTTOKENCOMMITMENT OP_3 OP_ROLL OP_ROT OP_CAT OP_EQUALVERIFY OP_2 OP_UTXOTOKENAMOUNT OP_2 OP_OUTPUTTOKENAMOUNT OP_NUMEQUALVERIFY OP_2 OP_OUTPUTVALUE 64 OP_MUL OP_2 OP_UTXOVALUE 69 OP_MUL OP_GREATERTHANOREQUAL OP_VERIFY OP_3 OP_OUTPUTBYTECODE 76a914 OP_ROT OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_3 OP_OUTPUTVALUE OP_2 OP_UTXOVALUE OP_NUMEQUALVERIFY OP_3 OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_5 OP_NUMEQUAL OP_IF OP_4 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_1',
+ 'source': "pragma cashscript 0.11.4;\n\n\ncontract Bid() {\n /**\n * Places a new bid on an active name registration auction.\n * \n * The function allows placing a new bid with:\n * - A minimum 5% increase over the previous bid.\n * - The previous bidder receives their bid amount back in the same transaction.\n * - A successful bid updates the auctionNFT by updating the PKH in the nftCommitment and satoshiValue.\n * capability: Mutable\n * category: registryInputCategory\n * tokenAmount: Represents the registrationId\n * satoshiValue: Represents the bid amount\n * commitment: new Bidder's PKH (20 bytes) + name (bytes)\n *\n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: auctionNFT from the Registry contract.\n * - Input3: Funding UTXO from the new bidder.\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Updated auctionNFT back to the Registry contract.\n * - Output3: Previous bid amount to the previous bidder.\n * - Output4: Optional change in BCH to the new bidder.\n */\n function call() {\n require(tx.inputs.length == 4, \"Transaction: must have exactly 4 inputs\");\n require(tx.outputs.length <= 5, \"Transaction: must have at most 5 outputs\");\n \n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1, \"Input 1: bid contract UTXO must be at this index\");\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, \"Input 1: locking bytecode must match output 1\");\n // Ensure that no tokenCategory is minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, \"Output 1: must not have any token category (pure BCH only)\");\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, \"Input 2: auction NFT locking bytecode does not match registry input's locking bytecode\");\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, \"Output 2: auction NFT locking bytecode does not match registry input's locking bytecode\"); \n\n // AuctionNFT should keep the same category and capability.\n require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory, \"Output 2: auction NFT token category must match input 2\");\n\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n // The second part of the pair changes with each new bid, hence it's marked as mutable.\n // Enforcing the structure of the pair results in predictable behavior.\n bytes auctionCategory, bytes auctionCapability = tx.outputs[2].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory, \"Output 2: auction NFT token category prefix must match registry\");\n require(auctionCapability == 0x01, \"Output 2: auction NFT capability must be mutable (0x01)\");\n\n // Ensure that the funding happens from a P2PKH UTXO, need to do a payout to present bidder and the information is stored in commitment.\n require(tx.inputs[3].lockingBytecode.length == 25, \"Input 3: locking bytecode must be 25 bytes (P2PKH)\");\n bytes pkhLockingBytecodeHead, bytes pkhLockingBytecodeBody = tx.inputs[3].lockingBytecode.split(3);\n // OP_DUP OP_HASH160 Push 20-byte\n require(pkhLockingBytecodeHead == 0x76a914, \"Input 3: locking bytecode must start with OP_DUP OP_HASH160 (0x76a914)\");\n bytes pkh, bytes pkhLockingBytecodeTail = pkhLockingBytecodeBody.split(20);\n // OP_EQUALVERIFY OP_CHECKSIG\n require(pkhLockingBytecodeTail == 0x88ac, \"Input 3: locking bytecode must end with OP_EQUALVERIFY OP_CHECKSIG (0x88ac)\");\n\n bytes20 previousPKH, bytes name = tx.inputs[2].nftCommitment.split(20);\n\n // AuctionNFT should have updated PKH in it's commitment.\n require(tx.outputs[2].nftCommitment == pkh + name, \"Output 2: auction NFT commitment must match new bidder PKH + name\");\n\n // Since tokenAmount is registrationID, make sure that it's not changing.\n require(tx.inputs[2].tokenAmount == tx.outputs[2].tokenAmount, \"Output 2: auction NFT token amount must match input 2\");\n\n // Ensure that the bid amount is greater than or equal to the previous bid amount + 5%.\n require(tx.outputs[2].value * 100 >= tx.inputs[2].value * 105, \"Output 2: bid amount must be at least 5 percentage higher\");\n\n // Locking bytecode of the previous bidder.\n require(tx.outputs[3].lockingBytecode == new LockingBytecodeP2PKH(previousPKH), \"Output 3: previous bidder locking bytecode must match previous PKH\");\n // The amount being sent back to the previous bidder.\n require(tx.outputs[3].value == tx.inputs[2].value, \"Output 3: previous bidder refund amount must match previous bid amount\");\n // Funding UTXO/ Bid UTXO\n require(tx.inputs[3].tokenCategory == 0x, \"Input 3: funding UTXO must be pure BCH\");\n\n if (tx.outputs.length == 5) {\n // If any change, then it must be pure BCH.\n require(tx.outputs[4].tokenCategory == 0x, \"Output 4: change must be pure BCH (no token category)\");\n }\n }\n}",
'debug': {
- 'bytecode': 'c3549dc455a169c0519dc0c7c0cd8800c752c7788852cd8852ce52d18800ce52d101207f7c7b88518853c7827701199d52cf01147f53c7537f7701147f7552d27c7b7e8852d052d39d52cc01649552c60164547a9395a26953cd0376a9147b7e0288ac7e8853cc52c69dc4559c6354d100886851',
- 'sourceMap': '34:12:34:28;:32::33;:4::35:1;35:12:35:29:0;:33::34;:12:::1;:4::36;38:12:38:33:0;:37::38;:4::40:1;39:22:39:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;45:51:45:52:0;:41::69:1;46:22:46:23:0;:12::40:1;:44::72:0;:4::74:1;47:23:47:24:0;:12::41:1;:4::75;50:22:50:23:0;:12::38:1;:53::54:0;:42::69:1;:4::71;52:44:52:45:0;:34::60:1;55:64:55:65:0;:53::80:1;:87::89:0;:53::90:1;56:12:56:27:0;:31::52;:4::54:1;57:33:57:37:0;:4::39:1;60:22:60:23:0;:12::40:1;:::47;;:51::53:0;:4::55:1;62:48:62:49:0;:38::64:1;:71::73:0;:38::74:1;65:26:65:27:0;:16::44:1;:51::52:0;:16::53:1;:::56;:63::65:0;:16::66:1;:::69;68:23:68:24:0;:12::39:1;:43::46:0;:49::53;:43:::1;:4::55;71:22:71:23:0;:12::36:1;:51::52:0;:40::65:1;:4::67;74:23:74:24:0;:12::31:1;:34::37:0;:12:::1;:51::52:0;:41::59:1;:63::66:0;:69::93;;:63:::1;:41::94;:12;:4::96;77:23:77:24:0;:12::41:1;:45::82:0;:70::81;:45::82:1;;;:4::84;79:23:79:24:0;:12::31:1;:45::46:0;:35::53:1;:4::55;81:8:81:25:0;:29::30;:8:::1;:32:84:5:0;83:25:83:26;:14::41:1;:45::47:0;:6::49:1;81:32:84:5;33:2:85:3',
+ 'bytecode': 'c3549dc455a169c0519dc0c7c0cd88c0d1008800c752c7788852cd8852ce52d18800ce52d101207f7c7b88518853c7827701199d53c7537f7c0376a9148801147f0288ac8852cf01147f52d2537a7b7e8852d052d39d52cc01649552c6016995a26953cd0376a9147b7e0288ac7e8853cc52c69d53ce0088c4559c6354d100886851',
+ 'sourceMap': '32:12:32:28;:32::33;:4::78:1;33:12:33:29:0;:33::34;:12:::1;:4::80;36:12:36:33:0;:37::38;:4::92:1;37:22:37:43:0;:12::60:1;:75::96:0;:64::113:1;:4::164;39:23:39:44:0;:12::59:1;:63::65:0;:4::129:1;45:51:45:52:0;:41::69:1;46:22:46:23:0;:12::40:1;:44::72:0;:4::164:1;47:23:47:24:0;:12::41:1;:4::166;50:22:50:23:0;:12::38:1;:53::54:0;:42::69:1;:4::130;52:44:52:45:0;:34::60:1;55:64:55:65:0;:53::80:1;:87::89:0;:53::90:1;56:12:56:27:0;:31::52;:4::121:1;57:33:57:37:0;:4::98:1;60:22:60:23:0;:12::40:1;:::47;;:51::53:0;:4::109:1;61:75:61:76:0;:65::93:1;:100::101:0;:65::102:1;63:12:63:34:0;:38::46;:4::122:1;64:75:64:77:0;:46::78:1;66:38:66:44:0;:4::125:1;68:48:68:49:0;:38::64:1;:71::73:0;:38::74:1;71:23:71:24:0;:12::39:1;:43::46:0;;:49::53;:43:::1;:4::124;74:22:74:23:0;:12::36:1;:51::52:0;:40::65:1;:4::124;77:23:77:24:0;:12::31:1;:34::37:0;:12:::1;:51::52:0;:41::59:1;:62::65:0;:41:::1;:12;:4::128;80:23:80:24:0;:12::41:1;:45::82:0;:70::81;:45::82:1;;;:4::154;82:23:82:24:0;:12::31:1;:45::46:0;:35::53:1;:4::129;84:22:84:23:0;:12::38:1;:42::44:0;:4::88:1;86:8:86:25:0;:29::30;:8:::1;:32:89:5:0;88:25:88:26;:14::41:1;:45::47:0;:6::106:1;86:32:89:5;31:2:90:3',
'logs': [],
'requires': [
{
- 'ip': 3,
- 'line': 34,
+ 'ip': 2,
+ 'line': 32,
+ 'message': 'Transaction: must have exactly 4 inputs',
},
{
- 'ip': 7,
- 'line': 35,
+ 'ip': 6,
+ 'line': 33,
+ 'message': 'Transaction: must have at most 5 outputs',
},
{
- 'ip': 10,
- 'line': 38,
+ 'ip': 9,
+ 'line': 36,
+ 'message': 'Input 1: bid contract UTXO must be at this index',
},
{
- 'ip': 15,
+ 'ip': 14,
+ 'line': 37,
+ 'message': 'Input 1: locking bytecode must match output 1',
+ },
+ {
+ 'ip': 18,
'line': 39,
+ 'message': 'Output 1: must not have any token category (pure BCH only)',
},
{
- 'ip': 21,
+ 'ip': 24,
'line': 46,
+ 'message': "Input 2: auction NFT locking bytecode does not match registry input's locking bytecode",
},
{
- 'ip': 24,
+ 'ip': 27,
'line': 47,
+ 'message': "Output 2: auction NFT locking bytecode does not match registry input's locking bytecode",
},
{
- 'ip': 29,
+ 'ip': 32,
'line': 50,
+ 'message': 'Output 2: auction NFT token category must match input 2',
},
{
- 'ip': 38,
+ 'ip': 41,
'line': 56,
+ 'message': 'Output 2: auction NFT token category prefix must match registry',
},
{
- 'ip': 40,
+ 'ip': 43,
'line': 57,
+ 'message': 'Output 2: auction NFT capability must be mutable (0x01)',
},
{
- 'ip': 46,
+ 'ip': 49,
'line': 60,
+ 'message': 'Input 3: locking bytecode must be 25 bytes (P2PKH)',
},
{
- 'ip': 64,
- 'line': 68,
+ 'ip': 56,
+ 'line': 63,
+ 'message': 'Input 3: locking bytecode must start with OP_DUP OP_HASH160 (0x76a914)',
},
{
- 'ip': 69,
+ 'ip': 60,
+ 'line': 66,
+ 'message': 'Input 3: locking bytecode must end with OP_EQUALVERIFY OP_CHECKSIG (0x88ac)',
+ },
+ {
+ 'ip': 71,
'line': 71,
+ 'message': 'Output 2: auction NFT commitment must match new bidder PKH + name',
},
{
- 'ip': 82,
+ 'ip': 76,
'line': 74,
+ 'message': 'Output 2: auction NFT token amount must match input 2',
},
{
- 'ip': 90,
+ 'ip': 86,
'line': 77,
+ 'message': 'Output 2: bid amount must be at least 5 percentage higher',
+ },
+ {
+ 'ip': 94,
+ 'line': 80,
+ 'message': 'Output 3: previous bidder locking bytecode must match previous PKH',
},
{
- 'ip': 95,
- 'line': 79,
+ 'ip': 99,
+ 'line': 82,
+ 'message': 'Output 3: previous bidder refund amount must match previous bid amount',
},
{
'ip': 103,
- 'line': 83,
+ 'line': 84,
+ 'message': 'Input 3: funding UTXO must be pure BCH',
+ },
+ {
+ 'ip': 111,
+ 'line': 88,
+ 'message': 'Output 4: change must be pure BCH (no token category)',
},
],
},
'compiler': {
'name': 'cashc',
- 'version': '0.11.2',
+ 'version': '0.11.4',
},
- 'updatedAt': '2025-07-15T19:31:08.613Z',
+ 'updatedAt': '2025-08-10T17:47:19.473Z',
};
diff --git a/lib/compiled/ConflictResolver.ts b/lib/compiled/ConflictResolver.ts
new file mode 100644
index 0000000..9798f44
--- /dev/null
+++ b/lib/compiled/ConflictResolver.ts
@@ -0,0 +1,99 @@
+export default {
+ 'contractName': 'ConflictResolver',
+ 'constructorInputs': [],
+ 'abi': [
+ {
+ 'name': 'call',
+ 'inputs': [],
+ },
+ ],
+ 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_3 OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_3 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_EQUALVERIFY OP_2 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_LESSTHAN OP_VERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL',
+ 'source': "pragma cashscript 0.11.4;\n\ncontract ConflictResolver() {\n /**\n * Resolves a conflict between two competing registration auctions for the same name.\n * \n * RULE:\n * - If any new auction is created when an auction already exists, then the new auction is open for penalization.\n *\n * Anyone can provide proof of an active auction's existence and take away the funds from the \"new\" invalid auction\n * as a form of reward for keeping the system secure and predictable.\n * Therefore, it's the responsibility of the application to check for any running auctions for the same name.\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: Valid auctionNFT from Registry Contract.\n * - Input3: Invalid auctionNFT from Registry Contract.\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Valid auctionNFT back to Registry Contract.\n * - Output3: BCH change/reward to caller.\n */\n function call() {\n require(tx.inputs.length == 4, \"Transaction: must have exactly 4 inputs\");\n require(tx.outputs.length == 4, \"Transaction: must have exactly 4 outputs\");\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1, \"Input 1: conflict resolver contract UTXO must be at this index\");\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, \"Input 1: locking bytecode must match output 1\");\n // Ensure that no tokenCategory is minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, \"Output 1: must not have any token category (pure BCH only)\");\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, \"Input 2: valid auction locking bytecode does not match registry input's locking bytecode\");\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, \"Input 3: invalid auction locking bytecode does not match registry input's locking bytecode\");\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, \"Output 2: valid auction locking bytecode does not match registry input's locking bytecode\");\n\n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n\n // auctionNFT should be mutable\n bytes auctionCategory, bytes auctionCapability = tx.inputs[2].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory, \"Input 2: valid auction token category prefix must match registry\");\n // Valid auction capability should be mutable.\n require(auctionCapability == 0x01, \"Input 2: valid auction capability must be mutable (0x01)\");\n\n // Invalid and valid auctionNFTs both should have the same category and capability.\n require(tx.inputs[2].tokenCategory == tx.inputs[3].tokenCategory, \"Input 2 and 3: auction token categories must match\");\n // Both auctionNFTs should also have the same 'name'\n require(tx.inputs[2].nftCommitment.split(20)[1] == tx.inputs[3].nftCommitment.split(20)[1], \"Input 2 and 3: auction names must match\");\n // The valid auctionNFT will have a lower registrationID\n require(tx.inputs[2].tokenAmount < tx.inputs[3].tokenAmount, \"Input 2: valid auction registration ID must be lower than input 3\");\n\n // tokenAmount from the invalid auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount, \"Output 0: token amount must equal input 0 + input 3 amounts (accumulation)\");\n\n // Attach any output to take away the funds as reward\n require(tx.outputs[3].tokenCategory == 0x, \"Output 3: reward must be pure BCH (no token category)\");\n }\n}",
+ 'debug': {
+ 'bytecode': 'c3549dc4549dc0519dc0c7c0cd88c0d1008800c752c7788853c7788852cd8800ce52ce01207f7c7b88518852ce53ce8852cf01147f7753cf01147f778852d053d09f6900d300d053d0939d53d10087',
+ 'sourceMap': '27:12:27:28;:32::33;:4::78:1;28:12:28:29:0;:33::34;:4::80:1;31:12:31:33:0;:37::38;:4::106:1;32:22:32:43:0;:12::60:1;:75::96:0;:64::113:1;:4::164;34:23:34:44:0;:12::59:1;:63::65:0;:4::129:1;40:51:40:52:0;:41::69:1;41:22:41:23:0;:12::40:1;:44::72:0;:4::166:1;42:22:42:23:0;:12::40:1;:44::72:0;:4::168:1;43:23:43:24:0;:12::41:1;:4::168;46:44:46:45:0;:34::60:1;49:63:49:64:0;:53::79:1;:86::88:0;:53::89:1;50:12:50:27:0;:31::52;:4::122:1;52:33:52:37:0;:4::99:1;55:22:55:23:0;:12::38:1;:52::53:0;:42::68:1;:4::124;57:22:57:23:0;:12::38:1;:45::47:0;:12::48:1;:::51;:65::66:0;:55::81:1;:88::90:0;:55::91:1;:::94;:4::139;59:22:59:23:0;:12::36:1;:49::50:0;:39::63:1;:12;:4::134;63:23:63:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::172;66:23:66:24:0;:12::39:1;:43::45:0;:4::104:1',
+ 'logs': [],
+ 'requires': [
+ {
+ 'ip': 2,
+ 'line': 27,
+ 'message': 'Transaction: must have exactly 4 inputs',
+ },
+ {
+ 'ip': 5,
+ 'line': 28,
+ 'message': 'Transaction: must have exactly 4 outputs',
+ },
+ {
+ 'ip': 8,
+ 'line': 31,
+ 'message': 'Input 1: conflict resolver contract UTXO must be at this index',
+ },
+ {
+ 'ip': 13,
+ 'line': 32,
+ 'message': 'Input 1: locking bytecode must match output 1',
+ },
+ {
+ 'ip': 17,
+ 'line': 34,
+ 'message': 'Output 1: must not have any token category (pure BCH only)',
+ },
+ {
+ 'ip': 23,
+ 'line': 41,
+ 'message': "Input 2: valid auction locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 27,
+ 'line': 42,
+ 'message': "Input 3: invalid auction locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 30,
+ 'line': 43,
+ 'message': "Output 2: valid auction locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 39,
+ 'line': 50,
+ 'message': 'Input 2: valid auction token category prefix must match registry',
+ },
+ {
+ 'ip': 41,
+ 'line': 52,
+ 'message': 'Input 2: valid auction capability must be mutable (0x01)',
+ },
+ {
+ 'ip': 46,
+ 'line': 55,
+ 'message': 'Input 2 and 3: auction token categories must match',
+ },
+ {
+ 'ip': 57,
+ 'line': 57,
+ 'message': 'Input 2 and 3: auction names must match',
+ },
+ {
+ 'ip': 63,
+ 'line': 59,
+ 'message': 'Input 2: valid auction registration ID must be lower than input 3',
+ },
+ {
+ 'ip': 71,
+ 'line': 63,
+ 'message': 'Output 0: token amount must equal input 0 + input 3 amounts (accumulation)',
+ },
+ {
+ 'ip': 76,
+ 'line': 66,
+ 'message': 'Output 3: reward must be pure BCH (no token category)',
+ },
+ ],
+ },
+ 'compiler': {
+ 'name': 'cashc',
+ 'version': '0.11.4',
+ },
+ 'updatedAt': '2025-08-10T17:47:20.907Z',
+};
diff --git a/lib/compiled/Domain.ts b/lib/compiled/Domain.ts
deleted file mode 100644
index 9e3bc73..0000000
--- a/lib/compiled/Domain.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-export default {
- 'contractName': 'Domain',
- 'constructorInputs': [
- {
- 'name': 'inactivityExpiryTime',
- 'type': 'int',
- },
- {
- 'name': 'name',
- 'type': 'bytes',
- },
- {
- 'name': 'domainCategory',
- 'type': 'bytes',
- },
- ],
- 'abi': [
- {
- 'name': 'useAuth',
- 'inputs': [
- {
- 'name': 'authID',
- 'type': 'int',
- },
- ],
- },
- {
- 'name': 'resolveOwnerConflict',
- 'inputs': [],
- },
- {
- 'name': 'burn',
- 'inputs': [],
- },
- ],
- 'bytecode': 'OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_INPUTINDEX OP_OUTPUTTOKENCOMMITMENT OP_EQUALVERIFY OP_4 OP_ROLL OP_1 OP_NUMEQUAL OP_IF OP_INPUTINDEX OP_1ADD OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_1ADD OP_UTXOTOKENCOMMITMENT OP_8 OP_SPLIT OP_DUP OP_4 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_2 OP_PICK OP_EQUALVERIFY OP_2DROP OP_ELSE OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_1 OP_ELSE OP_3 OP_PICK OP_1 OP_NUMEQUAL OP_IF OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_TXINPUTCOUNT OP_5 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_4 OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_0 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_0 OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCOMMITMENT OP_1 OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_1 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_3 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_3 OP_ROLL OP_EQUALVERIFY OP_1 OP_UTXOTOKENCOMMITMENT OP_REVERSEBYTES OP_BIN2NUM OP_3 OP_UTXOTOKENCOMMITMENT OP_REVERSEBYTES OP_BIN2NUM OP_LESSTHAN OP_NIP OP_NIP OP_NIP OP_ELSE OP_3 OP_ROLL OP_2 OP_NUMEQUALVERIFY OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_TXINPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_0 OP_EQUAL OP_IF OP_1 OP_INPUTSEQUENCENUMBER OP_OVER OP_NUMEQUALVERIFY OP_ELSE OP_2 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_8 OP_SPLIT OP_DROP OP_0 OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_ENDIF OP_INPUTINDEX OP_UTXOBYTECODE OP_0 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_UTXOBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_1 OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_1 OP_UTXOTOKENCATEGORY OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NIP OP_NIP OP_ENDIF OP_ENDIF',
- 'source': 'pragma cashscript 0.11.2;\n\n/**\n * @param inactivityExpiryTime The time period after which the domain is considered inactive.\n * @param name The name of the domain.\n * @param domainCategory The category of the domain.\n */\ncontract Domain(\n int inactivityExpiryTime,\n bytes name,\n bytes domainCategory\n ) {\n \n /**\n * This function can be used to perform a variety of actions.\n *\n * For example:\n * - It can be used to prove the the ownership of the domain by other contracts.\n * - This function allows the owner to perform any actions in conjunction with other contracts.\n * - This function can be used to add records and invalidate multiple records in a single transaction.\n *\n * Records are created using OP_RETURN outputs. To add a record, include the record data directly in the OP_RETURN output.\n * To invalidate a record, prefix "RMV" followed by the hash of the record content in the OP_RETURN output. This will signal\n * the library/indexers to exclude the record from the valid records.\n * \n * @inputs\n * - Inputx: Internal/External Auth NFT\n * - Inputx+1 (optional): Domain ownership NFT from the owner\n * \n * @outputs\n * - Outputx: Internal/External Auth NFT returned to this contract\n * - Outputx+1 (optional): Domain NFT returned\n * \n */\n function useAuth(int authID) {\n // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.\n require(tx.version == 2);\n\n // The activeInputIndex can be anything as long as the utxo properties are preserved and comes back to the\n // contract without alteration.\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n require(tx.inputs[this.activeInputIndex].tokenCategory == domainCategory);\n require(tx.outputs[this.activeInputIndex].tokenCategory == domainCategory);\n require(tx.inputs[this.activeInputIndex].nftCommitment == tx.outputs[this.activeInputIndex].nftCommitment);\n\n if(authID == 1) {\n // The next input from the InternalAuthNFT must be the ownershipNFT.\n require(tx.inputs[this.activeInputIndex + 1].tokenCategory == domainCategory);\n bytes registrationId, bytes nameFromOwnerNFT = tx.inputs[this.activeInputIndex + 1].nftCommitment.split(8);\n require(nameFromOwnerNFT == name);\n require(tx.inputs[this.activeInputIndex].nftCommitment == registrationId);\n } else {\n // One known use of ExternalAuthNFT in the `DomainOwnershipGuard` contract. ExternalAuthNFT is\n // used to prove that an owner exists.\n require(tx.inputs[this.activeInputIndex].nftCommitment == 0x);\n }\n }\n\n /**\n * If the incentive system fails, i.e `DomainOwnershipGuard` or `AuctionConflictResolver` fails to prevent a\n * a owner conflict. When this happens there will be > 1 owner for this domain.\n * The owner with the lowest registrationID must be the only owner for this domain.\n * To help enforce this rule, this function will allow anyone to burn both the Auth NFTs of the NEW owner.\n *\n * @inputs\n * - Input0: Valid External Auth NFT from self\n * - Input1: Valid Internal Auth NFT from self\n * - Input2: Invalid External Auth NFT from self\n * - Input3: Invalid Internal Auth NFT from self\n * - Input4: BCH input from anyone\n * \n * @outputs \n * - Output0: Valid External Auth NFT back to self\n * - Output1: Valid Internal Auth NFT back to self\n * - Output3: BCH change output\n */\n function resolveOwnerConflict(){\n // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.\n require(tx.version == 2);\n\n require(tx.inputs.length == 5);\n require(tx.outputs.length == 3);\n\n // Pure BCH input and output to fund the transaction\n require(tx.inputs[4].tokenCategory == 0x);\n require(tx.outputs[2].tokenCategory == 0x);\n\n bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;\n require(tx.inputs[0].lockingBytecode == selfLockingBytecode);\n require(tx.inputs[1].lockingBytecode == selfLockingBytecode);\n require(tx.inputs[2].lockingBytecode == selfLockingBytecode);\n require(tx.inputs[3].lockingBytecode == selfLockingBytecode);\n\n require(tx.outputs[0].lockingBytecode == selfLockingBytecode);\n require(tx.outputs[1].lockingBytecode == selfLockingBytecode);\n\n // External Auth NFTs\n require(tx.inputs[0].nftCommitment == 0x);\n require(tx.inputs[2].nftCommitment == 0x);\n\n // Commitments of Valid Auth NFts back to self\n require(tx.outputs[0].nftCommitment == 0x);\n require(tx.outputs[1].nftCommitment == tx.inputs[1].nftCommitment);\n\n // Ensure that all the token inputs and outputs have domainCategory\n require(tx.inputs[0].tokenCategory == domainCategory);\n require(tx.inputs[1].tokenCategory == domainCategory);\n require(tx.inputs[2].tokenCategory == domainCategory);\n require(tx.inputs[3].tokenCategory == domainCategory);\n\n require(tx.outputs[0].tokenCategory == domainCategory);\n require(tx.outputs[1].tokenCategory == domainCategory);\n\n // Compare the registrationID\n require(int(tx.inputs[1].nftCommitment.reverse()) < int(tx.inputs[3].nftCommitment.reverse()));\n }\n\n /**\n * Allows the domain owner or anyone to burn the InternalAuthNFT and externalAuthNFT making this domain available\n * for auction.\n * \n * - Owner can burn the AuthNFTs anytime.\n * - External party can burn the AuthNFTs when the internalAuth NFT has not been used for more than `inactivityExpiryTime`.\n *\n * @inputs\n * - Input0: External Auth NFT\n * - Input1: Internal Auth NFT\n * - Input2: Pure BCH or Domain ownership NFT from the owner\n *\n * @outputs \n * - Output0: BCH change\n *\n */\n function burn() {\n // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.\n // Need version 2 enforcement for relative timelocks.\n require(tx.version == 2);\n\n require(tx.inputs.length == 3);\n require(tx.outputs.length == 1);\n\n // If an external party is attempting to burn the authNFTs\n if (tx.inputs[2].tokenCategory == 0x) {\n // If pure BCH input, then allow anyone to burn given the time limit has passed.\n require(tx.inputs[1].sequenceNumber == inactivityExpiryTime);\n } else {\n // If domain ownership NFT input, then allow the owner to burn anytime.\n require(tx.inputs[2].tokenCategory == domainCategory);\n // Make sure that the registrationID in the domainOwnershipNFT and the internalAuthNFT are the same.\n require(tx.inputs[2].nftCommitment.split(8)[0] == tx.inputs[0].nftCommitment);\n }\n\n bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;\n require(tx.inputs[0].lockingBytecode == selfLockingBytecode);\n require(tx.inputs[1].lockingBytecode == selfLockingBytecode);\n\n // ExternalAuthNFT\n require(tx.inputs[0].nftCommitment == 0x);\n // Both InternalAuthNFT and externalAuthNFT are immutable and have the same tokenCategory\n require(tx.inputs[0].tokenCategory == tx.inputs[1].tokenCategory);\n require(tx.inputs[0].tokenCategory == domainCategory);\n require(tx.inputs[1].tokenCategory == domainCategory);\n\n // Return the BCH as change.\n require(tx.outputs[0].tokenCategory == 0x);\n }\n}\n',
- 'debug': {
- 'bytecode': '5379009c63c2529dc0c7c0cd88c0ce537988c0d1537988c0cfc0d288547a519c63c08bce537988c08bcf587f76547988c0cf5279886d67c0cf0088686d6d51675379519c63c2529dc3559dc4539d54ce008852d10088c0c700c7788851c7788852c7788853c7788800cd788851cd8800cf008852cf008800d2008851d251cf8800ce53798851ce53798852ce53798853ce53798800d153798851d1537a8851cfbc8153cfbc819f77777767537a529dc2529dc3539dc4519d52ce00876351cb789d6752ce53798852cf587f7500cf8868c0c700c7788851c78800cf008800ce51ce8800ce53798851ce537a8800d1008777776868',
- 'sourceMap': '35:2:57:3;;;;;37:12:37:22;:26::27;:4::29:1;41:22:41:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;42:22:42:43:0;:12::58:1;:62::76:0;;:4::78:1;43:23:43:44:0;:12::59:1;:63::77:0;;:4::79:1;44:22:44:43:0;:12::58:1;:73::94:0;:62::109:1;:4::111;46:7:46:13:0;;:17::18;:7:::1;:20:52:5:0;48:24:48:45;:::49:1;:14::64;:68::82:0;;:6::84:1;49:63:49::0;:::88:1;:53::103;:110::111:0;:53::112:1;50:14:50:30:0;:34::38;;:6::40:1;51:24:51:45:0;:14::60:1;:64::78:0;;:6::80:1;46:20:52:5;52:11:56::0;55:24:55:45;:14::60:1;:64::66:0;:6::68:1;52:11:56:5;35:2:57:3;;;;77::116::0;;;;;79:12:79:22;:26::27;:4::29:1;81:12:81:28:0;:32::33;:4::35:1;82:12:82:29:0;:33::34;:4::36:1;85:22:85:23:0;:12::38:1;:42::44:0;:4::46:1;86:23:86:24:0;:12::39:1;:43::45:0;:4::47:1;88:42:88:63:0;:32::80:1;89:22:89:23:0;:12::40:1;:44::63:0;:4::65:1;90:22:90:23:0;:12::40:1;:44::63:0;:4::65:1;91:22:91:23:0;:12::40:1;:44::63:0;:4::65:1;92:22:92:23:0;:12::40:1;:44::63:0;:4::65:1;94:23:94:24:0;:12::41:1;:45::64:0;:4::66:1;95:23:95:24:0;:12::41:1;:4::66;98:22:98:23:0;:12::38:1;:42::44:0;:4::46:1;99:22:99:23:0;:12::38:1;:42::44:0;:4::46:1;102:23:102:24:0;:12::39:1;:43::45:0;:4::47:1;103:23:103:24:0;:12::39:1;:53::54:0;:43::69:1;:4::71;106:22:106:23:0;:12::38:1;:42::56:0;;:4::58:1;107:22:107:23:0;:12::38:1;:42::56:0;;:4::58:1;108:22:108:23:0;:12::38:1;:42::56:0;;:4::58:1;109:22:109:23:0;:12::38:1;:42::56:0;;:4::58:1;111:23:111:24:0;:12::39:1;:43::57:0;;:4::59:1;112:23:112:24:0;:12::39:1;:43::57:0;;:4::59:1;115:26:115:27:0;:16::42:1;:::52;:12::53;:70::71:0;:60::86:1;:::96;:56::97;:4::99;77:2:116:3;;;;134::166::0;;;;137:12:137:22;:26::27;:4::29:1;139:12:139:28:0;:32::33;:4::35:1;140:12:140:29:0;:33::34;:4::36:1;143:18:143:19:0;:8::34:1;:38::40:0;:8:::1;:42:146:5:0;145:24:145:25;:14::41:1;:45::65:0;:6::67:1;146:11:151:5:0;148:24:148:25;:14::40:1;:44::58:0;;:6::60:1;150:24:150:25:0;:14::40:1;:47::48:0;:14::49:1;:::52;:66::67:0;:56::82:1;:6::84;146:11:151:5;153:42:153:63:0;:32::80:1;154:22:154:23:0;:12::40:1;:44::63:0;:4::65:1;155:22:155:23:0;:12::40:1;:4::65;158:22:158:23:0;:12::38:1;:42::44:0;:4::46:1;160:22:160:23:0;:12::38:1;:52::53:0;:42::68:1;:4::70;161:22:161:23:0;:12::38:1;:42::56:0;;:4::58:1;162:22:162:23:0;:12::38:1;:42::56:0;;:4::58:1;165:23:165:24:0;:12::39:1;:43::45:0;:4::47:1;134:2:166:3;;8:0:167:1;',
- 'logs': [],
- 'requires': [
- {
- 'ip': 10,
- 'line': 37,
- },
- {
- 'ip': 15,
- 'line': 41,
- },
- {
- 'ip': 20,
- 'line': 42,
- },
- {
- 'ip': 25,
- 'line': 43,
- },
- {
- 'ip': 30,
- 'line': 44,
- },
- {
- 'ip': 41,
- 'line': 48,
- },
- {
- 'ip': 50,
- 'line': 50,
- },
- {
- 'ip': 55,
- 'line': 51,
- },
- {
- 'ip': 61,
- 'line': 55,
- },
- {
- 'ip': 74,
- 'line': 79,
- },
- {
- 'ip': 77,
- 'line': 81,
- },
- {
- 'ip': 80,
- 'line': 82,
- },
- {
- 'ip': 84,
- 'line': 85,
- },
- {
- 'ip': 88,
- 'line': 86,
- },
- {
- 'ip': 94,
- 'line': 89,
- },
- {
- 'ip': 98,
- 'line': 90,
- },
- {
- 'ip': 102,
- 'line': 91,
- },
- {
- 'ip': 106,
- 'line': 92,
- },
- {
- 'ip': 110,
- 'line': 94,
- },
- {
- 'ip': 113,
- 'line': 95,
- },
- {
- 'ip': 117,
- 'line': 98,
- },
- {
- 'ip': 121,
- 'line': 99,
- },
- {
- 'ip': 125,
- 'line': 102,
- },
- {
- 'ip': 130,
- 'line': 103,
- },
- {
- 'ip': 135,
- 'line': 106,
- },
- {
- 'ip': 140,
- 'line': 107,
- },
- {
- 'ip': 145,
- 'line': 108,
- },
- {
- 'ip': 150,
- 'line': 109,
- },
- {
- 'ip': 155,
- 'line': 111,
- },
- {
- 'ip': 160,
- 'line': 112,
- },
- {
- 'ip': 170,
- 'line': 115,
- },
- {
- 'ip': 180,
- 'line': 137,
- },
- {
- 'ip': 183,
- 'line': 139,
- },
- {
- 'ip': 186,
- 'line': 140,
- },
- {
- 'ip': 195,
- 'line': 145,
- },
- {
- 'ip': 201,
- 'line': 148,
- },
- {
- 'ip': 209,
- 'line': 150,
- },
- {
- 'ip': 216,
- 'line': 154,
- },
- {
- 'ip': 219,
- 'line': 155,
- },
- {
- 'ip': 223,
- 'line': 158,
- },
- {
- 'ip': 228,
- 'line': 160,
- },
- {
- 'ip': 233,
- 'line': 161,
- },
- {
- 'ip': 238,
- 'line': 162,
- },
- {
- 'ip': 243,
- 'line': 165,
- },
- ],
- },
- 'compiler': {
- 'name': 'cashc',
- 'version': '0.11.2',
- },
- 'updatedAt': '2025-07-15T19:31:08.262Z',
-};
diff --git a/lib/compiled/DomainFactory.ts b/lib/compiled/DomainFactory.ts
deleted file mode 100644
index b9ae010..0000000
--- a/lib/compiled/DomainFactory.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-export default {
- 'contractName': 'DomainFactory',
- 'constructorInputs': [
- {
- 'name': 'domainContractBytecode',
- 'type': 'bytes',
- },
- {
- 'name': 'minWaitTime',
- 'type': 'int',
- },
- {
- 'name': 'maxPlatformFeePercentage',
- 'type': 'int',
- },
- ],
- 'abi': [
- {
- 'name': 'call',
- 'inputs': [],
- },
- ],
- 'bytecode': 'OP_TXINPUTCOUNT OP_5 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_8 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOVALUE OP_INPUTINDEX OP_OUTPUTVALUE OP_NUMEQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_4 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_5 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_2 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_3 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_2 OP_OUTPUTTOKENCOMMITMENT OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENAMOUNT OP_2 OP_UTXOTOKENAMOUNT OP_NUMEQUALVERIFY OP_2 OP_OUTPUTTOKENAMOUNT OP_0 OP_NUMEQUALVERIFY OP_2 OP_OUTPUTVALUE OP_2 OP_UTXOVALUE OP_NUMEQUALVERIFY OP_3 OP_INPUTSEQUENCENUMBER OP_3 OP_ROLL OP_NUMEQUALVERIFY OP_3 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_DUP OP_SIZE OP_NIP 20 OP_4 OP_ROLL OP_CAT OP_SWAP OP_CAT OP_OVER OP_CAT OP_3 OP_ROLL OP_CAT OP_HASH256 aa20 OP_SWAP OP_CAT 87 OP_CAT OP_3 OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_4 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_3 OP_OUTPUTTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_3 OP_OUTPUTVALUE e803 OP_NUMEQUALVERIFY OP_3 OP_UTXOTOKENAMOUNT OP_8 OP_NUM2BIN OP_REVERSEBYTES OP_4 OP_OUTPUTTOKENCOMMITMENT OP_OVER OP_EQUALVERIFY OP_4 OP_OUTPUTVALUE e803 OP_NUMEQUALVERIFY OP_5 OP_OUTPUTTOKENCOMMITMENT OP_SWAP OP_ROT OP_CAT OP_EQUALVERIFY OP_5 OP_OUTPUTBYTECODE 76a914 OP_ROT OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_5 OP_OUTPUTVALUE e803 OP_NUMEQUALVERIFY OP_4 OP_UTXOBYTECODE OP_5 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_4 OP_UTXOBYTECODE OP_6 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_4 OP_UTXOVALUE OP_6 OP_OUTPUTVALUE OP_NUMEQUALVERIFY OP_4 OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_6 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_7 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_7 OP_OUTPUTVALUE OP_3 OP_UTXOVALUE 64 OP_DIV OP_ROT OP_MUL OP_LESSTHANOREQUAL',
- 'source': "pragma cashscript 0.11.2;\n\n/**\n * @param domainContractBytecode - Partial bytecode of the domain contract\n * @param minWaitTime - Minimum wait time to consider an auction ended\n * @param maxPlatformFeePercentage - Maximum platform fee percentage\n */\ncontract DomainFactory(\n bytes domainContractBytecode,\n int minWaitTime,\n int maxPlatformFeePercentage\n) {\n /**\n * This function finalizes a domain registration auction by:\n * - Verifying the auction has ended and the winner's bid is valid\n * - Issuing an immutable externalAuthNFT to the Domain Contract\n * - Issuing an immutable internalAuthNFT to the Domain Contract\n * - Issuing an immutable domain NFT to the auction winner\n * - Distributing auction fees between the platform and miners\n * - Burning the auctionNFT\n * - Pure BCH input from bidder is used to prevent miners from taking away the funds from any or all transactions in the future.\n * Out of many possible ways, this method will be suitable to easily implement by applications.\n *\n *\n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract\n * - Input2: DomainMintingNFT from the Registry Contract\n * - Input3: auctionNFT from the Registry Contract\n * - Input4: Pure BCH from bidder\n *\n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change\n * - Output2: DomainMintingNFT back to the Registry contract\n * - Output3: External Auth NFT to the domain contract\n * - Output4: Internal Auth NFT to the domain contract\n * - Output5: Domain NFT to the auction winner\n * - Output6: Pure BCH back to the bidder\n * - Output7: Platform fee\n *\n */\n function call(){\n require(tx.inputs.length == 5);\n require(tx.outputs.length == 8);\n\n // This contract can only be used at input1 and it should return to itself.\n require(this.activeInputIndex == 1);\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n // Ensure that the domainCategory in not minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x);\n // Strict value checks to ensure the platform and miner get fee.\n require(tx.inputs[this.activeInputIndex].value == tx.outputs[this.activeInputIndex].value);\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode);\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode);\n\n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n require(tx.outputs[3].tokenCategory == registryInputCategory);\n require(tx.outputs[4].tokenCategory == registryInputCategory);\n require(tx.outputs[5].tokenCategory == registryInputCategory);\n\n // DomainMintingNFT should be minting and of the 'domainCategory' i.e registryInputCategory\n bytes domainMintingCategory, bytes domainMintingCapability = tx.inputs[2].tokenCategory.split(32);\n require(domainMintingCategory == registryInputCategory);\n require(domainMintingCapability == 0x02); // Mutable\n // DomainMintingNFT should keep the same category and capability\n require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory);\n\n // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory);\n require(auctionCapability == 0x01); // Mutable\n\n // Enforce strict restrictions on DomainMintingNFT\n require(tx.inputs[2].nftCommitment == tx.outputs[2].nftCommitment);\n // DomainMintingNFT has no nftCommitment\n require(tx.outputs[2].nftCommitment == 0x);\n // DomainMintingNFT has no tokenAmount\n require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount);\n require(tx.outputs[2].tokenAmount == 0);\n\n // Strict value check\n require(tx.outputs[2].value == tx.inputs[2].value);\n\n // Enforcing the relative timelock, the auctionNFT must be atleast `minWaitTime` old\n // to be considered ended.\n require(tx.inputs[3].sequenceNumber == minWaitTime);\n\n // Extract the PKH and name from the auctionNFT\n bytes20 bidderPKH, bytes name = tx.inputs[3].nftCommitment.split(20);\n \n // Get the name length to generate the complete bytecode of the domain contract\n int nameLength = name.length;\n // category + name + bytecode.\n // Note: `inactivityExpiryTime` in the domain is already added to the domainContractBytecode in the constructor.\n bytes domainBytecode = 0x20 + registryInputCategory + bytes(nameLength) + name + domainContractBytecode;\n bytes32 scriptHash = hash256(domainBytecode);\n bytes35 domainLockingBytecode = new LockingBytecodeP2SH32(scriptHash);\n \n // ExternalAuthNFT goes to the domain contract\n require(tx.outputs[3].lockingBytecode == domainLockingBytecode);\n // InternalAuthNFT goes to the domain contract\n require(tx.outputs[4].lockingBytecode == domainLockingBytecode);\n \n // ExternalAuthNFT does not have any commitment\n require(tx.outputs[3].nftCommitment == 0x);\n // Strict value check\n require(tx.outputs[3].value == 1000);\n\n // InternalAuthNFT has registrationID as the commitment so it can be used to authenticate\n // along with the ownershipNFT\n bytes8 registrationId = bytes8(tx.inputs[3].tokenAmount).reverse();\n require(tx.outputs[4].nftCommitment == registrationId);\n // Strict value check\n require(tx.outputs[4].value == 1000);\n\n // Send the domain ownership NFT to the bidder\n require(tx.outputs[5].nftCommitment == registrationId + name);\n require(tx.outputs[5].lockingBytecode == new LockingBytecodeP2PKH(bidderPKH));\n require(tx.outputs[5].value == 1000);\n\n // Ensure that the bidder receiving the domain ownership NFT is also receiving the pure BCH back\n require(tx.inputs[4].lockingBytecode == tx.outputs[5].lockingBytecode);\n require(tx.inputs[4].lockingBytecode == tx.outputs[6].lockingBytecode);\n // Ensure that the value of input from bidder is the same and goes back to the bidder\n require(tx.inputs[4].value == tx.outputs[6].value);\n\n // Ensure that input and output to the bidder does not have any tokenCategory\n require(tx.inputs[4].tokenCategory == 0x);\n require(tx.outputs[6].tokenCategory == 0x);\n\n // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount);\n\n // Output can be added by anyone (Mainly platforms)\n require(tx.outputs[7].tokenCategory == 0x);\n // Enforce that the other piece of the fee goes to the miners.\n require(tx.outputs[7].value <= (tx.inputs[3].value / 100) * maxPlatformFeePercentage);\n }\n\n}",
- 'debug': {
- 'bytecode': 'c3559dc4589dc0519dc0c7c0cd88c0d10088c0c6c0cc9d00c752c7788853c7788852cd8800ce53d1788854d1788855d1788852ce01207f7c527988528852ce52d18853ce01207f7c527988518852cf52d28852d2008852d352d09d52d3009d52cc52c69d53cb537a9d53cf01147f7682770120547a7e7c7e787e537a7eaa02aa207c7e01877e53cd788854cd8853d2008853cc02e8039d53d05880bc54d2788854cc02e8039d55d27c7b7e8855cd0376a9147b7e0288ac7e8855cc02e8039d54c755cd8854c756cd8854c656cc9d54ce008856d1008800d300d053d0939d57d1008857cc53c60164967b95a1',
- 'sourceMap': '44:12:44:28;:32::33;:4::35:1;45:12:45:29:0;:33::34;:4::36:1;48:12:48:33:0;:37::38;:4::40:1;49:22:49:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;51:23:51:44:0;:12::59:1;:63::65:0;:4::67:1;53:22:53:43:0;:12::50:1;:65::86:0;:54::93:1;:4::95;59:51:59:52:0;:41::69:1;60:22:60:23:0;:12::40:1;:44::72:0;:4::74:1;61:22:61:23:0;:12::40:1;:44::72:0;:4::74:1;62:23:62:24:0;:12::41:1;:4::75;65:44:65:45:0;:34::60:1;66:23:66:24:0;:12::39:1;:43::64:0;:4::66:1;67:23:67:24:0;:12::39:1;:43::64:0;:4::66:1;68:23:68:24:0;:12::39:1;:43::64:0;:4::66:1;71:75:71:76:0;:65::91:1;:98::100:0;:65::101:1;72:12:72:33:0;:37::58;;:4::60:1;73:39:73:43:0;:4::45:1;75:22:75:23:0;:12::38:1;:53::54:0;:42::69:1;:4::71;78:63:78:64:0;:53::79:1;:86::88:0;:53::89:1;79:12:79:27:0;:31::52;;:4::54:1;80:33:80:37:0;:4::39:1;83:22:83:23:0;:12::38:1;:53::54:0;:42::69:1;:4::71;85:23:85:24:0;:12::39:1;:43::45:0;:4::47:1;87:23:87:24:0;:12::37:1;:51::52:0;:41::65:1;:4::67;88:23:88:24:0;:12::37:1;:41::42:0;:4::44:1;91:23:91:24:0;:12::31:1;:45::46:0;:35::53:1;:4::55;95:22:95:23:0;:12::39:1;:43::54:0;;:4::56:1;98:46:98:47:0;:36::62:1;:69::71:0;:36::72:1;101:21:101:25:0;:::32:1;;104:27:104:31:0;:34::55;;:27:::1;:64::74:0;:27::75:1;:78::82:0;:27:::1;:85::107:0;;:27:::1;105:25:105:48;106:36:106:73:0;:62::72;:36::73:1;;;109:23:109:24:0;:12::41:1;:45::66:0;:4::68:1;111:23:111:24:0;:12::41:1;:4::68;114:23:114:24:0;:12::39:1;:43::45:0;:4::47:1;116:23:116:24:0;:12::31:1;:35::39:0;:4::41:1;120:45:120:46:0;:35::59:1;:28::60;;:::70;121:23:121:24:0;:12::39:1;:43::57:0;:4::59:1;123:23:123:24:0;:12::31:1;:35::39:0;:4::41:1;126:23:126:24:0;:12::39:1;:43::57:0;:60::64;:43:::1;:4::66;127:23:127:24:0;:12::41:1;:45::80:0;:70::79;:45::80:1;;;:4::82;128:23:128:24:0;:12::31:1;:35::39:0;:4::41:1;131:22:131:23:0;:12::40:1;:55::56:0;:44::73:1;:4::75;132:22:132:23:0;:12::40:1;:55::56:0;:44::73:1;:4::75;134:22:134:23:0;:12::30:1;:45::46:0;:34::53:1;:4::55;137:22:137:23:0;:12::38:1;:42::44:0;:4::46:1;138:23:138:24:0;:12::39:1;:43::45:0;:4::47:1;142:23:142:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::94;145:23:145:24:0;:12::39:1;:43::45:0;:4::47:1;147:23:147:24:0;:12::31:1;:46::47:0;:36::54:1;:57::60:0;:36:::1;:64::88:0;:35:::1;:4::90',
- 'logs': [],
- 'requires': [
- {
- 'ip': 5,
- 'line': 44,
- },
- {
- 'ip': 8,
- 'line': 45,
- },
- {
- 'ip': 11,
- 'line': 48,
- },
- {
- 'ip': 16,
- 'line': 49,
- },
- {
- 'ip': 20,
- 'line': 51,
- },
- {
- 'ip': 25,
- 'line': 53,
- },
- {
- 'ip': 31,
- 'line': 60,
- },
- {
- 'ip': 35,
- 'line': 61,
- },
- {
- 'ip': 38,
- 'line': 62,
- },
- {
- 'ip': 44,
- 'line': 66,
- },
- {
- 'ip': 48,
- 'line': 67,
- },
- {
- 'ip': 52,
- 'line': 68,
- },
- {
- 'ip': 60,
- 'line': 72,
- },
- {
- 'ip': 62,
- 'line': 73,
- },
- {
- 'ip': 67,
- 'line': 75,
- },
- {
- 'ip': 75,
- 'line': 79,
- },
- {
- 'ip': 77,
- 'line': 80,
- },
- {
- 'ip': 82,
- 'line': 83,
- },
- {
- 'ip': 86,
- 'line': 85,
- },
- {
- 'ip': 91,
- 'line': 87,
- },
- {
- 'ip': 95,
- 'line': 88,
- },
- {
- 'ip': 100,
- 'line': 91,
- },
- {
- 'ip': 105,
- 'line': 95,
- },
- {
- 'ip': 133,
- 'line': 109,
- },
- {
- 'ip': 136,
- 'line': 111,
- },
- {
- 'ip': 140,
- 'line': 114,
- },
- {
- 'ip': 144,
- 'line': 116,
- },
- {
- 'ip': 153,
- 'line': 121,
- },
- {
- 'ip': 157,
- 'line': 123,
- },
- {
- 'ip': 163,
- 'line': 126,
- },
- {
- 'ip': 171,
- 'line': 127,
- },
- {
- 'ip': 175,
- 'line': 128,
- },
- {
- 'ip': 180,
- 'line': 131,
- },
- {
- 'ip': 185,
- 'line': 132,
- },
- {
- 'ip': 190,
- 'line': 134,
- },
- {
- 'ip': 194,
- 'line': 137,
- },
- {
- 'ip': 198,
- 'line': 138,
- },
- {
- 'ip': 206,
- 'line': 142,
- },
- {
- 'ip': 210,
- 'line': 145,
- },
- {
- 'ip': 220,
- 'line': 147,
- },
- ],
- },
- 'compiler': {
- 'name': 'cashc',
- 'version': '0.11.2',
- },
- 'updatedAt': '2025-07-15T19:31:08.923Z',
-};
diff --git a/lib/compiled/DomainOwnershipGuard.ts b/lib/compiled/DomainOwnershipGuard.ts
deleted file mode 100644
index 6394a26..0000000
--- a/lib/compiled/DomainOwnershipGuard.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-export default {
- 'contractName': 'DomainOwnershipGuard',
- 'constructorInputs': [
- {
- 'name': 'domainContractBytecode',
- 'type': 'bytes',
- },
- ],
- 'abi': [
- {
- 'name': 'call',
- 'inputs': [],
- },
- ],
- 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_3 OP_UTXOBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_2 OP_OUTPUTTOKENCOMMITMENT OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_3 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_DUP OP_SIZE OP_NIP 20 OP_3 OP_ROLL OP_CAT OP_SWAP OP_CAT OP_SWAP OP_CAT OP_SWAP OP_CAT OP_HASH256 aa20 OP_SWAP OP_CAT 87 OP_CAT OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL',
- 'source': "pragma cashscript 0.11.2;\n\n/**\n * @param domainContractBytecode The the partial bytecode of the domain contract that has an Owner..\n */\ncontract DomainOwnershipGuard(bytes domainContractBytecode) {\n /**\n * If the domain being auctioned already has an `externalAuthNFT` with the same category, then the auction is invalid.\n * Because it means that an owner still exists. If it is known that the domain has been abandoned for > `inactivityExpiryTime`\n * then one must use the `burn` method of the domain.cash to burn the internalAuthNFT and externalAuthNFT making the \n * domain to be available for auction.\n *\n * Penalizes invalid domain registrations by allowing anyone to burn the auctionNFT and claim the funds as a reward.\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract\n * - Input2: External Auth NFT from the Domain Contract\n * - Input3: auctionNFT from Registry Contract\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change\n * - Output2: External Auth NFT back to the Domain Contract\n * - Output3: BCH change/reward to caller\n */\n function call(){\n require(tx.inputs.length == 4);\n require(tx.outputs.length == 4);\n \n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1);\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode);\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode);\n\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n require(tx.inputs[2].tokenCategory == registryInputCategory);\n require(tx.outputs[2].tokenCategory == registryInputCategory);\n\n // AuctionNFT should be mutable and of the 'domainCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory);\n require(auctionCapability == 0x01); // Mutable\n\n // nftCommiment of the externalAuthNFT must stay the same\n require(tx.inputs[2].nftCommitment == tx.outputs[2].nftCommitment);\n // Ensure that the externalAuth NFT is used and not the internalAuth NFT.\n require(tx.inputs[2].nftCommitment == 0x);\n\n // Get the name of the domain from the auctionNFT\n bytes name = tx.inputs[3].nftCommitment.split(20)[1];\n // Get the name length to generate the complete bytecode of the domain contract\n int nameLength = name.length;\n // category + name + bytecode.\n // Note: `inactivityExpiryTime` in the domain is already added to the domainContractBytecode in the constructor.\n bytes domainBytecode = 0x20 + registryInputCategory + bytes(nameLength) + name + domainContractBytecode;\n bytes32 scriptHash = hash256(domainBytecode);\n bytes35 domainLockingBytecode = new LockingBytecodeP2SH32(scriptHash);\n\n // Ensure that the externalAuthNFT is coming from the correct Domain Contract\n require(tx.inputs[2].lockingBytecode == domainLockingBytecode);\n require(tx.outputs[2].lockingBytecode == domainLockingBytecode);\n\n // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount);\n\n // Reward Output\n require(tx.outputs[3].tokenCategory == 0x);\n }\n}",
- 'debug': {
- 'bytecode': 'c3549dc4549dc0519dc0c7c0cd8800c753c78800ce52ce788852d1788853ce01207f7c527988518852cf52d28852cf008853cf01147f777682770120537a7e7c7e7c7e7c7eaa02aa207c7e01877e52c7788852cd8800d300d053d0939d53d10087',
- 'sourceMap': '28:12:28:28;:32::33;:4::35:1;29:12:29:29:0;:33::34;:4::36:1;32:12:32:33:0;:37::38;:4::40:1;33:22:33:43:0;:12::60:1;:75::96:0;:64::113:1;:4::115;39:51:39:52:0;:41::69:1;40:22:40:23:0;:12::40:1;:4::74;42:44:42:45:0;:34::60:1;43:22:43:23:0;:12::38:1;:42::63:0;:4::65:1;44:23:44:24:0;:12::39:1;:43::64:0;:4::66:1;47:63:47:64:0;:53::79:1;:86::88:0;:53::89:1;48:12:48:27:0;:31::52;;:4::54:1;49:33:49:37:0;:4::39:1;52:22:52:23:0;:12::38:1;:53::54:0;:42::69:1;:4::71;54:22:54:23:0;:12::38:1;:42::44:0;:4::46:1;57:27:57:28:0;:17::43:1;:50::52:0;:17::53:1;:::56;59:21:59:25:0;:::32:1;;62:27:62:31:0;:34::55;;:27:::1;:64::74:0;:27::75:1;:78::82:0;:27:::1;:85::107:0;:27:::1;63:25:63:48;64:36:64:73:0;:62::72;:36::73:1;;;67:22:67:23:0;:12::40:1;:44::65:0;:4::67:1;68:23:68:24:0;:12::41:1;:4::68;72:23:72:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::94;75:23:75:24:0;:12::39:1;:43::45:0;:4::47:1',
- 'logs': [],
- 'requires': [
- {
- 'ip': 3,
- 'line': 28,
- },
- {
- 'ip': 6,
- 'line': 29,
- },
- {
- 'ip': 9,
- 'line': 32,
- },
- {
- 'ip': 14,
- 'line': 33,
- },
- {
- 'ip': 19,
- 'line': 40,
- },
- {
- 'ip': 25,
- 'line': 43,
- },
- {
- 'ip': 29,
- 'line': 44,
- },
- {
- 'ip': 37,
- 'line': 48,
- },
- {
- 'ip': 39,
- 'line': 49,
- },
- {
- 'ip': 44,
- 'line': 52,
- },
- {
- 'ip': 48,
- 'line': 54,
- },
- {
- 'ip': 76,
- 'line': 67,
- },
- {
- 'ip': 79,
- 'line': 68,
- },
- {
- 'ip': 87,
- 'line': 72,
- },
- {
- 'ip': 92,
- 'line': 75,
- },
- ],
- },
- 'compiler': {
- 'name': 'cashc',
- 'version': '0.11.2',
- },
- 'updatedAt': '2025-07-15T19:31:09.531Z',
-};
diff --git a/lib/compiled/Factory.ts b/lib/compiled/Factory.ts
new file mode 100644
index 0000000..0277431
--- /dev/null
+++ b/lib/compiled/Factory.ts
@@ -0,0 +1,213 @@
+export default {
+ 'contractName': 'Factory',
+ 'constructorInputs': [
+ {
+ 'name': 'nameContractBytecode',
+ 'type': 'bytes',
+ },
+ {
+ 'name': 'creatorIncentivePKH',
+ 'type': 'bytes20',
+ },
+ ],
+ 'abi': [
+ {
+ 'name': 'call',
+ 'inputs': [],
+ },
+ ],
+ 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_7 OP_LESSTHANOREQUAL OP_VERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOVALUE OP_INPUTINDEX OP_OUTPUTVALUE OP_NUMEQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_4 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_5 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_2 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_2 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_3 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENAMOUNT OP_2 OP_UTXOTOKENAMOUNT OP_NUMEQUALVERIFY OP_2 OP_OUTPUTTOKENAMOUNT OP_0 OP_NUMEQUALVERIFY OP_2 OP_OUTPUTVALUE OP_2 OP_UTXOVALUE OP_NUMEQUALVERIFY OP_3 OP_INPUTSEQUENCENUMBER 020040 OP_NUMEQUALVERIFY OP_3 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_DUP OP_SIZE OP_NIP 2e626368 20 OP_5 OP_ROLL OP_CAT OP_OVER OP_SIZE OP_NIP OP_CAT OP_SWAP OP_CAT OP_SWAP OP_CAT OP_OVER OP_CAT OP_3 OP_ROLL OP_CAT OP_HASH256 aa20 OP_SWAP OP_CAT 87 OP_CAT OP_3 OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_4 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_3 OP_OUTPUTTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_3 OP_OUTPUTVALUE e803 OP_NUMEQUALVERIFY OP_4 OP_OUTPUTTOKENCOMMITMENT OP_BIN2NUM OP_3 OP_UTXOTOKENAMOUNT OP_NUMEQUALVERIFY OP_4 OP_OUTPUTVALUE e803 OP_NUMEQUALVERIFY OP_5 OP_OUTPUTTOKENCOMMITMENT OP_4 OP_OUTPUTTOKENCOMMITMENT OP_ROT OP_CAT OP_EQUALVERIFY OP_5 OP_OUTPUTBYTECODE 76a914 OP_ROT OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_5 OP_OUTPUTVALUE e803 OP_NUMEQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_3 OP_UTXOVALUE 8813 OP_SUB a08601 OP_3 OP_UTXOTOKENAMOUNT OP_SUB OP_MUL a08601 OP_DIV OP_DUP 204e OP_GREATERTHAN OP_IF OP_6 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_6 OP_OUTPUTVALUE OP_OVER OP_NUMEQUALVERIFY OP_6 OP_OUTPUTBYTECODE 76a914 OP_3 OP_PICK OP_CAT 88ac OP_CAT OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_1',
+ 'source': "pragma cashscript 0.11.4;\n\n/**\n * @param nameContractBytecode - Partial bytecode of the name contract\n * @param creatorIncentivePKH - PKH of the creator incentive\n */\ncontract Factory(\n bytes nameContractBytecode,\n bytes20 creatorIncentivePKH,\n) {\n /**\n * This function finalizes a name registration auction by:\n * - Verifying the auction has ended and the winner's bid is valid\n * - Issuing an immutable externalAuthNFT to the Name Contract\n * - Issuing an immutable internalAuthNFT to the Name Contract\n * - Issuing an immutable name NFT to the auction winner\n * - Distributing auction fees between the platform and miners\n * - Burning the auctionNFT\n *\n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract\n * - Input2: NameMintingNFT from the Registry Contract\n * - Input3: auctionNFT from the Registry Contract\n *\n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change\n * - Output2: NameMintingNFT back to the Registry contract\n * - Output3: External Auth NFT to the name contract\n * - Output4: Internal Auth NFT to the name contract\n * - Output5: Name NFT to the auction winner\n * - Output6: Platform fee [Reduces and the not included]\n *\n */\n function call(){\n require(tx.inputs.length == 4, \"Transaction: must have exactly 4 inputs\");\n require(tx.outputs.length <= 7, \"Transaction: must have at most 7 outputs\");\n\n // This contract can only be used at input1 and it should return to itself.\n require(this.activeInputIndex == 1, \"Input 1: factory contract UTXO must be at this index\");\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, \"Input 1: locking bytecode must match output 1\");\n // Ensure that no tokenCategory is minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, \"Output 1: must not have any token category (pure BCH only)\");\n // Strict value checks to ensure the platform and miner get fee.\n require(tx.inputs[this.activeInputIndex].value == tx.outputs[this.activeInputIndex].value, \"Input 1: satoshi value must match output 1\");\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, \"Input 2: name minting NFT locking bytecode does not match registry input's locking bytecode\");\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, \"Input 3: auction NFT locking bytecode does not match registry input's locking bytecode\");\n require(tx.outputs[2].lockingBytecode == registryInputLockingBytecode, \"Output 2: name minting NFT locking bytecode does not match registry input's locking bytecode\");\n\n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n require(tx.outputs[3].tokenCategory == registryInputCategory, \"Output 3: external auth NFT token category prefix must match registry\");\n require(tx.outputs[4].tokenCategory == registryInputCategory, \"Output 4: internal auth NFT token category prefix must match registry\");\n require(tx.outputs[5].tokenCategory == registryInputCategory, \"Output 5: name ownership NFT token category prefix must match registry\");\n\n // NameMintingNFT should be minting and of the 'nameCategory' i.e registryInputCategory\n bytes nameMintingCategory, bytes nameMintingCapability = tx.inputs[2].tokenCategory.split(32);\n require(nameMintingCategory == registryInputCategory, \"Input 2: name minting NFT token category prefix must match registry\");\n // Minting\n require(nameMintingCapability == 0x02, \"Input 2: name minting NFT capability must be minting (0x02)\");\n // NameMintingNFT should keep the same category and capability\n require(tx.inputs[2].tokenCategory == tx.outputs[2].tokenCategory, \"Output 2: name minting NFT token category must match input 2\");\n\n // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory, \"Input 3: auction NFT token category prefix must match registry\");\n // Mutable\n require(auctionCapability == 0x01, \"Input 3: auction NFT capability must be mutable (0x01)\");\n\n // NameMintingNFT has no nftCommitment\n require(tx.outputs[2].nftCommitment == 0x, \"Output 2: name minting NFT must have empty commitment\");\n require(tx.inputs[2].nftCommitment == 0x, \"Input 2: name minting NFT must have empty commitment\");\n\n // NameMintingNFT has no tokenAmount\n require(tx.outputs[2].tokenAmount == tx.inputs[2].tokenAmount, \"Output 2: name minting NFT token amount must match input 2\");\n require(tx.outputs[2].tokenAmount == 0, \"Output 2: name minting NFT token amount must be 0\");\n\n // Strict value check\n require(tx.outputs[2].value == tx.inputs[2].value, \"Output 2: name minting NFT satoshi value must match input 2\");\n\n // Enforcing the relative timelock, the auctionNFT must be atleast 4194306 old\n // to be considered ended.\n // 4194306 is sequence number in time, 2*512 seconds\n\n // TODO: Make this 3 hours\n require(tx.inputs[3].sequenceNumber == 4194306, \"Input 3: auction NFT sequence number must equal 4194306\");\n\n // Extract the PKH and name from the auctionNFT\n bytes20 bidderPKH, bytes name = tx.inputs[3].nftCommitment.split(20);\n \n // Get the name length to generate the complete bytecode of the name contract\n int nameLength = name.length;\n // category + name + bytecode.\n // Note: `inactivityExpiryTime` in the name is already added to the nameContractBytecode in the constructor.\n bytes constant tld = bytes('.bch');\n bytes nameBytecode = 0x20 + registryInputCategory + bytes(tld.length) + tld + bytes(nameLength) + name + nameContractBytecode;\n bytes32 scriptHash = hash256(nameBytecode);\n bytes35 nameLockingBytecode = new LockingBytecodeP2SH32(scriptHash);\n \n // ExternalAuthNFT goes to the name contract\n require(tx.outputs[3].lockingBytecode == nameLockingBytecode, \"Output 3: external auth NFT locking bytecode must match name contract\");\n // InternalAuthNFT goes to the name contract\n require(tx.outputs[4].lockingBytecode == nameLockingBytecode, \"Output 4: internal auth NFT locking bytecode must match name contract\");\n \n // ExternalAuthNFT does not have any commitment\n require(tx.outputs[3].nftCommitment == 0x, \"Output 3: external auth NFT must have empty commitment\");\n // Strict value check\n require(tx.outputs[3].value == 1000, \"Output 3: external auth NFT satoshi value must be 1000\");\n\n // InternalAuthNFT has registrationID as the commitment so it can be used to authenticate\n // along with the ownershipNFT\n int registrationId = int(tx.outputs[4].nftCommitment);\n require(tx.inputs[3].tokenAmount == registrationId, \"Output 4: internal auth NFT commitment must match registration ID\");\n // Strict value check\n require(tx.outputs[4].value == 1000, \"Output 4: internal auth NFT satoshi value must be 1000\");\n\n // Send the name ownership NFT to the bidder\n require(tx.outputs[5].nftCommitment == tx.outputs[4].nftCommitment + name, \"Output 5: name ownership NFT commitment must match registration ID + name\");\n require(tx.outputs[5].lockingBytecode == new LockingBytecodeP2PKH(bidderPKH), \"Output 5: name ownership NFT locking bytecode must match bidder PKH\");\n require(tx.outputs[5].value == 1000, \"Output 5: name ownership NFT satoshi value must be 1000\");\n\n // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount, \"Output 0: token amount must equal input 0 + input 3 amounts (accumulation)\");\n\n // Dual Decay mechanism, creator incentive decays linearly with the step.\n\n // Initial minimal fee is 5000; 1000 * 3 (nft outputs) + 2000 Miner Fee\n int minimalDeduction = tx.inputs[3].value - 5000;\n int creatorIncentive = (minimalDeduction * (1e5 - tx.inputs[3].tokenAmount) / 1e5);\n\n // If incentive is > 20000 satoshis, then it goes to the creator, else it goes to the miners.\n if(creatorIncentive > 20000) {\n require(tx.outputs[6].tokenCategory == 0x, \"Output 6: creator incentive must be pure BCH (no token category)\");\n // Enforce that the other piece of the fee goes to the miners.\n require(tx.outputs[6].value == creatorIncentive, \"Output 6: creator incentive satoshi value must match calculated incentive\");\n require(tx.outputs[6].lockingBytecode == new LockingBytecodeP2PKH(creatorIncentivePKH), \"Output 6: creator incentive locking bytecode must match creator PKH\");\n }\n }\n\n}",
+ 'debug': {
+ 'bytecode': 'c3549dc457a169c0519dc0c7c0cd88c0d10088c0c6c0cc9d00c752c7788853c7788852cd8800ce53d1788854d1788855d1788852ce01207f7c527988528852ce52d18853ce01207f7c527988518852d2008852cf008852d352d09d52d3009d52cc52c69d53cb030200409d53cf01147f768277042e6263680120557a7e7882777e7c7e7c7e787e537a7eaa02aa207c7e01877e53cd788854cd8853d2008853cc02e8039d54d28153d09d54cc02e8039d55d254d27b7e8855cd0376a9147b7e0288ac7e8855cc02e8039d00d300d053d0939d53c60288139403a0860153d0949503a08601967602204ea06356d1008856cc789d56cd0376a91453797e0288ac7e88686d51',
+ 'sourceMap': '37:12:37:28;:32::33;:4::78:1;38:12:38:29:0;:33::34;:12:::1;:4::80;41:12:41:33:0;:37::38;:4::96:1;42:22:42:43:0;:12::60:1;:75::96:0;:64::113:1;:4::164;44:23:44:44:0;:12::59:1;:63::65:0;:4::129:1;46:22:46:43:0;:12::50:1;:65::86:0;:54::93:1;:4::141;52:51:52:52:0;:41::69:1;53:22:53:23:0;:12::40:1;:44::72:0;:4::169:1;54:22:54:23:0;:12::40:1;:44::72:0;:4::164:1;55:23:55:24:0;:12::41:1;:4::171;58:44:58:45:0;:34::60:1;59:23:59:24:0;:12::39:1;:43::64:0;:4::139:1;60:23:60:24:0;:12::39:1;:43::64:0;:4::139:1;61:23:61:24:0;:12::39:1;:43::64:0;:4::140:1;64:71:64:72:0;:61::87:1;:94::96:0;:61::97:1;65:12:65:31:0;:35::56;;:4::129:1;67:37:67:41:0;:4::106:1;69:22:69:23:0;:12::38:1;:53::54:0;:42::69:1;:4::135;72:63:72:64:0;:53::79:1;:86::88:0;:53::89:1;73:12:73:27:0;:31::52;;:4::120:1;75:33:75:37:0;:4::97:1;78:23:78:24:0;:12::39:1;:43::45:0;:4::104:1;79:22:79:23:0;:12::38:1;:42::44:0;:4::102:1;82:23:82:24:0;:12::37:1;:51::52:0;:41::65:1;:4::129;83:23:83:24:0;:12::37:1;:41::42:0;:4::97:1;86:23:86:24:0;:12::31:1;:45::46:0;:35::53:1;:4::118;93:22:93:23:0;:12::39:1;:43::50:0;:4::111:1;96:46:96:47:0;:36::62:1;:69::71:0;:36::72:1;99:21:99:25:0;:::32:1;;102:31:102:37:0;103:25:103:29;:32::53;;:25:::1;:62::65:0;:::72:1;;:25::73;:76::79:0;:25:::1;:88::98:0;:25::99:1;:102::106:0;:25:::1;:109::129:0;;:25:::1;104::104:46;105:34:105:71:0;:60::70;:34::71:1;;;108:23:108:24:0;:12::41:1;:45::64:0;:4::139:1;110:23:110:24:0;:12::41:1;:4::139;113:23:113:24:0;:12::39:1;:43::45:0;:4::105:1;115:23:115:24:0;:12::31:1;:35::39:0;:4::99:1;119:40:119:41:0;:29::56:1;:25::57;120:22:120:23:0;:12::36:1;:4::125;122:23:122:24:0;:12::31:1;:35::39:0;:4::99:1;125:23:125:24:0;:12::39:1;:54::55:0;:43::70:1;:73::77:0;:43:::1;:4::156;126:23:126:24:0;:12::41:1;:45::80:0;:70::79;:45::80:1;;;:4::153;127:23:127:24:0;:12::31:1;:35::39:0;:4::100:1;131:23:131:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::172;136:37:136:38:0;:27::45:1;:48::52:0;:27:::1;137:48:137:51:0;:64::65;:54::78:1;:48;:28::79;:82::85:0;:28:::1;140:7:140:23:0;:26::31;:7:::1;:33:145:5:0;141:25:141:26;:14::41:1;:45::47:0;:6::117:1;143:25:143:26:0;:14::33:1;:37::53:0;:6::132:1;144:25:144:26:0;:14::43:1;:47::92:0;:72::91;;:47::92:1;;;:6::165;140:33:145:5;36:2:146:3;',
+ 'logs': [],
+ 'requires': [
+ {
+ 'ip': 4,
+ 'line': 37,
+ 'message': 'Transaction: must have exactly 4 inputs',
+ },
+ {
+ 'ip': 8,
+ 'line': 38,
+ 'message': 'Transaction: must have at most 7 outputs',
+ },
+ {
+ 'ip': 11,
+ 'line': 41,
+ 'message': 'Input 1: factory contract UTXO must be at this index',
+ },
+ {
+ 'ip': 16,
+ 'line': 42,
+ 'message': 'Input 1: locking bytecode must match output 1',
+ },
+ {
+ 'ip': 20,
+ 'line': 44,
+ 'message': 'Output 1: must not have any token category (pure BCH only)',
+ },
+ {
+ 'ip': 25,
+ 'line': 46,
+ 'message': 'Input 1: satoshi value must match output 1',
+ },
+ {
+ 'ip': 31,
+ 'line': 53,
+ 'message': "Input 2: name minting NFT locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 35,
+ 'line': 54,
+ 'message': "Input 3: auction NFT locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 38,
+ 'line': 55,
+ 'message': "Output 2: name minting NFT locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 44,
+ 'line': 59,
+ 'message': 'Output 3: external auth NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 48,
+ 'line': 60,
+ 'message': 'Output 4: internal auth NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 52,
+ 'line': 61,
+ 'message': 'Output 5: name ownership NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 60,
+ 'line': 65,
+ 'message': 'Input 2: name minting NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 62,
+ 'line': 67,
+ 'message': 'Input 2: name minting NFT capability must be minting (0x02)',
+ },
+ {
+ 'ip': 67,
+ 'line': 69,
+ 'message': 'Output 2: name minting NFT token category must match input 2',
+ },
+ {
+ 'ip': 75,
+ 'line': 73,
+ 'message': 'Input 3: auction NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 77,
+ 'line': 75,
+ 'message': 'Input 3: auction NFT capability must be mutable (0x01)',
+ },
+ {
+ 'ip': 81,
+ 'line': 78,
+ 'message': 'Output 2: name minting NFT must have empty commitment',
+ },
+ {
+ 'ip': 85,
+ 'line': 79,
+ 'message': 'Input 2: name minting NFT must have empty commitment',
+ },
+ {
+ 'ip': 90,
+ 'line': 82,
+ 'message': 'Output 2: name minting NFT token amount must match input 2',
+ },
+ {
+ 'ip': 94,
+ 'line': 83,
+ 'message': 'Output 2: name minting NFT token amount must be 0',
+ },
+ {
+ 'ip': 99,
+ 'line': 86,
+ 'message': 'Output 2: name minting NFT satoshi value must match input 2',
+ },
+ {
+ 'ip': 103,
+ 'line': 93,
+ 'message': 'Input 3: auction NFT sequence number must equal 4194306',
+ },
+ {
+ 'ip': 138,
+ 'line': 108,
+ 'message': 'Output 3: external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 141,
+ 'line': 110,
+ 'message': 'Output 4: internal auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 145,
+ 'line': 113,
+ 'message': 'Output 3: external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 149,
+ 'line': 115,
+ 'message': 'Output 3: external auth NFT satoshi value must be 1000',
+ },
+ {
+ 'ip': 155,
+ 'line': 120,
+ 'message': 'Output 4: internal auth NFT commitment must match registration ID',
+ },
+ {
+ 'ip': 159,
+ 'line': 122,
+ 'message': 'Output 4: internal auth NFT satoshi value must be 1000',
+ },
+ {
+ 'ip': 166,
+ 'line': 125,
+ 'message': 'Output 5: name ownership NFT commitment must match registration ID + name',
+ },
+ {
+ 'ip': 174,
+ 'line': 126,
+ 'message': 'Output 5: name ownership NFT locking bytecode must match bidder PKH',
+ },
+ {
+ 'ip': 178,
+ 'line': 127,
+ 'message': 'Output 5: name ownership NFT satoshi value must be 1000',
+ },
+ {
+ 'ip': 186,
+ 'line': 131,
+ 'message': 'Output 0: token amount must equal input 0 + input 3 amounts (accumulation)',
+ },
+ {
+ 'ip': 205,
+ 'line': 141,
+ 'message': 'Output 6: creator incentive must be pure BCH (no token category)',
+ },
+ {
+ 'ip': 209,
+ 'line': 143,
+ 'message': 'Output 6: creator incentive satoshi value must match calculated incentive',
+ },
+ {
+ 'ip': 218,
+ 'line': 144,
+ 'message': 'Output 6: creator incentive locking bytecode must match creator PKH',
+ },
+ ],
+ },
+ 'compiler': {
+ 'name': 'cashc',
+ 'version': '0.11.4',
+ },
+ 'updatedAt': '2025-08-10T17:47:19.846Z',
+};
diff --git a/lib/compiled/Name.ts b/lib/compiled/Name.ts
new file mode 100644
index 0000000..b8a6653
--- /dev/null
+++ b/lib/compiled/Name.ts
@@ -0,0 +1,349 @@
+export default {
+ 'contractName': 'Name',
+ 'constructorInputs': [
+ {
+ 'name': 'name',
+ 'type': 'bytes',
+ },
+ {
+ 'name': 'tld',
+ 'type': 'bytes',
+ },
+ {
+ 'name': 'nameCategory',
+ 'type': 'bytes',
+ },
+ ],
+ 'abi': [
+ {
+ 'name': 'useAuth',
+ 'inputs': [
+ {
+ 'name': 'authID',
+ 'type': 'int',
+ },
+ ],
+ },
+ {
+ 'name': 'penaliseInvalidName',
+ 'inputs': [
+ {
+ 'name': 'characterNumber',
+ 'type': 'int',
+ },
+ ],
+ },
+ {
+ 'name': 'resolveOwnerConflict',
+ 'inputs': [],
+ },
+ {
+ 'name': 'burn',
+ 'inputs': [],
+ },
+ ],
+ 'bytecode': 'OP_3 OP_PICK OP_0 OP_NUMEQUAL OP_IF OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_INPUTINDEX OP_OUTPUTTOKENCOMMITMENT OP_EQUALVERIFY OP_4 OP_ROLL OP_1 OP_NUMEQUAL OP_IF OP_INPUTINDEX OP_1ADD OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_1ADD OP_OUTPUTTOKENCATEGORY OP_INPUTINDEX OP_1ADD OP_UTXOTOKENCATEGORY OP_EQUALVERIFY OP_INPUTINDEX OP_1ADD OP_UTXOTOKENCOMMITMENT OP_8 OP_SPLIT OP_DUP OP_3 OP_PICK OP_5 OP_PICK OP_CAT OP_EQUALVERIFY OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_2 OP_PICK OP_EQUALVERIFY OP_INPUTINDEX OP_1ADD OP_OUTPUTTOKENCOMMITMENT OP_INPUTINDEX OP_1ADD OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_2DROP OP_ELSE OP_INPUTINDEX OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_ENDIF OP_2DROP OP_2DROP OP_1 OP_ELSE OP_3 OP_PICK OP_1 OP_NUMEQUAL OP_IF OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_TXINPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_0 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_UTXOBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_1 OP_UTXOTOKENCATEGORY OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_1 OP_UTXOTOKENCOMMITMENT OP_SIZE OP_NIP OP_ROT OP_SIZE OP_NIP OP_SUB OP_1 OP_UTXOTOKENCOMMITMENT OP_SWAP OP_SPLIT OP_DROP OP_8 OP_SPLIT OP_NIP OP_3 OP_PICK OP_SPLIT OP_DROP OP_3 OP_PICK OP_1SUB OP_SPLIT OP_NIP OP_BIN2NUM OP_DUP 2d OP_NUMNOTEQUAL OP_VERIFY OP_DUP 61 7b OP_WITHIN OP_NOT OP_VERIFY OP_DUP 41 5b OP_WITHIN OP_NOT OP_VERIFY 30 3a OP_WITHIN OP_NOT OP_VERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NIP OP_NIP OP_NIP OP_ELSE OP_3 OP_PICK OP_2 OP_NUMEQUAL OP_IF OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_TXINPUTCOUNT OP_5 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_3 OP_LESSTHANOREQUAL OP_VERIFY OP_4 OP_UTXOTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_TXOUTPUTCOUNT OP_3 OP_NUMEQUAL OP_IF OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_ENDIF OP_INPUTINDEX OP_UTXOBYTECODE OP_0 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_0 OP_OUTPUTBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCOMMITMENT OP_1 OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_1 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_3 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_1 OP_OUTPUTTOKENCATEGORY OP_3 OP_ROLL OP_EQUALVERIFY OP_1 OP_UTXOTOKENCOMMITMENT OP_BIN2NUM OP_3 OP_UTXOTOKENCOMMITMENT OP_BIN2NUM OP_LESSTHAN OP_NIP OP_NIP OP_NIP OP_ELSE OP_3 OP_ROLL OP_3 OP_NUMEQUALVERIFY OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_TXINPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_1 OP_NUMEQUALVERIFY OP_2 OP_UTXOTOKENCATEGORY OP_0 OP_EQUAL OP_IF OP_1 OP_INPUTSEQUENCENUMBER 010040 OP_NUMEQUALVERIFY OP_ELSE OP_2 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_8 OP_SPLIT OP_DROP OP_1 OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_ENDIF OP_INPUTINDEX OP_UTXOBYTECODE OP_0 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_1 OP_UTXOBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_3 OP_PICK OP_EQUALVERIFY OP_1 OP_UTXOTOKENCATEGORY OP_3 OP_ROLL OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NIP OP_NIP OP_ENDIF OP_ENDIF OP_ENDIF',
+ 'source': 'pragma cashscript 0.11.4;\n\n/**\n * @param name The name of the name.\n * @param tld The TLD of the name.\n * @param nameCategory The category of the name.\n */\ncontract Name(\n bytes name,\n bytes tld,\n bytes nameCategory\n ) {\n \n /**\n * This function can be used to perform a variety of actions.\n *\n * For example:\n * - It can be used to prove the the ownership of the name by other contracts.\n * - This function allows the owner to perform any actions in conjunction with other contracts.\n * - This function can be used to add records and invalidate multiple records in a single transaction.\n *\n * Records are created using OP_RETURN outputs. To add a record, include the record data directly in the OP_RETURN output.\n * To invalidate a record, include the hash of the record content in the OP_RETURN output. This will signal\n * the library/indexers to exclude the record from the valid records.\n * \n * @inputs\n * - Inputx: Internal/External Auth NFT\n * - Inputx+1 (optional): Name ownership NFT from the owner\n * \n * @outputs\n * - Outputx: Internal/External Auth NFT returned to this contract\n * - Outputx+1 (optional): Name NFT returned\n * \n */\n function useAuth(int authID) {\n // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.\n require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");\n\n // The activeInputIndex can be anything as long as the utxo properties are preserved and comes back to the\n // contract without alteration.\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, "Auth input: locking bytecode must match active output");\n require(tx.inputs[this.activeInputIndex].tokenCategory == nameCategory, "Auth input: token category must match name category");\n require(tx.outputs[this.activeInputIndex].tokenCategory == nameCategory, "Auth output: token category must match name category");\n require(tx.inputs[this.activeInputIndex].nftCommitment == tx.outputs[this.activeInputIndex].nftCommitment, "Auth input: NFT commitment must match active output");\n\n if(authID == 1) {\n // The next input from the InternalAuthNFT must be the ownershipNFT.\n require(tx.inputs[this.activeInputIndex + 1].tokenCategory == nameCategory, "Ownership input: ownership NFT token category must match name category");\n require(tx.outputs[this.activeInputIndex + 1].tokenCategory == tx.inputs[this.activeInputIndex + 1].tokenCategory, "Ownership output: token category must match active input");\n\n bytes registrationId, bytes nameFromOwnerNFT = tx.inputs[this.activeInputIndex + 1].nftCommitment.split(8);\n require(nameFromOwnerNFT == name + tld, "Ownership input: ownership NFT name must match contract name + TLD");\n require(tx.inputs[this.activeInputIndex].nftCommitment == registrationId, "Auth input: internal auth NFT commitment must match ownership NFT registration ID");\n require(tx.outputs[this.activeInputIndex + 1].nftCommitment == tx.inputs[this.activeInputIndex + 1].nftCommitment, "Ownership output: NFT commitment must match active input");\n\n // Note only the commitment and category are enforced on the next output index and not the locking bytecode.\n } else {\n // One known use of ExternalAuthNFT in the `OwnershipGuard` contract. ExternalAuthNFT is\n // used to prove that an owner exists.\n require(tx.inputs[this.activeInputIndex].nftCommitment == 0x, "Auth input: external auth NFT must have empty commitment");\n }\n }\n\n /**\n * If an invalid name is registered, this function allows anyone to burn the NFTs\n * @inputs\n * - Input0: External Auth NFT from self\n * - Input1: Internal Auth NFT from self\n * - Input2: BCH input from anyone\n * \n * @outputs \n * - Output0: BCH change output\n */\n function penaliseInvalidName(int characterNumber) {\n // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.\n require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");\n\n require(tx.inputs.length == 3, "Transaction: must have exactly 3 inputs");\n require(tx.outputs.length == 1, "Transaction: must have exactly 1 output");\n\n bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;\n require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: external auth NFT locking bytecode must match name contract");\n require(tx.inputs[1].lockingBytecode == selfLockingBytecode, "Input 1: internal auth NFT locking bytecode must match name contract");\n\n require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: external auth NFT token category must match name category");\n require(tx.inputs[1].tokenCategory == nameCategory, "Input 1: internal auth NFT token category must match name category");\n\n // External Auth NFT\n require(tx.inputs[0].nftCommitment == 0x, "Input 0: external auth NFT must have empty commitment");\n\n // Internal Auth NFT\n // First 8 bytes are the registrationID and the rest is the name.\n int sliceEndIndex = tx.inputs[1].nftCommitment.length - tld.length;\n bytes nameFromNFT = tx.inputs[1].nftCommitment.slice(8, sliceEndIndex);\n\n bytes characterSplitBytes = nameFromNFT.split(characterNumber)[0];\n characterNumber = characterNumber - 1;\n bytes character = characterSplitBytes.split(characterNumber)[1];\n int charVal = int(character);\n\n // Character is not a hyphen.\n require(charVal != 45, "Character is a hyphen"); \n // Character is not from a-z.\n require(!within(charVal, 97, 123), "Character is lowercase letter");\n // Character is not from A-Z.\n require(!within(charVal, 65, 91), "Character is uppercase letter");\n // Character is not from 0-9.\n require(!within(charVal, 48, 58), "Character is digit");\n\n // Pure BCH, ensures burn\n require(tx.outputs[0].tokenCategory == 0x, "Output 0: change must be pure BCH (no token category)");\n }\n\n /**\n * If the incentive system fails, i.e `OwnershipGuard` or `AuctionConflictResolver` fails to prevent a\n * a owner conflict. When this happens there will be > 1 owner for this name.\n * The owner with the lowest registrationID must be the only owner for this name.\n * To help enforce this rule, this function will allow anyone to burn both the Auth NFTs of the NEW owner.\n *\n * @inputs\n * - Input0: Valid External Auth NFT from self\n * - Input1: Valid Internal Auth NFT from self\n * - Input2: Invalid External Auth NFT from self\n * - Input3: Invalid Internal Auth NFT from self\n * - Input4: BCH input from anyone\n * \n * @outputs \n * - Output0: Valid External Auth NFT back to self\n * - Output1: Valid Internal Auth NFT back to self\n * - Output2: BCH change output\n */\n function resolveOwnerConflict(){\n // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.\n require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");\n\n require(tx.inputs.length == 5, "Transaction: must have exactly 5 inputs");\n require(tx.outputs.length <= 3, "Transaction: must have at most 3 outputs");\n\n // Pure BCH input and output to fund the transaction\n require(tx.inputs[4].tokenCategory == 0x, "Input 4: funding input must be pure BCH (no token category)");\n if (tx.outputs.length == 3) {\n require(tx.outputs[2].tokenCategory == 0x, "Output 2: change must be pure BCH (no token category)");\n }\n\n bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;\n require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: valid external auth NFT locking bytecode must match name contract");\n require(tx.inputs[1].lockingBytecode == selfLockingBytecode, "Input 1: valid internal auth NFT locking bytecode must match name contract");\n require(tx.inputs[2].lockingBytecode == selfLockingBytecode, "Input 2: invalid external auth NFT locking bytecode must match name contract");\n require(tx.inputs[3].lockingBytecode == selfLockingBytecode, "Input 3: invalid internal auth NFT locking bytecode must match name contract");\n\n require(tx.outputs[0].lockingBytecode == selfLockingBytecode, "Output 0: valid external auth NFT locking bytecode must match name contract");\n require(tx.outputs[1].lockingBytecode == selfLockingBytecode, "Output 1: valid internal auth NFT locking bytecode must match name contract");\n\n // External Auth NFTs\n require(tx.inputs[0].nftCommitment == 0x, "Input 0: valid external auth NFT must have empty commitment");\n require(tx.inputs[2].nftCommitment == 0x, "Input 2: invalid external auth NFT must have empty commitment");\n\n // Commitments of Valid Auth NFts back to self\n require(tx.outputs[0].nftCommitment == 0x, "Output 0: valid external auth NFT must have empty commitment");\n require(tx.outputs[1].nftCommitment == tx.inputs[1].nftCommitment, "Output 1: valid internal auth NFT commitment must match input 1");\n\n // Ensure that all the token inputs and outputs have nameCategory\n require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: valid external auth NFT token category must match name category");\n require(tx.inputs[1].tokenCategory == nameCategory, "Input 1: valid internal auth NFT token category must match name category");\n require(tx.inputs[2].tokenCategory == nameCategory, "Input 2: invalid external auth NFT token category must match name category");\n require(tx.inputs[3].tokenCategory == nameCategory, "Input 3: invalid internal auth NFT token category must match name category");\n\n require(tx.outputs[0].tokenCategory == nameCategory, "Output 0: valid external auth NFT token category must match name category");\n require(tx.outputs[1].tokenCategory == nameCategory, "Output 1: valid internal auth NFT token category must match name category");\n\n // Compare the registrationID\n require(int(tx.inputs[1].nftCommitment) < int(tx.inputs[3].nftCommitment), "Input 1: valid internal auth NFT registration ID must be lower than input 3");\n }\n\n /**\n * Allows the name owner or anyone to burn the InternalAuthNFT and externalAuthNFT making this name available\n * for auction.\n * \n * - Owner can burn the AuthNFTs anytime.\n * - External party can burn the AuthNFTs when the internalAuth NFT has not been used for more than `inactivityExpiryTime`.\n *\n * @inputs\n * - Input0: External Auth NFT\n * - Input1: Internal Auth NFT\n * - Input2: Pure BCH or Name ownership NFT from the owner\n *\n * @outputs \n * - Output0: BCH change\n *\n */\n function burn() {\n // Need transaction version 2 to prevent any vulnerabilities caused due to future versions.\n // Need version 2 enforcement for relative timelocks.\n require(tx.version == 2, "Transaction: Version must be 2 (relative timelocks required)");\n\n require(tx.inputs.length == 3, "Transaction: must have exactly 3 inputs");\n require(tx.outputs.length == 1, "Transaction: must have exactly 1 output");\n\n // If an external party is attempting to burn the authNFTs\n if (tx.inputs[2].tokenCategory == 0x) {\n // If pure BCH input, then allow anyone to burn given the time limit has passed.\n // 4194305 is sequence number in time, 1*512 seconds\n\n // TODO: Make this 2 years\n require(tx.inputs[1].sequenceNumber == 4194305, "Input 1: internal auth NFT sequence number must equal inactivity expiry time");\n } else {\n // If name ownership NFT input, then allow the owner to burn anytime.\n require(tx.inputs[2].tokenCategory == nameCategory, "Input 2: name ownership NFT token category must match name category");\n // Make sure that the registrationID in the nameOwnershipNFT and the internalAuthNFT are the same.\n require(tx.inputs[2].nftCommitment.split(8)[0] == tx.inputs[1].nftCommitment, "Input 2: name ownership NFT registration ID must match input 1 commitment");\n }\n\n bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;\n require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: external auth NFT locking bytecode must match name contract");\n require(tx.inputs[1].lockingBytecode == selfLockingBytecode, "Input 1: internal auth NFT locking bytecode must match name contract");\n\n // ExternalAuthNFT\n require(tx.inputs[0].nftCommitment == 0x, "Input 0: external auth NFT must have empty commitment");\n require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: external auth NFT token category must match name category");\n require(tx.inputs[1].tokenCategory == nameCategory, "Input 1: internal auth NFT token category must match name category");\n\n // Return the BCH as change.\n require(tx.outputs[0].tokenCategory == 0x, "Output 0: change must be pure BCH (no token category)");\n }\n}\n',
+ 'debug': {
+ 'bytecode': '5379009c63c2529dc0c7c0cd88c0ce537988c0d1537988c0cfc0d288547a519c63c08bce537988c08bd1c08bce88c08bcf587f76537955797e88c0cf527988c08bd2c08bcf886d67c0cf0088686d6d51675379519c63c2529dc3539dc4519dc0c700c7788851c78800ce53798851ce537a8800cf008851cf82777b82779451cf7c7f75587f7753797f7553798c7f778176012d9e69760161017ba59169760141015ba591690130013aa5916900d10087777777675379529c63c2529dc3559dc453a16954ce0088c4539c6352d1008868c0c700c7788851c7788852c7788853c7788800cd788851cd8800cf008852cf008800d2008851d251cf8800ce53798851ce53798852ce53798853ce53798800d153798851d1537a8851cf8153cf819f77777767537a539dc2529dc3539dc4519d52ce00876351cb030100409d6752ce53798852cf587f7551cf8868c0c700c7788851c78800cf008800ce53798851ce537a8800d100877777686868',
+ 'sourceMap': '35:2:62:3;;;;;37:12:37:22;:26::27;:4::93:1;41:22:41:43:0;:12::60:1;:75::96:0;:64::113:1;:4::172;42:22:42:43:0;:12::58:1;:62::74:0;;:4::131:1;43:23:43:44:0;:12::59:1;:63::75:0;;:4::133:1;44:22:44:43:0;:12::58:1;:73::94:0;:62::109:1;:4::166;46:7:46:13:0;;:17::18;:7:::1;:20:57:5:0;48:24:48:45;:::49:1;:14::64;:68::80:0;;:6::156:1;49:25:49:46:0;:::50:1;:14::65;:79::100:0;:::104:1;:69::119;:6::181;51:63:51:84:0;:::88:1;:53::103;:110::111:0;:53::112:1;52:14:52:30:0;:34::38;;:41::44;;:34:::1;:6::116;53:24:53:45:0;:14::60:1;:64::78:0;;:6::165:1;54:25:54:46:0;:::50:1;:14::65;:79::100:0;:::104:1;:69::119;:6::181;46:20:57:5;57:11:61::0;60:24:60:45;:14::60:1;:64::66:0;:6::128:1;57:11:61:5;35:2:62:3;;;;74::112::0;;;;;76:12:76:22;:26::27;:4::93:1;78:12:78:28:0;:32::33;:4::78:1;79:12:79:29:0;:33::34;:4::79:1;81:42:81:63:0;:32::80:1;82:22:82:23:0;:12::40:1;:44::63:0;:4::137:1;83:22:83:23:0;:12::40:1;:4::137;85:22:85:23:0;:12::38:1;:42::54:0;;:4::126:1;86:22:86:23:0;:12::38:1;:42::54:0;;:4::126:1;89:22:89:23:0;:12::38:1;:42::44:0;:4::103:1;93:34:93:35:0;:24::50:1;:::57;;:60::63:0;:::70:1;;:24;94:34:94:35:0;:24::50:1;:60::73:0;:24::74:1;;:57::58:0;:24::74:1;;96:50:96:65:0;;:32::66:1;:::69;97:22:97:37:0;;:::41:1;98::98:64;:::67;99:18:99:32;102:12:102:19:0;:23::25;:12:::1;:4::52;104:20:104:27:0;:29::31;:33::36;:13::37:1;:12;:4::72;106:20:106:27:0;:29::31;:33::35;:13::36:1;:12;:4::71;108:29:108:31:0;:33::35;:13::36:1;:12;:4::60;111:23:111:24:0;:12::39:1;:43::45:0;:4::104:1;74:2:112:3;;;;132::173::0;;;;;134:12:134:22;:26::27;:4::93:1;136:12:136:28:0;:32::33;:4::78:1;137:12:137:29:0;:33::34;:12:::1;:4::80;140:22:140:23:0;:12::38:1;:42::44:0;:4::109:1;141:8:141:25:0;:29::30;:8:::1;:32:143:5:0;142:25:142:26;:14::41:1;:45::47:0;:6::106:1;141:32:143:5;145:42:145:63:0;:32::80:1;146:22:146:23:0;:12::40:1;:44::63:0;:4::143:1;147:22:147:23:0;:12::40:1;:44::63:0;:4::143:1;148:22:148:23:0;:12::40:1;:44::63:0;:4::145:1;149:22:149:23:0;:12::40:1;:44::63:0;:4::145:1;151:23:151:24:0;:12::41:1;:45::64:0;:4::145:1;152:23:152:24:0;:12::41:1;:4::145;155:22:155:23:0;:12::38:1;:42::44:0;:4::109:1;156:22:156:23:0;:12::38:1;:42::44:0;:4::111:1;159:23:159:24:0;:12::39:1;:43::45:0;:4::111:1;160:23:160:24:0;:12::39:1;:53::54:0;:43::69:1;:4::138;163:22:163:23:0;:12::38:1;:42::54:0;;:4::132:1;164:22:164:23:0;:12::38:1;:42::54:0;;:4::132:1;165:22:165:23:0;:12::38:1;:42::54:0;;:4::134:1;166:22:166:23:0;:12::38:1;:42::54:0;;:4::134:1;168:23:168:24:0;:12::39:1;:43::55:0;;:4::134:1;169:23:169:24:0;:12::39:1;:43::55:0;;:4::134:1;172:26:172:27:0;:16::42:1;:12::43;:60::61:0;:50::76:1;:46::77;:4::158;132:2:173:3;;;;191::224::0;;;;194:12:194:22;:26::27;:4::93:1;196:12:196:28:0;:32::33;:4::78:1;197:12:197:29:0;:33::34;:4::79:1;200:18:200:19:0;:8::34:1;:38::40:0;:8:::1;:42:206:5:0;205:24:205:25;:14::41:1;:45::52:0;:6::134:1;206:11:211:5:0;208:24:208:25;:14::40:1;:44::56:0;;:6::129:1;210:24:210:25:0;:14::40:1;:47::48:0;:14::49:1;:::52;:66::67:0;:56::82:1;:6::161;206:11:211:5;213:42:213:63:0;:32::80:1;214:22:214:23:0;:12::40:1;:44::63:0;:4::137:1;215:22:215:23:0;:12::40:1;:4::137;218:22:218:23:0;:12::38:1;:42::44:0;:4::103:1;219:22:219:23:0;:12::38:1;:42::54:0;;:4::126:1;220:22:220:23:0;:12::38:1;:42::54:0;;:4::126:1;223:23:223:24:0;:12::39:1;:43::45:0;:4::104:1;191:2:224:3;;8:0:225:1;;',
+ 'logs': [],
+ 'requires': [
+ {
+ 'ip': 10,
+ 'line': 37,
+ 'message': 'Transaction: Version must be 2 (relative timelocks required)',
+ },
+ {
+ 'ip': 15,
+ 'line': 41,
+ 'message': 'Auth input: locking bytecode must match active output',
+ },
+ {
+ 'ip': 20,
+ 'line': 42,
+ 'message': 'Auth input: token category must match name category',
+ },
+ {
+ 'ip': 25,
+ 'line': 43,
+ 'message': 'Auth output: token category must match name category',
+ },
+ {
+ 'ip': 30,
+ 'line': 44,
+ 'message': 'Auth input: NFT commitment must match active output',
+ },
+ {
+ 'ip': 41,
+ 'line': 48,
+ 'message': 'Ownership input: ownership NFT token category must match name category',
+ },
+ {
+ 'ip': 48,
+ 'line': 49,
+ 'message': 'Ownership output: token category must match active input',
+ },
+ {
+ 'ip': 60,
+ 'line': 52,
+ 'message': 'Ownership input: ownership NFT name must match contract name + TLD',
+ },
+ {
+ 'ip': 65,
+ 'line': 53,
+ 'message': 'Auth input: internal auth NFT commitment must match ownership NFT registration ID',
+ },
+ {
+ 'ip': 72,
+ 'line': 54,
+ 'message': 'Ownership output: NFT commitment must match active input',
+ },
+ {
+ 'ip': 78,
+ 'line': 60,
+ 'message': 'Auth input: external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 91,
+ 'line': 76,
+ 'message': 'Transaction: Version must be 2 (relative timelocks required)',
+ },
+ {
+ 'ip': 94,
+ 'line': 78,
+ 'message': 'Transaction: must have exactly 3 inputs',
+ },
+ {
+ 'ip': 97,
+ 'line': 79,
+ 'message': 'Transaction: must have exactly 1 output',
+ },
+ {
+ 'ip': 103,
+ 'line': 82,
+ 'message': 'Input 0: external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 106,
+ 'line': 83,
+ 'message': 'Input 1: internal auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 111,
+ 'line': 85,
+ 'message': 'Input 0: external auth NFT token category must match name category',
+ },
+ {
+ 'ip': 116,
+ 'line': 86,
+ 'message': 'Input 1: internal auth NFT token category must match name category',
+ },
+ {
+ 'ip': 120,
+ 'line': 89,
+ 'message': 'Input 0: external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 150,
+ 'line': 102,
+ 'message': 'Character is a hyphen',
+ },
+ {
+ 'ip': 156,
+ 'line': 104,
+ 'message': 'Character is lowercase letter',
+ },
+ {
+ 'ip': 162,
+ 'line': 106,
+ 'message': 'Character is uppercase letter',
+ },
+ {
+ 'ip': 167,
+ 'line': 108,
+ 'message': 'Character is digit',
+ },
+ {
+ 'ip': 172,
+ 'line': 111,
+ 'message': 'Output 0: change must be pure BCH (no token category)',
+ },
+ {
+ 'ip': 183,
+ 'line': 134,
+ 'message': 'Transaction: Version must be 2 (relative timelocks required)',
+ },
+ {
+ 'ip': 186,
+ 'line': 136,
+ 'message': 'Transaction: must have exactly 5 inputs',
+ },
+ {
+ 'ip': 190,
+ 'line': 137,
+ 'message': 'Transaction: must have at most 3 outputs',
+ },
+ {
+ 'ip': 194,
+ 'line': 140,
+ 'message': 'Input 4: funding input must be pure BCH (no token category)',
+ },
+ {
+ 'ip': 202,
+ 'line': 142,
+ 'message': 'Output 2: change must be pure BCH (no token category)',
+ },
+ {
+ 'ip': 209,
+ 'line': 146,
+ 'message': 'Input 0: valid external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 213,
+ 'line': 147,
+ 'message': 'Input 1: valid internal auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 217,
+ 'line': 148,
+ 'message': 'Input 2: invalid external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 221,
+ 'line': 149,
+ 'message': 'Input 3: invalid internal auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 225,
+ 'line': 151,
+ 'message': 'Output 0: valid external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 228,
+ 'line': 152,
+ 'message': 'Output 1: valid internal auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 232,
+ 'line': 155,
+ 'message': 'Input 0: valid external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 236,
+ 'line': 156,
+ 'message': 'Input 2: invalid external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 240,
+ 'line': 159,
+ 'message': 'Output 0: valid external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 245,
+ 'line': 160,
+ 'message': 'Output 1: valid internal auth NFT commitment must match input 1',
+ },
+ {
+ 'ip': 250,
+ 'line': 163,
+ 'message': 'Input 0: valid external auth NFT token category must match name category',
+ },
+ {
+ 'ip': 255,
+ 'line': 164,
+ 'message': 'Input 1: valid internal auth NFT token category must match name category',
+ },
+ {
+ 'ip': 260,
+ 'line': 165,
+ 'message': 'Input 2: invalid external auth NFT token category must match name category',
+ },
+ {
+ 'ip': 265,
+ 'line': 166,
+ 'message': 'Input 3: invalid internal auth NFT token category must match name category',
+ },
+ {
+ 'ip': 270,
+ 'line': 168,
+ 'message': 'Output 0: valid external auth NFT token category must match name category',
+ },
+ {
+ 'ip': 275,
+ 'line': 169,
+ 'message': 'Output 1: valid internal auth NFT token category must match name category',
+ },
+ {
+ 'ip': 283,
+ 'line': 172,
+ 'message': 'Input 1: valid internal auth NFT registration ID must be lower than input 3',
+ },
+ {
+ 'ip': 293,
+ 'line': 194,
+ 'message': 'Transaction: Version must be 2 (relative timelocks required)',
+ },
+ {
+ 'ip': 296,
+ 'line': 196,
+ 'message': 'Transaction: must have exactly 3 inputs',
+ },
+ {
+ 'ip': 299,
+ 'line': 197,
+ 'message': 'Transaction: must have exactly 1 output',
+ },
+ {
+ 'ip': 308,
+ 'line': 205,
+ 'message': 'Input 1: internal auth NFT sequence number must equal inactivity expiry time',
+ },
+ {
+ 'ip': 314,
+ 'line': 208,
+ 'message': 'Input 2: name ownership NFT token category must match name category',
+ },
+ {
+ 'ip': 322,
+ 'line': 210,
+ 'message': 'Input 2: name ownership NFT registration ID must match input 1 commitment',
+ },
+ {
+ 'ip': 329,
+ 'line': 214,
+ 'message': 'Input 0: external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 332,
+ 'line': 215,
+ 'message': 'Input 1: internal auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 336,
+ 'line': 218,
+ 'message': 'Input 0: external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 341,
+ 'line': 219,
+ 'message': 'Input 0: external auth NFT token category must match name category',
+ },
+ {
+ 'ip': 346,
+ 'line': 220,
+ 'message': 'Input 1: internal auth NFT token category must match name category',
+ },
+ {
+ 'ip': 351,
+ 'line': 223,
+ 'message': 'Output 0: change must be pure BCH (no token category)',
+ },
+ ],
+ },
+ 'compiler': {
+ 'name': 'cashc',
+ 'version': '0.11.4',
+ },
+ 'updatedAt': '2025-08-10T17:47:19.018Z',
+};
diff --git a/lib/compiled/NameEnforcer.ts b/lib/compiled/NameEnforcer.ts
new file mode 100644
index 0000000..14db24e
--- /dev/null
+++ b/lib/compiled/NameEnforcer.ts
@@ -0,0 +1,99 @@
+export default {
+ 'contractName': 'NameEnforcer',
+ 'constructorInputs': [],
+ 'abi': [
+ {
+ 'name': 'call',
+ 'inputs': [
+ {
+ 'name': 'characterNumber',
+ 'type': 'int',
+ },
+ ],
+ },
+ ],
+ 'bytecode': 'OP_TXINPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_3 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_2 OP_UTXOBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_ROT OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_OVER OP_SPLIT OP_DROP OP_OVER OP_1SUB OP_SPLIT OP_NIP OP_BIN2NUM OP_DUP 2d OP_NUMNOTEQUAL OP_VERIFY OP_DUP 61 7b OP_WITHIN OP_NOT OP_VERIFY OP_DUP 41 5b OP_WITHIN OP_NOT OP_VERIFY 30 3a OP_WITHIN OP_NOT OP_VERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_2 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL OP_NIP',
+ 'source': "pragma cashscript 0.11.4;\n\ncontract NameEnforcer() {\n /**\n * Proves that a name contains invalid characters, burns the auctionNFT, and takes away the funds as a reward.\n * During the entire auction, this can be called at any time by anyone.\n * \n * Rules:\n * 1. The name must consist of only these characters:\n * - Letters (a-z or A-Z)\n * - Numbers (0-9)\n * - Hyphens (-)\n *\n * @param characterNumber - Number of the character in the name that is invalid (starting from 1)\n *\n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract.\n * - Input2: auctionNFT from Registry Contract.\n *\n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change.\n * - Output2: Reward to caller.\n *\n */\n function call(int characterNumber) {\n require(tx.inputs.length == 3, \"Transaction: must have exactly 3 inputs\");\n require(tx.outputs.length == 3, \"Transaction: must have exactly 3 outputs\");\n\n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1, \"Input 1: name enforcer contract UTXO must be at this index\");\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, \"Input 1: locking bytecode must match output 1\");\n // Ensure that no tokenCategory is minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, \"Output 1: must not have any token category (pure BCH only)\");\n\n // Lock this contract to only be used with the registry type contract.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[2].lockingBytecode == registryInputLockingBytecode, \"Input 2: locking bytecode does not match registry input's locking bytecode\");\n\n // All the token categories in the transaction should be the same.\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n\n // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.inputs[2].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory, \"Input 2: auction token category does not match registry\");\n // Auction capability should be mutable.\n require(auctionCapability == 0x01, \"Input 2: auction capability must be mutable (0x01)\");\n\n bytes name = tx.inputs[2].nftCommitment.split(20)[1];\n bytes characterSplitBytes = name.split(characterNumber)[0];\n characterNumber = characterNumber - 1;\n bytes character = characterSplitBytes.split(characterNumber)[1];\n int charVal = int(character);\n\n // Character is not a hyphen.\n require(charVal != 45, \"Character is a hyphen\"); \n // Character is not from a-z.\n require(!within(charVal, 97, 123), \"Character is lowercase letter\");\n // Character is not from A-Z.\n require(!within(charVal, 65, 91), \"Character is uppercase letter\");\n // Character is not from 0-9.\n require(!within(charVal, 48, 58), \"Character is a digit\");\n\n // tokenAmount from the invalid auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[2].tokenAmount, \"Output 0: token amount must equal input 0 + input 2 amounts (accumulation)\");\n\n // Pure BCH.\n require(tx.outputs[2].tokenCategory == 0x, \"Output 2: reward must be pure BCH (no token category)\");\n }\n}",
+ 'debug': {
+ 'bytecode': 'c3539dc4539dc0519dc0c7c0cd88c0d1008800c752c78800ce52ce01207f7c7b88518852cf01147f77787f75788c7f778176012d9e69760161017ba59169760141015ba591690130013aa5916900d300d052d0939d52d1008777',
+ 'sourceMap': '28:12:28:28;:32::33;:4::78:1;29:12:29:29:0;:33::34;:4::80:1;32:12:32:33:0;:37::38;:4::102:1;33:22:33:43:0;:12::60:1;:75::96:0;:64::113:1;:4::164;35:23:35:44:0;:12::59:1;:63::65:0;:4::129:1;38:51:38:52:0;:41::69:1;39:22:39:23:0;:12::40:1;:4::152;42:44:42:45:0;:34::60:1;45:63:45:64:0;:53::79:1;:86::88:0;:53::89:1;46:12:46:27:0;:31::52;:4::113:1;48:33:48:37:0;:4::93:1;50:27:50:28:0;:17::43:1;:50::52:0;:17::53:1;:::56;51:43:51:58:0;:32::59:1;:::62;52:22:52:37:0;:::41:1;53::53:64;:::67;54:18:54:32;57:12:57:19:0;:23::25;:12:::1;:4::52;59:20:59:27:0;:29::31;:33::36;:13::37:1;:12;:4::72;61:20:61:27:0;:29::31;:33::35;:13::36:1;:12;:4::71;63:29:63:31:0;:33::35;:13::36:1;:12;:4::62;67:23:67:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::172;70:23:70:24:0;:12::39:1;:43::45:0;:4::104:1;27:2:71:3',
+ 'logs': [],
+ 'requires': [
+ {
+ 'ip': 2,
+ 'line': 28,
+ 'message': 'Transaction: must have exactly 3 inputs',
+ },
+ {
+ 'ip': 5,
+ 'line': 29,
+ 'message': 'Transaction: must have exactly 3 outputs',
+ },
+ {
+ 'ip': 8,
+ 'line': 32,
+ 'message': 'Input 1: name enforcer contract UTXO must be at this index',
+ },
+ {
+ 'ip': 13,
+ 'line': 33,
+ 'message': 'Input 1: locking bytecode must match output 1',
+ },
+ {
+ 'ip': 17,
+ 'line': 35,
+ 'message': 'Output 1: must not have any token category (pure BCH only)',
+ },
+ {
+ 'ip': 22,
+ 'line': 39,
+ 'message': "Input 2: locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 31,
+ 'line': 46,
+ 'message': 'Input 2: auction token category does not match registry',
+ },
+ {
+ 'ip': 33,
+ 'line': 48,
+ 'message': 'Input 2: auction capability must be mutable (0x01)',
+ },
+ {
+ 'ip': 50,
+ 'line': 57,
+ 'message': 'Character is a hyphen',
+ },
+ {
+ 'ip': 56,
+ 'line': 59,
+ 'message': 'Character is lowercase letter',
+ },
+ {
+ 'ip': 62,
+ 'line': 61,
+ 'message': 'Character is uppercase letter',
+ },
+ {
+ 'ip': 67,
+ 'line': 63,
+ 'message': 'Character is a digit',
+ },
+ {
+ 'ip': 75,
+ 'line': 67,
+ 'message': 'Output 0: token amount must equal input 0 + input 2 amounts (accumulation)',
+ },
+ {
+ 'ip': 80,
+ 'line': 70,
+ 'message': 'Output 2: reward must be pure BCH (no token category)',
+ },
+ ],
+ },
+ 'compiler': {
+ 'name': 'cashc',
+ 'version': '0.11.4',
+ },
+ 'updatedAt': '2025-08-10T17:47:20.201Z',
+};
diff --git a/lib/compiled/OwnershipGuard.ts b/lib/compiled/OwnershipGuard.ts
new file mode 100644
index 0000000..98ec02f
--- /dev/null
+++ b/lib/compiled/OwnershipGuard.ts
@@ -0,0 +1,109 @@
+export default {
+ 'contractName': 'OwnershipGuard',
+ 'constructorInputs': [
+ {
+ 'name': 'nameContractBytecode',
+ 'type': 'bytes',
+ },
+ ],
+ 'abi': [
+ {
+ 'name': 'call',
+ 'inputs': [],
+ },
+ ],
+ 'bytecode': 'OP_TXINPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_TXOUTPUTCOUNT OP_4 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_1 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_INPUTINDEX OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_INPUTINDEX OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUALVERIFY OP_0 OP_UTXOBYTECODE OP_3 OP_UTXOBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_2 OP_UTXOTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_3 OP_UTXOTOKENCATEGORY 20 OP_SPLIT OP_SWAP OP_2 OP_PICK OP_EQUALVERIFY OP_1 OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_2 OP_OUTPUTTOKENCOMMITMENT OP_EQUALVERIFY OP_2 OP_UTXOTOKENCOMMITMENT OP_0 OP_EQUALVERIFY OP_3 OP_UTXOTOKENCOMMITMENT 14 OP_SPLIT OP_NIP OP_DUP OP_SIZE OP_NIP 2e626368 20 OP_4 OP_ROLL OP_CAT OP_OVER OP_SIZE OP_NIP OP_CAT OP_SWAP OP_CAT OP_SWAP OP_CAT OP_SWAP OP_CAT OP_SWAP OP_CAT OP_HASH256 aa20 OP_SWAP OP_CAT 87 OP_CAT OP_2 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_2 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENAMOUNT OP_0 OP_UTXOTOKENAMOUNT OP_3 OP_UTXOTOKENAMOUNT OP_ADD OP_NUMEQUALVERIFY OP_3 OP_OUTPUTTOKENCATEGORY OP_0 OP_EQUAL',
+ 'source': "pragma cashscript 0.11.4;\n\n/**\n * @param nameContractBytecode The the partial bytecode of the name contract that has an Owner.\n */\ncontract OwnershipGuard(bytes nameContractBytecode) {\n /**\n * If the name being auctioned already has an `externalAuthNFT` with the same category, then the auction is invalid.\n * Because it means that an owner still exists. If it is known that the name has been abandoned for > `inactivityExpiryTime`\n * then one must use the `burn` method of the name.cash to burn the internalAuthNFT and externalAuthNFT making the \n * name to be available for auction.\n *\n * Penalizes invalid name registrations by allowing anyone to burn the auctionNFT and claim the funds as a reward.\n * \n * @inputs\n * - Input0: Registry Contract's authorizedThreadNFT i.e immutable NFT with commitment that has the lockingBytecode of this contract\n * - Input1: Any input from this contract\n * - Input2: External Auth NFT from the Name Contract\n * - Input3: auctionNFT from Registry Contract\n * \n * @outputs\n * - Output0: Registry Contract's authorizedThreadNFT back to the Registry contract.\n * - Output1: Input1 back to this contract without any change\n * - Output2: External Auth NFT back to the Name Contract\n * - Output3: BCH change/reward to caller\n */\n function call(){\n require(tx.inputs.length == 4, \"Transaction: must have exactly 4 inputs\");\n require(tx.outputs.length == 4, \"Transaction: must have exactly 4 outputs\");\n \n // This contract can only be used at input1 and it should return the input1 back to itself.\n require(this.activeInputIndex == 1, \"Input 1: ownership guard contract UTXO must be at this index\");\n require(tx.inputs[this.activeInputIndex].lockingBytecode == tx.outputs[this.activeInputIndex].lockingBytecode, \"Input 1: locking bytecode must match output 1\");\n // Ensure that no tokenCategory is minted here.\n require(tx.outputs[this.activeInputIndex].tokenCategory == 0x, \"Output 1: must not have any token category (pure BCH only)\");\n\n // This contract can only be used with the 'lockingbytecode' used in the 0th input.\n // Note: This contract can be used with any contract that fulfills these conditions, and that is fine\n // because those contracts will not be manipulating the utxos of the Registry contract. Instead, they will\n // be manipulating their own utxos.\n bytes registryInputLockingBytecode = tx.inputs[0].lockingBytecode;\n require(tx.inputs[3].lockingBytecode == registryInputLockingBytecode, \"Input 3: auction NFT locking bytecode does not match registry input's locking bytecode\");\n\n bytes registryInputCategory = tx.inputs[0].tokenCategory;\n require(tx.inputs[2].tokenCategory == registryInputCategory, \"Input 2: external auth NFT token category prefix must match registry\");\n require(tx.outputs[2].tokenCategory == registryInputCategory, \"Output 2: external auth NFT token category prefix must match registry\");\n\n // AuctionNFT should be mutable and of the 'nameCategory' i.e registryInputCategory\n bytes auctionCategory, bytes auctionCapability = tx.inputs[3].tokenCategory.split(32);\n require(auctionCategory == registryInputCategory, \"Input 3: auction NFT token category prefix must match registry\");\n // Mutable\n require(auctionCapability == 0x01, \"Input 3: auction NFT capability must be mutable (0x01)\");\n\n // nftCommiment of the externalAuthNFT must stay the same\n require(tx.inputs[2].nftCommitment == tx.outputs[2].nftCommitment, \"Output 2: external auth NFT commitment must match input 2\");\n // Ensure that the externalAuth NFT is used and not the internalAuth NFT.\n require(tx.inputs[2].nftCommitment == 0x, \"Input 2: external auth NFT must have empty commitment\");\n\n // Get the name of the name from the auctionNFT\n bytes name = tx.inputs[3].nftCommitment.split(20)[1];\n // Get the name length to generate the complete bytecode of the name contract\n int nameLength = name.length;\n // category + name + bytecode.\n // Note: `inactivityExpiryTime` in the name is already added to the nameContractBytecode in the constructor.\n bytes constant tld = bytes('.bch');\n bytes nameBytecode = 0x20 + registryInputCategory + bytes(tld.length) + tld + bytes(nameLength) + name + nameContractBytecode;\n bytes32 scriptHash = hash256(nameBytecode);\n bytes35 nameLockingBytecode = new LockingBytecodeP2SH32(scriptHash);\n\n // Ensure that the externalAuthNFT is coming from the correct Name Contract\n require(tx.inputs[2].lockingBytecode == nameLockingBytecode, \"Input 2: external auth NFT locking bytecode must match name contract\");\n require(tx.outputs[2].lockingBytecode == nameLockingBytecode, \"Output 2: external auth NFT locking bytecode must match name contract\");\n\n // tokenAmount from the auctionNFT goes to the authorizedThreadNFT to be accumulated later\n // and merged back with the CounterNFT using the `Accumulator` Contract\n require(tx.outputs[0].tokenAmount == tx.inputs[0].tokenAmount + tx.inputs[3].tokenAmount, \"Output 0: token amount must equal input 0 + input 3 amounts (accumulation)\");\n\n // Reward Output\n require(tx.outputs[3].tokenCategory == 0x, \"Output 3: reward must be pure BCH (no token category)\");\n }\n}",
+ 'debug': {
+ 'bytecode': 'c3549dc4549dc0519dc0c7c0cd88c0d1008800c753c78800ce52ce788852d1788853ce01207f7c527988518852cf52d28852cf008853cf01147f77768277042e6263680120547a7e7882777e7c7e7c7e7c7e7c7eaa02aa207c7e01877e52c7788852cd8800d300d053d0939d53d10087',
+ 'sourceMap': '28:12:28:28;:32::33;:4::78:1;29:12:29:29:0;:33::34;:4::80:1;32:12:32:33:0;:37::38;:4::104:1;33:22:33:43:0;:12::60:1;:75::96:0;:64::113:1;:4::164;35:23:35:44:0;:12::59:1;:63::65:0;:4::129:1;41:51:41:52:0;:41::69:1;42:22:42:23:0;:12::40:1;:4::164;44:44:44:45:0;:34::60:1;45:22:45:23:0;:12::38:1;:42::63:0;:4::137:1;46:23:46:24:0;:12::39:1;:43::64:0;:4::139:1;49:63:49:64:0;:53::79:1;:86::88:0;:53::89:1;50:12:50:27:0;:31::52;;:4::120:1;52:33:52:37:0;:4::97:1;55:22:55:23:0;:12::38:1;:53::54:0;:42::69:1;:4::132;57:22:57:23:0;:12::38:1;:42::44:0;:4::103:1;60:27:60:28:0;:17::43:1;:50::52:0;:17::53:1;:::56;62:21:62:25:0;:::32:1;;65:31:65:37:0;66:25:66:29;:32::53;;:25:::1;:62::65:0;:::72:1;;:25::73;:76::79:0;:25:::1;:88::98:0;:25::99:1;:102::106:0;:25:::1;:109::129:0;:25:::1;67::67:46;68:34:68:71:0;:60::70;:34::71:1;;;71:22:71:23:0;:12::40:1;:44::63:0;:4::137:1;72:23:72:24:0;:12::41:1;:4::139;76:23:76:24:0;:12::37:1;:51::52:0;:41::65:1;:78::79:0;:68::92:1;:41;:4::172;79:23:79:24:0;:12::39:1;:43::45:0;:4::104:1',
+ 'logs': [],
+ 'requires': [
+ {
+ 'ip': 3,
+ 'line': 28,
+ 'message': 'Transaction: must have exactly 4 inputs',
+ },
+ {
+ 'ip': 6,
+ 'line': 29,
+ 'message': 'Transaction: must have exactly 4 outputs',
+ },
+ {
+ 'ip': 9,
+ 'line': 32,
+ 'message': 'Input 1: ownership guard contract UTXO must be at this index',
+ },
+ {
+ 'ip': 14,
+ 'line': 33,
+ 'message': 'Input 1: locking bytecode must match output 1',
+ },
+ {
+ 'ip': 18,
+ 'line': 35,
+ 'message': 'Output 1: must not have any token category (pure BCH only)',
+ },
+ {
+ 'ip': 23,
+ 'line': 42,
+ 'message': "Input 3: auction NFT locking bytecode does not match registry input's locking bytecode",
+ },
+ {
+ 'ip': 29,
+ 'line': 45,
+ 'message': 'Input 2: external auth NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 33,
+ 'line': 46,
+ 'message': 'Output 2: external auth NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 41,
+ 'line': 50,
+ 'message': 'Input 3: auction NFT token category prefix must match registry',
+ },
+ {
+ 'ip': 43,
+ 'line': 52,
+ 'message': 'Input 3: auction NFT capability must be mutable (0x01)',
+ },
+ {
+ 'ip': 48,
+ 'line': 55,
+ 'message': 'Output 2: external auth NFT commitment must match input 2',
+ },
+ {
+ 'ip': 52,
+ 'line': 57,
+ 'message': 'Input 2: external auth NFT must have empty commitment',
+ },
+ {
+ 'ip': 87,
+ 'line': 71,
+ 'message': 'Input 2: external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 90,
+ 'line': 72,
+ 'message': 'Output 2: external auth NFT locking bytecode must match name contract',
+ },
+ {
+ 'ip': 98,
+ 'line': 76,
+ 'message': 'Output 0: token amount must equal input 0 + input 3 amounts (accumulation)',
+ },
+ {
+ 'ip': 103,
+ 'line': 79,
+ 'message': 'Output 3: reward must be pure BCH (no token category)',
+ },
+ ],
+ },
+ 'compiler': {
+ 'name': 'cashc',
+ 'version': '0.11.4',
+ },
+ 'updatedAt': '2025-08-10T17:47:20.554Z',
+};
diff --git a/lib/compiled/Registry.ts b/lib/compiled/Registry.ts
index a5835fa..4788425 100644
--- a/lib/compiled/Registry.ts
+++ b/lib/compiled/Registry.ts
@@ -2,7 +2,7 @@ export default {
'contractName': 'Registry',
'constructorInputs': [
{
- 'name': 'domainCategory',
+ 'name': 'nameCategory',
'type': 'bytes',
},
],
@@ -13,49 +13,57 @@ export default {
},
],
'bytecode': 'OP_TXVERSION OP_2 OP_NUMEQUALVERIFY OP_INPUTINDEX OP_UTXOBYTECODE OP_0 OP_UTXOBYTECODE OP_OVER OP_EQUALVERIFY OP_0 OP_OUTPUTBYTECODE OP_EQUALVERIFY OP_0 OP_UTXOTOKENCATEGORY OP_OVER OP_EQUALVERIFY OP_0 OP_OUTPUTTOKENCATEGORY OP_EQUALVERIFY OP_0 OP_OUTPUTVALUE OP_0 OP_UTXOVALUE OP_NUMEQUALVERIFY OP_0 OP_OUTPUTTOKENCOMMITMENT OP_0 OP_UTXOTOKENCOMMITMENT OP_EQUALVERIFY OP_1 OP_UTXOBYTECODE OP_0 OP_UTXOTOKENCOMMITMENT OP_EQUAL',
- 'source': 'pragma cashscript 0.11.2;\n\n/**\n * @param domainCategory - The category of the domain NFTs that are authorized to be registered. [In reverse order]\n *\n * The Registry has two minting NFTs:\n * 1. CounterMintingNFT, has tokenAmount and nftCommitment.\n * 2. DomainMintingNFT, does not have any tokenAmount or nftCommitment.\n */\ncontract Registry(bytes domainCategory) {\n /**\n * The Registry contract serves as both a source and storage for authorized NFTs.\n * It holds: RegistrationNFTs, AuctionNFTs, and AuthorizedThreadNFTs\n *\n * AuthorizedThreadNFTs are NFTs with immutable capability that share the same category as domainCategory.\n * These NFTs contain the lockingBytecode of authorized contracts.\n * Multiple copies of these NFTs enable parallel processing through multiple threads.\n * \n * The contract can only be called in conjunction with one of the authorized contracts.\n *\n * Imagine that the authorised contracts are just function composition,\n * those contracts are being used for the code in them and to reduce the transaction size.\n * This design reduces the transaction size to a minimum while using every OP_CODE required\n * for a given action. To use the code in these authorized contracts, a random UTXO is used and\n * sent back to itself to be used again in future.\n *\n * All the utxos, except for the DomainNFTs (InternalAuth, ExternalAuth and DomainOwnershipNFT),\n * stay with the Registry contract.\n * \n * @note Authorized contracts and their thread counts:\n * - Auction: [1 thread] (Single-threaded registration)\n * - Bid: [~x threads]\n * - DomainFactory: [~x threads]\n * - AuctionNameEnforcer: [~x threads]\n * - DomainOwnershipGuard: [~x threads]\n * - AuctionConflictResolver: [~x threads]\n * - Accumulator: [~x threads]\n * \n * @inputs\n * - Input0: AuthorizedThreadNFT from self\n * - Input1: Any UTXO from Authorized contract\n * \n * @outputs\n * - Output0: AuthorizedThreadNFT back to self\n * - Output1: Output back to Authorized contract to be reused again\n */\n function call() {\n // 1. Since the registry contract is static, version check is required to prevent from any vulnerabilities\n // caused due to future versions.\n // 2. BitCANN uses relative timelocks, need to enforce version 2.\n require(tx.version == 2);\n\n // Registry Contract\n\n bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;\n // authorizedThreadNFT must stay with the Registry Contract.\n require(tx.inputs[0].lockingBytecode == selfLockingBytecode);\n require(tx.outputs[0].lockingBytecode == selfLockingBytecode);\n\n // Immutable NFTs of domainCategory in Registry Contract will always be authorizedThreadNFTs\n // Mutable NFTs of domainCategory in Registry Contract will always be auctionNFTs\n // Minting NFTs of domainCategory in Registry Contract will always be counterMintingNFT or DomainMintingNFT\n require(tx.inputs[0].tokenCategory == domainCategory);\n require(tx.outputs[0].tokenCategory == domainCategory);\n // Keeping the value same to not influence any satoshi movement in authorized contracts\n require(tx.outputs[0].value == tx.inputs[0].value);\n // The commitment that has the lockingbytecode of the authorized contract should never change.\n require(tx.outputs[0].nftCommitment == tx.inputs[0].nftCommitment);\n // Not checking the tokenAmount as it changes.\n\n // Authorized Contract\n\n // Expect the NFT commitment that contains the lockingBytecode of the authorized contract.\n require(tx.inputs[1].lockingBytecode == tx.inputs[0].nftCommitment);\n // With these prerequisites met, we just need to make sure that all the contracts that are deployed are written\n // and initialized properly, and they expect this structure and handle the inputs and outputs as expected.\n }\n}\n',
+ 'source': 'pragma cashscript 0.11.4;\n\n/**\n * @param nameCategory - The category of the name NFTs that are authorized to be registered. [In reverse order]\n *\n * The Registry has two minting NFTs:\n * 1. CounterMintingNFT, has tokenAmount and nftCommitment.\n * 2. NameMintingNFT, does not have any tokenAmount or nftCommitment.\n */\ncontract Registry(bytes nameCategory) {\n /**\n * The Registry contract serves as both a source and storage for authorized NFTs.\n * It holds: RegistrationNFTs, AuctionNFTs, and AuthorizedThreadNFTs\n *\n * AuthorizedThreadNFTs are NFTs with immutable capability that share the same category as nameCategory.\n * These NFTs contain the lockingBytecode of authorized contracts.\n * Multiple copies of these NFTs enable parallel processing through multiple threads.\n * \n * The contract can only be called in conjunction with one of the authorized contracts.\n *\n * Imagine that the authorised contracts are just function composition,\n * those contracts are being used for the code in them and to reduce the transaction size.\n * This design reduces the transaction size to a minimum while using every OP_CODE required\n * for a given action. To use the code in these authorized contracts, a random UTXO is used and\n * sent back to itself to be used again in future.\n *\n * All the utxos, except for the NameNFTs (InternalAuth, ExternalAuth and NameOwnershipNFT),\n * stay with the Registry contract.\n * \n * @note Authorized contracts and their thread counts:\n * - Auction: [1 thread] (Single-threaded registration)\n * - Bid: [~x threads]\n * - NameFactory: [~x threads]\n * - AuctionNameEnforcer: [~x threads]\n * - NameOwnershipGuard: [~x threads]\n * - AuctionConflictResolver: [~x threads]\n * - Accumulator: [~x threads]\n * \n * @inputs\n * - Input0: AuthorizedThreadNFT from self\n * - Input1: Any UTXO from Authorized contract\n * \n * @outputs\n * - Output0: AuthorizedThreadNFT back to self\n * - Output1: Output back to Authorized contract to be reused again\n */\n function call() {\n // 1. Since the registry contract is static, version check is required to prevent from any vulnerabilities\n // caused due to future versions.\n // 2. BitCANN uses relative timelocks, need to enforce version 2.\n require(tx.version == 2, "Registry: transaction version must be 2 (relative timelocks required)");\n\n // Registry Contract\n\n bytes selfLockingBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;\n // authorizedThreadNFT must stay with the Registry Contract.\n require(tx.inputs[0].lockingBytecode == selfLockingBytecode, "Input 0: locking bytecode must match registry contract");\n require(tx.outputs[0].lockingBytecode == selfLockingBytecode, "Output 0: locking bytecode must match registry contract");\n\n // Immutable NFTs of nameCategory in Registry Contract will always be authorizedThreadNFTs\n // Mutable NFTs of nameCategory in Registry Contract will always be auctionNFTs\n // Minting NFTs of nameCategory in Registry Contract will always be counterMintingNFT or NameMintingNFT\n require(tx.inputs[0].tokenCategory == nameCategory, "Input 0: token category must match name category");\n require(tx.outputs[0].tokenCategory == nameCategory, "Output 0: token category must match name category");\n // Keeping the value same to not influence any satoshi movement in authorized contracts\n require(tx.outputs[0].value == tx.inputs[0].value, "Output 0: satoshi value must match input 0");\n // The commitment that has the lockingbytecode of the authorized contract should never change.\n // It is possible that in other contracts a minting NFT of the nameCategory is used, in that case\n // it becomes possible to change the nft commitment of 0th output.\n require(tx.outputs[0].nftCommitment == tx.inputs[0].nftCommitment, "Output 0: NFT commitment must match input 0");\n // Not checking the tokenAmount as it changes.\n\n // Authorized Contract\n\n // Expect the NFT commitment that contains the lockingBytecode of the authorized contract.\n require(tx.inputs[1].lockingBytecode == tx.inputs[0].nftCommitment, "Input 1: locking bytecode must match authorized contract from input 0 NFT commitment");\n // With these prerequisites met, we just need to make sure that all the contracts that are deployed are written\n // and initialized properly, and they expect this structure and handle the inputs and outputs as expected.\n }\n}\n',
'debug': {
'bytecode': 'c2529dc0c700c7788800cd8800ce788800d18800cc00c69d00d200cf8851c700cf87',
- 'sourceMap': '51:12:51:22;:26::27;:4::29:1;55:42:55:63:0;:32::80:1;57:22:57:23:0;:12::40:1;:44::63:0;:4::65:1;58:23:58:24:0;:12::41:1;:4::66;63:22:63:23:0;:12::38:1;:42::56:0;:4::58:1;64:23:64:24:0;:12::39:1;:4::59;66:23:66:24:0;:12::31:1;:45::46:0;:35::53:1;:4::55;68:23:68:24:0;:12::39:1;:53::54:0;:43::69:1;:4::71;74:22:74:23:0;:12::40:1;:54::55:0;:44::70:1;:4::72',
+ 'sourceMap': '51:12:51:22;:26::27;:4::102:1;55:42:55:63:0;:32::80:1;57:22:57:23:0;:12::40:1;:44::63:0;:4::123:1;58:23:58:24:0;:12::41:1;:4::125;63:22:63:23:0;:12::38:1;:42::54:0;:4::108:1;64:23:64:24:0;:12::39:1;:4::110;66:23:66:24:0;:12::31:1;:45::46:0;:35::53:1;:4::101;70:23:70:24:0;:12::39:1;:53::54:0;:43::69:1;:4::118;76:22:76:23:0;:12::40:1;:54::55:0;:44::70:1;:4::160',
'logs': [],
'requires': [
{
'ip': 3,
'line': 51,
+ 'message': 'Registry: transaction version must be 2 (relative timelocks required)',
},
{
'ip': 9,
'line': 57,
+ 'message': 'Input 0: locking bytecode must match registry contract',
},
{
'ip': 12,
'line': 58,
+ 'message': 'Output 0: locking bytecode must match registry contract',
},
{
'ip': 16,
'line': 63,
+ 'message': 'Input 0: token category must match name category',
},
{
'ip': 19,
'line': 64,
+ 'message': 'Output 0: token category must match name category',
},
{
'ip': 24,
'line': 66,
+ 'message': 'Output 0: satoshi value must match input 0',
},
{
'ip': 29,
- 'line': 68,
+ 'line': 70,
+ 'message': 'Output 0: NFT commitment must match input 0',
},
{
'ip': 35,
- 'line': 74,
+ 'line': 76,
+ 'message': 'Input 1: locking bytecode must match authorized contract from input 0 NFT commitment',
},
],
},
'compiler': {
'name': 'cashc',
- 'version': '0.11.2',
+ 'version': '0.11.4',
},
- 'updatedAt': '2025-07-15T19:31:07.619Z',
+ 'updatedAt': '2025-08-10T17:47:18.265Z',
};
diff --git a/lib/index.ts b/lib/index.ts
index 5c6bf2c..3e099d7 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -1,21 +1,21 @@
import Registry from './compiled/Registry.js';
import Auction from './compiled/Auction.js';
import Bid from './compiled/Bid.js';
-import Domain from './compiled/Domain.js';
-import DomainFactory from './compiled/DomainFactory.js';
-import AuctionConflictResolver from './compiled/AuctionConflictResolver.js';
-import AuctionNameEnforcer from './compiled/AuctionNameEnforcer.js';
-import DomainOwnershipGuard from './compiled/DomainOwnershipGuard.js';
+import Name from './compiled/Name.js';
+import Factory from './compiled/Factory.js';
+import NameEnforcer from './compiled/NameEnforcer.js';
+import OwnershipGuard from './compiled/OwnershipGuard.js';
+import ConflictResolver from './compiled/ConflictResolver.js';
import Accumulator from './compiled/Accumulator.js';
export const BitCANNArtifacts = {
Registry,
Auction,
Bid,
- Domain,
- DomainFactory,
- AuctionConflictResolver,
- AuctionNameEnforcer,
- DomainOwnershipGuard,
+ Name,
+ Factory,
+ NameEnforcer,
+ OwnershipGuard,
+ ConflictResolver,
Accumulator,
};
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index d8353f0..f78c401 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,8 +15,8 @@
"@types/node": "^22.14.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
- "cashc": "0.11.2",
- "cashscript": "0.11.2",
+ "cashc": "^0.11.4",
+ "cashscript": "^0.11.4",
"eslint": "^8.57.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-plugin-import": "^2.31.0",
@@ -562,9 +562,9 @@
}
},
"node_modules/@cashscript/utils": {
- "version": "0.11.2",
- "resolved": "https://registry.npmjs.org/@cashscript/utils/-/utils-0.11.2.tgz",
- "integrity": "sha512-/H5Fvg8L6sRCs1v1XwyA1rYqgZXlojmK1Qr7Co5Wn3WDPv+Z6oKkvi9m283dOAO7U0D8HILJW1yF3fr1n4BsEw==",
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/@cashscript/utils/-/utils-0.11.4.tgz",
+ "integrity": "sha512-YU6jiH8dnd4ur/PgTRrRvFK2haOl2oVZUU5faQLLNqXtmAbBwfNi8aKh4kZnbCZQcacdsz5PT4OLjBg3sGSw0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -606,9 +606,9 @@
}
},
"node_modules/@electrum-cash/network": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@electrum-cash/network/-/network-4.1.1.tgz",
- "integrity": "sha512-v5abF2qGRTnBoi9tcS/iz7j82D8HYsK9iY0NM5v8/Qu8SnlMGGNz8UDFl+YzRPFXb4SUL3K0uf3Oydy82DB3oA==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@electrum-cash/network/-/network-4.1.3.tgz",
+ "integrity": "sha512-amMvdcEfHhquoUkhN7x/H04KPYfqd5LilOGcg6O1OdUks1Mcrcah8WfHICHW/qyZ3Rgoos9o7Wx8gKz8qcSNzg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1475,9 +1475,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.15.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz",
- "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==",
+ "version": "22.17.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz",
+ "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2345,37 +2345,38 @@
"license": "CC-BY-4.0"
},
"node_modules/cashc": {
- "version": "0.11.2",
- "resolved": "https://registry.npmjs.org/cashc/-/cashc-0.11.2.tgz",
- "integrity": "sha512-5pUka/cI3l744J99Am7tQN6AbSnFOfDxcYqX8hXY9VsqEefJK7lrTyvE3FKL8RC7mixh6qwbxnY4tVYoWoh74w==",
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/cashc/-/cashc-0.11.4.tgz",
+ "integrity": "sha512-rxZFtZD4AGC4bm5o1nKNflfpuDd9GE2YJTyS6Nl/V6uJg5wnBtP6LS3NOwCZ/8xckxGr1OjpdZ2GXz5j87GpUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.2",
- "@cashscript/utils": "^0.11.2",
+ "@cashscript/utils": "^0.11.4",
"antlr4": "^4.13.2",
- "commander": "^13.1.0",
- "semver": "^7.6.3"
+ "commander": "^14.0.0",
+ "semver": "^7.7.2"
},
"bin": {
"cashc": "dist/cashc-cli.js"
}
},
"node_modules/cashscript": {
- "version": "0.11.2",
- "resolved": "https://registry.npmjs.org/cashscript/-/cashscript-0.11.2.tgz",
- "integrity": "sha512-Ab+V8+PyktENKWXK4vRcf/wdmiEHSAg1wNJrilRFB2DYuJDfSyL3ZcstzfQZEg0+0P2ZmCT0RE4jUAjia2rAQw==",
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/cashscript/-/cashscript-0.11.4.tgz",
+ "integrity": "sha512-V2atEcxaAozsbw0EqbyxOc0VzQUPbOa+PdOx1B+ZbQs3uaqbFxYzn8yJw9WrCt21GWNIi8xEfPBuvmK1E9Y7WA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bitauth/libauth": "^3.1.0-next.2",
- "@cashscript/utils": "^0.11.2",
- "@electrum-cash/network": "^4.1.1",
+ "@cashscript/utils": "^0.11.4",
+ "@electrum-cash/network": "^4.1.3",
"@mr-zwets/bchn-api-wrapper": "^1.0.1",
+ "@types/node": "^22.17.0",
"delay": "^6.0.0",
"fast-deep-equal": "^3.1.3",
"pako": "^2.1.0",
- "semver": "^7.6.3"
+ "semver": "^7.7.2"
}
},
"node_modules/chalk": {
@@ -2482,13 +2483,13 @@
"license": "MIT"
},
"node_modules/commander": {
- "version": "13.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
- "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz",
+ "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=18"
+ "node": ">=20"
}
},
"node_modules/concat-map": {
@@ -5529,9 +5530,9 @@
"license": "MIT"
},
"node_modules/lossless-json": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.0.2.tgz",
- "integrity": "sha512-+z0EaLi2UcWi8MZRxA5iTb6m4Ys4E80uftGY+yG5KNFJb5EceQXOhdW/pWJZ8m97s26u7yZZAYMcKWNztSZssA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.1.1.tgz",
+ "integrity": "sha512-HusN80C0ohtT9kOHQH7EuUaqzRQsnekpa+2ot8OzvW0iC08dq/YtM/7uKwwajldQsCrHyC8q9fz3t3L+TmDltA==",
"dev": true,
"license": "MIT"
},
@@ -6447,9 +6448,9 @@
}
},
"node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -7496,9 +7497,9 @@
}
},
"node_modules/ws": {
- "version": "8.18.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
- "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index 5840574..101f17d 100644
--- a/package.json
+++ b/package.json
@@ -20,15 +20,15 @@
"scripts": {
"build": "npm run compile && tsc",
"compile:registry": "cashc ./contracts/Registry.cash -o ./contracts/Registry.json",
- "compile:domain": "cashc ./contracts/Domain.cash -o ./contracts/Domain.json",
+ "compile:name": "cashc ./contracts/Name.cash -o ./contracts/Name.json",
"compile:bid": "cashc ./contracts/Bid.cash -o ./contracts/Bid.json",
"compile:auction": "cashc ./contracts/Auction.cash -o ./contracts/Auction.json",
- "compile:domainfactory": "cashc ./contracts/DomainFactory.cash -o ./contracts/DomainFactory.json",
- "compile:auctionnameenforcer": "cashc ./contracts/AuctionNameEnforcer.cash -o ./contracts/AuctionNameEnforcer.json",
- "compile:domainownershipguard": "cashc ./contracts/DomainOwnershipGuard.cash -o ./contracts/DomainOwnershipGuard.json",
- "compile:auctionconflictresolver": "cashc ./contracts/AuctionConflictResolver.cash -o ./contracts/AuctionConflictResolver.json",
+ "compile:factory": "cashc ./contracts/Factory.cash -o ./contracts/Factory.json",
+ "compile:nameenforcer": "cashc ./contracts/NameEnforcer.cash -o ./contracts/NameEnforcer.json",
+ "compile:ownershipguard": "cashc ./contracts/OwnershipGuard.cash -o ./contracts/OwnershipGuard.json",
+ "compile:conflictresolver": "cashc ./contracts/ConflictResolver.cash -o ./contracts/ConflictResolver.json",
"compile:accumulator": "cashc ./contracts/Accumulator.cash -o ./contracts/Accumulator.json",
- "compile": "npm run compile:registry && npm run compile:auction && npm run compile:domain && npm run compile:bid && npm run compile:domainfactory && npm run compile:auctionnameenforcer && npm run compile:domainownershipguard && npm run compile:auctionconflictresolver && npm run compile:accumulator",
+ "compile": "npm run compile:registry && npm run compile:auction && npm run compile:name && npm run compile:bid && npm run compile:factory && npm run compile:nameenforcer && npm run compile:ownershipguard && npm run compile:conflictresolver && npm run compile:accumulator",
"postcompile": "node scripts/json-to-ts.js && npm run lint-fix",
"prepublishOnly": "npm run build",
"test": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest",
@@ -43,8 +43,8 @@
"@types/node": "^22.14.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
- "cashc": "0.11.2",
- "cashscript": "0.11.2",
+ "cashc": "^0.11.4",
+ "cashscript": "^0.11.4",
"eslint": "^8.57.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-plugin-import": "^2.31.0",
diff --git a/test/artifacts.ts b/test/artifacts.ts
new file mode 100644
index 0000000..e70eef6
--- /dev/null
+++ b/test/artifacts.ts
@@ -0,0 +1,28 @@
+export default {
+ 'contractName': 'Test',
+ 'constructorInputs': [],
+ 'abi': [
+ {
+ 'name': 'call',
+ 'inputs': [],
+ },
+ ],
+ 'bytecode': 'OP_1 OP_2 OP_ADD OP_0 OP_NUMNOTEQUAL',
+ 'source': 'pragma cashscript ^0.11.0;\n\ncontract Test() {\n function call() {\n require(1 + 2 != 0);\n }\n}',
+ 'debug': {
+ 'bytecode': '515293009e',
+ 'sourceMap': '5:16:5:17;:20::21;:16:::1;:25::26:0;:8::28:1',
+ 'logs': [],
+ 'requires': [
+ {
+ 'ip': 5,
+ 'line': 5,
+ },
+ ],
+ },
+ 'compiler': {
+ 'name': 'cashc',
+ 'version': '0.11.4',
+ },
+ 'updatedAt': '2025-08-09T20:20:24.674Z',
+};
\ No newline at end of file
diff --git a/test/common.ts b/test/common.ts
index f0a91bc..9be1fda 100644
--- a/test/common.ts
+++ b/test/common.ts
@@ -8,19 +8,22 @@ import {
binToHex,
hexToBin,
} from '@bitauth/libauth';
-import { SignatureTemplate } from 'cashscript';
+import { SignatureTemplate, randomNFT } from 'cashscript';
+
+export const nameTokenCategory = randomNFT().category;
+export const reversedNameTokenCategory = binToHex(hexToBin(nameTokenCategory).reverse());
+
+export const invalidNameTokenCategory = randomNFT().category;
+export const reversedInvalidNameTokenCategory = binToHex(hexToBin(invalidNameTokenCategory).reverse());
-export const domainTokenCategory = '98570f00cad2991de0ab25f14ffae29a0c61da97ba6d466acbc8476e2e612ada';
-export const reversedDomainTokenCategory = binToHex(hexToBin(domainTokenCategory).reverse());
export const mockOptions =
{
- category: domainTokenCategory,
+ category: nameTokenCategory,
minStartingBid: 10000,
minBidIncreasePercentage: 5,
- inactivityExpiryTime: 1,
- minWaitTime: 1,
- maxPlatformFeePercentage: 50,
+ minWaitTime: 4194306,
+ tld: '.bch',
};
// @ts-ignore
const seed = deriveSeedFromBip39Mnemonic('bitcann test seed');
@@ -29,11 +32,22 @@ const baseDerivationPath = "m/44'/145'/0'/0";
// Derive Alice's private key, public key, public key hash and address
const aliceNode = deriveHdPath(rootNode, `${baseDerivationPath}/0`);
-if(typeof aliceNode === 'string') throw new Error();
+
export const alicePub = secp256k1.derivePublicKeyCompressed(aliceNode.privateKey);
export const alicePriv = aliceNode.privateKey;
// @ts-ignore
export const alicePkh = hash160(alicePub);
export const aliceAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: alicePkh, throwErrors: true }).address;
export const aliceTokenAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: alicePkh, throwErrors: true }).address;
-export const aliceTemplate = new SignatureTemplate(alicePriv);
\ No newline at end of file
+export const aliceTemplate = new SignatureTemplate(alicePriv);
+
+// Derive Bob's private key, public key, public key hash and address
+const bobNode = deriveHdPath(rootNode, `${baseDerivationPath}/1`);
+
+export const bobPub = secp256k1.derivePublicKeyCompressed(bobNode.privateKey);
+export const bobPriv = bobNode.privateKey;
+// @ts-ignore
+export const bobPkh = hash160(bobPub);
+export const bobAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkh', payload: bobPkh, throwErrors: true }).address;
+export const bobTokenAddress = encodeCashAddress({ prefix: 'bchtest', type: 'p2pkhWithTokens', payload: bobPkh, throwErrors: true }).address;
+export const bobTemplate = new SignatureTemplate(bobPriv);
\ No newline at end of file
diff --git a/test/contracts/accumulator.test.ts b/test/contracts/accumulator.test.ts
new file mode 100644
index 0000000..a03d717
--- /dev/null
+++ b/test/contracts/accumulator.test.ts
@@ -0,0 +1,1493 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { binToHex, cashAddressToLockingBytecode } from '@bitauth/libauth';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, aliceTokenAddress, nameTokenCategory, reversedNameTokenCategory } from '../common.js';
+import artifacts from '../artifacts.js';
+
+describe('Accumulator', () =>
+{
+ const provider = new MockNetworkProvider();
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+
+ const accumulatorContract = new Contract(BitCANNArtifacts.Accumulator, [], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
+
+ let threadNFTUTXO: Utxo;
+ let accumulatorUTXO: Utxo;
+ let counterNFTUTXO: Utxo;
+ let authorizedThreadNFTUTXO: Utxo;
+ let pureBCHUTXO: Utxo;
+ let transaction: TransactionBuilder;
+
+ beforeAll(() =>
+ {
+ // Get the accumulator contract locking bytecode
+ const accumulatorLockingBytecode = cashAddressToLockingBytecode(accumulatorContract.address);
+ if(typeof accumulatorLockingBytecode === 'string')
+ {
+ throw new Error(`Failed to get locking bytecode: ${accumulatorLockingBytecode}`);
+ }
+
+ // Create thread NFT UTXO
+ threadNFTUTXO =
+ {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: binToHex(accumulatorLockingBytecode.bytecode),
+ capability: 'none',
+ },
+ },
+ };
+
+ // Create accumulator contract UTXO
+ accumulatorUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ };
+
+ // Create counter NFT UTXO
+ counterNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(10),
+ nft: {
+ commitment: '0000000000000001',
+ capability: 'minting',
+ },
+ },
+ };
+
+ // Create authorized thread NFT UTXO
+ authorizedThreadNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(5),
+ nft: {
+ commitment: binToHex(accumulatorLockingBytecode.bytecode),
+ capability: 'none',
+ },
+ },
+ };
+
+ // Create pure BCH UTXO
+ pureBCHUTXO = {
+ ...randomUtxo({ satoshis: BigInt(500000) }),
+ };
+
+ // Add UTXOs to provider
+ provider.addUtxo(registryContract.address, threadNFTUTXO);
+ provider.addUtxo(accumulatorContract.address, accumulatorUTXO);
+ provider.addUtxo(registryContract.address, counterNFTUTXO);
+ provider.addUtxo(registryContract.address, authorizedThreadNFTUTXO);
+ provider.addUtxo(aliceAddress, pureBCHUTXO);
+ });
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 6 inputs instead of 5
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 5 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 6 outputs instead of 5
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: BigInt(1000),
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 5 outputs');
+ });
+
+ it('should fail when accumulator contract is not at input index 1', async () =>
+ {
+ // Construct the transaction with accumulator contract at wrong index
+ // Put it at index 2 instead of index 1, but this will fail at registry validation first
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: locking bytecode must match authorized contract from input 0 NFT commitment');
+ });
+
+ it('should fail when input 1 locking bytecode does not match output 1', async () =>
+ {
+ // Create a different contract for output 1
+ const differentContract = new Contract(artifacts, [], { provider });
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: differentContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: locking bytecode must match output 1');
+ });
+
+ it('should fail when output 1 has token category', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 1: must not have any token category (pure BCH only)');
+ });
+
+ it('should fail when input 2 locking bytecode does not match registry', async () =>
+ {
+ // Create a different contract for input 2
+ const differentContract = new Contract(artifacts, [], { provider });
+ const differentCounterNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(10),
+ nft: {
+ commitment: '0000000000000001',
+ capability: 'minting' as const,
+ },
+ },
+ };
+ provider.addUtxo(differentContract.address, differentCounterNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(differentCounterNFTUTXO, differentContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: differentCounterNFTUTXO.satoshis,
+ token: {
+ category: differentCounterNFTUTXO.token!.category,
+ amount: differentCounterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: differentCounterNFTUTXO.token!.nft!.capability,
+ commitment: differentCounterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when input 3 locking bytecode does not match registry', async () =>
+ {
+ // Create a different contract for input 3
+ const differentContract = new Contract(artifacts, [], { provider });
+ const accumulatorLockingBytecode = cashAddressToLockingBytecode(accumulatorContract.address);
+ if(typeof accumulatorLockingBytecode === 'string')
+ {
+ throw new Error(`Failed to get locking bytecode: ${accumulatorLockingBytecode}`);
+ }
+ const differentAuthorizedThreadNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(5),
+ nft: {
+ commitment: binToHex(accumulatorLockingBytecode.bytecode),
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(differentContract.address, differentAuthorizedThreadNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(differentAuthorizedThreadNFTUTXO, differentContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + differentAuthorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: differentAuthorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: differentAuthorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: differentAuthorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: differentAuthorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when output 2 locking bytecode does not match registry', async () =>
+ {
+ // Create a different contract for output 2
+ const differentContract = new Contract(artifacts, [], { provider });
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: differentContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when output 3 locking bytecode does not match registry', async () =>
+ {
+ // Create a different contract for output 3
+ const differentContract = new Contract(artifacts, [], { provider });
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: differentContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when output 2 token category does not match input 2', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ // Different category
+ token: {
+ category: nameTokenCategory + '03',
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: token category does not match input 2');
+ });
+
+ it('should fail when output 3 token category does not match input 3', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ // Different category
+ token: {
+ category: nameTokenCategory + '01',
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: token category does not match input 3');
+ });
+
+ it('should fail when input 3 token category does not match registry', async () =>
+ {
+ // Get the accumulator contract locking bytecode
+ const accumulatorLockingBytecode = cashAddressToLockingBytecode(accumulatorContract.address);
+ if(typeof accumulatorLockingBytecode === 'string')
+ {
+ throw new Error(`Failed to get locking bytecode: ${accumulatorLockingBytecode}`);
+ }
+
+ // Create authorized thread NFT with different category
+ const differentCategoryAuthorizedThreadNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ // Different category
+ category: nameTokenCategory + '01',
+ amount: BigInt(5),
+ nft: {
+ commitment: binToHex(accumulatorLockingBytecode.bytecode),
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(registryContract.address, differentCategoryAuthorizedThreadNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(differentCategoryAuthorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + differentCategoryAuthorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: differentCategoryAuthorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: differentCategoryAuthorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: differentCategoryAuthorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: differentCategoryAuthorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: token category does not match registry (immutable NFT check)');
+ });
+
+ it('should fail when counter NFT category prefix does not match registry', async () =>
+ {
+ // Create counter NFT with different category prefix
+ const differentCategoryCounterNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ // Different prefix - use a completely different category
+ category: '0000000000000000000000000000000000000000000000000000000000000001',
+ amount: BigInt(10),
+ nft: {
+ commitment: '0000000000000001',
+ capability: 'minting' as const,
+ },
+ },
+ };
+ provider.addUtxo(registryContract.address, differentCategoryCounterNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(differentCategoryCounterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: differentCategoryCounterNFTUTXO.satoshis,
+ token: {
+ category: differentCategoryCounterNFTUTXO.token!.category,
+ amount: differentCategoryCounterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: differentCategoryCounterNFTUTXO.token!.nft!.capability,
+ commitment: differentCategoryCounterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: token category prefix does not match registry');
+ });
+
+ it('should fail when counter NFT capability is not minting', async () =>
+ {
+ // Create counter NFT with wrong capability
+ const wrongCapabilityCounterNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(10),
+ nft: {
+ commitment: '0000000000000001',
+ capability: 'mutable' as const,
+ },
+ },
+ };
+ provider.addUtxo(registryContract.address, wrongCapabilityCounterNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(wrongCapabilityCounterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: wrongCapabilityCounterNFTUTXO.satoshis,
+ token: {
+ category: wrongCapabilityCounterNFTUTXO.token!.category,
+ amount: wrongCapabilityCounterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: wrongCapabilityCounterNFTUTXO.token!.nft!.capability,
+ commitment: wrongCapabilityCounterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: counter capability must be minting capability (0x02)');
+ });
+
+ it('should fail when input 3 NFT commitment length is not 35 bytes', async () =>
+ {
+ // Create authorized thread NFT with wrong commitment length
+ const wrongCommitmentLengthAuthorizedThreadNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(5),
+ nft: {
+ commitment: '00',
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(registryContract.address, wrongCommitmentLengthAuthorizedThreadNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(wrongCommitmentLengthAuthorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + wrongCommitmentLengthAuthorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: wrongCommitmentLengthAuthorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: wrongCommitmentLengthAuthorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: wrongCommitmentLengthAuthorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: wrongCommitmentLengthAuthorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: NFT commitment length must be 35 bytes (authorized contract locking bytecode)');
+ });
+
+ it('should fail when counter NFT has empty commitment', async () =>
+ {
+ // Create counter NFT with empty commitment
+ const emptyCommitmentCounterNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(10),
+ nft: {
+ // Empty commitment
+ commitment: '',
+ capability: 'minting' as const,
+ },
+ },
+ };
+ provider.addUtxo(registryContract.address, emptyCommitmentCounterNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(emptyCommitmentCounterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: emptyCommitmentCounterNFTUTXO.satoshis,
+ token: {
+ category: emptyCommitmentCounterNFTUTXO.token!.category,
+ amount: emptyCommitmentCounterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: emptyCommitmentCounterNFTUTXO.token!.nft!.capability,
+ commitment: emptyCommitmentCounterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: counter NFT must have a non-empty commitment (registration ID)');
+ });
+
+ it('should fail when counter NFT has zero token amount', async () =>
+ {
+ // Create counter NFT with zero amount
+ const zeroAmountCounterNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '0000000000000001',
+ capability: 'minting' as const,
+ },
+ },
+ };
+ provider.addUtxo(registryContract.address, zeroAmountCounterNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(zeroAmountCounterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: zeroAmountCounterNFTUTXO.satoshis,
+ token: {
+ category: zeroAmountCounterNFTUTXO.token!.category,
+ amount: zeroAmountCounterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: zeroAmountCounterNFTUTXO.token!.nft!.capability,
+ commitment: zeroAmountCounterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: counter NFT must have token amount greater than 0');
+ });
+
+ it('should fail when output 2 token amount is not sum of input amounts', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount + BigInt(1),
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: token amount must equal input 2 + input 3 amounts (accumulation)');
+ });
+
+ it('should fail when input 4 has token category', async () =>
+ {
+ // Create input 4 with token category
+ const tokenInput4UTXO = {
+ ...randomUtxo({ satoshis: BigInt(500000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ },
+ };
+ provider.addUtxo(aliceAddress, tokenInput4UTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(tokenInput4UTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: tokenInput4UTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 4: must be pure BCH (no token category)');
+ });
+
+ it('should fail when output 4 has token category', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: pureBCHUTXO.satoshis,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 4: must be pure BCH (no token category)');
+ });
+
+ it('should succeed with valid transaction', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(accumulatorUTXO, accumulatorContract.unlock.call())
+ .addInput(counterNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: accumulatorContract.tokenAddress,
+ amount: accumulatorUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: counterNFTUTXO.satoshis,
+ token: {
+ category: counterNFTUTXO.token!.category,
+ amount: counterNFTUTXO.token!.amount + authorizedThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: counterNFTUTXO.token!.nft!.capability,
+ commitment: counterNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: authorizedThreadNFTUTXO.satoshis,
+ token: {
+ category: authorizedThreadNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: authorizedThreadNFTUTXO.token!.nft!.capability,
+ commitment: authorizedThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).resolves.toBeDefined();
+ });
+});
\ No newline at end of file
diff --git a/test/contracts/auction.test.ts b/test/contracts/auction.test.ts
new file mode 100644
index 0000000..348692b
--- /dev/null
+++ b/test/contracts/auction.test.ts
@@ -0,0 +1,1668 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { binToHex, cashAddressToLockingBytecode, hexToBin } from '@bitauth/libauth';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, alicePkh, aliceTemplate, nameTokenCategory, mockOptions, reversedNameTokenCategory, invalidNameTokenCategory, aliceTokenAddress } from '../common.js';
+import { getTxOutputs, getAuctionPrice, getRegistrationIdCommitment } from '../utils.js';
+import artifacts from '../artifacts.js';
+
+describe('Auction', () =>
+{
+ const provider = new MockNetworkProvider();
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+ const auctionContract = new Contract(BitCANNArtifacts.Auction, [], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
+ const auctionLockingBytecode = cashAddressToLockingBytecode(auctionContract.address);
+ // @ts-ignore
+ const auctionLockingBytecodeHex = binToHex(auctionLockingBytecode.bytecode);
+
+ const name = 'test';
+ const nameHex = Buffer.from(name).toString('hex');
+ const nameBin = hexToBin(nameHex);
+
+ let threadNFTUTXO: Utxo;
+ let registrationCounterUTXO: Utxo;
+ let mintingNFTUTXO: Utxo;
+ let authorizedContractUTXO: Utxo;
+ let userUTXO: Utxo;
+ let userUTXOA: Utxo;
+ let transaction: TransactionBuilder;
+ let auctionAmount: bigint;
+ let newRegistrationId: number;
+ let newRegistrationIdCommitment: string;
+
+ beforeAll(() =>
+ {
+ userUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000000) }),
+ };
+ userUTXOA = {
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(aliceAddress, userUTXO);
+ provider.addUtxo(aliceAddress, userUTXOA);
+
+ authorizedContractUTXO = {
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(auctionContract.address, authorizedContractUTXO);
+
+ threadNFTUTXO = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: auctionLockingBytecodeHex,
+ capability: 'none',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(registryContract.address, threadNFTUTXO);
+
+ registrationCounterUTXO = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt('9223372036854775807'),
+ nft: {
+ commitment: '00',
+ capability: 'minting',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // Create the counterNFT and the minting NFT for the registry contract
+ provider.addUtxo(registryContract.address, registrationCounterUTXO);
+
+ mintingNFTUTXO = {
+ token: {
+ amount: BigInt(0),
+ category: nameTokenCategory,
+ nft: {
+ commitment: '',
+ capability: 'minting',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(registryContract.address, mintingNFTUTXO);
+
+ newRegistrationId = parseInt(registrationCounterUTXO.token!.nft!.commitment, 16) + 1;
+ newRegistrationIdCommitment = getRegistrationIdCommitment(BigInt(newRegistrationId));
+
+ auctionAmount = getAuctionPrice(BigInt(newRegistrationId), BigInt(mockOptions.minStartingBid));
+ });
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addInput(userUTXOA, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 4 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: 1000n,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: 1000n,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have at most 5 outputs');
+ });
+
+ it('should fail when contract is not used at input index 1', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with contract at input 0 instead of 1
+ transaction = new TransactionBuilder({ provider })
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: auction contract UTXO must be at this index');
+ });
+
+ it('should fail when attaching a token to the auction output', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 1: must not have any token category (pure BCH only)');
+ });
+
+ it('should fail when using a non registry contract in input 2', async () =>
+ {
+ provider.addUtxo(testContract.address, registrationCounterUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, testContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when using a non registry contract in output 2', async () =>
+ {
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when using a non registry contract in output 3', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail due to incorrect new registration id', async () =>
+ {
+ const customRegistrationIdCommitment = getRegistrationIdCommitment(BigInt(newRegistrationId) + 2n);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: customRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: registration ID must increase by 1');
+ });
+
+ it('should fail due to incorrect new registration tokenAmount deducted from counterNFT', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId + 1),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: counter NFT token amount must decrease by currentRegistrationId');
+ });
+
+ it('should fail due to incorrect token amount in auctionNFT', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId + 1),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: auction NFT token amount must equal currentRegistrationId');
+ });
+
+ it('should fail due to invalid auction amount, value less than minimum', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount - BigInt(1)),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: auction price must be at least minimum calculated price');
+ });
+
+ it('should pass due to correct auction amount, value greater than minimum ', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount + BigInt(1)),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: userUTXO.satoshis,
+ });
+
+ const transactionSize = transaction.build().length;
+ const changeAmount = userUTXO.satoshis - (auctionAmount + BigInt(transactionSize));
+ transaction.outputs[transaction.outputs.length - 1].amount = changeAmount;
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs).toEqual(expect.arrayContaining([{ to: aliceAddress, amount: changeAmount, token: undefined }]));
+ });
+
+ it('should pass for registrationID 120001', async () =>
+ {
+ const customRegistrationId = 120000n;
+ const customRegistrationIdCommitment = getRegistrationIdCommitment(customRegistrationId);
+
+ const customPlusOneRegistrationIdCommitment = getRegistrationIdCommitment(customRegistrationId + 1n);
+
+ const tempRegistrationCounterUTXO: Utxo = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt('9223372036854775807'),
+ nft: {
+ commitment: customRegistrationIdCommitment,
+ capability: 'minting',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // Create the counterNFT and the minting NFT for the registry contract
+ // @ts-ignore
+ provider.addUtxo(registryContract.address, tempRegistrationCounterUTXO);
+
+ const currentAuctionAmount = getAuctionPrice(customRegistrationId + 1n, BigInt(mockOptions.minStartingBid));
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(tempRegistrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: tempRegistrationCounterUTXO.satoshis,
+ token: {
+ category: tempRegistrationCounterUTXO.token!.category,
+ amount: tempRegistrationCounterUTXO.token!.amount - (customRegistrationId + 1n),
+ nft: {
+ capability: tempRegistrationCounterUTXO.token!.nft!.capability,
+ commitment: customPlusOneRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: currentAuctionAmount + 1n,
+ token: {
+ category: tempRegistrationCounterUTXO.token!.category,
+ amount: customRegistrationId + 1n,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: userUTXO.satoshis,
+ });
+
+ const transactionSize = transaction.build().length;
+ const changeAmount = userUTXO.satoshis - (currentAuctionAmount + BigInt(transactionSize));
+ transaction.outputs[transaction.outputs.length - 1].amount = changeAmount;
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs).toEqual(expect.arrayContaining([{ to: aliceAddress, amount: changeAmount, token: undefined }]));
+ });
+
+ it('should pass for registrationID 1000001', async () =>
+ {
+ const customRegistrationId = 1000000n;
+ const customRegistrationIdCommitment = getRegistrationIdCommitment(customRegistrationId);
+
+ const customPlusOneRegistrationIdCommitment = getRegistrationIdCommitment(customRegistrationId + 1n);
+
+ const tempRegistrationCounterUTXO: Utxo = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt('9223372036854775807'),
+ nft: {
+ commitment: customRegistrationIdCommitment,
+ capability: 'minting',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // Create the counterNFT and the minting NFT for the registry contract
+ // @ts-ignore
+ provider.addUtxo(registryContract.address, tempRegistrationCounterUTXO);
+
+ const currentAuctionAmount = getAuctionPrice(customRegistrationId + 1n, BigInt(mockOptions.minStartingBid));
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(tempRegistrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: tempRegistrationCounterUTXO.satoshis,
+ token: {
+ category: tempRegistrationCounterUTXO.token!.category,
+ amount: tempRegistrationCounterUTXO.token!.amount - (customRegistrationId + 1n),
+ nft: {
+ capability: tempRegistrationCounterUTXO.token!.nft!.capability,
+ commitment: customPlusOneRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: currentAuctionAmount,
+ token: {
+ category: tempRegistrationCounterUTXO.token!.category,
+ amount: customRegistrationId + 1n,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: userUTXO.satoshis,
+ });
+
+ const transactionSize = transaction.build().length;
+ const changeAmount = userUTXO.satoshis - (currentAuctionAmount + BigInt(transactionSize));
+ transaction.outputs[transaction.outputs.length - 1].amount = changeAmount;
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs).toEqual(expect.arrayContaining([{ to: aliceAddress, amount: changeAmount, token: undefined }]));
+ });
+
+ it('should fail for overflow registrationID 9223372036854775807n', async () =>
+ {
+ const customRegistrationId = 9223372036854775807n;
+ const customRegistrationIdCommitment = getRegistrationIdCommitment(customRegistrationId);
+
+ const customPlusOneRegistrationIdCommitment = getRegistrationIdCommitment(customRegistrationId + 1n);
+
+ const tempRegistrationCounterUTXO: Utxo = {
+ token: {
+ category: nameTokenCategory,
+ amount: 9223372036854775807n,
+ nft: {
+ commitment: customRegistrationIdCommitment,
+ capability: 'minting',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // Create the counterNFT and the minting NFT for the registry contract
+ // @ts-ignore
+ provider.addUtxo(registryContract.address, tempRegistrationCounterUTXO);
+
+ const currentAuctionAmount = getAuctionPrice(customRegistrationId + 1n, BigInt(mockOptions.minStartingBid));
+
+ try
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(tempRegistrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: tempRegistrationCounterUTXO.satoshis,
+ token: {
+ category: tempRegistrationCounterUTXO.token!.category,
+ amount: tempRegistrationCounterUTXO.token!.amount - (customRegistrationId + 1n),
+ nft: {
+ capability: tempRegistrationCounterUTXO.token!.nft!.capability,
+ commitment: customPlusOneRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: currentAuctionAmount,
+ token: {
+ category: tempRegistrationCounterUTXO.token!.category,
+ amount: customRegistrationId + 1n,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: userUTXO.satoshis,
+ });
+
+ }
+ catch(error)
+ {
+ await expect(Promise.reject(error)).rejects.toThrow('Tried to add an output with -1 tokens, which is invalid');
+ }
+ });
+
+ it('should fail due to non p2pkh user input', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: locking bytecode must be 25 bytes (P2PKH)');
+ });
+
+ it('should fail due to nft commitment mismatch in auction NFT output', async () =>
+ {
+ const differentName = '123';
+ const differentNameHex = Buffer.from(differentName).toString('hex');
+ const differentNameBin = hexToBin(differentNameHex);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(differentNameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: NFT commitment must match bidder PKH + name');
+ });
+
+ it('should fail due to long name', async () =>
+ {
+ const longName = '123456789123456789';
+ const longNameHex = Buffer.from(longName).toString('hex');
+ const longNameBin = hexToBin(longNameHex);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(longNameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(longNameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Name: length must be at most 16 characters');
+ });
+
+ it('should fail due to change in token category of counter nft', async () =>
+ {
+ const differentRegistrationCounterUTXO: Utxo = {
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt('9223372036854775807'),
+ nft: {
+ commitment: '00',
+ capability: 'minting',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // Create the counterNFT and the minting NFT for the registry contract
+ provider.addUtxo(registryContract.address, differentRegistrationCounterUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(differentRegistrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: 'none',
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: counter NFT token category must match input 2');
+ });
+
+ it('should fail due to invalid capability in token category of counter nft', async () =>
+ {
+ const differentRegistrationCounterUTXO: Utxo = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt('9223372036854775807'),
+ nft: {
+ commitment: '00',
+ capability: 'none',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // Create the counterNFT and the minting NFT for the registry contract
+ provider.addUtxo(registryContract.address, differentRegistrationCounterUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(differentRegistrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: 'none',
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: counter NFT capability must be minting (0x02)');
+ });
+
+ it('should fail due to invalid capability in token category of counter nft', async () =>
+ {
+ const differentRegistrationCounterUTXO: Utxo = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt('9223372036854775807'),
+ nft: {
+ commitment: '00',
+ capability: 'none',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // Create the counterNFT and the minting NFT for the registry contract
+ provider.addUtxo(registryContract.address, differentRegistrationCounterUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(differentRegistrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: 'none',
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: counter NFT capability must be minting (0x02)');
+ });
+
+ it('should fail due to invalid capability in token category of auction nft', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'none',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: auction NFT capability must be mutable (0x01)');
+ });
+
+ it('should fail due to token attached to change output', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: 'none',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 4: change must be pure BCH (no token category)');
+ });
+
+ it('should pass without change output', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(4);
+ });
+
+ it('should pass auction without fail', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: userUTXO.satoshis,
+ });
+
+ const transactionSize = transaction.build().length;
+ const changeAmount = userUTXO.satoshis - (auctionAmount + BigInt(transactionSize));
+ transaction.outputs[transaction.outputs.length - 1].amount = changeAmount;
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs).toEqual(expect.arrayContaining([{ to: aliceAddress, amount: changeAmount, token: undefined }]));
+ });
+});
\ No newline at end of file
diff --git a/test/contracts/bid.test.ts b/test/contracts/bid.test.ts
new file mode 100644
index 0000000..efdea58
--- /dev/null
+++ b/test/contracts/bid.test.ts
@@ -0,0 +1,914 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { binToHex, cashAddressToLockingBytecode, hexToBin } from '@bitauth/libauth';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, alicePkh, aliceTemplate, nameTokenCategory, mockOptions, reversedNameTokenCategory, invalidNameTokenCategory, aliceTokenAddress } from '../common.js';
+import { getTxOutputs, getAuctionPrice } from '../utils.js';
+import artifacts from '../artifacts.js';
+
+describe('Bid', () =>
+{
+ const provider = new MockNetworkProvider();
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+ const bidContract = new Contract(BitCANNArtifacts.Bid, [ ], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
+ const bidLockingBytecode = cashAddressToLockingBytecode(bidContract.address);
+ // @ts-ignore
+ const bidLockingBytecodeHex = binToHex(bidLockingBytecode.bytecode);
+
+ const name = 'test';
+ const nameHex = Buffer.from(name).toString('hex');
+ const nameBin = hexToBin(nameHex);
+
+ let threadNFTUTXO: Utxo;
+ let auctionNFTUTXO: Utxo;
+ let authorizedContractUTXO: Utxo;
+ let userUTXO: Utxo;
+ let userUTXOA: Utxo;
+ let transaction: TransactionBuilder;
+ let previousBidAmount: bigint;
+ let newBidAmount: bigint;
+ let registrationId: number;
+
+ beforeAll(() =>
+ {
+ userUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000000) }),
+ };
+ userUTXOA = {
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(aliceAddress, userUTXO);
+ provider.addUtxo(aliceAddress, userUTXOA);
+
+ authorizedContractUTXO = {
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(bidContract.address, authorizedContractUTXO);
+
+ threadNFTUTXO = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: bidLockingBytecodeHex,
+ capability: 'none',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(registryContract.address, threadNFTUTXO);
+
+ registrationId = 1;
+ previousBidAmount = getAuctionPrice(BigInt(registrationId), BigInt(mockOptions.minStartingBid));
+ newBidAmount = previousBidAmount + (previousBidAmount * BigInt(mockOptions.minBidIncreasePercentage) / 100n) + 1n;
+
+ // Create auctionNFT with previous bidder's PKH
+ auctionNFTUTXO = {
+ ...randomUtxo({ satoshis: previousBidAmount }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, auctionNFTUTXO);
+ });
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 5 inputs instead of 4
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addInput(userUTXOA, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 4 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 6 outputs instead of max 5
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: 1000n,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: 1000n,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have at most 5 outputs');
+ });
+
+ it('should fail when contract is not used at input index 1', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with contract at input 0 instead of 1
+ transaction = new TransactionBuilder({ provider })
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: bid contract UTXO must be at this index');
+ });
+
+ it('should fail when using a non registry contract in input 2', async () =>
+ {
+ provider.addUtxo(testContract.address, auctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, testContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: auction NFT locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when using a non registry contract in output 2', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: auction NFT locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail due to invalid bid amount, less than minimum increase', async () =>
+ {
+ const insufficientBidAmount = previousBidAmount + (previousBidAmount * BigInt(mockOptions.minBidIncreasePercentage) / 100n) - 1n;
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: insufficientBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: bid amount must be at least 5 percentage higher');
+ });
+
+ it('should fail due to non p2pkh funding input', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: locking bytecode must be 25 bytes (P2PKH)');
+ });
+
+ it('should fail due to token attached to change output', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: 1000n,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: BigInt(0),
+ nft: {
+ capability: 'none',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 4: change must be pure BCH (no token category)');
+ });
+
+ it('should fail due to token category mismatch in auctionNFT', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: auction NFT token category must match input 2');
+ });
+
+ it('should fail due to invalid auction category', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: auction NFT token category must match input 2');
+ });
+
+ it('should fail due to invalid auction capability', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'none',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: auction NFT token category must match input 2');
+ });
+
+
+
+ it('should fail due to NFT commitment mismatch', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + '646966666572656e746e616d65',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: auction NFT commitment must match new bidder PKH + name');
+ });
+
+ it('should fail due to token amount mismatch', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount + 1n,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: auction NFT token amount must match input 2');
+ });
+
+ it('should fail due to previous bidder locking bytecode mismatch', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: previous bidder locking bytecode must match previous PKH');
+ });
+
+ it('should fail due to previous bidder refund amount mismatch', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount + 1n,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: previous bidder refund amount must match previous bid amount');
+ });
+
+ it('should pass with valid bid transaction', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: userUTXO.satoshis - newBidAmount - previousBidAmount - 1000n,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(5);
+ });
+
+ it('should pass without change output', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, bidContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: bidContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: newBidAmount,
+ token: {
+ category: auctionNFTUTXO.token!.category,
+ amount: auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: previousBidAmount,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(4);
+ });
+
+});
\ No newline at end of file
diff --git a/test/contracts/conflict-resolver.test.ts b/test/contracts/conflict-resolver.test.ts
new file mode 100644
index 0000000..4902f53
--- /dev/null
+++ b/test/contracts/conflict-resolver.test.ts
@@ -0,0 +1,948 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { binToHex, cashAddressToLockingBytecode } from '@bitauth/libauth';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, alicePkh, aliceTokenAddress, nameTokenCategory, reversedNameTokenCategory, invalidNameTokenCategory } from '../common.js';
+import { getTxOutputs } from '../utils.js';
+import artifacts from '../artifacts.js';
+
+describe('ConflictResolver', () =>
+{
+ const provider = new MockNetworkProvider();
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+ const conflictResolverContract = new Contract(BitCANNArtifacts.ConflictResolver, [], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
+ const conflictResolverLockingBytecode = cashAddressToLockingBytecode(conflictResolverContract.address);
+ // @ts-ignore
+ const conflictResolverLockingBytecodeHex = binToHex(conflictResolverLockingBytecode.bytecode);
+
+ const name = 'test';
+ const nameHex = Buffer.from(name).toString('hex');
+
+ let threadNFTUTXO: Utxo;
+ let validAuctionNFTUTXO: Utxo;
+ let invalidAuctionNFTUTXO: Utxo;
+ let authorizedContractUTXO: Utxo;
+ let transaction: TransactionBuilder;
+ let validRegistrationId: number;
+ let invalidRegistrationId: number;
+
+ beforeAll(() =>
+ {
+ authorizedContractUTXO = {
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(conflictResolverContract.address, authorizedContractUTXO);
+
+ threadNFTUTXO = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: conflictResolverLockingBytecodeHex,
+ capability: 'none',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(registryContract.address, threadNFTUTXO);
+
+ validRegistrationId = 1;
+ invalidRegistrationId = 2;
+
+ // Create valid auctionNFT (lower registration ID)
+ validAuctionNFTUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(validRegistrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ // Create invalid auctionNFT (higher registration ID)
+ invalidAuctionNFTUTXO = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(invalidRegistrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, validAuctionNFTUTXO);
+ provider.addUtxo(registryContract.address, invalidAuctionNFTUTXO);
+ });
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 5 inputs instead of 4
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 4 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 5 outputs instead of 4
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: 1000n,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 4 outputs');
+ });
+
+ it('should fail when contract is not used at input index 1', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with contract at input 0 instead of 1
+ transaction = new TransactionBuilder({ provider })
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: conflict resolver contract UTXO must be at this index');
+ });
+
+ it('should fail when using a non registry contract for valid auction', async () =>
+ {
+ provider.addUtxo(testContract.address, validAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, testContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: valid auction locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when using a non registry contract for invalid auction', async () =>
+ {
+ provider.addUtxo(testContract.address, invalidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: invalid auction locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail due to invalid auction category', async () =>
+ {
+ // Create valid auctionNFT with invalid category
+ const customValidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(validRegistrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ } as any;
+
+ provider.addUtxo(registryContract.address, customValidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(customValidAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: customValidAuctionNFTUTXO.satoshis,
+ token: {
+ category: customValidAuctionNFTUTXO.token!.category,
+ amount: customValidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: customValidAuctionNFTUTXO.token!.nft!.capability,
+ commitment: customValidAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: valid auction token category prefix must match registry');
+ });
+
+ it('should fail due to invalid valid auction capability', async () =>
+ {
+ // Create valid auctionNFT with invalid capability
+ const customValidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(validRegistrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, customValidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(customValidAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: customValidAuctionNFTUTXO.satoshis,
+ token: {
+ category: customValidAuctionNFTUTXO.token!.category,
+ amount: customValidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: customValidAuctionNFTUTXO.token!.nft!.capability,
+ commitment: customValidAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: valid auction capability must be mutable (0x01)');
+ });
+
+ it('should fail due to auction token category mismatch', async () =>
+ {
+ // Create invalid auctionNFT with different category
+ const customInvalidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(invalidRegistrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ } as any;
+
+ provider.addUtxo(registryContract.address, customInvalidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(customInvalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + customInvalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: customInvalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2 and 3: auction token categories must match');
+ });
+
+ it('should fail due to auction name mismatch', async () =>
+ {
+ // Create invalid auctionNFT with different name
+ const differentName = 'different';
+ const differentNameHex = Buffer.from(differentName).toString('hex');
+
+ const customInvalidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(invalidRegistrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + differentNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, customInvalidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(customInvalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + customInvalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: customInvalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2 and 3: auction names must match');
+ });
+
+ it('should fail when valid auction registration ID is not lower', async () =>
+ {
+ // Create valid auctionNFT with higher registration ID (should fail)
+ const customValidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ // Higher than invalid auction's ID of 2
+ amount: BigInt(3),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, customValidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(customValidAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: customValidAuctionNFTUTXO.satoshis,
+ token: {
+ category: customValidAuctionNFTUTXO.token!.category,
+ amount: customValidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: customValidAuctionNFTUTXO.token!.nft!.capability,
+ commitment: customValidAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: valid auction registration ID must be lower than input 3');
+ });
+
+ it('should fail due to token amount mismatch', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ // Wrong amount
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount - 1n,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: token amount must equal input 0 + input 3 amounts (accumulation)');
+ });
+
+ it('should fail due to reward not being pure BCH', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ capability: 'none',
+ commitment: binToHex(alicePkh) + nameHex,
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: reward must be pure BCH (no token category)');
+ });
+
+ // Tests for valid conflict resolution scenarios
+ it('should pass with valid conflict resolution', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionNFTUTXO.satoshis,
+ token: {
+ category: validAuctionNFTUTXO.token!.category,
+ amount: validAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: validAuctionNFTUTXO.token!.nft!.capability,
+ commitment: validAuctionNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(4);
+ });
+
+ it('should pass with valid conflict resolution for different names', async () =>
+ {
+ // Create auctions with different names but same registration ID pattern
+ const name1 = 'domain1';
+ const name1Hex = Buffer.from(name1).toString('hex');
+
+ const validAuction1UTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + name1Hex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ const invalidAuction1UTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(2),
+ nft: {
+ commitment: binToHex(alicePkh) + name1Hex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, validAuction1UTXO);
+ provider.addUtxo(registryContract.address, invalidAuction1UTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuction1UTXO, registryContract.unlock.call())
+ .addInput(invalidAuction1UTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuction1UTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuction1UTXO.satoshis,
+ token: {
+ category: validAuction1UTXO.token!.category,
+ amount: validAuction1UTXO.token!.amount,
+ nft: {
+ capability: validAuction1UTXO.token!.nft!.capability,
+ commitment: validAuction1UTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuction1UTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(4);
+ });
+
+ it('should pass with valid conflict resolution for large registration ID difference', async () =>
+ {
+ // Create auctions with large registration ID difference
+ const validAuctionLargeUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ // Very low registration ID
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ const invalidAuctionLargeUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ // Very high registration ID
+ amount: BigInt(1000),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, validAuctionLargeUTXO);
+ provider.addUtxo(registryContract.address, invalidAuctionLargeUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, conflictResolverContract.unlock.call())
+ .addInput(validAuctionLargeUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionLargeUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionLargeUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: conflictResolverContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: validAuctionLargeUTXO.satoshis,
+ token: {
+ category: validAuctionLargeUTXO.token!.category,
+ amount: validAuctionLargeUTXO.token!.amount,
+ nft: {
+ capability: validAuctionLargeUTXO.token!.nft!.capability,
+ commitment: validAuctionLargeUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionLargeUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(4);
+ });
+
+});
\ No newline at end of file
diff --git a/test/contracts/factory.test.ts b/test/contracts/factory.test.ts
new file mode 100644
index 0000000..3ac0064
--- /dev/null
+++ b/test/contracts/factory.test.ts
@@ -0,0 +1,3194 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { binToHex, cashAddressToLockingBytecode } from '@bitauth/libauth';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, alicePkh, aliceTokenAddress, nameTokenCategory, reversedNameTokenCategory, invalidNameTokenCategory, mockOptions, bobAddress, bobTokenAddress } from '../common.js';
+import { getCreatorIncentive, getDomainPartialBytecode, padVmNumber } from '../utils.js';
+import artifacts from '../artifacts.js';
+
+describe('Factory', () =>
+{
+ const provider = new MockNetworkProvider();
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
+
+ const name = 'test';
+ const nameHex = Buffer.from(name).toString('hex');
+
+ // Get the domain partial bytecode for the Factory contract
+ const domainPartialBytecode = getDomainPartialBytecode(nameTokenCategory, {
+ provider,
+ addressType: 'p2sh32',
+ });
+
+ const factoryContract = new Contract(BitCANNArtifacts.Factory, [ domainPartialBytecode, alicePkh ], { provider });
+
+ const tldHex = Buffer.from(mockOptions.tld).toString('hex');
+ const nameContract = new Contract(BitCANNArtifacts.Name, [ nameHex, tldHex, reversedNameTokenCategory ], { provider });
+
+ let threadNFTUTXO: Utxo;
+ let factoryUTXO: Utxo;
+ let nameMintingNFTUTXO: Utxo;
+ let auctionNFTUTXO: Utxo;
+ let transaction: TransactionBuilder;
+ let registrationId: number;
+
+ beforeAll(() =>
+ {
+ // Get the factory contract locking bytecode
+ const factoryLockingBytecode = cashAddressToLockingBytecode(factoryContract.address);
+ if(typeof factoryLockingBytecode === 'string')
+ {
+ throw new Error(`Failed to get locking bytecode: ${factoryLockingBytecode}`);
+ }
+
+ registrationId = 1;
+
+ // Create thread NFT UTXO
+ threadNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: binToHex(factoryLockingBytecode.bytecode),
+ capability: 'none',
+ },
+ },
+ };
+
+ // Create factory contract UTXO
+ factoryUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ };
+
+ // Create name minting NFT UTXO - use the same category as registry input
+ nameMintingNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ // Use the same category prefix as registry input
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'minting',
+ },
+ },
+ };
+
+ // Create auction NFT UTXO
+ auctionNFTUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ // Add UTXOs to provider
+ provider.addUtxo(registryContract.address, threadNFTUTXO);
+ provider.addUtxo(factoryContract.address, factoryUTXO);
+ provider.addUtxo(registryContract.address, nameMintingNFTUTXO);
+ provider.addUtxo(registryContract.address, auctionNFTUTXO);
+ });
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 5 inputs instead of 4
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 4 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 8 outputs instead of 7 max
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: BigInt(1000),
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: BigInt(1000),
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have at most 7 outputs');
+ });
+
+ it('should fail when contract is not used at input index 1', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with contract at input 0 instead of 1
+ transaction = new TransactionBuilder({ provider })
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: factory contract UTXO must be at this index');
+ });
+
+ it('should fail when factory output has token category', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 1: must not have any token category (pure BCH only)');
+ });
+
+ it('should fail when factory output value does not match input', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis + 1000n,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: satoshi value must match output 1');
+ });
+
+ it('should fail when name minting NFT locking bytecode does not match registry', async () =>
+ {
+ // Create name minting NFT with different locking bytecode
+ const differentNameMintingNFTUTXO: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'minting',
+ },
+ },
+ };
+
+ provider.addUtxo(testContract.address, differentNameMintingNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(differentNameMintingNFTUTXO, testContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: differentNameMintingNFTUTXO.satoshis,
+ token: {
+ category: differentNameMintingNFTUTXO.token!.category,
+ amount: differentNameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: differentNameMintingNFTUTXO.token!.nft!.capability,
+ commitment: differentNameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: name minting NFT locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when auction NFT locking bytecode does not match registry', async () =>
+ {
+ // Create auction NFT with different locking bytecode
+ const differentAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(testContract.address, differentAuctionNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(differentAuctionNFTUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + differentAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: auction NFT locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when name minting NFT output locking bytecode does not match registry', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: name minting NFT locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when external auth NFT token category does not match registry', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: external auth NFT token category prefix must match registry');
+ });
+
+ it('should fail when internal auth NFT token category does not match registry', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 4: internal auth NFT token category prefix must match registry');
+ });
+
+ it('should fail when name ownership NFT token category does not match registry', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 5: name ownership NFT token category prefix must match registry');
+ });
+
+ it('should fail when name minting NFT token category does not match registry', async () =>
+ {
+ // Create name minting NFT with invalid category
+ const invalidNameMintingNFTUTXO: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory + '02',
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'minting',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, invalidNameMintingNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(invalidNameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: invalidNameMintingNFTUTXO.satoshis,
+ token: {
+ category: invalidNameMintingNFTUTXO.token!.category,
+ amount: invalidNameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: invalidNameMintingNFTUTXO.token!.nft!.capability,
+ commitment: invalidNameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: name minting NFT token category prefix must match registry');
+ });
+
+ it('should fail when name minting NFT capability is not minting', async () =>
+ {
+ // Create name minting NFT with invalid capability
+ const invalidCapabilityNameMintingNFTUTXO: Utxo = {
+ ...randomUtxo(),
+ token: {
+ // mutable instead of minting
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, invalidCapabilityNameMintingNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(invalidCapabilityNameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: invalidCapabilityNameMintingNFTUTXO.satoshis,
+ token: {
+ category: invalidCapabilityNameMintingNFTUTXO.token!.category,
+ amount: invalidCapabilityNameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: invalidCapabilityNameMintingNFTUTXO.token!.nft!.capability,
+ commitment: invalidCapabilityNameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: name minting NFT capability must be minting (0x02)');
+ });
+
+ it('should fail when name minting NFT output token category does not match input', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: invalidNameTokenCategory + '02',
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: name minting NFT token category must match input 2');
+ });
+
+ it('should fail when auction NFT token category does not match registry', async () =>
+ {
+ // Create auction NFT with invalid category
+ const invalidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: invalidNameTokenCategory + '01',
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, invalidAuctionNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: auction NFT token category prefix must match registry');
+ });
+
+ it('should fail when auction NFT capability is not mutable', async () =>
+ {
+ // Create auction NFT with invalid capability
+ const invalidCapabilityAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ // none instead of mutable
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, invalidCapabilityAuctionNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidCapabilityAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidCapabilityAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: auction NFT capability must be mutable (0x01)');
+ });
+
+ it('should fail when name minting NFT output commitment does not match input', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: Buffer.from('different').toString('hex'),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: name minting NFT must have empty commitment');
+ });
+
+ it('should fail when name minting NFT output has non-empty commitment', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: Buffer.from('notempty').toString('hex'),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: name minting NFT must have empty commitment');
+ });
+
+ it('should fail when name minting NFT output token amount does not match input', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ // different from input
+ amount: BigInt(1),
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: name minting NFT token amount must match input 2');
+ });
+
+ it('should fail when name minting NFT output token amount is not zero', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount + 1n,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: name minting NFT token amount must match input 2');
+ });
+
+ it('should fail when name minting NFT output value does not match input', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount - 1n,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis - 1n,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: name minting NFT satoshi value must match input 2');
+ });
+
+ it('should fail when thread NFT output token amount does not accumulate correctly', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount - 1n,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: token amount must equal input 0 + input 3 amounts (accumulation)');
+ });
+
+ it('should fail when external auth NFT has non-empty commitment', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: external auth NFT must have empty commitment');
+ });
+
+ it('should fail when external auth NFT value is not 1000', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1100),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: external auth NFT satoshi value must be 1000');
+ });
+
+ it('should fail when internal auth NFT commitment does not match registration ID', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(2), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 4: internal auth NFT commitment must match registration ID');
+ });
+
+ it('should fail when internal auth NFT value is not 1000', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1100),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 4: internal auth NFT satoshi value must be 1000');
+ });
+
+ it('should fail when name ownership NFT commitment does not match registration ID + name', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(2), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 5: name ownership NFT commitment must match registration ID + name');
+ });
+
+ it('should fail when name ownership NFT value is not 1000', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1100),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 5: name ownership NFT satoshi value must be 1000');
+ });
+
+ it('should fail when external auth NFT locking bytecode does not match name contract', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: bobTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: external auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail when internal auth NFT locking bytecode does not match name contract', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: bobTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 4: internal auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail when name ownership NFT locking bytecode does not match bidder PKH', async () =>
+ {
+
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: bobTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 5: name ownership NFT locking bytecode must match bidder PKH');
+ });
+
+ it('should fail when creator incentive is provided but token category is not pure BCH', async () =>
+ {
+ // Create auction NFT with high token amount to trigger creator incentive
+ const highValueAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, highValueAuctionNFTUTXO);
+ const expectedCreatorIncentive = getCreatorIncentive(highValueAuctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(highValueAuctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + highValueAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: expectedCreatorIncentive,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 6: creator incentive must be pure BCH (no token category)');
+ });
+
+ it('should fail when creator incentive value does not match calculated incentive', async () =>
+ {
+ // Create auction NFT with high token amount to trigger creator incentive
+ const highValueAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, highValueAuctionNFTUTXO);
+ const expectedCreatorIncentive = getCreatorIncentive(highValueAuctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(highValueAuctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + highValueAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive - BigInt(5),
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 6: creator incentive satoshi value must match calculated incentive');
+ });
+
+ it('should fail when creator incentive locking bytecode does not match creator PKH', async () =>
+ {
+ // Create auction NFT with high token amount (triggers creator incentive)
+ const highValueAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(100000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, highValueAuctionNFTUTXO);
+ const expectedCreatorIncentive = getCreatorIncentive(highValueAuctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(highValueAuctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + highValueAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: bobAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 6: creator incentive locking bytecode must match creator PKH');
+ });
+
+ it('should successfully finalize auction when all conditions are met', async () =>
+ {
+ const expectedCreatorIncentive = getCreatorIncentive(auctionNFTUTXO.satoshis, BigInt(registrationId));
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.address,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+
+ const txPromise = transaction.send();
+
+ // This should succeed
+ await expect(txPromise).resolves.toBeDefined();
+ });
+
+ it('should successfully finalize auction with creator incentive when conditions are met', async () =>
+ {
+ let auctionPrice = BigInt(700000);
+ const rID = BigInt(196858);
+ // Create auction NFT with high token amount to trigger creator incentive
+ const highValueAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: auctionPrice }),
+ token: {
+ category: nameTokenCategory,
+ amount: rID,
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, highValueAuctionNFTUTXO);
+ const expectedCreatorIncentive = getCreatorIncentive(auctionPrice, rID);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(factoryUTXO, factoryContract.unlock.call())
+ .addInput(nameMintingNFTUTXO, registryContract.unlock.call())
+ .addInput(highValueAuctionNFTUTXO, registryContract.unlock.call(), { sequence: mockOptions.minWaitTime })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + highValueAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: factoryContract.tokenAddress,
+ amount: factoryUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: nameMintingNFTUTXO.satoshis,
+ token: {
+ category: nameMintingNFTUTXO.token!.category,
+ amount: nameMintingNFTUTXO.token!.amount,
+ nft: {
+ capability: nameMintingNFTUTXO.token!.nft!.capability,
+ commitment: nameMintingNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(rID, 8),
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: BigInt(1000),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(rID, 8) + nameHex,
+ capability: 'none',
+ },
+ },
+ });
+
+ if(expectedCreatorIncentive > BigInt(20000))
+ {
+ transaction.addOutput({
+ to: aliceAddress,
+ amount: expectedCreatorIncentive,
+ });
+ }
+
+
+ const txPromise = transaction.send();
+
+ // This should succeed
+ await expect(txPromise).resolves.toBeDefined();
+ });
+});
\ No newline at end of file
diff --git a/test/contracts/name-enforcer.test.ts b/test/contracts/name-enforcer.test.ts
new file mode 100644
index 0000000..a49f311
--- /dev/null
+++ b/test/contracts/name-enforcer.test.ts
@@ -0,0 +1,1130 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { binToHex, cashAddressToLockingBytecode } from '@bitauth/libauth';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, alicePkh, aliceTokenAddress, nameTokenCategory, reversedNameTokenCategory, invalidNameTokenCategory } from '../common.js';
+import { getTxOutputs } from '../utils.js';
+import artifacts from '../artifacts.js';
+
+describe('NameEnforcer', () =>
+{
+ const provider = new MockNetworkProvider();
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+ const nameEnforcerContract = new Contract(BitCANNArtifacts.NameEnforcer, [], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
+ const nameEnforcerLockingBytecode = cashAddressToLockingBytecode(nameEnforcerContract.address);
+ // @ts-ignore
+ const nameEnforcerLockingBytecodeHex = binToHex(nameEnforcerLockingBytecode.bytecode);
+
+ const validName = 'test';
+ const validNameHex = Buffer.from(validName).toString('hex');
+
+ const invalidNameWithSpecialChar = 'test@';
+ const invalidNameWithSpecialCharHex = Buffer.from(invalidNameWithSpecialChar).toString('hex');
+
+ const invalidNameWithSpace = 'test name';
+ const invalidNameWithSpaceHex = Buffer.from(invalidNameWithSpace).toString('hex');
+
+ let threadNFTUTXO: Utxo;
+ let validAuctionNFTUTXO: Utxo;
+ let invalidAuctionNFTUTXO: Utxo;
+ let authorizedContractUTXO: Utxo;
+ let transaction: TransactionBuilder;
+ let registrationId: number;
+
+ beforeAll(() =>
+ {
+ authorizedContractUTXO = {
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(nameEnforcerContract.address, authorizedContractUTXO);
+
+ threadNFTUTXO = {
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: nameEnforcerLockingBytecodeHex,
+ capability: 'none',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ provider.addUtxo(registryContract.address, threadNFTUTXO);
+
+ registrationId = 1;
+
+ // Create auctionNFT with valid name
+ validAuctionNFTUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + validNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ // Create auctionNFT with invalid name
+ invalidAuctionNFTUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + invalidNameWithSpecialCharHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, validAuctionNFTUTXO);
+ provider.addUtxo(registryContract.address, invalidAuctionNFTUTXO);
+ });
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 4 inputs instead of 3
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addInput(validAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 3 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 4 outputs instead of 3
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: 1000n,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 3 outputs');
+ });
+
+ it('should fail when contract is not used at input index 1', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with contract at input 0 instead of 1
+ transaction = new TransactionBuilder({ provider })
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: name enforcer contract UTXO must be at this index');
+ });
+
+ it('should fail when using a non registry contract in input 2', async () =>
+ {
+ provider.addUtxo(testContract.address, invalidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(invalidAuctionNFTUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail due to invalid auction category', async () =>
+ {
+ // Create auctionNFT with invalid category
+ const customInvalidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + invalidNameWithSpecialCharHex,
+ capability: 'mutable',
+ },
+ },
+ } as any;
+
+ provider.addUtxo(registryContract.address, customInvalidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(customInvalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + customInvalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: customInvalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: auction token category does not match registry');
+ });
+
+ it('should fail due to invalid auction capability', async () =>
+ {
+ // Create auctionNFT with invalid capability
+ const customInvalidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + invalidNameWithSpecialCharHex,
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, customInvalidAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(customInvalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + customInvalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: customInvalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: auction capability must be mutable (0x01)');
+ });
+
+ it('should fail when character is a hyphen', async () =>
+ {
+ // Create auctionNFT with name containing hyphen
+ const hyphenName = 'test-name';
+ const hyphenNameHex = Buffer.from(hyphenName).toString('hex');
+
+ const hyphenAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + hyphenNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, hyphenAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ // Character at position 5 is '-'
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(hyphenAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + hyphenAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: hyphenAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Character is a hyphen');
+ });
+
+ it('should fail when character is a lowercase letter', async () =>
+ {
+ // Create auctionNFT with name containing lowercase letter
+ const lowercaseName = 'testname';
+ const lowercaseNameHex = Buffer.from(lowercaseName).toString('hex');
+
+ const lowercaseAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + lowercaseNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, lowercaseAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ // Character at position 1 is 'e'
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(lowercaseAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + lowercaseAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: lowercaseAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Character is lowercase letter');
+ });
+
+ it('should fail when character is an uppercase letter', async () =>
+ {
+ // Create auctionNFT with name containing uppercase letter
+ const uppercaseName = 'TestName';
+ const uppercaseNameHex = Buffer.from(uppercaseName).toString('hex');
+
+ const uppercaseAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + uppercaseNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, uppercaseAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ // Character at position 1 is 'T'
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(uppercaseAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + uppercaseAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: uppercaseAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Character is uppercase letter');
+ });
+
+ it('should fail when character is a digit', async () =>
+ {
+ // Create auctionNFT with name containing digit
+ const digitName = 'test123';
+ const digitNameHex = Buffer.from(digitName).toString('hex');
+
+ const digitAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + digitNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, digitAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ // Character at position 5 is '1'
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(digitAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + digitAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: digitAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Character is a digit');
+ });
+
+ it('should fail due to token amount mismatch', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount - 1n,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: token amount must equal input 0 + input 2 amounts (accumulation)');
+ });
+
+ it('should fail due to reward not being pure BCH', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ capability: 'none',
+ commitment: binToHex(alicePkh) + validNameHex,
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: reward must be pure BCH (no token category)');
+ });
+
+ it('should pass with valid invalid character detection', async () =>
+ {
+ // Create auctionNFT with name containing special character
+ const specialCharName = 'test@name';
+ const specialCharNameHex = Buffer.from(specialCharName).toString('hex');
+
+ const specialCharAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + specialCharNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, specialCharAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(specialCharAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + specialCharAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: specialCharAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(3);
+ });
+
+ it('should pass with space character detection', async () =>
+ {
+ // Create auctionNFT with name containing space
+ const spaceCharAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + invalidNameWithSpaceHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, spaceCharAuctionNFTUTXO);
+
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(spaceCharAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + spaceCharAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: spaceCharAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(3);
+ });
+
+ // Tests for valid names that should fail when trying to enforce them
+ it('should fail when trying to enforce valid name with only letters', async () =>
+ {
+ // Create auctionNFT with valid name containing only letters
+ const validLetterName = 'testname';
+ const validLetterNameHex = Buffer.from(validLetterName).toString('hex');
+
+ const validLetterAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + validLetterNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, validLetterAuctionNFTUTXO);
+
+ // Try to enforce a valid character (should fail)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(validLetterAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + validLetterAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: validLetterAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when trying to enforce valid name with letters and numbers', async () =>
+ {
+ // Create auctionNFT with valid name containing letters and numbers
+ const validMixedName = 'test123';
+ const validMixedNameHex = Buffer.from(validMixedName).toString('hex');
+
+ const validMixedAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + validMixedNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, validMixedAuctionNFTUTXO);
+
+ // Try to enforce a valid character (should fail)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(validMixedAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + validMixedAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: validMixedAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when trying to enforce valid name with letters, numbers, and hyphens', async () =>
+ {
+ // Create auctionNFT with valid name containing letters, numbers, and hyphens
+ const validHyphenName = 'test-name123';
+ const validHyphenNameHex = Buffer.from(validHyphenName).toString('hex');
+
+ const validHyphenAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + validHyphenNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, validHyphenAuctionNFTUTXO);
+
+ // Try to enforce a valid character (should fail)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(1n))
+ .addInput(validHyphenAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + validHyphenAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: validHyphenAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ // Tests for invalid names that should pass when enforcing them
+ it('should pass when enforcing name with special character @', async () =>
+ {
+ // Create auctionNFT with name containing @ symbol
+ const atSymbolName = 'test@name';
+ const atSymbolNameHex = Buffer.from(atSymbolName).toString('hex');
+
+ const atSymbolAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + atSymbolNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, atSymbolAuctionNFTUTXO);
+
+ // Enforce the invalid character (should pass)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(atSymbolAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + atSymbolAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: atSymbolAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(3);
+ });
+
+ it('should pass when enforcing name with underscore', async () =>
+ {
+ // Create auctionNFT with name containing underscore
+ const underscoreName = 'test_name';
+ const underscoreNameHex = Buffer.from(underscoreName).toString('hex');
+
+ const underscoreAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + underscoreNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, underscoreAuctionNFTUTXO);
+
+ // Enforce the invalid character (should pass)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(underscoreAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + underscoreAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: underscoreAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(3);
+ });
+
+ it('should pass when enforcing name with period', async () =>
+ {
+ // Create auctionNFT with name containing period
+ const periodName = 'test.name';
+ const periodNameHex = Buffer.from(periodName).toString('hex');
+
+ const periodAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + periodNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, periodAuctionNFTUTXO);
+
+ // Enforce the invalid character (should pass)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(periodAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + periodAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: periodAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(3);
+ });
+
+ it('should pass when enforcing name with exclamation mark', async () =>
+ {
+ // Create auctionNFT with name containing exclamation mark
+ const exclamationName = 'test!name';
+ const exclamationNameHex = Buffer.from(exclamationName).toString('hex');
+
+ const exclamationAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + exclamationNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, exclamationAuctionNFTUTXO);
+
+ // Enforce the invalid character (should pass)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(exclamationAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + exclamationAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: exclamationAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(3);
+ });
+
+ it('should pass when enforcing name with dollar sign', async () =>
+ {
+ // Create auctionNFT with name containing dollar sign
+ const dollarName = 'test$name';
+ const dollarNameHex = Buffer.from(dollarName).toString('hex');
+
+ const dollarAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(registrationId),
+ nft: {
+ commitment: binToHex(alicePkh) + dollarNameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, dollarAuctionNFTUTXO);
+
+ // Enforce the invalid character (should pass)
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, nameEnforcerContract.unlock.call(5n))
+ .addInput(dollarAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + dollarAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameEnforcerContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: dollarAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = await transaction.send();
+ // @ts-ignore
+ const txOutputs = getTxOutputs(txPromise);
+ expect(txOutputs.length).toBe(3);
+ });
+
+});
diff --git a/test/contracts/name.test.ts b/test/contracts/name.test.ts
new file mode 100644
index 0000000..db976b3
--- /dev/null
+++ b/test/contracts/name.test.ts
@@ -0,0 +1,2200 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, nameTokenCategory, reversedNameTokenCategory, mockOptions, aliceTemplate, aliceTokenAddress, invalidNameTokenCategory } from '../common.js';
+import artifacts from '../artifacts.js';
+import { padVmNumber } from '../utils.js';
+
+describe('Name', () =>
+{
+ const provider = new MockNetworkProvider();
+
+ const name = 'test';
+ const nameHex = Buffer.from(name).toString('hex');
+
+ const differentName = 'test2';
+ const differentNameHex = Buffer.from(differentName).toString('hex');
+
+ const tldHex = Buffer.from(mockOptions.tld).toString('hex');
+
+ const nameContract = new Contract(BitCANNArtifacts.Name, [ nameHex, tldHex, reversedNameTokenCategory ], { provider });
+ const differentNameContract = new Contract(BitCANNArtifacts.Name, [ differentNameHex, tldHex, reversedNameTokenCategory ], { provider });
+
+
+ const testContract = new Contract(artifacts, [], { provider });
+
+ let externalAuthNFTUTXO: Utxo;
+ let internalAuthNFTUTXO: Utxo;
+ let newExternalAuthNFTUTXO: Utxo;
+ let newInternalAuthNFTUTXO: Utxo;
+ let differentNameExternalAuthNFTUTXO: Utxo;
+ let differentNameInternalAuthNFTUTXO: Utxo;
+
+ let pureBCHUTXO: Utxo;
+ let ownershipNFTUTXO: Utxo;
+ let newOwnershipNFTUTXO: Utxo;
+ let differentNameOwnershipNFTUTXO: Utxo;
+ let transaction: TransactionBuilder;
+
+ beforeAll(() =>
+ {
+
+ // Create external auth NFT UTXO
+ externalAuthNFTUTXO =
+ {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ // Create internal auth NFT UTXO
+ internalAuthNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ // Create pure BCH UTXO
+ pureBCHUTXO = {
+ ...randomUtxo({ satoshis: BigInt(500000) }),
+ };
+
+ // Create invalid external auth NFT UTXO (for resolveOwnerConflict)
+ newExternalAuthNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ // Create invalid internal auth NFT UTXO (for resolveOwnerConflict)
+ newInternalAuthNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(2), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ differentNameExternalAuthNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ differentNameInternalAuthNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(3), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ // Create valid ownership NFT UTXO (for resolveOwnerConflict)
+
+ ownershipNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ newOwnershipNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(2), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ differentNameOwnershipNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(3), 8) + differentNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ // Create invalid ownership NFT UTXO (for resolveOwnerConflict)
+
+ // Add UTXOs to provider
+ provider.addUtxo(nameContract.address, externalAuthNFTUTXO);
+ provider.addUtxo(nameContract.address, internalAuthNFTUTXO);
+
+ provider.addUtxo(nameContract.address, newExternalAuthNFTUTXO);
+ provider.addUtxo(nameContract.address, newInternalAuthNFTUTXO);
+
+ provider.addUtxo(aliceAddress, pureBCHUTXO);
+ provider.addUtxo(aliceAddress, ownershipNFTUTXO);
+ provider.addUtxo(aliceAddress, newOwnershipNFTUTXO);
+
+ provider.addUtxo(differentNameContract.address, differentNameExternalAuthNFTUTXO);
+ provider.addUtxo(differentNameContract.address, differentNameInternalAuthNFTUTXO);
+
+ provider.addUtxo(aliceAddress, pureBCHUTXO);
+ provider.addUtxo(aliceAddress, differentNameOwnershipNFTUTXO);
+ provider.addUtxo(aliceAddress, newOwnershipNFTUTXO);
+ });
+
+ // Tests for burn function
+ describe('useAuth', () =>
+ {
+ // NOTE: useAuth value non 1 is already tested in the `ownership-guard.test.ts`.
+
+ it('should pass with valid use of internal and external auth NFT', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: ownershipNFTUTXO.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).resolves.toBeDefined();
+ });
+
+ it('should fail due to incorrect lockingbytecode of internalauth output', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: ownershipNFTUTXO.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Auth input: locking bytecode must match active output');
+ });
+
+ it('should fail due to incorrect token category of internalauth input', async () =>
+ {
+ const internalAuthNFTUTXOWithDIfferentCategory = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ provider.addUtxo(nameContract.address, internalAuthNFTUTXOWithDIfferentCategory);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXOWithDIfferentCategory, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXOWithDIfferentCategory.satoshis,
+ token: {
+ category: internalAuthNFTUTXOWithDIfferentCategory.token!.category,
+ amount: internalAuthNFTUTXOWithDIfferentCategory.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXOWithDIfferentCategory.token!.nft!.capability,
+ commitment: internalAuthNFTUTXOWithDIfferentCategory.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: ownershipNFTUTXO.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Auth input: token category must match name category');
+ });
+
+ it('should fail due to incorrect token category of internalauth output', async () =>
+ {
+ const internalAuthNFTUTXOCloneWithDIfferentCategory: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'minting',
+ },
+ },
+ };
+
+ provider.addUtxo(testContract.address, internalAuthNFTUTXOCloneWithDIfferentCategory);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXOCloneWithDIfferentCategory, testContract.unlock.call())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: internalAuthNFTUTXOCloneWithDIfferentCategory.satoshis,
+ token: {
+ category: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.category,
+ amount: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.nft!.capability,
+ commitment: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXOCloneWithDIfferentCategory.satoshis,
+ token: {
+ category: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.category,
+ amount: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.nft!.capability,
+ commitment: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Auth output: token category must match name category');
+ });
+
+ it('should fail due to incorrect nft commitment of internalauth output', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(3), 8),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: ownershipNFTUTXO.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Auth input: NFT commitment must match active output');
+ });
+
+ it('should fail due to incorrect tokencategory of ownership input', async () =>
+ {
+ const ownershipNFTUTXOWithDIfferentCategory = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ provider.addUtxo(nameContract.address, ownershipNFTUTXOWithDIfferentCategory);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXOWithDIfferentCategory, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(1), 8),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXOWithDIfferentCategory.satoshis,
+ token: {
+ category: ownershipNFTUTXOWithDIfferentCategory.token!.category,
+ amount: ownershipNFTUTXOWithDIfferentCategory.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXOWithDIfferentCategory.token!.nft!.capability,
+ commitment: ownershipNFTUTXOWithDIfferentCategory.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Ownership input: ownership NFT token category must match name category');
+ });
+
+ it('should fail due to incorrect tokencategory of ownership output', async () =>
+ {
+ const internalAuthNFTUTXOCloneWithDIfferentCategory: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'minting',
+ },
+ },
+ };
+
+ provider.addUtxo(testContract.address, internalAuthNFTUTXOCloneWithDIfferentCategory);
+
+ const ownershipNFTUTXOWithDIfferentCategory = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ provider.addUtxo(nameContract.address, ownershipNFTUTXOWithDIfferentCategory);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXOCloneWithDIfferentCategory, testContract.unlock.call())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: internalAuthNFTUTXOCloneWithDIfferentCategory.satoshis,
+ token: {
+ category: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.category,
+ amount: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.nft!.capability,
+ commitment: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(1), 8),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXOCloneWithDIfferentCategory.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Ownership output: token category must match active input');
+ });
+
+ it('should fail due to incorrect commitment of ownership output', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(differentNameOwnershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: differentNameOwnershipNFTUTXO.satoshis,
+ token: {
+ category: differentNameOwnershipNFTUTXO.token!.category,
+ amount: differentNameOwnershipNFTUTXO.token!.amount,
+ nft: {
+ capability: differentNameOwnershipNFTUTXO.token!.nft!.capability,
+ commitment: differentNameOwnershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Ownership input: ownership NFT name must match contract name + TLD');
+ });
+
+ it('should fail due to mismatching commitment of internal auth NFT and ownership NFT registration ID', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: newInternalAuthNFTUTXO.satoshis,
+ token: {
+ category: newInternalAuthNFTUTXO.token!.category,
+ amount: newInternalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: newInternalAuthNFTUTXO.token!.nft!.capability,
+ commitment: newInternalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: ownershipNFTUTXO.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Auth input: internal auth NFT commitment must match ownership NFT registration ID');
+ });
+
+ it('should fail due to mismatching commitment of ownership nft output', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.useAuth(BigInt(1)))
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: ownershipNFTUTXO.satoshis,
+ token: {
+ category: ownershipNFTUTXO.token!.category,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(3), 8) + nameHex + tldHex,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Ownership output: NFT commitment must match active input');
+ });
+ });
+
+
+ // Tests for penaliseInvalidName function
+ describe('penaliseInvalidName', () =>
+ {
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Create internal auth NFT with actual name for penaliseInvalidName tests
+ const internalAuthNFTWithName: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(internalAuthNFTWithName, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: aliceAddress,
+ amount: externalAuthNFTUTXO.satoshis + internalAuthNFTUTXO.satoshis + pureBCHUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 3 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Create internal auth NFT with actual name for penaliseInvalidName tests
+ const internalAuthNFTWithName: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(internalAuthNFTWithName, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: aliceAddress,
+ amount: externalAuthNFTUTXO.satoshis + internalAuthNFTUTXO.satoshis + pureBCHUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: BigInt(1000),
+ });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 1 output');
+ });
+
+ it('should fail if input 0 locking bytecode does not match contract', async () =>
+ {
+ const specialName = 'test_ame';
+ const specialNameHex = Buffer.from(specialName).toString('hex');
+ const internalAuthNFTUTXOWithSpecial: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + specialNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(testContract.address, internalAuthNFTUTXOWithSpecial);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(internalAuthNFTUTXOWithSpecial, testContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: external auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 1 locking bytecode does not match contract', async () =>
+ {
+ const specialName = 'test_ame';
+ const specialNameHex = Buffer.from(specialName).toString('hex');
+ const internalAuthNFTUTXOWithSpecial: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + specialNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(testContract.address, internalAuthNFTUTXOWithSpecial);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithSpecial, testContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: internal auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 0 token category does not match name category', async () =>
+ {
+ const wrongCategoryUTXO: Utxo = {
+ ...externalAuthNFTUTXO,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(aliceAddress, wrongCategoryUTXO);
+ const internalAuthNFTUTXOWithName: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(nameContract.address, internalAuthNFTUTXOWithName);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(wrongCategoryUTXO, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(internalAuthNFTUTXOWithName, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: aliceAddress,
+ amount: wrongCategoryUTXO.satoshis + internalAuthNFTUTXOWithName.satoshis + pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: external auth NFT token category must match name category');
+ });
+
+ it('should fail if input 1 token category does not match name category', async () =>
+ {
+ const wrongCategoryUTXO: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(aliceAddress, wrongCategoryUTXO);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(wrongCategoryUTXO, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: aliceAddress,
+ amount: externalAuthNFTUTXO.satoshis + wrongCategoryUTXO.satoshis + pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: internal auth NFT token category must match name category');
+ });
+
+ it('should fail if input 0 external auth NFT does not have empty commitment', async () =>
+ {
+ const wrongCommitmentUTXO: Utxo = {
+ ...externalAuthNFTUTXO,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: 'abcd',
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(aliceAddress, wrongCommitmentUTXO);
+ const internalAuthNFTUTXOWithName: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + nameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(nameContract.address, internalAuthNFTUTXOWithName);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(wrongCommitmentUTXO, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(internalAuthNFTUTXOWithName, nameContract.unlock.penaliseInvalidName(BigInt(1)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: aliceAddress,
+ amount: wrongCommitmentUTXO.satoshis + internalAuthNFTUTXOWithName.satoshis + pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: external auth NFT must have empty commitment');
+ });
+
+ it('should fail when character is a hyphen', async () =>
+ {
+ const hyphenName = 'test-name';
+ const hyphenNameHex = Buffer.from(hyphenName).toString('hex');
+ const internalAuthNFTUTXOWithHyphen: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + hyphenNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithHyphen, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when character is lowercase', async () =>
+ {
+ const lowerName = 'testname';
+ const lowerNameHex = Buffer.from(lowerName).toString('hex');
+ const internalAuthNFTUTXOWithLower: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + lowerNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithLower, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when character is uppercase', async () =>
+ {
+ const upperName = 'testName';
+ const upperNameHex = Buffer.from(upperName).toString('hex');
+ const internalAuthNFTUTXOWithUpper: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + upperNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithUpper, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when character is a digit', async () =>
+ {
+ const digitName = 'test1ame';
+ const digitNameHex = Buffer.from(digitName).toString('hex');
+ const internalAuthNFTUTXOWithDigit: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + digitNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithDigit, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should pass for special character @', async () =>
+ {
+ const specialName = 'test@ame';
+ const specialNameHex = Buffer.from(specialName).toString('hex');
+ const internalAuthNFTUTXOWithSpecial: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + specialNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithSpecial, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).resolves.not.toThrow();
+ });
+
+ it('should pass for special character _', async () =>
+ {
+ const specialName = 'test_ame';
+ const specialNameHex = Buffer.from(specialName).toString('hex');
+ const internalAuthNFTUTXOWithSpecial: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + specialNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithSpecial, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).resolves.not.toThrow();
+ });
+
+ it('should pass for special character .', async () =>
+ {
+ const specialName = 'test.ame';
+ const specialNameHex = Buffer.from(specialName).toString('hex');
+ const internalAuthNFTUTXOWithSpecial: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + specialNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithSpecial, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).resolves.not.toThrow();
+ });
+
+ it('should pass for special character !', async () =>
+ {
+ const specialName = 'test!ame';
+ const specialNameHex = Buffer.from(specialName).toString('hex');
+ const internalAuthNFTUTXOWithSpecial: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + specialNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithSpecial, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).resolves.not.toThrow();
+ });
+
+ it('should pass for special character $', async () =>
+ {
+ const specialName = 'test$ame';
+ const specialNameHex = Buffer.from(specialName).toString('hex');
+ const internalAuthNFTUTXOWithSpecial: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ ...internalAuthNFTUTXO.token!,
+ nft: {
+ ...internalAuthNFTUTXO.token!.nft!,
+ commitment: padVmNumber(BigInt(1), 8) + specialNameHex + tldHex,
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(internalAuthNFTUTXOWithSpecial, nameContract.unlock.penaliseInvalidName(BigInt(5)))
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ await expect(transaction.send()).resolves.not.toThrow();
+ });
+ });
+
+ // Tests for resolveOwnerConflict function
+ describe('resolveOwnerConflict', () =>
+ {
+ it('should fail with invalid number of inputs', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 5 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: BigInt(1000),
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have at most 3 outputs');
+ });
+
+ it('should fail if input 0 locking bytecode does not match contract', async () =>
+ {
+ const fakeContract = new Contract(artifacts, [], { provider });
+ const fakeUtxo: Utxo = { ...externalAuthNFTUTXO };
+ provider.addUtxo(fakeContract.address, fakeUtxo);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(fakeUtxo, fakeContract.unlock.call())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: fakeUtxo.satoshis,
+ token: {
+ category: fakeUtxo.token!.category,
+ amount: fakeUtxo.token!.amount,
+ nft: {
+ capability: fakeUtxo.token!.nft!.capability,
+ commitment: fakeUtxo.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: valid external auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 1 locking bytecode does not match contract', async () =>
+ {
+ const fakeContract = new Contract(artifacts, [], { provider });
+ const fakeUtxo: Utxo = { ...internalAuthNFTUTXO };
+ provider.addUtxo(fakeContract.address, fakeUtxo);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(fakeUtxo, fakeContract.unlock.call())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: fakeUtxo.satoshis,
+ token: {
+ category: fakeUtxo.token!.category,
+ amount: fakeUtxo.token!.amount,
+ nft: {
+ capability: fakeUtxo.token!.nft!.capability,
+ commitment: fakeUtxo.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: valid internal auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 2 locking bytecode does not match contract', async () =>
+ {
+ const fakeContract = new Contract(artifacts, [], { provider });
+ const fakeUtxo: Utxo = { ...newExternalAuthNFTUTXO };
+ provider.addUtxo(fakeContract.address, fakeUtxo);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(fakeUtxo, fakeContract.unlock.call())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: invalid external auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 3 locking bytecode does not match contract', async () =>
+ {
+ const fakeContract = new Contract(artifacts, [], { provider });
+ const fakeUtxo: Utxo = { ...newInternalAuthNFTUTXO };
+ provider.addUtxo(fakeContract.address, fakeUtxo);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(fakeUtxo, fakeContract.unlock.call())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: invalid internal auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if output 0 locking bytecode does not match contract', async () =>
+ {
+ const fakeContract = new Contract(artifacts, [], { provider });
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: fakeContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: valid external auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if output 1 locking bytecode does not match contract', async () =>
+ {
+ const fakeContract = new Contract(artifacts, [], { provider });
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: fakeContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 1: valid internal auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 0 nftCommitment does not match output 0 nftCommitment', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(1), 8),
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(1), 8),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: valid external auth NFT must have empty commitment');
+ });
+
+ it('should fail if input 2 nftCommitment does not match output 0 nftCommitment', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(3), 8),
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 1: valid internal auth NFT commitment must match input 1');
+ });
+
+ it('should fail if input 0 valid external auth NFT does not have empty commitment', async () =>
+ {
+ const wrongCommitmentUtxo: Utxo = {
+ ...externalAuthNFTUTXO,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: 'abcd',
+ },
+ },
+ };
+ provider.addUtxo(nameContract.address, wrongCommitmentUtxo);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(wrongCommitmentUtxo, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: wrongCommitmentUtxo.satoshis,
+ token: {
+ category: wrongCommitmentUtxo.token!.category,
+ amount: wrongCommitmentUtxo.token!.amount,
+ nft: {
+ capability: wrongCommitmentUtxo.token!.nft!.capability,
+ commitment: wrongCommitmentUtxo.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: valid external auth NFT must have empty commitment');
+ });
+
+ it('should fail if input 2 invalid external auth NFT does not have empty commitment', async () =>
+ {
+ const wrongCommitmentUtxo: Utxo = {
+ ...newExternalAuthNFTUTXO,
+ token: {
+ category: newExternalAuthNFTUTXO.token!.category,
+ amount: newExternalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: newExternalAuthNFTUTXO.token!.nft!.capability,
+ commitment: 'abcd',
+ },
+ },
+ };
+ provider.addUtxo(nameContract.address, wrongCommitmentUtxo);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(wrongCommitmentUtxo, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: invalid external auth NFT must have empty commitment');
+ });
+
+ it('should fail if output 0 valid external auth NFT does not have empty commitment', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: 'abcd',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: valid external auth NFT must have empty commitment');
+ });
+
+ it('should fail if output 1 valid internal auth NFT must match input 1', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: 'abcd',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 1: valid internal auth NFT commitment must match input 1');
+ });
+
+ it('should fail if output 2 valid external auth NFT does not have empty commitment', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: newExternalAuthNFTUTXO.satoshis,
+ token: {
+ category: newExternalAuthNFTUTXO.token!.category,
+ amount: newExternalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: newExternalAuthNFTUTXO.token!.nft!.capability,
+ commitment: nameHex,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: change must be pure BCH (no token category)');
+ });
+
+ it('should fail if input 1 registration ID is not lower than input 3', async () =>
+ {
+ const highRegInternalAuth: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(10), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+ const lowRegNewInternalAuth: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(highRegInternalAuth, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(lowRegNewInternalAuth, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: highRegInternalAuth.satoshis,
+ token: {
+ category: highRegInternalAuth.token!.category,
+ amount: highRegInternalAuth.token!.amount,
+ nft: {
+ capability: highRegInternalAuth.token!.nft!.capability,
+ commitment: highRegInternalAuth.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: valid internal auth NFT registration ID must be lower than input 3');
+ });
+
+ it('should pass with all correct inputs/outputs', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ await expect(transaction.send()).resolves.toBeDefined();
+ });
+
+ test('should fail if input 4 is not pure BCH', async () =>
+ {
+ const fakeBchUtxo = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+
+ provider.addUtxo(nameContract.tokenAddress, fakeBchUtxo);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(fakeBchUtxo, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: pureBCHUTXO.satoshis,
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 4: funding input must be pure BCH (no token category)');
+ });
+
+ test('should fail if output 2 is not pure BCH', async () =>
+ {
+ const fakeBchUtxo = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: padVmNumber(BigInt(1), 8),
+ capability: 'none' as const,
+ },
+ },
+ };
+ provider.addUtxo(nameContract.tokenAddress, fakeBchUtxo);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newExternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(newInternalAuthNFTUTXO, nameContract.unlock.resolveOwnerConflict())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: internalAuthNFTUTXO.satoshis,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: fakeBchUtxo.satoshis,
+ token: {
+ category: fakeBchUtxo.token!.category,
+ amount: fakeBchUtxo.token!.amount,
+ nft: {
+ capability: fakeBchUtxo.token!.nft!.capability,
+ commitment: fakeBchUtxo.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: change must be pure BCH (no token category)');
+ });
+ });
+
+ // Tests for burn function
+ describe('burn', () =>
+ {
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 3 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis })
+ .addOutput({ to: aliceAddress, amount: BigInt(1000) });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 1 output');
+ });
+
+ it('should fail if sequence number is wrong for pure BCH', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn(), { sequence: 419405 })
+ .addInput(pureBCHUTXO, testContract.unlock.call())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: internal auth NFT sequence number must equal inactivity expiry time');
+ });
+
+ it('should fail if input 2 token category is not nameCategory (owner burn)', async () =>
+ {
+ const ownershipWithDifferentCategory: Utxo = {
+ ...ownershipNFTUTXO,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: ownershipNFTUTXO.token!.amount,
+ nft: {
+ capability: ownershipNFTUTXO.token!.nft!.capability,
+ commitment: ownershipNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ };
+ provider.addUtxo(aliceAddress, ownershipWithDifferentCategory);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(ownershipWithDifferentCategory, aliceTemplate.unlockP2PKH())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: name ownership NFT token category must match name category');
+ });
+
+ it('should fail if input 2 registration ID does not match input 0 (owner burn)', async () =>
+ {
+ const utxoWithDifferentRegId: Utxo = {
+ ...externalAuthNFTUTXO,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(11), 8),
+ },
+ },
+ };
+ provider.addUtxo(nameContract.address, utxoWithDifferentRegId);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(utxoWithDifferentRegId, nameContract.unlock.burn())
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: name ownership NFT registration ID must match input 1 commitment');
+ });
+
+ it('should fail if input 0 locking bytecode does not match contract', async () =>
+ {
+ const utxoFromOtherContract: Utxo = {
+ ...externalAuthNFTUTXO,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ };
+ provider.addUtxo(testContract.address, utxoFromOtherContract);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(utxoFromOtherContract, testContract.unlock.call())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: external auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 1 locking bytecode does not match contract', async () =>
+ {
+ const utxoFromOtherContract: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ category: internalAuthNFTUTXO.token!.category,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ };
+ provider.addUtxo(testContract.address, utxoFromOtherContract);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(utxoFromOtherContract, testContract.unlock.call())
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: internal auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail if input 0 external auth NFT does not have empty commitment', async () =>
+ {
+ const wrongCommitment: Utxo = {
+ ...externalAuthNFTUTXO,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: padVmNumber(BigInt(1), 8),
+ },
+ },
+ };
+ provider.addUtxo(nameContract.address, wrongCommitment);
+ transaction = new TransactionBuilder({ provider })
+ .addInput(wrongCommitment, nameContract.unlock.burn())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: external auth NFT must have empty commitment');
+ });
+
+ it('should fail if input 0 token category does not match name category', async () =>
+ {
+ const wrongCategory: Utxo = {
+ ...externalAuthNFTUTXO,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(wrongCategory, nameContract.unlock.burn())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: external auth NFT token category must match name category');
+ });
+
+ it('should fail if input 1 token category does not match name category', async () =>
+ {
+ const wrongCategory: Utxo = {
+ ...internalAuthNFTUTXO,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: internalAuthNFTUTXO.token!.amount,
+ nft: {
+ commitment: internalAuthNFTUTXO.token!.nft!.commitment,
+ capability: internalAuthNFTUTXO.token!.nft!.capability,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(wrongCategory, nameContract.unlock.burn())
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({ to: aliceAddress, amount: pureBCHUTXO.satoshis });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: internal auth NFT token category must match name category');
+ });
+
+ it('should fail if output 0 is not pure BCH', async () =>
+ {
+ const fakeBchUtxo: Utxo = {
+ ...pureBCHUTXO,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: '',
+ capability: 'none' as const,
+ },
+ },
+ };
+ transaction = new TransactionBuilder({ provider })
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(internalAuthNFTUTXO, nameContract.unlock.burn())
+ .addInput(ownershipNFTUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: fakeBchUtxo.satoshis,
+ token: {
+ category: fakeBchUtxo.token!.category,
+ amount: fakeBchUtxo.token!.amount,
+ nft: {
+ capability: fakeBchUtxo.token!.nft!.capability,
+ commitment: fakeBchUtxo.token!.nft!.commitment,
+ },
+ },
+ });
+ const txPromise = transaction.send();
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: change must be pure BCH (no token category)');
+ });
+ });
+
+});
\ No newline at end of file
diff --git a/test/contracts/ownership-guard.test.ts b/test/contracts/ownership-guard.test.ts
new file mode 100644
index 0000000..0d2c165
--- /dev/null
+++ b/test/contracts/ownership-guard.test.ts
@@ -0,0 +1,1274 @@
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
+import { binToHex, cashAddressToLockingBytecode } from '@bitauth/libauth';
+import { BitCANNArtifacts } from '../../lib/index.js';
+import { aliceAddress, alicePkh, aliceTokenAddress, nameTokenCategory, reversedNameTokenCategory, invalidNameTokenCategory, mockOptions } from '../common.js';
+import { getDomainPartialBytecode } from '../utils.js';
+import artifacts from '../artifacts.js';
+
+describe('OwnershipGuard', () =>
+{
+ const provider = new MockNetworkProvider();
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+
+ // Get the domain partial bytecode for the OwnershipGuard contract
+ let domainPartialBytecode = getDomainPartialBytecode(nameTokenCategory, {
+ provider,
+ addressType: 'p2sh32',
+ });
+ // OwnershipGuard expects the partial AFTER both name and TLD pushes.
+ // The util returns the partial after the name push; remove the initial TLD push.
+ const tldHex = Buffer.from(mockOptions.tld).toString('hex');
+
+ const ownershipGuardContract = new Contract(BitCANNArtifacts.OwnershipGuard, [ domainPartialBytecode ], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
+
+ const name = 'test';
+ const nameHex = Buffer.from(name).toString('hex');
+
+ // Create name contract for the external auth NFT
+ // Use the same TLD that OwnershipGuard expects (".bch") so the locking bytecode matches
+ const nameContract = new Contract(BitCANNArtifacts.Name, [ nameHex, tldHex, reversedNameTokenCategory ], { provider });
+
+ let threadNFTUTXO: Utxo;
+ let ownershipGuardUTXO: Utxo;
+ let externalAuthNFTUTXO: Utxo;
+ let internalAuthNFTUTXO: Utxo;
+ let auctionNFTUTXO: Utxo;
+ let transaction: TransactionBuilder;
+
+ beforeAll(() =>
+ {
+ // Get the ownership guard contract locking bytecode
+ const ownershipGuardLockingBytecode = cashAddressToLockingBytecode(ownershipGuardContract.address);
+ if(typeof ownershipGuardLockingBytecode === 'string')
+ {
+ throw new Error(`Failed to get locking bytecode: ${ownershipGuardLockingBytecode}`);
+ }
+
+ // Create thread NFT UTXO
+ threadNFTUTXO =
+ {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: binToHex(ownershipGuardLockingBytecode.bytecode),
+ capability: 'none',
+ },
+ },
+ };
+
+ // Create ownership guard contract UTXO
+ ownershipGuardUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000) }),
+ };
+
+ // Create external auth NFT UTXO from the name contract (empty commitment)
+
+ externalAuthNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ };
+
+ // Create internal auth NFT UTXO from the name contract (with registration ID and name)
+ // 8 bytes registration ID
+ const registrationId = '0000000000000001';
+ // name + '.bch' in hex
+ const fullName = nameHex + '2e626368';
+ internalAuthNFTUTXO = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: registrationId + fullName,
+ capability: 'none',
+ },
+ },
+ };
+
+ // Create auction NFT UTXO for the same name (this is the invalid auction)
+ auctionNFTUTXO = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ // Add UTXOs to provider
+ provider.addUtxo(registryContract.address, threadNFTUTXO);
+ provider.addUtxo(ownershipGuardContract.address, ownershipGuardUTXO);
+ provider.addUtxo(nameContract.address, externalAuthNFTUTXO);
+ provider.addUtxo(nameContract.address, internalAuthNFTUTXO);
+ provider.addUtxo(registryContract.address, auctionNFTUTXO);
+ });
+
+ it('should fail with invalid number of inputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 5 inputs instead of 4
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 4 inputs');
+ });
+
+ it('should fail with invalid number of outputs', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with 5 outputs instead of 4
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: 1000n,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Transaction: must have exactly 4 outputs');
+ });
+
+ it('should fail when contract is not used at input index 1', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder with contract at input 0 instead of 1
+ transaction = new TransactionBuilder({ provider })
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: ownership guard contract UTXO must be at this index');
+ });
+
+ it('should fail when output 1 has token category', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 1: must not have any token category (pure BCH only)');
+ });
+
+ it('should fail when output 1 locking bytecode does not match input 1', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: locking bytecode must match output 1');
+ });
+
+ it('should fail when auction NFT locking bytecode does not match registry', async () =>
+ {
+ // Create auction NFT with different locking bytecode
+ const differentAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(testContract.address, differentAuctionNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(differentAuctionNFTUTXO, testContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + differentAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: differentAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: auction NFT locking bytecode does not match registry input\'s locking bytecode');
+ });
+
+ it('should fail when external auth NFT token category does not match registry', async () =>
+ {
+ // Create external auth NFT with invalid category
+ const invalidExternalAuthNFTUTXO: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(nameContract.address, invalidExternalAuthNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(invalidExternalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: invalidExternalAuthNFTUTXO.satoshis,
+ token: {
+ category: invalidExternalAuthNFTUTXO.token!.category,
+ amount: invalidExternalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: invalidExternalAuthNFTUTXO.token!.nft!.capability,
+ commitment: invalidExternalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: external auth NFT token category prefix must match registry');
+ });
+
+ it('should fail when output 2 token category does not match registry', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: external auth NFT token category prefix must match registry');
+ });
+
+ it('should fail when auction NFT token category does not match registry', async () =>
+ {
+ // Create auction NFT with invalid category
+ const invalidAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: invalidNameTokenCategory + '01',
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, invalidAuctionNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(invalidAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: auction NFT token category prefix must match registry');
+ });
+
+ it('should fail when auction NFT capability is not mutable', async () =>
+ {
+ // Create auction NFT with invalid capability
+ const invalidCapabilityAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + nameHex,
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, invalidCapabilityAuctionNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(invalidCapabilityAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + invalidCapabilityAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: invalidCapabilityAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 3: auction NFT capability must be mutable (0x01)');
+ });
+
+ it('should fail when external auth NFT commitment does not match output', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ // 'different' in hex
+ commitment: '646966666572656e74',
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 2: external auth NFT commitment must match input 2');
+ });
+
+ it('should fail when external auth NFT has non-empty commitment', async () =>
+ {
+ // Create external auth NFT with non-empty commitment
+ const nonEmptyCommitmentExternalAuthNFTUTXO: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ // 'notempty' in hex
+ commitment: '6e6f74656d707479',
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(nameContract.address, nonEmptyCommitmentExternalAuthNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(nonEmptyCommitmentExternalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: nonEmptyCommitmentExternalAuthNFTUTXO.satoshis,
+ token: {
+ category: nonEmptyCommitmentExternalAuthNFTUTXO.token!.category,
+ amount: nonEmptyCommitmentExternalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: nonEmptyCommitmentExternalAuthNFTUTXO.token!.nft!.capability,
+ commitment: nonEmptyCommitmentExternalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: external auth NFT must have empty commitment');
+ });
+
+ it('should fail when external auth NFT locking bytecode does not match name contract', async () =>
+ {
+ // Create external auth NFT with different locking bytecode
+ const differentLockingBytecodeExternalAuthNFTUTXO: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(testContract.address, differentLockingBytecodeExternalAuthNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(differentLockingBytecodeExternalAuthNFTUTXO, testContract.unlock.call())
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: differentLockingBytecodeExternalAuthNFTUTXO.satoshis,
+ token: {
+ category: differentLockingBytecodeExternalAuthNFTUTXO.token!.category,
+ amount: differentLockingBytecodeExternalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: differentLockingBytecodeExternalAuthNFTUTXO.token!.nft!.capability,
+ commitment: differentLockingBytecodeExternalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 2: external auth NFT locking bytecode must match name contract');
+ });
+
+ it('should fail when token amount accumulation does not match', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount - 1n,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: token amount must equal input 0 + input 3 amounts (accumulation)');
+ });
+
+ it('should fail when reward output has token category', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceTokenAddress,
+ amount: auctionNFTUTXO.satoshis,
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: '',
+ capability: 'none',
+ },
+ },
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 3: reward must be pure BCH (no token category)');
+ });
+
+ it('should successfully penalize invalid registration when all conditions are met', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ // This should succeed
+ await expect(txPromise).resolves.toBeDefined();
+ });
+
+ it('should fail when thread NFT commitment does not match ownership guard locking bytecode', async () =>
+ {
+ // Create thread NFT with wrong commitment
+ const wrongThreadNFTUTXO: Utxo = {
+ ...randomUtxo(),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ // 'wrongcommitment' in hex
+ commitment: '77726f6e67636f6d6d69746d656e74',
+ capability: 'none',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, wrongThreadNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(wrongThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: wrongThreadNFTUTXO.satoshis,
+ token: {
+ category: wrongThreadNFTUTXO.token!.category,
+ amount: wrongThreadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: wrongThreadNFTUTXO.token!.nft!.capability,
+ commitment: wrongThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ // This should fail because the thread NFT commitment doesn't match the ownership guard locking bytecode
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when auction NFT name does not match external auth NFT name', async () =>
+ {
+ // Create auction NFT with different name
+ const differentNameAuctionNFTUTXO: Utxo = {
+ ...randomUtxo({ satoshis: BigInt(2000000) }),
+ token: {
+ category: nameTokenCategory,
+ amount: BigInt(1),
+ nft: {
+ commitment: binToHex(alicePkh) + Buffer.from('different').toString('hex'),
+ capability: 'mutable',
+ },
+ },
+ };
+
+ provider.addUtxo(registryContract.address, differentNameAuctionNFTUTXO);
+
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(differentNameAuctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + differentNameAuctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: differentNameAuctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ // This should fail because the auction NFT name doesn't match the external auth NFT name
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when output 0 locking bytecode does not match registry', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: testContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when output 0 token category does not match registry', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: invalidNameTokenCategory,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when output 0 token capability does not match input 0', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: 'mutable',
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+
+ it('should fail when output 0 token commitment does not match input 0', async () =>
+ {
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(ownershipGuardUTXO, ownershipGuardContract.unlock.call())
+ .addInput(externalAuthNFTUTXO, nameContract.unlock.useAuth(2n))
+ .addInput(auctionNFTUTXO, registryContract.unlock.call())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount + auctionNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ // 'differentcommitment' in hex
+ commitment: '646966666572656e74636f6d6d69746d656e74',
+ },
+ },
+ })
+ .addOutput({
+ to: ownershipGuardContract.tokenAddress,
+ amount: ownershipGuardUTXO.satoshis,
+ })
+ .addOutput({
+ to: nameContract.tokenAddress,
+ amount: externalAuthNFTUTXO.satoshis,
+ token: {
+ category: externalAuthNFTUTXO.token!.category,
+ amount: externalAuthNFTUTXO.token!.amount,
+ nft: {
+ capability: externalAuthNFTUTXO.token!.nft!.capability,
+ commitment: externalAuthNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: aliceAddress,
+ amount: auctionNFTUTXO.satoshis,
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ });
+});
diff --git a/test/e2e/auction.test.ts b/test/contracts/registry.test.ts
similarity index 57%
rename from test/e2e/auction.test.ts
rename to test/contracts/registry.test.ts
index 18c0249..c3b0b5c 100644
--- a/test/e2e/auction.test.ts
+++ b/test/contracts/registry.test.ts
@@ -1,14 +1,16 @@
-import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo } from 'cashscript';
+import { MockNetworkProvider, randomUtxo, TransactionBuilder, Contract, type Utxo, FailedRequireError } from 'cashscript';
import { binToHex, cashAddressToLockingBytecode, hexToBin } from '@bitauth/libauth';
import { BitCANNArtifacts } from '../../lib/index.js';
-import { aliceAddress, alicePkh, aliceTemplate, domainTokenCategory, mockOptions, reversedDomainTokenCategory } from '../common.js';
-import { intToBytesToHex, getTxOutputs } from '../utils.js';
+import { aliceAddress, alicePkh, aliceTemplate, nameTokenCategory, mockOptions, reversedNameTokenCategory, invalidNameTokenCategory } from '../common.js';
+import { getAuctionPrice, getRegistrationIdCommitment } from '../utils.js';
+import artifacts from '../artifacts.js';
-describe('Auction', () =>
+describe('Registry', () =>
{
const provider = new MockNetworkProvider();
- const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedDomainTokenCategory ], { provider });
- const auctionContract = new Contract(BitCANNArtifacts.Auction, [ BigInt(mockOptions.minStartingBid) ], { provider });
+ const registryContract = new Contract(BitCANNArtifacts.Registry, [ reversedNameTokenCategory ], { provider });
+ const auctionContract = new Contract(BitCANNArtifacts.Auction, [ ], { provider });
+ const testContract = new Contract(artifacts, [], { provider });
const auctionLockingBytecode = cashAddressToLockingBytecode(auctionContract.address);
// @ts-ignore
const auctionLockingBytecodeHex = binToHex(auctionLockingBytecode.bytecode);
@@ -18,10 +20,12 @@ describe('Auction', () =>
const nameBin = hexToBin(nameHex);
let threadNFTUTXO: Utxo;
+ let invalidThreadNFTUTXO: Utxo;
let registrationCounterUTXO: Utxo;
let mintingNFTUTXO: Utxo;
let authorizedContractUTXO: Utxo;
let userUTXO: Utxo;
+ let userUTXOA: Utxo;
let transaction: TransactionBuilder;
let auctionAmount: bigint;
let newRegistrationId: number;
@@ -30,10 +34,14 @@ describe('Auction', () =>
beforeAll(() =>
{
userUTXO = {
+ ...randomUtxo({ satoshis: BigInt(1000000000) }),
+ };
+ userUTXOA = {
...randomUtxo(),
};
provider.addUtxo(aliceAddress, userUTXO);
+ provider.addUtxo(aliceAddress, userUTXOA);
authorizedContractUTXO = {
...randomUtxo(),
@@ -43,7 +51,7 @@ describe('Auction', () =>
threadNFTUTXO = {
token: {
- category: domainTokenCategory,
+ category: nameTokenCategory,
amount: BigInt(0),
nft: {
commitment: auctionLockingBytecodeHex,
@@ -53,15 +61,29 @@ describe('Auction', () =>
...randomUtxo(),
};
- // @ts-ignore
provider.addUtxo(registryContract.address, threadNFTUTXO);
+ invalidThreadNFTUTXO = {
+ token: {
+ category: invalidNameTokenCategory,
+ amount: BigInt(0),
+ nft: {
+ commitment: auctionLockingBytecodeHex,
+ capability: 'none',
+ },
+ },
+ ...randomUtxo(),
+ };
+
+ // @ts-ignore
+ provider.addUtxo(registryContract.address, invalidThreadNFTUTXO);
+
registrationCounterUTXO = {
token: {
- category: domainTokenCategory,
+ category: nameTokenCategory,
amount: BigInt('9223372036854775807'),
nft: {
- commitment: intToBytesToHex({ value: 0, length: 8 }),
+ commitment: '00',
capability: 'minting',
},
},
@@ -75,7 +97,7 @@ describe('Auction', () =>
mintingNFTUTXO = {
token: {
amount: BigInt(0),
- category: domainTokenCategory,
+ category: nameTokenCategory,
nft: {
commitment: '',
capability: 'minting',
@@ -88,12 +110,12 @@ describe('Auction', () =>
provider.addUtxo(registryContract.address, mintingNFTUTXO);
newRegistrationId = parseInt(registrationCounterUTXO.token!.nft!.commitment, 16) + 1;
- newRegistrationIdCommitment = newRegistrationId.toString(16).padStart(16, '0');
+ newRegistrationIdCommitment = getRegistrationIdCommitment(BigInt(newRegistrationId));
- auctionAmount = BigInt(mockOptions.minStartingBid);
+ auctionAmount = getAuctionPrice(BigInt(newRegistrationId), BigInt(mockOptions.minStartingBid));
});
- it('should start an auction without fail', async () =>
+ it('should fail when sending authorizedThreadNFT to any other address than registry contract', async () =>
{
// Construct the transaction using the TransactionBuilder
transaction = new TransactionBuilder({ provider })
@@ -102,7 +124,7 @@ describe('Auction', () =>
.addInput(registrationCounterUTXO, registryContract.unlock.call())
.addInput(userUTXO, aliceTemplate.unlockP2PKH())
.addOutput({
- to: registryContract.tokenAddress,
+ to: auctionContract.tokenAddress,
amount: threadNFTUTXO.satoshis,
token: {
category: threadNFTUTXO.token!.category,
@@ -140,24 +162,70 @@ describe('Auction', () =>
commitment: binToHex(alicePkh) + binToHex(nameBin),
},
},
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: locking bytecode must match registry contract');
+ });
+
+ it('should fail when using incorrect token category from registry contract', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(invalidThreadNFTUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: invalidThreadNFTUTXO.satoshis,
+ token: {
+ category: invalidThreadNFTUTXO.token!.category,
+ amount: invalidThreadNFTUTXO.token!.amount,
+ nft: {
+ capability: invalidThreadNFTUTXO.token!.nft!.capability,
+ commitment: invalidThreadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
})
- .addOpReturnOutput([ name ])
.addOutput({
- to: aliceAddress,
- amount: userUTXO.satoshis,
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
});
- const transactionSize = transaction.build().length;
- const changeAmount = userUTXO.satoshis - (auctionAmount + BigInt(transactionSize));
- transaction.outputs[transaction.outputs.length - 1].amount = changeAmount;
+ const txPromise = transaction.send();
- const txPromise = await transaction.send();
- // @ts-ignore
- const txOutputs = getTxOutputs(txPromise);
- expect(txOutputs).toEqual(expect.arrayContaining([{ to: aliceAddress, amount: changeAmount, token: undefined }]));
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 0: token category must match name category');
});
- it('should pass without change output', async () =>
+ it('should fail when sending incorrect amount to authorizedThreadNFT from registry contract', async () =>
{
// Construct the transaction using the TransactionBuilder
transaction = new TransactionBuilder({ provider })
@@ -167,7 +235,7 @@ describe('Auction', () =>
.addInput(userUTXO, aliceTemplate.unlockP2PKH())
.addOutput({
to: registryContract.tokenAddress,
- amount: threadNFTUTXO.satoshis,
+ amount: threadNFTUTXO.satoshis + BigInt(1),
token: {
category: threadNFTUTXO.token!.category,
amount: threadNFTUTXO.token!.amount,
@@ -204,16 +272,15 @@ describe('Auction', () =>
commitment: binToHex(alicePkh) + binToHex(nameBin),
},
},
- })
- .addOpReturnOutput([ name ]);
+ });
- const txPromise = await transaction.send();
- // @ts-ignore
- const txOutputs = getTxOutputs(txPromise);
- expect(txOutputs.length).toBe(5);
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: satoshi value must match input 0');
});
- it('should fail without op return output', async () =>
+ it('should fail when trying to change nft commitment of 0th output', async () =>
{
// Construct the transaction using the TransactionBuilder
transaction = new TransactionBuilder({ provider })
@@ -229,7 +296,7 @@ describe('Auction', () =>
amount: threadNFTUTXO.token!.amount,
nft: {
capability: threadNFTUTXO.token!.nft!.capability,
- commitment: threadNFTUTXO.token!.nft!.commitment,
+ commitment: binToHex(alicePkh),
},
},
})
@@ -263,16 +330,20 @@ describe('Auction', () =>
});
const txPromise = transaction.send();
- await expect(txPromise).rejects.toThrow('Auction.cash:93 Error in transaction at input 1 in contract Auction.cash at line 93.');
- await expect(txPromise).rejects.toThrow('Failing statement: tx.outputs[4].lockingBytecode');
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Output 0: NFT commitment must match input 0');
});
- it('should fail setting auction capability to none', async () =>
+
+ it('should fail when different contract is used in input 1', async () =>
{
+ provider.addUtxo(testContract.address, authorizedContractUTXO);
+
// Construct the transaction using the TransactionBuilder
transaction = new TransactionBuilder({ provider })
.addInput(threadNFTUTXO, registryContract.unlock.call())
- .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(authorizedContractUTXO, testContract.unlock.call())
.addInput(registrationCounterUTXO, registryContract.unlock.call())
.addInput(userUTXO, aliceTemplate.unlockP2PKH())
.addOutput({
@@ -310,15 +381,70 @@ describe('Auction', () =>
category: registrationCounterUTXO.token!.category,
amount: BigInt(newRegistrationId),
nft: {
- capability: 'none',
+ capability: 'mutable',
commitment: binToHex(alicePkh) + binToHex(nameBin),
},
},
+ });
+
+ const txPromise = transaction.send();
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: locking bytecode must match authorized contract from input 0 NFT commitment');
+ });
+
+ it('should fail with invalid active input index for auction contract', async () =>
+ {
+ // Construct the transaction using the TransactionBuilder
+ transaction = new TransactionBuilder({ provider })
+ .addInput(threadNFTUTXO, registryContract.unlock.call())
+ .addInput(registrationCounterUTXO, registryContract.unlock.call())
+ .addInput(authorizedContractUTXO, auctionContract.unlock.call(nameBin))
+ .addInput(userUTXO, aliceTemplate.unlockP2PKH())
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: threadNFTUTXO.satoshis,
+ token: {
+ category: threadNFTUTXO.token!.category,
+ amount: threadNFTUTXO.token!.amount,
+ nft: {
+ capability: threadNFTUTXO.token!.nft!.capability,
+ commitment: threadNFTUTXO.token!.nft!.commitment,
+ },
+ },
+ })
+ .addOutput({
+ to: auctionContract.tokenAddress,
+ amount: authorizedContractUTXO.satoshis,
})
- .addOpReturnOutput([ name ]);
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: registrationCounterUTXO.satoshis,
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: registrationCounterUTXO.token!.amount - BigInt(newRegistrationId),
+ nft: {
+ capability: registrationCounterUTXO.token!.nft!.capability,
+ commitment: newRegistrationIdCommitment,
+ },
+ },
+ })
+ .addOutput({
+ to: registryContract.tokenAddress,
+ amount: BigInt(auctionAmount),
+ token: {
+ category: registrationCounterUTXO.token!.category,
+ amount: BigInt(newRegistrationId),
+ nft: {
+ capability: 'mutable',
+ commitment: binToHex(alicePkh) + binToHex(nameBin),
+ },
+ },
+ });
const txPromise = transaction.send();
- await expect(txPromise).rejects.toThrow('Auction.cash:90 Require statement failed at input 1 in contract Auction.cash at line 90.');
- await expect(txPromise).rejects.toThrow('Failing statement: require(auctionCapability == 0x01);');
+
+ await expect(txPromise).rejects.toThrow(FailedRequireError);
+ await expect(txPromise).rejects.toThrow('Input 1: locking bytecode must match authorized contract from input 0 NFT commitment');
});
});
\ No newline at end of file
diff --git a/test/unit/import.test.ts b/test/import.test.ts
similarity index 84%
rename from test/unit/import.test.ts
rename to test/import.test.ts
index db7b189..b3f806a 100644
--- a/test/unit/import.test.ts
+++ b/test/import.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from '@jest/globals';
-import { BitCANNArtifacts } from '../../lib/index.js';
+import { BitCANNArtifacts } from '../lib/index.js';
interface ContractArtifact
{
@@ -20,11 +20,11 @@ describe('imports', () =>
'Registry',
'Auction',
'Bid',
- 'Domain',
- 'DomainFactory',
- 'AuctionConflictResolver',
- 'AuctionNameEnforcer',
- 'DomainOwnershipGuard',
+ 'Name',
+ 'Factory',
+ 'NameEnforcer',
+ 'OwnershipGuard',
+ 'ConflictResolver',
'Accumulator',
] as const;
diff --git a/test/utils.ts b/test/utils.ts
index 39efc3d..b9ec2de 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -1,11 +1,6 @@
-import { cashAddressToLockingBytecode,
- encodeLockingBytecodeP2sh32,
- lockingBytecodeToCashAddress,
- hash256,
- hexToBin,
- binToHex,
- numberToBinUint16BE } from '@bitauth/libauth';
-import { type Output, Network, Transaction } from 'cashscript';
+import { lockingBytecodeToCashAddress, hexToBin, binToHex, bigIntToVmNumber, padMinimallyEncodedVmNumber } from '@bitauth/libauth';
+import { type Output, type AddressType, type NetworkProvider, Contract, Network, Transaction } from 'cashscript';
+import { BitCANNArtifacts } from '../lib/index.js';
export interface LibauthTokenDetails
{
@@ -24,120 +19,26 @@ export interface LibauthOutput
token?: LibauthTokenDetails;
}
-export const intToBytesToHex = ({ value, length }: { value: number; length: number }): string =>
+export const padVmNumber = (num: bigint, length: number): string =>
{
- const bin = numberToBinUint16BE(value);
- const bytes = new Uint8Array(bin.buffer, bin.byteOffset, bin.byteLength);
- if(bytes.length > length)
- {
- throw new Error(`Value ${value} exceeds the specified length of ${length} bytes`);
- }
- const result = new Uint8Array(length);
- result.set(bytes, length - bytes.length);
-
- return binToHex(result);
-};
-
-export const hexToInt = (hex: string): number =>
-{
- const bytes = hexToBin(hex);
- let intValue = 0;
- for(let i = 0; i < bytes.length; i++)
- {
- intValue = (intValue << 8) | bytes[i];
- }
-
- return intValue;
-};
-
-export const pushDataHex = (data: string): string =>
-{
- const hexData = Buffer.from(data, 'utf8').toString('hex');
- const length = hexData.length / 2;
-
- if(length <= 75)
- {
- return length.toString(16).padStart(2, '0') + hexData;
- }
- else if(length <= 255)
- {
- return '4c' + length.toString(16).padStart(2, '0') + hexData;
- }
- else if(length <= 65535)
- {
- return '4d' + length.toString(16).padStart(4, '0') + hexData;
- }
- else
- {
- return '4e' + length.toString(16).padStart(8, '0') + hexData;
- }
-};
-
-export const findOwnershipNFTUTXO = (utxos: any[], category: string): any =>
-{
- const utxo = utxos.find((u: any) =>
- u.token?.nft?.capability === 'none' && u.token?.category === category,
- );
- if(!utxo) throw new Error('Could not find ownership NFT UTXO');
-
- return utxo;
+ return binToHex(padMinimallyEncodedVmNumber(bigIntToVmNumber(num), length).slice(0, length));
};
-export const findPureUTXO = (utxos: any[]): any =>
+export const getCreatorIncentive = (auctionPrice: bigint, registrationId: bigint): bigint =>
{
- const utxo = utxos.reduce((max: any, u: any) =>
- (!u.token && u.satoshis > (max?.satoshis || 0)) ? u : max,
- null,
- );
- if(!utxo) throw new Error('Could not find user UTXO without token');
+ const minimalDeduction = auctionPrice - BigInt(5000);
+ const creatorIncentive = (minimalDeduction * (BigInt(1e5) - registrationId) / BigInt(1e5));
- return utxo;
+ return creatorIncentive;
};
-export const lockScriptToAddress = (lockScript: string): string =>
+export const getAuctionPrice = (registrationId: bigint, minStartingBid: bigint): bigint =>
{
- // Convert the lock script to a cashaddress (with bitcoincash: prefix).
- const result = lockingBytecodeToCashAddress({ bytecode: hexToBin(lockScript), prefix: 'bitcoincash' });
- // A successful conversion will result in a string, unsuccessful will return AddressContents
+ const decayPoints = BigInt(minStartingBid) * registrationId * 3n;
+ const currentPricePoints = minStartingBid * 1000000n;
+ const currentAuctionPrice = (currentPricePoints - decayPoints) / 1000000n;
- console.log('result: ', result);
-
- // @ts-ignore
- if(typeof result.address !== 'string')
- {
- throw(new Error(`Provided lock script ${lockScript} cannot be converted to address ${JSON.stringify(result)}`));
- }
-
- // @ts-ignore
- return result.address;
-};
-
-export const buildLockScriptP2SH32 = (scriptBytecodeHex: string): string =>
-{
- // Hash the lockscript for p2sh32 (using hash256)
- const scriptHashBin = hash256(hexToBin(scriptBytecodeHex));
-
- // Get the lockscript
- const lockScriptBin = encodeLockingBytecodeP2sh32(scriptHashBin);
-
- // Convert back to the library's convention of hex
- const lockScriptHex = binToHex(lockScriptBin);
-
- return lockScriptHex;
-};
-
-
-export const addressToLockScript = (address: string): string =>
-{
- const result = cashAddressToLockingBytecode(address);
-
- // The `cashAddressToLockingBytecode()` call returns an error string OR the correct bytecode
- // so we check if it errors, in which case we throw the error, otherwise return the result
- if(typeof result === 'string') throw(new Error(result));
-
- const lockScript = binToHex(result.bytecode);
-
- return lockScript;
+ return BigInt(Math.max(Number(currentAuctionPrice), 20000));
};
export const libauthOutputToCashScriptOutput = (output: LibauthOutput): Output =>
@@ -202,3 +103,49 @@ export const getTxOutputs = (tx: Transaction, network: Network = Network.MOCKNET
};
});
};
+
+export const getRegistrationIdCommitment = (newRegistrationId: bigint): string =>
+{
+ const regIdHex = newRegistrationId.toString(16).padStart(16, '0');
+ const regIdBytes = [];
+ for(let i = 0; i < regIdHex.length; i += 2)
+ {
+ regIdBytes.push(regIdHex.slice(i, i + 2));
+ }
+ const newRegistrationIdCommitment = regIdBytes.reverse().join('');
+
+ return newRegistrationIdCommitment;
+};
+
+/**
+ * Retrieves the partial bytecode of the Name contract.
+ *
+ * @param {string} category - The category identifier for the name.
+ * @param {Object} options - The options for constructing the Name contract.
+ * @param {NetworkProvider} options.provider - The network provider.
+ * @param {AddressType} options.addressType - The address type.
+ * @returns {string} The partial bytecode of the Name contract.
+ */
+export const getDomainPartialBytecode = (category: string, options: { provider: NetworkProvider; addressType: AddressType }): string =>
+{
+ // Reverse the category bytes for use in contract parameters.
+ const reversedCategory = binToHex(hexToBin(category).reverse());
+
+ // Placeholder name used for constructing a partial domain contract bytecode.
+ const placeholderName = 'test';
+ const placeholderNameHex = Array.from(placeholderName).map(char => char.charCodeAt(0).toString(16)
+ .padStart(2, '0'))
+ .join('');
+
+ const placeTLD = '.bch';
+ const placeTLDHex = Array.from(placeTLD).map(char => char.charCodeAt(0).toString(16)
+ .padStart(2, '0'))
+ .join('');
+
+ // Construct a placeholder name contract to extract partial bytecode.
+ const PlaceholderNameContract = new Contract(BitCANNArtifacts.Name, [ placeholderNameHex, placeTLDHex, reversedCategory ], options);
+ const sliceIndex = 2 + 64 + 2 + placeholderName.length * 2 + 2 + placeTLD.length * 2;
+ const namePartialBytecode = PlaceholderNameContract.bytecode.slice(sliceIndex, PlaceholderNameContract.bytecode.length);
+
+ return namePartialBytecode;
+};
\ No newline at end of file