From f2ef847eaf1f4876fadbd4f71315db27cb7a6169 Mon Sep 17 00:00:00 2001 From: huth-stacks <230392931+huth-stacks@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:01:53 -0700 Subject: [PATCH 1/2] Add Hiro developer tutorials in correct documentation locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Faithfully migrates 4 Hiro guides to their proper locations: - NFT Marketplace, Decentralized Kickstarter, No-Loss Lottery → tutorials/ - Using Clarity Values → cookbook/stacks.js/ All content preserved verbatim from Hiro source including full introductions, explanatory prose, code examples, and testing sections. The Kickstarter guide (previously a placeholder stub) is now fully migrated. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/cookbook/SUMMARY.md | 1 + .../stacks.js/using-clarity-values.md | 55 ++++ docs/tutorials/SUMMARY.md | 6 + .../build-a-decentralized-kickstarter.md | 286 ++++++++++++++++++ docs/tutorials/build-an-nft-marketplace.md | 282 +++++++++++++++++ docs/tutorials/no-loss-lottery.md | 195 ++++++++++++ 6 files changed, 825 insertions(+) create mode 100644 docs/cookbook/stacks.js/using-clarity-values.md create mode 100644 docs/tutorials/build-a-decentralized-kickstarter.md create mode 100644 docs/tutorials/build-an-nft-marketplace.md create mode 100644 docs/tutorials/no-loss-lottery.md diff --git a/docs/cookbook/SUMMARY.md b/docs/cookbook/SUMMARY.md index 7382a935dd..70779e7c87 100644 --- a/docs/cookbook/SUMMARY.md +++ b/docs/cookbook/SUMMARY.md @@ -20,6 +20,7 @@ * [Build an ft pc](stacks.js/cryptography-and-security/build-an-ft-pc.md) * [Build an nft pc](stacks.js/cryptography-and-security/build-an-nft-pc.md) * [Build a STX pc](stacks.js/cryptography-and-security/build-a-stx-pc.md) +* [Using Clarity Values](stacks.js/using-clarity-values.md) ## Clarity diff --git a/docs/cookbook/stacks.js/using-clarity-values.md b/docs/cookbook/stacks.js/using-clarity-values.md new file mode 100644 index 0000000000..bf6776088a --- /dev/null +++ b/docs/cookbook/stacks.js/using-clarity-values.md @@ -0,0 +1,55 @@ +# Using Clarity Values + +{% hint style="info" %} +This guide is migrated from the [Hiro documentation](https://docs.hiro.so/en/resources/guides/using-clarity-values). +{% endhint %} + +Some endpoints, like the [read-only function contract call](https://docs.hiro.so/api#operation/call_read_only_function), require input to a serialized [Clarity value](https://docs.stacks.co/docs/clarity/). Other endpoints return serialized values that need to be deserialized. + +The example shown below illustrates Clarity value usage in combination with the API. + +The `@stacks/transactions` library supports typed contract calls and makes [response value utilization much simpler](https://docs.stacks.co/docs/clarity/). + +## Usage example + +```ts +import { + Configuration, + SmartContractsApiInterface, + SmartContractsApi, + ReadOnlyFunctionSuccessResponse, +} from '@stacks/blockchain-api-client'; +import { uintCV, UIntCV, cvToHex, hexToCV, ClarityType } from '@stacks/transactions'; + +(async () => { + const apiConfig: Configuration = new Configuration({ + fetchApi: fetch, + // for mainnet, replace `testnet` with `mainnet` + basePath: 'https://api.testnet.hiro.so', // defaults to http://localhost:3999 + }); + + const contractsApi: SmartContractsApiInterface = new SmartContractsApi(apiConfig); + + const principal: string = 'ST000000000000000000002AMW42H'; + + // use most recent from: https://api..hiro.so/v2/pox + const rewardCycle: UIntCV = uintCV(22); + + // call a read-only function + const fnCall: ReadOnlyFunctionSuccessResponse = await contractsApi.callReadOnlyFunction({ + contractAddress: principal, + contractName: 'pox', + functionName: 'is-pox-active', + readOnlyFunctionArgs: { + sender: principal, + arguments: [cvToHex(rewardCycle)], + }, + }); + + console.log({ + status: fnCall.okay, + result: fnCall.result, + representation: hexToCV(fnCall.result).type === ClarityType.BoolTrue, + }); +})().catch(console.error); +``` diff --git a/docs/tutorials/SUMMARY.md b/docs/tutorials/SUMMARY.md index 2476420c7e..94df4e2125 100644 --- a/docs/tutorials/SUMMARY.md +++ b/docs/tutorials/SUMMARY.md @@ -33,3 +33,9 @@ * [Testing Clarity Contracts](bitcoin-primer/stacks-development-fundamentals/testing-clarity-contracts.md) * [Frontend with Stacks.js](bitcoin-primer/stacks-development-fundamentals/frontend-with-stacks.js.md) * [Deploying Stacks Apps](bitcoin-primer/stacks-development-fundamentals/deploying-stacks-apps.md) + +## Hiro Guides + +* [Build an NFT Marketplace](build-an-nft-marketplace.md) +* [Build a Decentralized Kickstarter](build-a-decentralized-kickstarter.md) +* [Build a No-Loss Lottery Pool](no-loss-lottery.md) diff --git a/docs/tutorials/build-a-decentralized-kickstarter.md b/docs/tutorials/build-a-decentralized-kickstarter.md new file mode 100644 index 0000000000..0714e902b8 --- /dev/null +++ b/docs/tutorials/build-a-decentralized-kickstarter.md @@ -0,0 +1,286 @@ +# Build a Decentralized Kickstarter + +{% hint style="info" %} +This guide is migrated from the [Hiro documentation](https://docs.hiro.so/en/resources/guides/build-a-decentralized-kickstarter). +{% endhint %} + +{% hint style="warning" %} +**Upstream content mismatch:** The Hiro source file for this guide (`build-a-decentralized-kickstarter.mdx`) has the title "Build a decentralized Kickstarter" and description "Learn how to create a crowdfunding app, enabling creators to fund their projects without a third party," but its body content is identical to the NFT marketplace guide. This appears to be an error in the upstream Hiro documentation. The content below is migrated faithfully as-is from the Hiro source. +{% endhint %} + +In this guide, you will learn how to create an NFT marketplace that allows users to list NFTs for sale. Users can specify the following details for their listings: +- The NFT token to sell. +- Listing expiry in block height. +- The payment asset, either STX or a SIP010 fungible token. +- The NFT price in the chosen payment asset. +- An optional intended taker. If set, only that principal will be able to fulfil the listing. + +This marketplace leverages the following Clarity traits: +- `nft-trait` for handling NFTs. +- `ft-trait` for handling fungible tokens. + +Over the course of this guide, you will learn how to: +1. Define and handle errors. +2. Create and manage NFT listings. +3. Whitelist asset contracts. +4. Fulfil NFT purchases. + +--- + +## Define and handle errors + +First, define constants for various errors that may occur during listing, cancelling, or fulfilling NFT transactions. This helps in maintaining clean and readable code. + +```clarity +;; Define listing errors +(define-constant ERR_EXPIRY_IN_PAST (err u1000)) +(define-constant ERR_PRICE_ZERO (err u1001)) + +;; Define cancelling and fulfilling errors +(define-constant ERR_UNKNOWN_LISTING (err u2000)) +(define-constant ERR_UNAUTHORISED (err u2001)) +(define-constant ERR_LISTING_EXPIRED (err u2002)) +(define-constant ERR_NFT_ASSET_MISMATCH (err u2003)) +(define-constant ERR_PAYMENT_ASSET_MISMATCH (err u2004)) +(define-constant ERR_MAKER_TAKER_EQUAL (err u2005)) +(define-constant ERR_UNINTENDED_TAKER (err u2006)) +(define-constant ERR_ASSET_CONTRACT_NOT_WHITELISTED (err u2007)) +(define-constant ERR_PAYMENT_CONTRACT_NOT_WHITELISTED (err u2008)) +``` + +## Create and manage NFT listings + +### Define data structures + +Create a map data structure for the asset listings and a data variable for unique IDs. + +```clarity +;; Define a map data structure for the asset listings +(define-map listings + uint + { + maker: principal, + taker: (optional principal), + token-id: uint, + nft-asset-contract: principal, + expiry: uint, + price: uint, + payment-asset-contract: (optional principal) + } +) + +;; Used for unique IDs for each listing +(define-data-var listing-nonce uint u0) +``` + +### List an asset + +Create a public function to list an asset along with its contract. This function verifies the contract, checks expiry and price, and transfers the NFT ownership to the marketplace. + +```clarity +(define-public (list-asset + (nft-asset-contract ) + (nft-asset { + taker: (optional principal), + token-id: uint, + expiry: uint, + price: uint, + payment-asset-contract: (optional principal) + }) +) + (let ((listing-id (var-get listing-nonce))) + ;; Verify that the contract of this asset is whitelisted + (asserts! (is-whitelisted (contract-of nft-asset-contract)) ERR_ASSET_CONTRACT_NOT_WHITELISTED) + ;; Verify that the asset is not expired + (asserts! (> (get expiry nft-asset) block-height) ERR_EXPIRY_IN_PAST) + ;; Verify that the asset price is greater than zero + (asserts! (> (get price nft-asset) u0) ERR_PRICE_ZERO) + ;; Verify that the contract of the payment is whitelisted + (asserts! (match (get payment-asset-contract nft-asset) + payment-asset + (is-whitelisted payment-asset) + true + ) ERR_PAYMENT_CONTRACT_NOT_WHITELISTED) + ;; Transfer the NFT ownership to this contract's principal + (try! (transfer-nft + nft-asset-contract + (get token-id nft-asset) + tx-sender + (as-contract tx-sender) + )) + ;; List the NFT in the listings map + (map-set listings listing-id (merge + { maker: tx-sender, nft-asset-contract: (contract-of nft-asset-contract) } + nft-asset + )) + ;; Increment the nonce to use for the next unique listing ID + (var-set listing-nonce (+ listing-id u1)) + ;; Return the created listing ID + (ok listing-id) + ) +) +``` + +### Retrieve an asset + +Create a read-only function to retrieve an asset, or listing, by its ID. + +```clarity +(define-read-only (get-listing (listing-id uint)) + (map-get? listings listing-id) +) +``` + +### Cancel a listing + +Create a public function to cancel a listing. Only the NFT's creator can cancel the listing, and it must use the same asset contract that the NFT uses. + +```clarity +(define-public (cancel-listing (listing-id uint) (nft-asset-contract )) + (let ( + (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) + (maker (get maker listing)) + ) + ;; Verify that the caller of the function is the creator of the NFT to be cancelled + (asserts! (is-eq maker tx-sender) ERR_UNAUTHORISED) + ;; Verify that the asset contract to use is the same one that the NFT uses + (asserts! (is-eq + (get nft-asset-contract listing) + (contract-of nft-asset-contract) + ) ERR_NFT_ASSET_MISMATCH) + ;; Delete the listing + (map-delete listings listing-id) + ;; Transfer the NFT from this contract's principal back to the creator's principal + (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender maker)) + ) +) +``` + +## Whitelist asset contracts + +### Whitelist contracts + +The marketplace requires any contracts used for assets or payments to be whitelisted by the contract owner. Create a map to store whitelisted asset contracts and a function to check if a contract is whitelisted. + +```clarity +(define-map whitelisted-asset-contracts principal bool) + +(define-read-only (is-whitelisted (asset-contract principal)) + (default-to false (map-get? whitelisted-asset-contracts asset-contract)) +) +``` + +### Set whitelisted contracts + +Only the contract owner can whitelist an asset contract. Create a public function to set whitelisted asset contracts. + +```clarity +(define-public (set-whitelisted (asset-contract principal) (whitelisted bool)) + (begin + (asserts! (is-eq contract-owner tx-sender) ERR_UNAUTHORISED) + (ok (map-set whitelisted-asset-contracts asset-contract whitelisted)) + ) +) +``` + +## Fulfill NFT purchases + +### Fulfill listing with STX + +Create a public function to purchase a listing using STX as payment. + +```clarity +(define-public (fulfil-listing-stx (listing-id uint) (nft-asset-contract )) + (let ( + ;; Verify the given listing ID exists + (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) + ;; Set the NFT's taker to the purchaser (caller of the function) + (taker tx-sender) + ) + ;; Validate that the purchase can be fulfilled + (try! (assert-can-fulfil (contract-of nft-asset-contract) none listing)) + ;; Transfer the NFT to the purchaser (caller of the function) + (try! (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender taker))) + ;; Transfer the STX payment from the purchaser to the creator of the NFT + (try! (stx-transfer? (get price listing) taker (get maker listing))) + ;; Remove the NFT from the marketplace listings + (map-delete listings listing-id) + ;; Return the listing ID that was just purchased + (ok listing-id) + ) +) +``` + +### Fulfill listing with SIP-010 + +Create a public function to purchase a listing using another fungible token that follows the SIP-010 standard as payment. + +```clarity +(define-public (fulfil-listing-ft + (listing-id uint) + (nft-asset-contract ) + (payment-asset-contract ) +) + (let ( + ;; Verify the given listing ID exists + (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) + ;; Set the NFT's taker to the purchaser (caller of the function) + (taker tx-sender) + ) + ;; Validate that the purchase can be fulfilled + (try! (assert-can-fulfil + (contract-of nft-asset-contract) + (some (contract-of payment-asset-contract)) + listing + )) + ;; Transfer the NFT to the purchaser (caller of the function) + (try! (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender taker))) + ;; Transfer the tokens as payment from the purchaser to the creator of the NFT + (try! (transfer-ft payment-asset-contract (get price listing) taker (get maker listing))) + ;; Remove the NFT from the marketplace listings + (map-delete listings listing-id) + ;; Return the listing ID that was just purchased + (ok listing-id) + ) +) +``` + +### Validate purchase can be fulfilled + +Create a private function to validate that a purchase can be fulfilled. This function checks the listing's expiry, the NFT's contract, and the payment's contract. + +```clarity +(define-private (assert-can-fulfil + (nft-asset-contract principal) + (payment-asset-contract (optional principal)) + (listing { + maker: principal, + taker: (optional principal), + token-id: uint, + nft-asset-contract: principal, + expiry: uint, + price: uint, + payment-asset-contract: (optional principal) + }) +) + (begin + ;; Verify that the buyer is not the same as the NFT creator + (asserts! (not (is-eq (get maker listing) tx-sender)) ERR_MAKER_TAKER_EQUAL) + ;; Verify the buyer has been set in the listing metadata as its `taker` + (asserts! + (match (get taker listing) intended-taker (is-eq intended-taker tx-sender) true) + ERR_UNINTENDED_TAKER + ) + ;; Verify the listing for purchase is not expired + (asserts! (< block-height (get expiry listing)) ERR_LISTING_EXPIRED) + ;; Verify the asset contract used to purchase the NFT is the same as the one set on the NFT + (asserts! (is-eq (get nft-asset-contract listing) nft-asset-contract) ERR_NFT_ASSET_MISMATCH) + ;; Verify the payment contract used to purchase the NFT is the same as the one set on the NFT + (asserts! + (is-eq (get payment-asset-contract listing) payment-asset-contract) + ERR_PAYMENT_ASSET_MISMATCH + ) + (ok true) + ) +) +``` diff --git a/docs/tutorials/build-an-nft-marketplace.md b/docs/tutorials/build-an-nft-marketplace.md new file mode 100644 index 0000000000..faf133cb4a --- /dev/null +++ b/docs/tutorials/build-an-nft-marketplace.md @@ -0,0 +1,282 @@ +# Build an NFT Marketplace + +{% hint style="info" %} +This guide is migrated from the [Hiro documentation](https://docs.hiro.so/en/resources/guides/build-an-nft-marketplace). +{% endhint %} + +In this guide, you will learn how to create an NFT marketplace that allows users to list NFTs for sale. Users can specify the following details for their listings: +- The NFT token to sell. +- Listing expiry in block height. +- The payment asset, either STX or a SIP010 fungible token. +- The NFT price in the chosen payment asset. +- An optional intended taker. If set, only that principal will be able to fulfil the listing. + +This marketplace leverages the following Clarity traits: +- `nft-trait` for handling NFTs. +- `ft-trait` for handling fungible tokens. + +Over the course of this guide, you will learn how to: +1. Define and handle errors. +2. Create and manage NFT listings. +3. Whitelist asset contracts. +4. Fulfil NFT purchases. + +--- + +## Define and handle errors + +First, define constants for various errors that may occur during listing, cancelling, or fulfilling NFT transactions. This helps in maintaining clean and readable code. + +```clarity +;; Define listing errors +(define-constant ERR_EXPIRY_IN_PAST (err u1000)) +(define-constant ERR_PRICE_ZERO (err u1001)) + +;; Define cancelling and fulfilling errors +(define-constant ERR_UNKNOWN_LISTING (err u2000)) +(define-constant ERR_UNAUTHORISED (err u2001)) +(define-constant ERR_LISTING_EXPIRED (err u2002)) +(define-constant ERR_NFT_ASSET_MISMATCH (err u2003)) +(define-constant ERR_PAYMENT_ASSET_MISMATCH (err u2004)) +(define-constant ERR_MAKER_TAKER_EQUAL (err u2005)) +(define-constant ERR_UNINTENDED_TAKER (err u2006)) +(define-constant ERR_ASSET_CONTRACT_NOT_WHITELISTED (err u2007)) +(define-constant ERR_PAYMENT_CONTRACT_NOT_WHITELISTED (err u2008)) +``` + +## Create and manage NFT listings + +### Define data structures + +Create a map data structure for the asset listings and a data variable for unique IDs. + +```clarity +;; Define a map data structure for the asset listings +(define-map listings + uint + { + maker: principal, + taker: (optional principal), + token-id: uint, + nft-asset-contract: principal, + expiry: uint, + price: uint, + payment-asset-contract: (optional principal) + } +) + +;; Used for unique IDs for each listing +(define-data-var listing-nonce uint u0) +``` + +### List an asset + +Create a public function to list an asset along with its contract. This function verifies the contract, checks expiry and price, and transfers the NFT ownership to the marketplace. + +```clarity +(define-public (list-asset + (nft-asset-contract ) + (nft-asset { + taker: (optional principal), + token-id: uint, + expiry: uint, + price: uint, + payment-asset-contract: (optional principal) + }) +) + (let ((listing-id (var-get listing-nonce))) + ;; Verify that the contract of this asset is whitelisted + (asserts! (is-whitelisted (contract-of nft-asset-contract)) ERR_ASSET_CONTRACT_NOT_WHITELISTED) + ;; Verify that the asset is not expired + (asserts! (> (get expiry nft-asset) block-height) ERR_EXPIRY_IN_PAST) + ;; Verify that the asset price is greater than zero + (asserts! (> (get price nft-asset) u0) ERR_PRICE_ZERO) + ;; Verify that the contract of the payment is whitelisted + (asserts! (match (get payment-asset-contract nft-asset) + payment-asset + (is-whitelisted payment-asset) + true + ) ERR_PAYMENT_CONTRACT_NOT_WHITELISTED) + ;; Transfer the NFT ownership to this contract's principal + (try! (transfer-nft + nft-asset-contract + (get token-id nft-asset) + tx-sender + (as-contract tx-sender) + )) + ;; List the NFT in the listings map + (map-set listings listing-id (merge + { maker: tx-sender, nft-asset-contract: (contract-of nft-asset-contract) } + nft-asset + )) + ;; Increment the nonce to use for the next unique listing ID + (var-set listing-nonce (+ listing-id u1)) + ;; Return the created listing ID + (ok listing-id) + ) +) +``` + +### Retrieve an asset + +Create a read-only function to retrieve an asset, or listing, by its ID. + +```clarity +(define-read-only (get-listing (listing-id uint)) + (map-get? listings listing-id) +) +``` + +### Cancel a listing + +Create a public function to cancel a listing. Only the NFT's creator can cancel the listing, and it must use the same asset contract that the NFT uses. + +```clarity +(define-public (cancel-listing (listing-id uint) (nft-asset-contract )) + (let ( + (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) + (maker (get maker listing)) + ) + ;; Verify that the caller of the function is the creator of the NFT to be cancelled + (asserts! (is-eq maker tx-sender) ERR_UNAUTHORISED) + ;; Verify that the asset contract to use is the same one that the NFT uses + (asserts! (is-eq + (get nft-asset-contract listing) + (contract-of nft-asset-contract) + ) ERR_NFT_ASSET_MISMATCH) + ;; Delete the listing + (map-delete listings listing-id) + ;; Transfer the NFT from this contract's principal back to the creator's principal + (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender maker)) + ) +) +``` + +## Whitelist asset contracts + +### Whitelist contracts + +The marketplace requires any contracts used for assets or payments to be whitelisted by the contract owner. Create a map to store whitelisted asset contracts and a function to check if a contract is whitelisted. + +```clarity +(define-map whitelisted-asset-contracts principal bool) + +(define-read-only (is-whitelisted (asset-contract principal)) + (default-to false (map-get? whitelisted-asset-contracts asset-contract)) +) +``` + +### Set whitelisted contracts + +Only the contract owner can whitelist an asset contract. Create a public function to set whitelisted asset contracts. + +```clarity +(define-public (set-whitelisted (asset-contract principal) (whitelisted bool)) + (begin + (asserts! (is-eq contract-owner tx-sender) ERR_UNAUTHORISED) + (ok (map-set whitelisted-asset-contracts asset-contract whitelisted)) + ) +) +``` + +## Fulfill NFT purchases + +### Fulfill listing with STX + +Create a public function to purchase a listing using STX as payment. + +```clarity +(define-public (fulfil-listing-stx (listing-id uint) (nft-asset-contract )) + (let ( + ;; Verify the given listing ID exists + (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) + ;; Set the NFT's taker to the purchaser (caller of the function) + (taker tx-sender) + ) + ;; Validate that the purchase can be fulfilled + (try! (assert-can-fulfil (contract-of nft-asset-contract) none listing)) + ;; Transfer the NFT to the purchaser (caller of the function) + (try! (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender taker))) + ;; Transfer the STX payment from the purchaser to the creator of the NFT + (try! (stx-transfer? (get price listing) taker (get maker listing))) + ;; Remove the NFT from the marketplace listings + (map-delete listings listing-id) + ;; Return the listing ID that was just purchased + (ok listing-id) + ) +) +``` + +### Fulfill listing with SIP-010 + +Create a public function to purchase a listing using another fungible token that follows the SIP-010 standard as payment. + +```clarity +(define-public (fulfil-listing-ft + (listing-id uint) + (nft-asset-contract ) + (payment-asset-contract ) +) + (let ( + ;; Verify the given listing ID exists + (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) + ;; Set the NFT's taker to the purchaser (caller of the function) + (taker tx-sender) + ) + ;; Validate that the purchase can be fulfilled + (try! (assert-can-fulfil + (contract-of nft-asset-contract) + (some (contract-of payment-asset-contract)) + listing + )) + ;; Transfer the NFT to the purchaser (caller of the function) + (try! (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender taker))) + ;; Transfer the tokens as payment from the purchaser to the creator of the NFT + (try! (transfer-ft payment-asset-contract (get price listing) taker (get maker listing))) + ;; Remove the NFT from the marketplace listings + (map-delete listings listing-id) + ;; Return the listing ID that was just purchased + (ok listing-id) + ) +) +``` + +### Validate purchase can be fulfilled + +Create a private function to validate that a purchase can be fulfilled. This function checks the listing's expiry, the NFT's contract, and the payment's contract. + +```clarity +(define-private (assert-can-fulfil + (nft-asset-contract principal) + (payment-asset-contract (optional principal)) + (listing { + maker: principal, + taker: (optional principal), + token-id: uint, + nft-asset-contract: principal, + expiry: uint, + price: uint, + payment-asset-contract: (optional principal) + }) +) + (begin + ;; Verify that the buyer is not the same as the NFT creator + (asserts! (not (is-eq (get maker listing) tx-sender)) ERR_MAKER_TAKER_EQUAL) + ;; Verify the buyer has been set in the listing metadata as its `taker` + (asserts! + (match (get taker listing) intended-taker (is-eq intended-taker tx-sender) true) + ERR_UNINTENDED_TAKER + ) + ;; Verify the listing for purchase is not expired + (asserts! (< block-height (get expiry listing)) ERR_LISTING_EXPIRED) + ;; Verify the asset contract used to purchase the NFT is the same as the one set on the NFT + (asserts! (is-eq (get nft-asset-contract listing) nft-asset-contract) ERR_NFT_ASSET_MISMATCH) + ;; Verify the payment contract used to purchase the NFT is the same as the one set on the NFT + (asserts! + (is-eq (get payment-asset-contract listing) payment-asset-contract) + ERR_PAYMENT_ASSET_MISMATCH + ) + (ok true) + ) +) +``` diff --git a/docs/tutorials/no-loss-lottery.md b/docs/tutorials/no-loss-lottery.md new file mode 100644 index 0000000000..c126775f30 --- /dev/null +++ b/docs/tutorials/no-loss-lottery.md @@ -0,0 +1,195 @@ +# Build a No-Loss Lottery Pool + +{% hint style="info" %} +This guide is migrated from the [Hiro documentation](https://docs.hiro.so/en/resources/guides/no-loss-lottery). +{% endhint %} + +A no-loss lottery contract offers a unique way for participants to stack their assets and potentially earn a larger reward without the risk of losing their initial deposit. + +This contract ensures that participants can stack their assets in a yield-generating pool, receive an NFT ticket, and have a chance to win additional rewards while retaining their original investment. + +In this guide, you will learn how to: + +1. Define constants and data variables +2. Create and manage participants and tickets +3. Implement the lottery functionality +4. Handle the selection of winners +5. Claim and distribute rewards + +{% hint style="warning" %} +This example uses the CityCoins protocol for the stacking yield mechanism, but leveraging a Stacking pool using Proof of Transfer (PoX4) can also be used. Note that the CityCoins protocol is from an earlier era of Stacks development and some of its contracts may no longer be active on mainnet. +{% endhint %} + +--- + +## Define constants and data variables + +First, define some constants and data variables to manage the state of your contract. Constants are used for fixed values, and data variables store the state that can change during the contract execution. + +```clarity +(define-constant OWNER tx-sender) +(define-constant ERR_UNAUTHORIZED (err u101000)) + +(define-data-var lotteryPool uint u0) +(define-data-var totalYield uint u0) +(define-data-var ticketCounter uint u0) +``` + +The `OWNER` constant defines the contract owner with administrative privileges, while `ERR_UNAUTHORIZED` handles unauthorized access attempts. + +Key data variables include `lotteryPool` for tracking stacked assets, `totalYield` for accumulated earnings, and `ticketCounter` for managing issued tickets. + +## Create and manage participants and tickets + +Next, define an NFT to represent a ticket and a map to store participant information. + +```clarity +(define-map Participants + { participant: principal } + { ticketId: uint, amount: uint, cycle: uint, ticketExpirationAtCycle: uint, isWinner: bool } +) + +(define-map Tickets + { ticketId: uint } + { owner: principal } +) + +(define-non-fungible-token LotteryTicket uint) +``` + +The `Participants` map links participants to their ticket details, while the `Tickets` map associates tickets with their owners. + +The `LotteryTicket` defines an NFT for the lottery tickets, crucial for managing entries. + +## Implement the lottery functionality + +Now, it's time to implement the core function of the contract, where participants can enter the lottery by depositing their assets. + +```clarity +(define-public (roll-the-dice (cityName (string-ascii 10)) (amount uint) (lockPeriod (optional uint))) + (let + ( + (ticketId (+ (var-get ticketCounter) u1)) + (actualLockPeriod (default-to u1 lockPeriod)) + (rewardCycle (+ (contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd007-citycoin-stacking get-current-reward-cycle) u1)) + ) + + (begin + (try! (contract-call? 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 transfer amount tx-sender (as-contract tx-sender) none)) + (try! (nft-mint? LotteryTicket ticketId tx-sender)) + (map-insert Participants { participant: tx-sender } { ticketId: ticketId, amount: amount, cycle: rewardCycle, ticketExpirationAtCycle: (+ rewardCycle actualLockPeriod), isWinner: false }) + (map-set Tickets { ticketId: ticketId } { owner: tx-sender }) + (var-set lotteryPool (+ (var-get lotteryPool) amount)) + (var-set ticketCounter (+ (var-get ticketCounter) u1)) + (ok ticketId) + ) + ) +) +``` + +The `roll-the-dice` function enables participants to join the lottery by depositing assets, specifying the city (`cityName`), the deposit amount (`amount`), and the lock period (`lockPeriod`). + +This function is crucial for managing lottery entries and ensuring assets are properly locked for the specified duration. + +When a participant calls this function, the following steps occur: + +1. **Generate ticket and determine lock period**: A new ticket ID is generated, and the lock period is set (defaulting to 1 if not specified). +2. **Transfer assets and mint NFT ticket**: The specified amount of assets is transferred from the participant to the contract, and an NFT representing the lottery ticket is minted and assigned to the participant. +3. **Update participant and ticket data**: The participant's information, including ticket ID, amount, cycle, and expiration, is stored, and the ticket's ownership information is updated. +4. **Update lottery pool and return ticket ID**: The total amount in the lottery pool is updated, the ticket counter is incremented, and the function returns the newly generated ticket ID to the participant. + +This streamlined process ensures that each participant's entry is properly recorded and their assets are securely managed within the lottery system. + +## Handling the selection of winners + +Now, implement the function to select a winner randomly from the participants. + +```clarity +(define-private (select-winner) + (match (contract-call? 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.citycoin-vrf-v2 get-save-rnd (- block-height u1)) randomTicketNumber + (let + ( + (ticketCount (var-get ticketCounter)) + (winningTicketId (mod randomTicketNumber ticketCount)) + (winner (unwrap! (get-ticket winningTicketId) (err u404))) + (owner (get owner winner)) + (participant (unwrap! (get-participant owner) (err u404))) + ) + (map-set Participants { participant: owner } + { ticketId: winningTicketId, amount: (get amount participant), cycle: (get cycle participant), ticketExpirationAtCycle: (get ticketExpirationAtCycle participant), isWinner: true }) + (ok owner) + ) + selectionError (err selectionError) + ) +) +``` +The `select-winner` function randomly selects a winner using a number from the VRF contract (`get-save-rnd`) and updates their status with `unwrap!`. This ensures a fair and transparent winner selection process. + +When this function is called, the following steps occur: + +1. **Fetch random number**: A random number is obtained from the VRF contract by calling `get-save-rnd` with the previous block height. +2. **Determine winning ticket**: The winning ticket ID is calculated by taking the modulus of the random number with the total number of tickets (`ticketCounter`). +3. **Retrieve winner information**: The winner's ticket and participant information are retrieved using the calculated ticket ID. +4. **Update winner status**: The participant's status is updated to indicate they are a winner by setting `isWinner` to `true` in the `Participants` map. + +This process ensures that the winner is selected fairly and their status is accurately updated in the system. + +## Claim and distribute rewards + +Lastly, implement the function to claim and distribute rewards to the winners. + +```clarity +(define-public (claim-rewards (cityName (string-ascii 10)) (cycle uint)) + (let + ( + (cityId (unwrap-panic (contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd004-city-registry get-city-id cityName))) + (cycleAmount (contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd002-treasury-mia-stacking get-balance-stx)) + ) + (if (is-eq cityName "mia") + (try! (contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd011-stacking-payouts send-stacking-reward-mia cycle cycleAmount)) + (try! (contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd011-stacking-payouts send-stacking-reward-nyc cycle cycleAmount)) + ) + (as-contract (contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd007-citycoin-stacking claim-stacking-reward cityName cycle)) + ) +) + +(define-public (distribute-rewards) + (select-winner) +) +``` + +The `claim-rewards` function enables participants to claim their staking rewards, while the `distribute-rewards` function calls `select-winner` to randomly select and reward winners, ensuring a fair distribution process. + +When the `claim-rewards` function is called, the following steps occur: + +1. **Retrieve city ID and balance**: The city ID is retrieved using the `get-city-id` function, and the balance for the specified cycle is obtained from the treasury contract. +2. **Send stacking rewards**: Depending on the city (`mia` or `nyc`), the appropriate stacking reward function is called to send the rewards for the specified cycle. +3. **Claim stacking reward**: The contract claims the stacking reward for the specified city and cycle. + +When the `distribute-rewards` function is called, it performs the following step: + +1. **Select winner**: The `select-winner` function is called to randomly select a winner from the participants and update their status. + +This process ensures that participants can claim their rewards and that winners are selected and rewarded fairly. + +## Testing the contract + +To test the contracts, use the following steps inside of `clarinet console`: + +```terminal +$ ::advance_chain_tip 700000 +$ ::set_tx_sender +$ (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.no-loss-lottery-pool roll-the-dice "mia" u500 none) +$ ::advance_chain_tip 2000 +$ (contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd002-treasury-mia-stacking deposit-stx u5000000000) +$ (contract-call? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.no-loss-lottery-pool claim "mia" u17) +``` + +To bootstrap the CityCoins contracts, follow these steps: + +```clarity +(contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd004-city-registry get-or-create-city-id "mia") +(contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd004-city-registry get-or-create-city-id "nyc") +(contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd002-treasury-mia-stacking set-allowed 'SP1H1733V5MZ3SZ9XRW9FKYGEZT0JDGEB8Y634C7R.miamicoin-token-v2 true) +(contract-call? 'SP8A9HZ3PKST0S42VM9523Z9NV42SZ026V4K39WH.ccd002-treasury-nyc-stacking set-allowed 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11.newyorkcitycoin-token-v2 true) +``` From c9dad9004ebd763b43cb951d42b49bbfd0c19c39 Mon Sep 17 00:00:00 2001 From: huth-stacks <230392931+huth-stacks@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:53:45 -0700 Subject: [PATCH 2/2] Remove duplicate Kickstarter tutorial (upstream Hiro content bug) The Hiro source file build-a-decentralized-kickstarter.mdx has the title "Build a decentralized Kickstarter" but its body is identical to the NFT marketplace guide. Rather than ship duplicate content, removing it until the upstream source is corrected. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/tutorials/SUMMARY.md | 1 - .../build-a-decentralized-kickstarter.md | 286 ------------------ 2 files changed, 287 deletions(-) delete mode 100644 docs/tutorials/build-a-decentralized-kickstarter.md diff --git a/docs/tutorials/SUMMARY.md b/docs/tutorials/SUMMARY.md index 94df4e2125..56d858c177 100644 --- a/docs/tutorials/SUMMARY.md +++ b/docs/tutorials/SUMMARY.md @@ -37,5 +37,4 @@ ## Hiro Guides * [Build an NFT Marketplace](build-an-nft-marketplace.md) -* [Build a Decentralized Kickstarter](build-a-decentralized-kickstarter.md) * [Build a No-Loss Lottery Pool](no-loss-lottery.md) diff --git a/docs/tutorials/build-a-decentralized-kickstarter.md b/docs/tutorials/build-a-decentralized-kickstarter.md deleted file mode 100644 index 0714e902b8..0000000000 --- a/docs/tutorials/build-a-decentralized-kickstarter.md +++ /dev/null @@ -1,286 +0,0 @@ -# Build a Decentralized Kickstarter - -{% hint style="info" %} -This guide is migrated from the [Hiro documentation](https://docs.hiro.so/en/resources/guides/build-a-decentralized-kickstarter). -{% endhint %} - -{% hint style="warning" %} -**Upstream content mismatch:** The Hiro source file for this guide (`build-a-decentralized-kickstarter.mdx`) has the title "Build a decentralized Kickstarter" and description "Learn how to create a crowdfunding app, enabling creators to fund their projects without a third party," but its body content is identical to the NFT marketplace guide. This appears to be an error in the upstream Hiro documentation. The content below is migrated faithfully as-is from the Hiro source. -{% endhint %} - -In this guide, you will learn how to create an NFT marketplace that allows users to list NFTs for sale. Users can specify the following details for their listings: -- The NFT token to sell. -- Listing expiry in block height. -- The payment asset, either STX or a SIP010 fungible token. -- The NFT price in the chosen payment asset. -- An optional intended taker. If set, only that principal will be able to fulfil the listing. - -This marketplace leverages the following Clarity traits: -- `nft-trait` for handling NFTs. -- `ft-trait` for handling fungible tokens. - -Over the course of this guide, you will learn how to: -1. Define and handle errors. -2. Create and manage NFT listings. -3. Whitelist asset contracts. -4. Fulfil NFT purchases. - ---- - -## Define and handle errors - -First, define constants for various errors that may occur during listing, cancelling, or fulfilling NFT transactions. This helps in maintaining clean and readable code. - -```clarity -;; Define listing errors -(define-constant ERR_EXPIRY_IN_PAST (err u1000)) -(define-constant ERR_PRICE_ZERO (err u1001)) - -;; Define cancelling and fulfilling errors -(define-constant ERR_UNKNOWN_LISTING (err u2000)) -(define-constant ERR_UNAUTHORISED (err u2001)) -(define-constant ERR_LISTING_EXPIRED (err u2002)) -(define-constant ERR_NFT_ASSET_MISMATCH (err u2003)) -(define-constant ERR_PAYMENT_ASSET_MISMATCH (err u2004)) -(define-constant ERR_MAKER_TAKER_EQUAL (err u2005)) -(define-constant ERR_UNINTENDED_TAKER (err u2006)) -(define-constant ERR_ASSET_CONTRACT_NOT_WHITELISTED (err u2007)) -(define-constant ERR_PAYMENT_CONTRACT_NOT_WHITELISTED (err u2008)) -``` - -## Create and manage NFT listings - -### Define data structures - -Create a map data structure for the asset listings and a data variable for unique IDs. - -```clarity -;; Define a map data structure for the asset listings -(define-map listings - uint - { - maker: principal, - taker: (optional principal), - token-id: uint, - nft-asset-contract: principal, - expiry: uint, - price: uint, - payment-asset-contract: (optional principal) - } -) - -;; Used for unique IDs for each listing -(define-data-var listing-nonce uint u0) -``` - -### List an asset - -Create a public function to list an asset along with its contract. This function verifies the contract, checks expiry and price, and transfers the NFT ownership to the marketplace. - -```clarity -(define-public (list-asset - (nft-asset-contract ) - (nft-asset { - taker: (optional principal), - token-id: uint, - expiry: uint, - price: uint, - payment-asset-contract: (optional principal) - }) -) - (let ((listing-id (var-get listing-nonce))) - ;; Verify that the contract of this asset is whitelisted - (asserts! (is-whitelisted (contract-of nft-asset-contract)) ERR_ASSET_CONTRACT_NOT_WHITELISTED) - ;; Verify that the asset is not expired - (asserts! (> (get expiry nft-asset) block-height) ERR_EXPIRY_IN_PAST) - ;; Verify that the asset price is greater than zero - (asserts! (> (get price nft-asset) u0) ERR_PRICE_ZERO) - ;; Verify that the contract of the payment is whitelisted - (asserts! (match (get payment-asset-contract nft-asset) - payment-asset - (is-whitelisted payment-asset) - true - ) ERR_PAYMENT_CONTRACT_NOT_WHITELISTED) - ;; Transfer the NFT ownership to this contract's principal - (try! (transfer-nft - nft-asset-contract - (get token-id nft-asset) - tx-sender - (as-contract tx-sender) - )) - ;; List the NFT in the listings map - (map-set listings listing-id (merge - { maker: tx-sender, nft-asset-contract: (contract-of nft-asset-contract) } - nft-asset - )) - ;; Increment the nonce to use for the next unique listing ID - (var-set listing-nonce (+ listing-id u1)) - ;; Return the created listing ID - (ok listing-id) - ) -) -``` - -### Retrieve an asset - -Create a read-only function to retrieve an asset, or listing, by its ID. - -```clarity -(define-read-only (get-listing (listing-id uint)) - (map-get? listings listing-id) -) -``` - -### Cancel a listing - -Create a public function to cancel a listing. Only the NFT's creator can cancel the listing, and it must use the same asset contract that the NFT uses. - -```clarity -(define-public (cancel-listing (listing-id uint) (nft-asset-contract )) - (let ( - (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) - (maker (get maker listing)) - ) - ;; Verify that the caller of the function is the creator of the NFT to be cancelled - (asserts! (is-eq maker tx-sender) ERR_UNAUTHORISED) - ;; Verify that the asset contract to use is the same one that the NFT uses - (asserts! (is-eq - (get nft-asset-contract listing) - (contract-of nft-asset-contract) - ) ERR_NFT_ASSET_MISMATCH) - ;; Delete the listing - (map-delete listings listing-id) - ;; Transfer the NFT from this contract's principal back to the creator's principal - (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender maker)) - ) -) -``` - -## Whitelist asset contracts - -### Whitelist contracts - -The marketplace requires any contracts used for assets or payments to be whitelisted by the contract owner. Create a map to store whitelisted asset contracts and a function to check if a contract is whitelisted. - -```clarity -(define-map whitelisted-asset-contracts principal bool) - -(define-read-only (is-whitelisted (asset-contract principal)) - (default-to false (map-get? whitelisted-asset-contracts asset-contract)) -) -``` - -### Set whitelisted contracts - -Only the contract owner can whitelist an asset contract. Create a public function to set whitelisted asset contracts. - -```clarity -(define-public (set-whitelisted (asset-contract principal) (whitelisted bool)) - (begin - (asserts! (is-eq contract-owner tx-sender) ERR_UNAUTHORISED) - (ok (map-set whitelisted-asset-contracts asset-contract whitelisted)) - ) -) -``` - -## Fulfill NFT purchases - -### Fulfill listing with STX - -Create a public function to purchase a listing using STX as payment. - -```clarity -(define-public (fulfil-listing-stx (listing-id uint) (nft-asset-contract )) - (let ( - ;; Verify the given listing ID exists - (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) - ;; Set the NFT's taker to the purchaser (caller of the function) - (taker tx-sender) - ) - ;; Validate that the purchase can be fulfilled - (try! (assert-can-fulfil (contract-of nft-asset-contract) none listing)) - ;; Transfer the NFT to the purchaser (caller of the function) - (try! (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender taker))) - ;; Transfer the STX payment from the purchaser to the creator of the NFT - (try! (stx-transfer? (get price listing) taker (get maker listing))) - ;; Remove the NFT from the marketplace listings - (map-delete listings listing-id) - ;; Return the listing ID that was just purchased - (ok listing-id) - ) -) -``` - -### Fulfill listing with SIP-010 - -Create a public function to purchase a listing using another fungible token that follows the SIP-010 standard as payment. - -```clarity -(define-public (fulfil-listing-ft - (listing-id uint) - (nft-asset-contract ) - (payment-asset-contract ) -) - (let ( - ;; Verify the given listing ID exists - (listing (unwrap! (map-get? listings listing-id) ERR_UNKNOWN_LISTING)) - ;; Set the NFT's taker to the purchaser (caller of the function) - (taker tx-sender) - ) - ;; Validate that the purchase can be fulfilled - (try! (assert-can-fulfil - (contract-of nft-asset-contract) - (some (contract-of payment-asset-contract)) - listing - )) - ;; Transfer the NFT to the purchaser (caller of the function) - (try! (as-contract (transfer-nft nft-asset-contract (get token-id listing) tx-sender taker))) - ;; Transfer the tokens as payment from the purchaser to the creator of the NFT - (try! (transfer-ft payment-asset-contract (get price listing) taker (get maker listing))) - ;; Remove the NFT from the marketplace listings - (map-delete listings listing-id) - ;; Return the listing ID that was just purchased - (ok listing-id) - ) -) -``` - -### Validate purchase can be fulfilled - -Create a private function to validate that a purchase can be fulfilled. This function checks the listing's expiry, the NFT's contract, and the payment's contract. - -```clarity -(define-private (assert-can-fulfil - (nft-asset-contract principal) - (payment-asset-contract (optional principal)) - (listing { - maker: principal, - taker: (optional principal), - token-id: uint, - nft-asset-contract: principal, - expiry: uint, - price: uint, - payment-asset-contract: (optional principal) - }) -) - (begin - ;; Verify that the buyer is not the same as the NFT creator - (asserts! (not (is-eq (get maker listing) tx-sender)) ERR_MAKER_TAKER_EQUAL) - ;; Verify the buyer has been set in the listing metadata as its `taker` - (asserts! - (match (get taker listing) intended-taker (is-eq intended-taker tx-sender) true) - ERR_UNINTENDED_TAKER - ) - ;; Verify the listing for purchase is not expired - (asserts! (< block-height (get expiry listing)) ERR_LISTING_EXPIRED) - ;; Verify the asset contract used to purchase the NFT is the same as the one set on the NFT - (asserts! (is-eq (get nft-asset-contract listing) nft-asset-contract) ERR_NFT_ASSET_MISMATCH) - ;; Verify the payment contract used to purchase the NFT is the same as the one set on the NFT - (asserts! - (is-eq (get payment-asset-contract listing) payment-asset-contract) - ERR_PAYMENT_ASSET_MISMATCH - ) - (ok true) - ) -) -```