Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ryder Mint V2 on Stacks #16

Merged
merged 12 commits into from
Jan 9, 2023
10 changes: 6 additions & 4 deletions backend-stacks/Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ contract_id = 'SP2KAF9RF86PVX3NEE27DFV1CQX0T4WGR41X3S45C.commission-trait'

[[project.requirements]]
contract_id = 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait'

[contracts.commission-free]
path = 'contracts/external/commission-free.clar'
clarity_version = 1

[contracts.ryder-mint]
path = 'contracts/ryder-mint.clar'
clarity_version = 1

[contracts.ryder-mint-free]
path = 'contracts/external/ryder-mint-free.clar'
clarity_version = 1

[contracts.ryder-mint]
path = 'contracts/ryder-mint.clar'
[contracts.ryder-mint-v2]
path = 'contracts/ryder-mint-v2.clar'
clarity_version = 1

[contracts.ryder-nft]
Expand All @@ -30,7 +33,6 @@ clarity_version = 1
[contracts.ryder-nft-allow-list]
path = 'contracts/ryder-nft-allow-list.clar'
clarity_version = 1

[repl.analysis]
passes = ['check_checker']

Expand Down
130 changes: 130 additions & 0 deletions backend-stacks/contracts/ryder-mint-v2.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
(define-constant contract-principal (as-contract tx-sender))

(define-constant err-forbidden (err u403))
(define-constant err-same-principal (err u508))
(define-constant err-bad-mint-status (err u600))
(define-constant err-sold-out (err u601))
(define-constant err-failed (err u602))
(define-constant err-no-claims (err u603))
(define-constant err-cannot-claim-future (err u604))

(define-constant block-height-increment u1)

