From 12760b4ad00f4ab95992768671e6f6fd0da3b80b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:30:31 -0600 Subject: [PATCH] add HybridCustody setup & cross-account listing txns --- .../sell_item_in_child_from_parent.cdc | 118 ++++++++++++++++++ .../setup_nft_filter_and_factory_manager.cdc | 99 +++++++++++++++ .../setup/linking/redeem_account.cdc | 33 +++++ ...up_owned_account_and_publish_to_parent.cdc | 61 +++++++++ 4 files changed, 311 insertions(+) create mode 100644 transactions/hybrid-custody/sell_item_in_child_from_parent.cdc create mode 100644 transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc create mode 100644 transactions/hybrid-custody/setup/linking/redeem_account.cdc create mode 100644 transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc diff --git a/transactions/hybrid-custody/sell_item_in_child_from_parent.cdc b/transactions/hybrid-custody/sell_item_in_child_from_parent.cdc new file mode 100644 index 0000000..38e88eb --- /dev/null +++ b/transactions/hybrid-custody/sell_item_in_child_from_parent.cdc @@ -0,0 +1,118 @@ +import "NonFungibleToken" +import "MetadataViews" +import "FungibleToken" +import "FlowToken" + +import "HybridCustody" + +import "NFTStorefrontV2" + +/// Cross-account NFT listing transaction +/// +/// Lists an NFT located in the signer's child account for sale in the storefront of the signing parent account with +/// the parent account as beneficiary of the sale. +/// +transaction( + childAddress: Address, + collectionProviderPath: PrivatePath, + collectionPublicPath: PublicPath, + nftTypeIdentifier: String, + saleItemID: UInt64, + saleItemPrice: UFix64, + customID: String?, + commissionAmount: UFix64, + expiry: UInt64, + marketplacesAddress: [Address] +) { + let flowReceiverCap: Capability<&{FungibleToken.Receiver}> + let providerCap: Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}> + let storefront: &NFTStorefrontV2.Storefront + var saleCuts: [NFTStorefrontV2.SaleCut] + var marketplaceCaps: [Capability<&{FungibleToken.Receiver}>] + let nftType: Type + + prepare(acct: AuthAccount) { + self.saleCuts = [] + self.marketplaceCaps = [] + self.nftType = CompositeType(nftTypeIdentifier) ?? panic("Invalid NFT Type Identifier provided") + + // Configure Storefront if one doesn't yet exist + if acct.borrow<&NFTStorefrontV2.Storefront>(from: NFTStorefrontV2.StorefrontStoragePath) == nil { + acct.save(<-NFTStorefrontV2.createStorefront(), to: NFTStorefrontV2.StorefrontStoragePath) + acct.link<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>( + NFTStorefrontV2.StorefrontPublicPath, + target: NFTStorefrontV2.StorefrontStoragePath + ) + } + // Borrow a reference to the signer's Storefront + self.storefront = acct.borrow<&NFTStorefrontV2.Storefront>(from: NFTStorefrontV2.StorefrontStoragePath) + ?? panic("Missing or mis-typed NFTStorefront Storefront") + + // Get a FlowToken Receiver as beneficiary of listing & validate + self.flowReceiverCap = acct.getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + assert(self.flowReceiverCap.check(), message: "Missing or mis-typed FlowToken receiver") + + // Get reference to the child account + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("Could not borrow reference to HybridCustody Manager") + let childAccount = manager.borrowAccount(addr: childAddress) + ?? panic("No child account exists for the given address") + + // Get the NFT provider capability from the child account & validate + self.providerCap = childAccount.getCapability( + path: collectionProviderPath, + type: Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>() + ) as! Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>? + ?? panic("NFT Provider Capability is not accessible from child account for specified path") + assert(self.providerCap.check(), message: "Missing or mis-typed Provider Capability") + + // Borrow the NFT as ViewResolver to get Royalties information + let collection = getAccount(childAddress).getCapability<&{MetadataViews.ResolverCollection}>( + collectionPublicPath + ).borrow() + ?? panic("Could not borrow a reference to the child account's collection") + var totalRoyaltyCut = 0.0 + let effectiveSaleItemPrice = saleItemPrice - commissionAmount + let resolver = collection.borrowViewResolver(id: saleItemID) + assert(resolver.getType() == self.nftType, message: "NFT Type mismatch") + + // Check whether the NFT implements the MetadataResolver or not. + if resolver.getViews().contains(Type()) { + let royaltiesRef = resolver.resolveView(Type())?? panic("Unable to retrieve the royalties") + let royalties = (royaltiesRef as! MetadataViews.Royalties).getRoyalties() + for royalty in royalties { + self.saleCuts.append( + NFTStorefrontV2.SaleCut(receiver: royalty.receiver, amount: royalty.cut * effectiveSaleItemPrice) + ) + totalRoyaltyCut = totalRoyaltyCut + royalty.cut * effectiveSaleItemPrice + } + } + self.saleCuts.append(NFTStorefrontV2.SaleCut( + receiver: self.flowReceiverCap, + amount: effectiveSaleItemPrice - totalRoyaltyCut + )) + + for marketplace in marketplacesAddress { + // Here we are making a fair assumption that all given addresses would have + // the capability to receive the `FlowToken` + self.marketplaceCaps.append( + getAccount(marketplace).getCapability<&{FungibleToken.Receiver}>(/public/flowTokenReceiver) + ) + } + } + + execute { + // Create listing + self.storefront.createListing( + nftProviderCapability: self.providerCap, + nftType: self.nftType, + nftID: saleItemID, + salePaymentVaultType: Type<@FlowToken.Vault>(), + saleCuts: self.saleCuts, + marketplacesCapability: self.marketplaceCaps.length == 0 ? nil : self.marketplaceCaps, + customID: customID, + commissionAmount: commissionAmount, + expiry: expiry + ) + } +} diff --git a/transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc b/transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc new file mode 100644 index 0000000..8510adf --- /dev/null +++ b/transactions/hybrid-custody/setup/dev-setup/setup_nft_filter_and_factory_manager.cdc @@ -0,0 +1,99 @@ +import "CapabilityFilter" +import "CapabilityFactory" +import "NFTCollectionPublicFactory" +import "NFTProviderAndCollectionFactory" +import "NFTProviderFactory" +import "FTProviderFactory" + +import "NonFungibleToken" +import "FungibleToken" + +/* --- Helper Methods --- */ +// +/// Returns a type identifier for an NFT Collection +/// +access(all) fun deriveCollectionTypeIdentifier(_ contractAddress: Address, _ contractName: String): String { + return "A.".concat(withoutPrefix(contractAddress.toString())).concat(".").concat(contractName).concat(".Collection") +} + +/// Taken from AddressUtils private method +/// +access(all) fun withoutPrefix(_ input: String): String{ + var address=input + + //get rid of 0x + if address.length>1 && address.utf8[1] == 120 { + address = address.slice(from: 2, upTo: address.length) + } + + //ensure even length + if address.length%2==1{ + address="0".concat(address) + } + return address +} + +/* --- Transaction Block --- */ +// +/// This transaction can be used by most developers implementing HybridCustody as the single pre-requisite transaction +/// to setup filter functionality between linked parent and child accounts. +/// +/// Creates a CapabilityFactory Manager and CapabilityFilter.AllowlistFilter in the signing account (if needed), adding +/// NFTCollectionPublicFactory, NFTProviderAndCollectionFactory, & NFTProviderFactory to the CapabilityFactory Manager +/// and the Collection Type to the CapabilityFilter.AllowlistFilter +/// +/// For more info, see docs at https://developers.onflow.org/docs/hybrid-custody/ +//// +transaction(nftContractAddress: Address, nftContractName: String) { + prepare(acct: AuthAccount) { + + /* --- CapabilityFactory Manager configuration --- */ + // + if acct.borrow<&AnyResource>(from: CapabilityFactory.StoragePath) == nil { + let f <- CapabilityFactory.createFactoryManager() + acct.save(<-f, to: CapabilityFactory.StoragePath) + } + + if !acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath).check() { + acct.unlink(CapabilityFactory.PublicPath) + acct.link<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath, target: CapabilityFactory.StoragePath) + } + + assert( + acct.getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath).check(), + message: "CapabilityFactory is not setup properly" + ) + + let factoryManager = acct.borrow<&CapabilityFactory.Manager>(from: CapabilityFactory.StoragePath) + ?? panic("CapabilityFactory Manager not found") + + // Add NFT-related Factories to the Manager + factoryManager.updateFactory(Type<&{NonFungibleToken.CollectionPublic}>(), NFTCollectionPublicFactory.Factory()) + factoryManager.updateFactory(Type<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>(), NFTProviderAndCollectionFactory.Factory()) + factoryManager.updateFactory(Type<&{NonFungibleToken.Provider}>(), NFTProviderFactory.Factory()) + + /* --- AllowlistFilter configuration --- */ + // + if acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath) == nil { + acct.save(<-CapabilityFilter.create(Type<@CapabilityFilter.AllowlistFilter>()), to: CapabilityFilter.StoragePath) + } + + if !acct.getCapability<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath).check() { + acct.unlink(CapabilityFilter.PublicPath) + acct.link<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath, target: CapabilityFilter.StoragePath) + } + + assert( + acct.getCapability<&CapabilityFilter.AllowlistFilter{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath).check(), + message: "AllowlistFilter is not setup properly" + ) + + let filter = acct.borrow<&CapabilityFilter.AllowlistFilter>(from: CapabilityFilter.StoragePath) + ?? panic("AllowlistFilter does not exist") + + // Construct an NFT Collection Type from the provided args & add to the AllowlistFilter + let c = CompositeType(deriveCollectionTypeIdentifier(nftContractAddress, nftContractName)) + ?? panic("Problem constructing CompositeType from given NFT contract address and name") + filter.addType(c) + } +} diff --git a/transactions/hybrid-custody/setup/linking/redeem_account.cdc b/transactions/hybrid-custody/setup/linking/redeem_account.cdc new file mode 100644 index 0000000..70016db --- /dev/null +++ b/transactions/hybrid-custody/setup/linking/redeem_account.cdc @@ -0,0 +1,33 @@ +import "MetadataViews" + +import "HybridCustody" +import "CapabilityFilter" + +transaction(childAddress: Address, filterAddress: Address?, filterPath: PublicPath?) { + prepare(acct: AuthAccount) { + var filter: Capability<&{CapabilityFilter.Filter}>? = nil + if filterAddress != nil && filterPath != nil { + filter = getAccount(filterAddress!).getCapability<&{CapabilityFilter.Filter}>(filterPath!) + } + + if acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) == nil { + let m <- HybridCustody.createManager(filter: filter) + acct.save(<- m, to: HybridCustody.ManagerStoragePath) + + acct.unlink(HybridCustody.ManagerPublicPath) + acct.unlink(HybridCustody.ManagerPrivatePath) + + acct.link<&HybridCustody.Manager{HybridCustody.ManagerPrivate, HybridCustody.ManagerPublic}>(HybridCustody.ManagerPrivatePath, target: HybridCustody.ManagerStoragePath) + acct.link<&HybridCustody.Manager{HybridCustody.ManagerPublic}>(HybridCustody.ManagerPublicPath, target: HybridCustody.ManagerStoragePath) + } + + let inboxName = HybridCustody.getChildAccountIdentifier(acct.address) + let cap = acct.inbox.claim<&HybridCustody.ChildAccount{HybridCustody.AccountPrivate, HybridCustody.AccountPublic, MetadataViews.Resolver}>(inboxName, provider: childAddress) + ?? panic("child account cap not found") + + let manager = acct.borrow<&HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath) + ?? panic("manager no found") + + manager.addAccount(cap: cap) + } +} \ No newline at end of file diff --git a/transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc b/transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc new file mode 100644 index 0000000..2349962 --- /dev/null +++ b/transactions/hybrid-custody/setup/linking/setup_owned_account_and_publish_to_parent.cdc @@ -0,0 +1,61 @@ +#allowAccountLinking + +import "MetadataViews" + +import "HybridCustody" +import "CapabilityFactory" +import "CapabilityFilter" +import "CapabilityDelegator" + +/// This transaction configures an OwnedAccount in the signer if needed, and proceeds to create a ChildAccount +/// using CapabilityFactory.Manager and CapabilityFilter.Filter Capabilities from the given addresses. A +/// Capability on the ChildAccount is then published to the specified parent account. +/// +transaction( + parent: Address, + factoryAddress: Address, + filterAddress: Address, + name: String?, + desc: String?, + thumbnailURL: String? + ) { + + prepare(acct: AuthAccount) { + // Configure OwnedAccount if it doesn't exist + if acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) == nil { + var acctCap = acct.getCapability<&AuthAccount>(HybridCustody.LinkedAccountPrivatePath) + if !acctCap.check() { + acctCap = acct.linkAccount(HybridCustody.LinkedAccountPrivatePath)! + } + let ownedAccount <- HybridCustody.createOwnedAccount(acct: acctCap) + acct.save(<-ownedAccount, to: HybridCustody.OwnedAccountStoragePath) + } + + // check that paths are all configured properly + acct.unlink(HybridCustody.OwnedAccountPrivatePath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.BorrowableAccount, HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPrivatePath, target: HybridCustody.OwnedAccountStoragePath) + + acct.unlink(HybridCustody.OwnedAccountPublicPath) + acct.link<&HybridCustody.OwnedAccount{HybridCustody.OwnedAccountPublic, MetadataViews.Resolver}>(HybridCustody.OwnedAccountPublicPath, target: HybridCustody.OwnedAccountStoragePath) + + let owned = acct.borrow<&HybridCustody.OwnedAccount>(from: HybridCustody.OwnedAccountStoragePath) + ?? panic("owned account not found") + + // Set the display metadata for the OwnedAccount + if name != nil && desc != nil && thumbnailURL != nil { + let thumbnail = MetadataViews.HTTPFile(url: thumbnailURL!) + let display = MetadataViews.Display(name: name!, description: desc!, thumbnail: thumbnail!) + owned.setDisplay(display) + } + + // Get CapabilityFactory & CapabilityFilter Capabilities + let factory = getAccount(factoryAddress).getCapability<&CapabilityFactory.Manager{CapabilityFactory.Getter}>(CapabilityFactory.PublicPath) + assert(factory.check(), message: "factory address is not configured properly") + + let filter = getAccount(filterAddress).getCapability<&{CapabilityFilter.Filter}>(CapabilityFilter.PublicPath) + assert(filter.check(), message: "capability filter is not configured properly") + + // Finally publish a ChildAccount capability on the signing account to the specified parent + owned.publishToParent(parentAddress: parent, factory: factory, filter: filter) + } +} \ No newline at end of file