diff --git a/contracts/standard/ExampleNFT.cdc b/contracts/standard/ExampleNFT.cdc index b80e993..f82b1c1 100644 --- a/contracts/standard/ExampleNFT.cdc +++ b/contracts/standard/ExampleNFT.cdc @@ -14,7 +14,7 @@ import "MetadataViews" import "ViewResolver" import "FungibleToken" -access(all) contract ExampleNFT: ViewResolver { +access(all) contract ExampleNFT: NonFungibleToken { access(all) var totalSupply: UInt64 @@ -99,7 +99,7 @@ access(all) contract ExampleNFT: ViewResolver { publicCollection: Type<&ExampleNFT.Collection>(), publicLinkedType: Type<&ExampleNFT.Collection>(), createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} { - return <-ExampleNFT.createEmptyCollection() + return <-ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()) }) ) case Type(): @@ -124,7 +124,7 @@ access(all) contract ExampleNFT: ViewResolver { } access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { - return <- ExampleNFT.createEmptyCollection() + return <- ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()) } } @@ -215,7 +215,7 @@ access(all) contract ExampleNFT: ViewResolver { } // public function that anyone can call to create a new empty collection - access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { return <- create Collection() } @@ -311,7 +311,7 @@ access(all) contract ExampleNFT: ViewResolver { publicCollection: Type<&ExampleNFT.Collection>(), publicLinkedType: Type<&ExampleNFT.Collection>(), createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} { - return <-ExampleNFT.createEmptyCollection() + return <-ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()) }) ) case Type(): diff --git a/contracts/standard/ExampleNFT2.cdc b/contracts/standard/ExampleNFT2.cdc index cc65ad7..1877ff3 100644 --- a/contracts/standard/ExampleNFT2.cdc +++ b/contracts/standard/ExampleNFT2.cdc @@ -14,7 +14,7 @@ import "MetadataViews" import "ViewResolver" import "FungibleToken" -access(all) contract ExampleNFT2: ViewResolver { +access(all) contract ExampleNFT2: NonFungibleToken { access(all) var totalSupply: UInt64 @@ -99,7 +99,7 @@ access(all) contract ExampleNFT2: ViewResolver { publicCollection: Type<&ExampleNFT2.Collection>(), publicLinkedType: Type<&ExampleNFT2.Collection>(), createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} { - return <-ExampleNFT2.createEmptyCollection() + return <-ExampleNFT2.createEmptyCollection(nftType: Type<@ExampleNFT2.NFT>()) }) ) case Type(): @@ -124,7 +124,7 @@ access(all) contract ExampleNFT2: ViewResolver { } access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { - return <- ExampleNFT2.createEmptyCollection() + return <- ExampleNFT2.createEmptyCollection(nftType: Type<@ExampleNFT2.NFT>()) } } @@ -215,7 +215,7 @@ access(all) contract ExampleNFT2: ViewResolver { } // public function that anyone can call to create a new empty collection - access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { + access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { return <- create Collection() } @@ -311,7 +311,7 @@ access(all) contract ExampleNFT2: ViewResolver { publicCollection: Type<&ExampleNFT2.Collection>(), publicLinkedType: Type<&ExampleNFT2.Collection>(), createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} { - return <-ExampleNFT2.createEmptyCollection() + return <-ExampleNFT2.createEmptyCollection(nftType: Type<@ExampleNFT2.NFT>()) }) ) case Type(): diff --git a/scripts/example-nft/setup_full.cdc b/scripts/example-nft/setup_full.cdc index 89dd0af..0baf143 100644 --- a/scripts/example-nft/setup_full.cdc +++ b/scripts/example-nft/setup_full.cdc @@ -8,7 +8,7 @@ transaction { let d = ExampleNFT.resolveContractView(resourceType: nil, viewType: Type())! as! MetadataViews.NFTCollectionData if acct.storage.borrow<&ExampleNFT.Collection>(from: d.storagePath) == nil { - acct.storage.save(<- ExampleNFT.createEmptyCollection(), to: d.storagePath) + acct.storage.save(<- ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()), to: d.storagePath) } acct.capabilities.unpublish(d.publicPath) diff --git a/scripts/example-nft/setup_only_save.cdc b/scripts/example-nft/setup_only_save.cdc index 98c8e8d..a37b3e9 100644 --- a/scripts/example-nft/setup_only_save.cdc +++ b/scripts/example-nft/setup_only_save.cdc @@ -6,7 +6,7 @@ import "ExampleNFT" transaction { prepare(acct: auth(BorrowValue, SaveValue) &Account) { if acct.storage.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath) == nil { - acct.storage.save(<- ExampleNFT.createEmptyCollection(), to: ExampleNFT.CollectionStoragePath) + acct.storage.save(<- ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()), to: ExampleNFT.CollectionStoragePath) } } } diff --git a/scripts/test/test_get_all_collection_data_from_storage.cdc b/scripts/test/test_get_all_collection_data_from_storage.cdc index 362d80d..f07daa1 100644 --- a/scripts/test/test_get_all_collection_data_from_storage.cdc +++ b/scripts/test/test_get_all_collection_data_from_storage.cdc @@ -35,7 +35,7 @@ access(all) fun getAllViewsFromAddress(_ address: Address): [MetadataViews.NFTCo // Iterate over each public path account.storage.forEachStored(fun (path: StoragePath, type: Type): Bool { // Return if not the type we're looking for - if !type.isInstance(collectionType) && !type.isSubtype(of: collectionType) { + if type.isRecovered || (!type.isInstance(collectionType) && !type.isSubtype(of: collectionType)) { return true } if let collectionRef = account.storage diff --git a/test/HybridCustody_tests.cdc b/test/HybridCustody_tests.cdc index 44a85af..b44d498 100644 --- a/test/HybridCustody_tests.cdc +++ b/test/HybridCustody_tests.cdc @@ -718,7 +718,7 @@ fun testSendChildFtsWithParentSigner() { let parent = Test.createAccount() let child = Test.createAccount() let child2 = Test.createAccount() - let exampleToken = Test.createAccount() + let tmpExampleToken = Test.createAccount() setupChildAndParent_FilterKindAll(child: child, parent: parent) @@ -728,19 +728,54 @@ fun testSendChildFtsWithParentSigner() { setupFT(child) setupFTProvider(child) - txExecutor("example-token/mint_tokens.cdc", [exampleToken], [child.address, mintAmount], nil) + txExecutor("example-token/mint_tokens.cdc", [tmpExampleToken], [child.address, mintAmount], nil) let balance: UFix64? = getBalance(child) Test.assert(balance == mintAmount, message: "balance should be".concat(mintAmount.toString())) let recipientBalanceBefore: UFix64? = getBalance(child2) Test.assert(recipientBalanceBefore == 0.0, message: "recipient balance should be 0") - txExecutor("hybrid-custody/send_child_ft_with_parent.cdc", [parent], [amount, child2.address, child.address], nil) + let vaultIdentifier = buildTypeIdentifier(getTestAccount(exampleToken), exampleToken, "Vault") + + txExecutor("hybrid-custody/send_child_ft_with_parent.cdc", [parent], [vaultIdentifier, amount, child2.address, child.address], nil) let recipientBalanceAfter: UFix64? = getBalance(child2) Test.assert(recipientBalanceAfter == amount, message: "recipient balance should be".concat(amount.toString())) } +access(all) +fun testSendChildNFTsWithParentSigner() { + let parent = Test.createAccount() + let child = Test.createAccount() + let recipient = Test.createAccount() + + setupChildAndParent_FilterKindAll(child: child, parent: parent) + + let mintAmount: UFix64 = 100.0 + let amount: UFix64 = 10.0 + setupNFTCollection(child) + setupNFTCollection(recipient) + + mintNFTDefault(accounts[exampleNFT]!, receiver: child) + mintNFTDefault(accounts[exampleNFT]!, receiver: child) + + let childIDs = (scriptExecutor("example-nft/get_ids.cdc", [child.address]) as! [UInt64]?)! + Test.assert(childIDs.length == 2, message: "no NFTs found in child account after minting") + + let recipientIDs = (scriptExecutor("example-nft/get_ids.cdc", [recipient.address]) as! [UInt64]?)! + Test.assert(recipientIDs.length == 0, message: "NFTs found in recipient account without minting") + + let nftIdentifier = buildTypeIdentifier(getTestAccount(exampleNFT), exampleNFT, "NFT") + + txExecutor("hybrid-custody/send_child_nfts_with_parent.cdc", [parent], [nftIdentifier, childIDs, recipient.address, child.address], nil) + + let childIDsAfter = (scriptExecutor("example-nft/get_ids.cdc", [child.address]) as! [UInt64]?)! + Test.assert(childIDsAfter.length == 0, message: "NFTs found in child account after transfer - collection should be empty") + + let recipientIDsAfter = (scriptExecutor("example-nft/get_ids.cdc", [recipient.address]) as! [UInt64]?)! + Test.assert(recipientIDsAfter.length == 2, message: "recipient should have received 2 NFTs from child") +} + access(all) fun testAddExampleTokenToBalance() { let child = Test.createAccount() diff --git a/transactions/example-nft-2/setup_full.cdc b/transactions/example-nft-2/setup_full.cdc index b72b7a1..8ab7962 100644 --- a/transactions/example-nft-2/setup_full.cdc +++ b/transactions/example-nft-2/setup_full.cdc @@ -8,7 +8,7 @@ transaction { let d = ExampleNFT2.resolveContractView(resourceType: nil, viewType: Type())! as! MetadataViews.NFTCollectionData if acct.storage.borrow<&ExampleNFT2.Collection>(from: d.storagePath) == nil { - acct.storage.save(<- ExampleNFT2.createEmptyCollection(), to: ExampleNFT2.CollectionStoragePath) + acct.storage.save(<- ExampleNFT2.createEmptyCollection(nftType: Type<@ExampleNFT2.NFT>()), to: ExampleNFT2.CollectionStoragePath) } acct.capabilities.unpublish(d.publicPath) diff --git a/transactions/example-nft/setup_full.cdc b/transactions/example-nft/setup_full.cdc index 8218855..a9524c9 100644 --- a/transactions/example-nft/setup_full.cdc +++ b/transactions/example-nft/setup_full.cdc @@ -8,7 +8,7 @@ transaction { let d = ExampleNFT.resolveContractView(resourceType: nil, viewType: Type())! as! MetadataViews.NFTCollectionData if acct.storage.borrow<&ExampleNFT.Collection>(from: d.storagePath) == nil { - acct.storage.save(<- ExampleNFT.createEmptyCollection(), to: ExampleNFT.CollectionStoragePath) + acct.storage.save(<- ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()), to: ExampleNFT.CollectionStoragePath) } acct.capabilities.unpublish(d.publicPath) diff --git a/transactions/example-nft/setup_only_save.cdc b/transactions/example-nft/setup_only_save.cdc index 98c8e8d..a37b3e9 100644 --- a/transactions/example-nft/setup_only_save.cdc +++ b/transactions/example-nft/setup_only_save.cdc @@ -6,7 +6,7 @@ import "ExampleNFT" transaction { prepare(acct: auth(BorrowValue, SaveValue) &Account) { if acct.storage.borrow<&ExampleNFT.Collection>(from: ExampleNFT.CollectionStoragePath) == nil { - acct.storage.save(<- ExampleNFT.createEmptyCollection(), to: ExampleNFT.CollectionStoragePath) + acct.storage.save(<- ExampleNFT.createEmptyCollection(nftType: Type<@ExampleNFT.NFT>()), to: ExampleNFT.CollectionStoragePath) } } } diff --git a/transactions/hybrid-custody/send_child_ft_with_parent.cdc b/transactions/hybrid-custody/send_child_ft_with_parent.cdc index 6a20b6e..0c7c957 100644 --- a/transactions/hybrid-custody/send_child_ft_with_parent.cdc +++ b/transactions/hybrid-custody/send_child_ft_with_parent.cdc @@ -1,14 +1,32 @@ import "FungibleToken" -import "ExampleToken" import "HybridCustody" import "FungibleTokenMetadataViews" -transaction(amount: UFix64, to: Address, child: Address) { +/// Returns the contract address from a type identifier. Type identifiers are in the form of +/// A.
.. where ADDRESS omits the `0x` prefix +/// +access(all) +view fun getContractAddress(from identifier: String): Address? { + let parts = identifier.split(separator: ".") + return parts.length == 4 ? Address.fromString("0x".concat(parts[1])) : nil +} + +/// Returns the contract name from a type identifier. Type identifiers are in the form of +/// A.
.. where ADDRESS omits the `0x` prefix +/// +access(all) +view fun getContractName(from identifier: String): String? { + let parts = identifier.split(separator: ".") + return parts.length == 4 ? parts[2] : nil +} + +transaction(vaultIdentifier: String, amount: UFix64, to: Address, child: Address) { // The Vault resource that holds the tokens that are being transferred let paymentVault: @{FungibleToken.Vault} let vaultData: FungibleTokenMetadataViews.FTVaultData + let vaultType: Type prepare(signer: auth(Storage) &Account) { // signer is the parent account @@ -16,19 +34,30 @@ transaction(amount: UFix64, to: Address, child: Address) { let m = signer.storage.borrow(from: HybridCustody.ManagerStoragePath) ?? panic("manager does not exist") let childAcct = m.borrowAccount(addr: child) ?? panic("child account not found") - - self.vaultData = ExampleToken.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? - ?? panic("Could not get the vault data view for ExampleToken") + + // derive the type and defining contract address & name + self.vaultType = CompositeType(vaultIdentifier) ?? panic("Malformed identifer: ".concat(vaultIdentifier)) + let contractAddress = getContractAddress(from: vaultIdentifier) + ?? panic("Malformed identifer: ".concat(vaultIdentifier)) + let contractName = getContractName(from: vaultIdentifier) + ?? panic("Malformed identifer: ".concat(vaultIdentifier)) + // borrow a reference to the defining contract as a FungibleToken contract reference + let ftContract = getAccount(contractAddress).contracts.borrow<&{FungibleToken}>(name: contractName) + ?? panic("Provided identifier ".concat(vaultIdentifier).concat(" is not defined as a FungibleToken")) + + // gather the default asset storage data + self.vaultData = ftContract.resolveContractView(resourceType: nil, viewType: Type()) as! FungibleTokenMetadataViews.FTVaultData? + ?? panic("Could not get the vault data view for vault ".concat(vaultIdentifier)) //get Ft cap from child account let capType = Type() let controllerID = childAcct.getControllerIDForType(type: capType, forPath: self.vaultData.storagePath) ?? panic("no controller found for capType") - + let cap = childAcct.getCapability(controllerID: controllerID, type: capType) ?? panic("no cap found") let providerCap = cap as! Capability assert(providerCap.check(), message: "invalid provider capability") - + // Get a reference to the child's stored vault let vaultRef = providerCap.borrow()! @@ -36,6 +65,12 @@ transaction(amount: UFix64, to: Address, child: Address) { self.paymentVault <- vaultRef.withdraw(amount: amount) } + pre { + self.paymentVault.getType() == self.vaultType: + "Expected vault type ".concat(vaultIdentifier) + .concat(" but got ").concat(self.paymentVault.getType().identifier) + } + execute { // Get the recipient's public account object @@ -43,10 +78,9 @@ transaction(amount: UFix64, to: Address, child: Address) { // Get a reference to the recipient's Receiver let receiverRef = recipient.capabilities.get<&{FungibleToken.Receiver}>(self.vaultData.receiverPath)!.borrow() - ?? panic("Could not borrow receiver reference to the recipient's Vault") + ?? panic("Could not borrow receiver reference to the recipient's Vault") // Deposit the withdrawn tokens in the recipient's receiver receiverRef.deposit(from: <-self.paymentVault) } } - \ No newline at end of file diff --git a/transactions/hybrid-custody/send_child_nfts_with_parent.cdc b/transactions/hybrid-custody/send_child_nfts_with_parent.cdc new file mode 100644 index 0000000..8fa54ff --- /dev/null +++ b/transactions/hybrid-custody/send_child_nfts_with_parent.cdc @@ -0,0 +1,85 @@ +import "NonFungibleToken" +import "MetadataViews" + +import "HybridCustody" + +/// Returns the contract address from a type identifier. Type identifiers are in the form of +/// A.
.. where ADDRESS omits the `0x` prefix +/// +access(all) +view fun getContractAddress(from identifier: String): Address? { + let parts = identifier.split(separator: ".") + return parts.length == 4 ? Address.fromString("0x".concat(parts[1])) : nil +} + +/// Returns the contract name from a type identifier. Type identifiers are in the form of +/// A.
.. where ADDRESS omits the `0x` prefix +/// +access(all) +view fun getContractName(from identifier: String): String? { + let parts = identifier.split(separator: ".") + return parts.length == 4 ? parts[2] : nil +} + +transaction(nftIdentifier: String, ids: [UInt64], to: Address, child: Address) { + + // reference to the child account's Collection Provider that holds the NFT being transferred + let provider: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider} + let collectionData: MetadataViews.NFTCollectionData + let nftType: Type + + // signer is the parent account + prepare(signer: auth(Storage) &Account) { + // get the manager resource and borrow childAccount + let m = signer.storage.borrow(from: HybridCustody.ManagerStoragePath) + ?? panic("manager does not exist") + let childAcct = m.borrowAccount(addr: child) ?? panic("child account not found") + + // derive the type and defining contract address & name + self.nftType = CompositeType(nftIdentifier) ?? panic("Malformed identifer: ".concat(nftIdentifier)) + let contractAddress = getContractAddress(from: nftIdentifier) + ?? panic("Malformed identifer: ".concat(nftIdentifier)) + let contractName = getContractName(from: nftIdentifier) + ?? panic("Malformed identifer: ".concat(nftIdentifier)) + // borrow a reference to the defining contract as a FungibleToken contract reference + let nftContract = getAccount(contractAddress).contracts.borrow<&{NonFungibleToken}>(name: contractName) + ?? panic("Provided identifier ".concat(nftIdentifier).concat(" is not defined as a NonFungibleToken")) + + // gather the default asset storage data + self.collectionData = nftContract.resolveContractView(resourceType: nil, viewType: Type()) as! MetadataViews.NFTCollectionData? + ?? panic("Could not get the vault data view for NFT ".concat(nftIdentifier)) + + // get Provider capability from child account + let capType = Type() + let controllerID = childAcct.getControllerIDForType(type: capType, forPath: self.collectionData.storagePath) + ?? panic("no controller found for capType") + + let cap = childAcct.getCapability(controllerID: controllerID, type: capType) ?? panic("no cap found") + let providerCap = cap as! Capability + assert(providerCap.check(), message: "invalid provider capability") + + // get a reference to the child's stored NFT Collection Provider + self.provider = providerCap.borrow()! + } + + execute { + // get the recipient's public account object + let recipient = getAccount(to) + + // get a reference to the recipient's NFT Receiver + let receiverRef = recipient.capabilities.borrow<&{NonFungibleToken.Receiver}>(self.collectionData.publicPath) + ?? panic("Could not borrow receiver reference to the recipient's Vault") + + for id in ids { + // withdraw the NFT from the child account's collection & deposit to the recipient's Receiver + let nft <- self.provider.withdraw(withdrawID: id) + assert( + nft.getType() == self.nftType, + message: "Expected NFT ".concat(nftIdentifier).concat(" got NFT ".concat(nft.getType().identifier)) + ) + receiverRef.deposit( + token: <-nft + ) + } + } +}