(define-data-var mint-enabled bool false)
(define-data-var price-in-ustx uint u1130000000)
(define-data-var payment-recipient principal 'SP1YZSSPWJ5D3S1G48ZPW8NGXVG0K2TZJJXDM6N0Q)

(define-data-var lower-mint-id uint u0)
(define-data-var upper-mint-id uint u0)
(define-data-var last-transferred-id uint u0)
(define-data-var amount-available-for-purchase uint u0)

(define-map admins principal bool)
(map-set admins tx-sender true)

(define-map nft-claims {height: uint, buyer: principal} uint)
(define-map token-mapping uint uint)

(define-read-only (nfts-available)
(var-get amount-available-for-purchase))

(define-read-only (get-nft-claims (height uint) (buyer principal))
(default-to u0 (map-get? nft-claims {height: height, buyer: buyer})))

(define-read-only (get-vrf (height uint))
(get-block-info? vrf-seed height))

(define-private (pick-next-random-token-id (lower-bound uint) (upper-bound uint) (height uint))
(begin
(asserts! (> upper-bound lower-bound) (some lower-bound))
(let ((seed (sha256 (concat (unwrap! (get-vrf height) none) (sha256 (var-get last-transferred-id)))))
(number (+
(match (element-at seed u0) byte (unwrap-panic (index-of byte-list byte)) u0)
(match (element-at seed u1) byte (* (unwrap-panic (index-of byte-list byte)) u256) u0)
(match (element-at seed u2) byte (* (unwrap-panic (index-of byte-list byte)) u65536) u0))))
(some (+ lower-bound (mod number (- upper-bound lower-bound)))))))

(define-public (buy (amount uint))
(let ((available (var-get amount-available-for-purchase))
(target-height (+ block-height block-height-increment)))
(asserts! (var-get mint-enabled) err-bad-mint-status)
(asserts! (>= available amount) err-sold-out)
(var-set amount-available-for-purchase (- available amount))
(map-set nft-claims {height: target-height, buyer: tx-sender} (+ (get-nft-claims target-height tx-sender) amount))
(try! (stx-transfer? (* (var-get price-in-ustx) amount) tx-sender (var-get payment-recipient)))
(ok target-height)))

(define-public (claim (height uint))
(claim-for height tx-sender))

(define-public (claim-for (height uint) (buyer principal))
(let ((upper-bound (var-get upper-mint-id))
(index (unwrap! (pick-next-random-token-id (var-get lower-mint-id) upper-bound height) err-cannot-claim-future))
(transfer-id (default-to index (map-get? token-mapping index)))
(claims (get-nft-claims height buyer)))
(asserts! (or (is-eq buyer tx-sender) (default-to false (map-get? admins contract-caller))) err-forbidden)
(asserts! (> claims u0) err-no-claims)
(try! (contract-call? .ryder-nft transfer transfer-id contract-principal buyer))
(map-set token-mapping index (default-to upper-bound (map-get? token-mapping upper-bound)))
(var-set upper-mint-id (- upper-bound u1))
(var-set last-transferred-id transfer-id)
(map-set nft-claims {height: height, buyer: buyer} (- claims u1))
(ok transfer-id)))

(define-public (claim-many (heights (list 20 uint)))
(ok (map claim heights)))

(define-read-only (get-upper-bound)
(var-get upper-mint-id))

;; admin function
(define-read-only (check-is-admin)
(ok (asserts! (default-to false (map-get? admins contract-caller)) err-forbidden)))

(define-private (mint-to-contract-iter (c (buff 1)) (p (optional (response bool uint))))
(some (contract-call? .ryder-nft mint contract-principal)))

(define-public (mint-to-contract (iterations (buff 200)))
(begin
(try! (check-is-admin))
(asserts! (not (var-get mint-enabled)) err-bad-mint-status)
(and (is-eq (var-get lower-mint-id) u0)
(var-set lower-mint-id (contract-call? .ryder-nft get-token-id-nonce)))
(fold mint-to-contract-iter iterations none)
(var-set upper-mint-id (- (contract-call? .ryder-nft get-token-id-nonce) u1))
(ok (var-set amount-available-for-purchase (- (+ (var-get upper-mint-id) u1) (var-get lower-mint-id))))))

(define-public (set-mint-enabled (enabled bool))
(begin
(try! (check-is-admin))
(ok (var-set mint-enabled enabled))))

(define-public (set-admin (new-admin principal) (value bool))
(begin
(try! (check-is-admin))
(asserts! (not (is-eq tx-sender new-admin)) err-same-principal)
(ok (map-set admins new-admin value))))

(define-private (burn-top-iter (c (buff 1)) (data {i: uint, p: (response bool uint)}))
(begin
(unwrap! (get p data) data)
{i: (- (get i data) u1), p: (as-contract (contract-call? .ryder-nft burn (get i data)))}))

;; once burn-top is used, mint-to-contract can never be used again
(define-public (burn-contract-tokens-top (iterations (buff 200)))
(let ((result (fold burn-top-iter iterations {i: (var-get upper-mint-id), p: (ok true)})))
(try! (check-is-admin))
(unwrap! (get p result) err-failed)
(ok (var-set upper-mint-id (get i result)))))

(define-public (set-payment-recipient (recipient principal))
(begin
(try! (check-is-admin))
(ok (var-set payment-recipient recipient))))

(define-public (set-price-in-ustx (price uint))
(begin
(try! (check-is-admin))
(ok (var-set price-in-ustx price))))

(define-constant byte-list 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff)
21 changes: 13 additions & 8 deletions backend-stacks/deployments/default.simnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,27 @@ plan:
path: contracts/ryder-nft.clar
clarity-version: 1
- emulated-contract-publish:
contract-name: ryder-mint-free
contract-name: ryder-mint
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/external/ryder-mint-free.clar
path: contracts/ryder-mint.clar
clarity-version: 1
- emulated-contract-publish:
contract-name: commission-free
contract-name: ryder-nft-allow-list
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/external/commission-free.clar
path: contracts/ryder-nft-allow-list.clar
clarity-version: 1
- emulated-contract-publish:
contract-name: ryder-mint
contract-name: ryder-mint-v2
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/ryder-mint.clar
path: contracts/ryder-mint-v2.clar
clarity-version: 1
- emulated-contract-publish:
contract-name: ryder-nft-allow-list
contract-name: ryder-mint-free
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/ryder-nft-allow-list.clar
path: contracts/external/ryder-mint-free.clar
clarity-version: 1
- emulated-contract-publish:
contract-name: commission-free
emulated-sender: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM
path: contracts/external/commission-free.clar
clarity-version: 1
54 changes: 54 additions & 0 deletions backend-stacks/tests/clients/ryder-mint-v2-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Chain, Tx, types, Account } from "../deps.ts";
import { setMinter } from "./ryder-nft-client.ts";

export function buy(amount: number, userAddress: string) {
return Tx.contractCall(
"ryder-mint-v2",
"buy",
[types.uint(amount)],
userAddress
);
}

export function claim(height: number, userAddress: string) {
return Tx.contractCall("ryder-mint-v2", "claim", [types.uint(height)], userAddress);
}

export function claimTwo(height: number, userAddress: string) {
return Tx.contractCall("ryder-mint-v2", "claim-many", [types.list([types.uint(height), types.uint(height)])], userAddress);
}

export function claimFive(height: number, userAddress: string) {
return Tx.contractCall("ryder-mint-v2", "claim-many", [types.list([types.uint(height), types.uint(height), types.uint(height), types.uint(height), types.uint(height)])], userAddress);
}

