From f92b7152d1e140b9609eae9891431900464677fe Mon Sep 17 00:00:00 2001 From: publishednft Date: Sat, 29 Nov 2025 04:09:59 +0000 Subject: [PATCH] Add GenesisPassV4 Purchase Transaction for Published NFT Adds support for purchasing Genesis Library Pass NFTs using DapperUtilityCoin via NFTStorefrontV2. - Transaction: purchase-genesispass-v4.cdc - Metadata Script: purchase-genesispass-v4-metadata.cdc - Contract: GenesisPassV4 at 0x4c55dc21a9da7476 (testnet) - Payment: DapperUtilityCoin (DUC) Features: - Auto-initializes buyer's NFT collection - DUC leakage protection - Optional commission support - MetadataViews compliant --- purchase-genesispass-v4/README.md | 41 +++++++++++ .../purchase-genesispass-v4-metadata.cdc | 64 ++++++++++++++++ .../purchase-genesispass-v4.cdc | 73 +++++++++++++++++++ purchase-genesispass-v4/testnet.env | 7 ++ 4 files changed, 185 insertions(+) create mode 100644 purchase-genesispass-v4/README.md create mode 100644 purchase-genesispass-v4/purchase-genesispass-v4-metadata.cdc create mode 100644 purchase-genesispass-v4/purchase-genesispass-v4.cdc create mode 100644 purchase-genesispass-v4/testnet.env diff --git a/purchase-genesispass-v4/README.md b/purchase-genesispass-v4/README.md new file mode 100644 index 0000000..e44061e --- /dev/null +++ b/purchase-genesispass-v4/README.md @@ -0,0 +1,41 @@ +# Purchase Genesis Pass V4 + +This transaction allows users to purchase a Genesis Library Pass NFT from the NFTStorefrontV2 marketplace using DapperUtilityCoin (DUC). + +## Overview + +Genesis Library Pass is a lifetime access pass to the Published NFT library ecosystem, providing access to 2,000+ books, audiobooks, magazines, and exclusive content. + +## Transaction Details + +- **Contract**: GenesisPassV4 +- **Contract Address (Testnet)**: 0x4c55dc21a9da7476 +- **Payment Token**: DapperUtilityCoin (DUC) +- **Marketplace**: NFTStorefrontV2 + +## Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| storefrontAddress | Address | Address of the NFT seller's storefront | +| listingResourceID | UInt64 | ID of the listing to purchase | +| commissionRecipient | Address? | Optional commission recipient address | + +## Features + +- Auto-initializes buyer's NFT collection if needed +- Supports optional commission payments +- DUC leakage protection via post-condition +- MetadataViews compliant for proper NFT display + +## Files + +- `purchase-genesispass-v4.cdc` - Main purchase transaction +- `purchase-genesispass-v4-metadata.cdc` - Metadata script for purchase preview +- `testnet.env` - Testnet contract addresses + +## Security + +- All DUC must remain in Dapper's vault (no leakage) +- Collection auto-initialization prevents failed deposits +- Commission recipient validation ensures proper capability diff --git a/purchase-genesispass-v4/purchase-genesispass-v4-metadata.cdc b/purchase-genesispass-v4/purchase-genesispass-v4-metadata.cdc new file mode 100644 index 0000000..06ed3b7 --- /dev/null +++ b/purchase-genesispass-v4/purchase-genesispass-v4-metadata.cdc @@ -0,0 +1,64 @@ +import NonFungibleToken from ${NonFungibleToken} +import MetadataViews from ${MetadataViews} +import NFTStorefrontV2 from ${NFTStorefrontV2} +import ${NFTContractName} from ${NFTContractAddress} + +access(all) struct PurchaseData { + access(all) let id: UInt64 + access(all) let name: String + access(all) let amount: UFix64 + access(all) let description: String + access(all) let imageURL: String + access(all) let paymentVaultTypeID: Type + + init(id: UInt64, name: String, amount: UFix64, description: String, imageURL: String, paymentVaultTypeID: Type) { + self.id = id + self.name = name + self.amount = amount + self.description = description + self.imageURL = imageURL + self.paymentVaultTypeID = paymentVaultTypeID + } +} + +access(all) fun main(storefrontAddress: Address, listingResourceID: UInt64, commissionRecipient: Address?): PurchaseData { + + let account = getAccount(storefrontAddress) + let marketCollectionRef = account.capabilities.get<&NFTStorefrontV2.Storefront>( + NFTStorefrontV2.StorefrontPublicPath + ).borrow() + ?? panic("Could not borrow Storefront from provided address") + + let saleItem = marketCollectionRef.borrowListing(listingResourceID: listingResourceID) + ?? panic("No item with that ID") + + let listingDetails = saleItem.getDetails()! + + let collection = account.capabilities.get<&{NonFungibleToken.Collection}>( + ${NFTContractName}.CollectionPublicPath + ).borrow() + ?? panic("Could not borrow a reference to the collection") + + let nft = collection.borrowNFT(listingDetails.nftID) + ?? panic("Could not borrow a reference to the NFT") + + let viewSerial = nft.resolveView(Type())! + let displaySerial = viewSerial as! MetadataViews.Serial + + if let view = nft.resolveView(Type()) { + + let display = view as! MetadataViews.Display + + let purchaseData = PurchaseData( + id: displaySerial.number, + name: display.name, + amount: listingDetails.salePrice, + description: display.description, + imageURL: display.thumbnail.uri(), + paymentVaultTypeID: listingDetails.salePaymentVaultType + ) + + return purchaseData + } + panic("No NFT") +} diff --git a/purchase-genesispass-v4/purchase-genesispass-v4.cdc b/purchase-genesispass-v4/purchase-genesispass-v4.cdc new file mode 100644 index 0000000..50ab7b0 --- /dev/null +++ b/purchase-genesispass-v4/purchase-genesispass-v4.cdc @@ -0,0 +1,73 @@ +import FungibleToken from ${FungibleToken} +import NonFungibleToken from ${NonFungibleToken} +import MetadataViews from ${MetadataViews} +import DapperUtilityCoin from ${DapperUtilityCoin} +import NFTStorefrontV2 from ${NFTStorefrontV2} +import ${NFTContractName} from ${NFTContractAddress} + +transaction(storefrontAddress: Address, listingResourceID: UInt64, commissionRecipient: Address?) { + + let mainVault: auth(FungibleToken.Withdraw) &DapperUtilityCoin.Vault + let paymentVault: @{FungibleToken.Vault} + let nftReceiver: &{NonFungibleToken.Receiver} + let storefront: &{NFTStorefrontV2.StorefrontPublic} + let listing: &{NFTStorefrontV2.ListingPublic} + let balanceBeforeTransfer: UFix64 + var commissionRecipientCap: Capability<&{FungibleToken.Receiver}>? + + prepare(dapper: auth(BorrowValue) &Account, buyer: auth(Storage, Capabilities) &Account) { + self.commissionRecipientCap = nil + + if buyer.capabilities.borrow<&${NFTContractName}.Collection>(${NFTContractName}.CollectionPublicPath) == nil { + let collection <- ${NFTContractName}.createEmptyCollection(nftType: Type<@${NFTContractName}.NFT>()) + buyer.storage.save(<-collection, to: ${NFTContractName}.CollectionStoragePath) + + let collectionCap = buyer.capabilities.storage.issue<&${NFTContractName}.Collection>(${NFTContractName}.CollectionStoragePath) + buyer.capabilities.publish(collectionCap, at: ${NFTContractName}.CollectionPublicPath) + } + + self.storefront = getAccount(storefrontAddress).capabilities.borrow<&{NFTStorefrontV2.StorefrontPublic}>( + NFTStorefrontV2.StorefrontPublicPath + ) ?? panic("Could not borrow Storefront from provided address") + + self.listing = self.storefront.borrowListing(listingResourceID: listingResourceID) + ?? panic("No Offer with that ID in Storefront") + let price = self.listing.getDetails().salePrice + + self.mainVault = dapper.storage.borrow(from: /storage/dapperUtilityCoinVault) + ?? panic("Cannot borrow DapperUtilityCoin vault from acct storage") + self.balanceBeforeTransfer = self.mainVault.balance + self.paymentVault <- self.mainVault.withdraw(amount: price) + + let collectionData = ${NFTContractName}.resolveContractView(resourceType: nil, viewType: Type()) as! MetadataViews.NFTCollectionData? + ?? panic("ViewResolver does not resolve NFTCollectionData view") + self.nftReceiver = buyer.capabilities.borrow<&{NonFungibleToken.Receiver}>(collectionData.publicPath) + ?? panic("Cannot borrow NFT collection receiver from account") + + let commissionAmount = self.listing.getDetails().commissionAmount + + if commissionRecipient != nil && commissionAmount != 0.0 { + let _commissionRecipientCap = getAccount(commissionRecipient!).capabilities.get<&{FungibleToken.Receiver}>( + /public/dapperUtilityCoinReceiver + ) + assert(_commissionRecipientCap.check(), message: "Commission Recipient doesn't have DapperUtilityCoin receiving capability") + self.commissionRecipientCap = _commissionRecipientCap + } else if commissionAmount == 0.0 { + self.commissionRecipientCap = nil + } else { + panic("Commission recipient can not be empty when commission amount is non zero") + } + } + + post { + self.mainVault.balance == self.balanceBeforeTransfer: "DapperUtilityCoin leakage" + } + + execute { + let item <- self.listing.purchase( + payment: <-self.paymentVault, + commissionRecipient: self.commissionRecipientCap + ) + self.nftReceiver.deposit(token: <-item) + } +} diff --git a/purchase-genesispass-v4/testnet.env b/purchase-genesispass-v4/testnet.env new file mode 100644 index 0000000..b1c1f64 --- /dev/null +++ b/purchase-genesispass-v4/testnet.env @@ -0,0 +1,7 @@ +FungibleToken=0x9a0766d93b6608b7 +NonFungibleToken=0x631e88ae7f1d7c20 +DapperUtilityCoin=0x82ec283f88a62e65 +NFTStorefrontV2=0x2d55b98eb200daef +MetadataViews=0x631e88ae7f1d7c20 +NFTContractName=GenesisPassV4 +NFTContractAddress=0x4c55dc21a9da7476