From a3a770a416faab2d6b010e719ce87535d25f573f Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Sun, 28 Nov 2021 12:05:30 +0000 Subject: [PATCH] updated contract and traits in line with developments here https://github.com/stacksgov/sips/issues/52 and 51 --- Clarinet.toml | 8 ++-- contracts/loopbomb.clar | 63 +++++++++++++++++++++---------- contracts/nft-operable-trait.clar | 17 +++++++++ contracts/template.clar | 2 +- src/loopbomb-client.ts | 4 +- tests/loopbomb_test.ts | 16 +++++--- 6 files changed, 78 insertions(+), 32 deletions(-) create mode 100644 contracts/nft-operable-trait.clar diff --git a/Clarinet.toml b/Clarinet.toml index c9f2662..71aeb3e 100644 --- a/Clarinet.toml +++ b/Clarinet.toml @@ -1,6 +1,6 @@ [project] name = "clarinet-clarity-market" -requirements = ["nft-trait.clar", "nft-tradable-trait.clar"] +requirements = ["nft-trait.clar", "nft-operable-trait.clar"] costs_version = 1 [contracts.appmap] path = "contracts/appmap.clar" @@ -10,10 +10,10 @@ depends_on = [] path = "contracts/nft-trait.clar" depends_on = [] -[contracts.nft-tradable-trait] -path = "contracts/nft-tradable-trait.clar" +[contracts.nft-operable-trait] +path = "contracts/nft-operable-trait.clar" depends_on = [] [contracts.loopbomb] path = "contracts/loopbomb.clar" -depends_on = ["nft-trait", "nft-tradable-trait"] +depends_on = ["nft-trait", "nft-operable-trait"] diff --git a/contracts/loopbomb.clar b/contracts/loopbomb.clar index a0a33c4..44faf75 100644 --- a/contracts/loopbomb.clar +++ b/contracts/loopbomb.clar @@ -1,6 +1,6 @@ ;; Interface definitions -;; (impl-trait .nft-trait.nft-trait) -;; (impl-trait .nft-approvable-trait.nft-approvable-trait) +(impl-trait .nft-trait.nft-trait) +(impl-trait .nft-operable-trait.nft-operable-trait) ;; (impl-trait 'ST1ESYCGJB5Z5NBHS39XPC70PGC14WAQK5XXNQYDW.nft-approvable-trait.nft-approvable-trait) ;; (impl-trait 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9.nft-trait.nft-trait) @@ -92,12 +92,12 @@ (maxEditions (unwrap! (get max-editions (map-get? nft-data {nft-index: nftIndex})) not-allowed)) (seriesOriginal (unwrap! (get series-original (map-get? nft-data {nft-index: nftIndex})) not-allowed)) ) - (asserts! (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) not-allowed) + (asserts! (unwrap! (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) not-allowed) not-allowed) (ok (map-set nft-data {nft-index: nftIndex} {asset-hash: ahash, meta-data-url: newMetaDataUrl, max-editions: maxEditions, edition: edition, edition-cost: editionCost, series-original: seriesOriginal})) ) ) -;; from nft-trait: Gets the owner of the 'SPecified token ID. +;; from nft-trait: Gets the owner of the 'Specified token ID. (define-read-only (get-owner (nftIndex uint)) (ok (nft-get-owner? loopbomb nftIndex)) ) @@ -164,7 +164,7 @@ ;; Transfers tokens to a 'SPecified principal. (define-public (transfer (nftIndex uint) (owner principal) (recipient principal)) - (if (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) nft-not-owned-err)) + (if (unwrap! (is-approved nftIndex owner) nft-not-owned-err) (match (nft-transfer? loopbomb nftIndex owner recipient) success (begin (ok success)) error (nft-transfer-err error)) @@ -173,7 +173,7 @@ ;; Burns tokens (define-public (burn (nftIndex uint) (owner principal)) - (if (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) nft-not-owned-err)) + (if (unwrap! (is-approved nftIndex owner) nft-not-owned-err) (match (nft-burn? loopbomb nftIndex owner) success (begin (ok success) @@ -192,17 +192,35 @@ (err code))))) ;; see nft-approvable-trait -(define-public (set-approved (operator principal) (token-id uint) (approved bool)) - (ok (map-set approvals {owner: tx-sender, operator: operator, nft-index: token-id} approved)) +(define-public (set-approved (nftIndex uint) (operator principal) (approved bool)) + (let + ( + (owner (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) + ) + (begin + (if (is-eq owner contract-caller) + (ok (map-set approvals {owner: owner, operator: operator, nft-index: nftIndex} approved)) + not-allowed + ) + ) + ) ) -(define-read-only (is-approved (nftIndex uint) (owner principal)) - (or - (is-eq owner tx-sender) - (is-eq owner contract-caller) - (default-to false (map-get? approvals {owner: owner, operator: tx-sender, nft-index: nftIndex})) - (default-to false (map-get? approvals {owner: owner, operator: contract-caller, nft-index: nftIndex})) - ) +(define-read-only (is-approved (nftIndex uint) (address principal)) + (let + ( + (owner (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) + ) + (begin + (if (or + (is-eq owner tx-sender) + (is-eq owner contract-caller) + (default-to false (map-get? approvals {owner: owner, operator: tx-sender, nft-index: nftIndex})) + (default-to false (map-get? approvals {owner: owner, operator: contract-caller, nft-index: nftIndex})) + ) (ok true) nft-not-owned-err + ) + ) + ) ) ;; public methods @@ -485,12 +503,13 @@ (define-public (set-edition-cost (nftIndex uint) (maxEditions uint) (editionCost uint)) (let ( + (owner (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) (ahash (unwrap! (get asset-hash (map-get? nft-data {nft-index: nftIndex})) not-allowed)) (metaDataUrl (unwrap! (get meta-data-url (map-get? nft-data {nft-index: nftIndex})) not-allowed)) (edition (unwrap! (get edition (map-get? nft-data {nft-index: nftIndex})) not-allowed)) (seriesOriginal (unwrap! (get series-original (map-get? nft-data {nft-index: nftIndex})) not-allowed)) ) - (asserts! (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) nft-not-owned-err)) nft-not-owned-err) + (asserts! (unwrap! (is-approved nftIndex owner) nft-not-owned-err) nft-not-owned-err) (asserts! (is-eq nftIndex seriesOriginal) not-originale) (ok (map-set nft-data {nft-index: nftIndex} {asset-hash: ahash, meta-data-url: metaDataUrl, max-editions: maxEditions, edition: edition, edition-cost: editionCost, series-original: seriesOriginal})) ) @@ -505,6 +524,7 @@ (let ( ;; keeps track of the sale cycles for this NFT. + (owner (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) (saleCycleIndex (unwrap! (get sale-cycle-index (map-get? nft-sale-data {nft-index: nftIndex})) amount-not-set)) (saleType (unwrap! (get sale-type (map-get? nft-sale-data {nft-index: nftIndex})) amount-not-set)) (currentBidIndex (default-to u0 (get high-bid-counter (map-get? nft-high-bid-counter {nft-index: nftIndex})))) @@ -513,7 +533,7 @@ ;; u2 means bidding is in progress and the sale data can't be changed. (asserts! (not (and (> currentAmount u0) (is-eq saleType u2))) bidding-error) ;; owner or approval can do this. - (asserts! (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) nft-not-owned-err)) not-allowed) + (asserts! (unwrap! (is-approved nftIndex owner) nft-not-owned-err) nft-not-owned-err) ;; Note - don't override the sale cyle index here as this is a public method and can be called ad hoc. Sale cycle is update at end of sale! (asserts! (map-set nft-sale-data {nft-index: nftIndex} {sale-cycle-index: saleCycleIndex, sale-type: sale-type, increment-stx: increment-stx, reserve-stx: reserve-stx, amount-stx: amount-stx, bidding-end-time: bidding-end-time}) not-allowed) (print {evt: "set-sale-data", nftIndex: nftIndex, saleType: sale-type, increment: increment-stx, reserve: reserve-stx, amount: amount-stx, biddingEndTime: bidding-end-time}) @@ -526,8 +546,9 @@ (let ( (saleCycleIndex (unwrap! (get sale-cycle-index (map-get? nft-sale-data {nft-index: nftIndex})) amount-not-set)) + (owner (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) ) - (asserts! (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) nft-not-owned-err)) not-allowed) + (asserts! (unwrap! (is-approved nftIndex owner) nft-not-owned-err) nft-not-owned-err) ;; Note - don't override the sale cyle index here as this is a public method and can be called ad hoc. Sale cycle is update at end of sale! (asserts! (map-set nft-sale-data {nft-index: nftIndex} {sale-cycle-index: saleCycleIndex, sale-type: u0, increment-stx: u0, reserve-stx: u0, amount-stx: u0, bidding-end-time: u0}) not-allowed) (print {evt: "unlist-item", nftIndex: nftIndex}) @@ -540,8 +561,9 @@ (let ( (saleCycleIndex (unwrap! (get sale-cycle-index (map-get? nft-sale-data {nft-index: nftIndex})) amount-not-set)) + (owner (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) ) - (asserts! (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) nft-not-owned-err)) not-allowed) + (asserts! (unwrap! (is-approved nftIndex owner) nft-not-owned-err) nft-not-owned-err) ;; (map-set approvals {owner: tx-sender, operator: operator, nft-index: token-id} true) ;; Note - don't override the sale cyle index here as this is a public method and can be called ad hoc. Sale cycle is update at end of sale! (asserts! (map-set nft-sale-data {nft-index: nftIndex} {sale-cycle-index: saleCycleIndex, sale-type: u1, increment-stx: u0, reserve-stx: u0, amount-stx: amount, bidding-end-time: u0}) not-allowed) @@ -704,10 +726,11 @@ (currentBidder (unwrap! (get-current-bidder nftIndex currentBidIndex) bidding-error)) (currentAmount (unwrap! (get-current-bid-amount nftIndex currentBidIndex) bidding-error)) (seriesOriginal (unwrap! (get series-original (map-get? nft-data {nft-index: nftIndex})) not-allowed)) + (owner (unwrap! (nft-get-owner? loopbomb nftIndex) not-allowed)) ) (asserts! (or (is-eq closeType u1) (is-eq closeType u2)) failed-to-close-1) ;; only the owner or administrator can call close - (asserts! (or (is-approved nftIndex (unwrap! (nft-get-owner? loopbomb nftIndex) nft-not-owned-err)) (unwrap! (is-administrator) not-allowed)) not-allowed) + (asserts! (or (unwrap! (is-approved nftIndex owner) nft-not-owned-err) (unwrap! (is-administrator) not-allowed)) not-allowed) ;; only the administrator can call close BEFORE the end time - note we use the less accurate ;; but fool proof block time here to prevent owner/client code jerry mandering the close function (asserts! (or (> block-time bidding-end-time) (unwrap! (is-administrator) failed-to-close-3)) failed-to-close-3) diff --git a/contracts/nft-operable-trait.clar b/contracts/nft-operable-trait.clar new file mode 100644 index 0000000..956cb79 --- /dev/null +++ b/contracts/nft-operable-trait.clar @@ -0,0 +1,17 @@ +(define-trait nft-operable-trait + ( + ;; set approval for an operator to handle a specified id or amount of the asset + ;; must return `(ok true)` on success, never `(ok false)` + ;; @param id-or-amount; identifier of NFT or amount of FTs + ;; @param operator: principal that wants top operate the asset + ;; @param bool: if true operator can transfer id or up to amount + (set-approved (uint principal bool) (response bool uint)) + + ;; read-only function to return the current status of given operator + ;; if returned `(ok true)` the operator can transfer the NFT with the given id or up to the requested amount of FT + ;; @param id-or-amount; identifier of NFT or amount of FTs + ;; @param operator: principal that wants to operate the asset + ;; @param bool: if true operator can transfer id or up to amount + (is-approved (uint principal) (response bool uint)) + ) +) diff --git a/contracts/template.clar b/contracts/template.clar index a42c57a..b1cf973 100644 --- a/contracts/template.clar +++ b/contracts/template.clar @@ -193,7 +193,7 @@ (err code))))) ;; see nft-approvable-trait -(define-public (set-approved (operator principal) (token-id uint) (approved bool)) +(define-public (set-approved (token-id uint) (operator principal) (approved bool)) (ok (map-set approvals {owner: tx-sender, operator: operator, nft-index: token-id} approved)) ) diff --git a/src/loopbomb-client.ts b/src/loopbomb-client.ts index 426811b..bc6d919 100644 --- a/src/loopbomb-client.ts +++ b/src/loopbomb-client.ts @@ -65,11 +65,11 @@ export class LoopbombClient { return result; } - setApproved(operator: string, nftIndex: number, approved: boolean, sender: string): Tx { + setApproved(nftIndex: number, operator: string, approved: boolean, sender: string): Tx { return Tx.contractCall( this.contractName, "set-approved", - [types.principal(operator), types.uint(nftIndex), types.bool(approved)], + [types.uint(nftIndex), types.principal(operator), types.bool(approved)], sender ); } diff --git a/tests/loopbomb_test.ts b/tests/loopbomb_test.ts index f56d78f..0cf6634 100644 --- a/tests/loopbomb_test.ts +++ b/tests/loopbomb_test.ts @@ -705,17 +705,23 @@ Clarinet.test({ block.receipts[0].result .expectErr().expectUint(ErrCode.ERR_NFT_NOT_OWNED_ERR); - // wallet 1 can still set approval - see https://github.com/stacksgov/sips/issues/40 + // wallet 2 can set approval - see https://github.com/stacksgov/sips/issues/40 block = chain.mineBlock([ - client.setApproved(wallet3.address, 0, true, wallet1.address), + client.setApproved(0, wallet3.address, true, wallet2.address), ]); block.receipts[0].result .expectOk() .expectBool(true); - - // wallet 2 sets approval for wallet 3 + + block = chain.mineBlock([ + client.setApproved(0, wallet3.address, true, wallet3.address), + ]); + block.receipts[0].result + .expectErr().expectUint(ErrCode.ERR_NOT_ALLOWED); + + // wallet 2 sets approval for wallet 3 block = chain.mineBlock([ - client.setApproved(wallet3.address, 0, true, wallet2.address), + client.setApproved(0, wallet3.address, true, wallet2.address), ]); block.receipts[0].result.expectOk().expectBool(true);