export function claimTen(height: number, userAddress: string) {
return Tx.contractCall("ryder-mint-v2", "claim-many", [types.list([types.uint(height), types.uint(height), types.uint(height), types.uint(height), types.uint(height), types.uint(height), types.uint(height), types.uint(height), types.uint(height), types.uint(height)])], userAddress);
}

export function setMintEnabled(enabled: boolean, userAddress: string) {
return Tx.contractCall(
"ryder-mint-v2",
"set-mint-enabled",
[types.bool(enabled)],
userAddress
);
}

export function mintToContract(amount: number, userAddress: string) {
return Tx.contractCall(
"ryder-mint-v2",
"mint-to-contract",
[types.buff("x".repeat(amount))],
userAddress
);
}

export function burnContractTokensTop(amount: number, userAddress: string) {
return Tx.contractCall(
"ryder-mint-v2",
"burn-contract-tokens-top",
[types.buff("x".repeat(amount))],
userAddress
);
}
53 changes: 53 additions & 0 deletions backend-stacks/tests/ryder-mint-v2_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Clarinet, Tx, Chain, Account, types, assertEquals } from "./deps.ts";

import { enabledPublicMint, claimTwenty } from "./clients/ryder-mint-client.ts";
import { shufflePrepare, shuffleIds } from "./clients/ryder-mint-client.ts";
import { setMintLimit, setMinter } from "./clients/ryder-nft-client.ts";
import {
mintToContract,
setMintEnabled,
buy,
claimTen,
} from "./clients/ryder-mint-v2-client.ts";

Clarinet.test({
name: "Ensure that all nfts after reveal can be minted",
async fn(chain: Chain, accounts: Map<string, Account>) {
const deployer = accounts.get("deployer")!;
const wallet_1 = accounts.get("wallet_1")!;
const wallet_2 = accounts.get("wallet_2")!;
enabledPublicMint(chain, deployer);

let block = chain.mineBlock([claimTwenty(wallet_1.address)]);
block.receipts[0].result.expectOk();

block = chain.mineBlock([shufflePrepare(deployer.address)]);
block.receipts[0].result.expectOk();
block = chain.mineBlock([
shuffleIds(deployer.address),
setMintLimit(40, deployer.address),
]);
block.receipts[0].result.expectOk();
block.receipts[1].result.expectOk();

block = chain.mineBlock([
setMinter(`'${deployer.address}.ryder-mint-v2`, true, deployer.address),
mintToContract(10, deployer.address),
setMintEnabled(true, deployer.address),
]);
block.receipts[0].result.expectOk();
block.receipts[1].result.expectOk();
block.receipts[2].result.expectOk();

block = chain.mineBlock([buy(10, wallet_1.address)]);
let expectedClaimHeight = 7;
block.receipts[0].result.expectOk().expectUint(expectedClaimHeight);

chain.mineEmptyBlock(1);

block = chain.mineBlock([claimTen(expectedClaimHeight, wallet_1.address)]);

console.log(block.receipts);
block.receipts[0].result.expectOk();
},
});
7 changes: 4 additions & 3 deletions backend-stacks/tests/ryder-mint_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
import { setMinter, setMintLimit } from "./clients/ryder-nft-client.ts";
import * as Errors from "./clients/error-codes.ts";

const MINT_PRICE = 1000_000_000;
const MINT_PRICE = 1130_000_000;
const payment_recipient = 'SP1YZSSPWJ5D3S1G48ZPW8NGXVG0K2TZJJXDM6N0Q';

Clarinet.test({
name: "Ensure that users can mint",
Expand All @@ -29,7 +30,7 @@ Clarinet.test({
block.receipts[0].events.expectSTXTransferEvent(
MINT_PRICE,
wallet_1.address,
deployer.address
payment_recipient
);
block.receipts[0].events.expectNonFungibleTokenMintEvent(
types.uint(1),
Expand Down Expand Up @@ -141,7 +142,7 @@ Clarinet.test({
block.receipts[0].events.expectSTXTransferEvent(
MINT_PRICE,
wallet_1.address,
deployer.address
payment_recipient
);
block.receipts[0].events.expectNonFungibleTokenMintEvent(
types.uint(1),
Expand Down
4 changes: 1 addition & 3 deletions backend-stacks/tests/ryder-nft-allow-list_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ Clarinet.test({
block.receipts[0].result.expectErr().expectUint(Errors.ERR_UNAUTHORIZED);
block.receipts[1].result.expectOk();
block.receipts[2].result.expectOk();
block.receipts[3].result
.expectErr()
.expectUint(Errors.ERR_SAME_SENDER_RECIPIENT);
block.receipts[3].result.expectOk(); // payment sent to SP1YZSSPWJ5D3S1G48ZPW8NGXVG0K2TZJJXDM6N0Q
},
});