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

Add example transaction transferring child NFTs from parent account #177

Merged
merged 6 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions contracts/standard/ExampleNFT.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<MetadataViews.NFTCollectionDisplay>():
Expand All @@ -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>())
}
}

Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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<MetadataViews.NFTCollectionDisplay>():
Expand Down
10 changes: 5 additions & 5 deletions contracts/standard/ExampleNFT2.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<MetadataViews.NFTCollectionDisplay>():
Expand All @@ -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>())
}
}

Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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<MetadataViews.NFTCollectionDisplay>():
Expand Down
2 changes: 1 addition & 1 deletion scripts/example-nft/setup_full.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ transaction {
let d = ExampleNFT.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())! 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)
Expand Down
2 changes: 1 addition & 1 deletion scripts/example-nft/setup_only_save.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
2 changes: 1 addition & 1 deletion scripts/test/test_get_all_collection_data_from_storage.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 38 additions & 3 deletions test/HybridCustody_tests.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion transactions/example-nft-2/setup_full.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ transaction {
let d = ExampleNFT2.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())! 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)
Expand Down
2 changes: 1 addition & 1 deletion transactions/example-nft/setup_full.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ transaction {
let d = ExampleNFT.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())! 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)
Expand Down
2 changes: 1 addition & 1 deletion transactions/example-nft/setup_only_save.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
52 changes: 43 additions & 9 deletions transactions/hybrid-custody/send_child_ft_with_parent.cdc
Original file line number Diff line number Diff line change
@@ -1,52 +1,86 @@
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.<ADDRESS>.<CONTRACT_NAME>.<OBJECT_NAME> 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.<ADDRESS>.<CONTRACT_NAME>.<OBJECT_NAME> 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
// get the manager resource and borrow childAccount
let m = signer.storage.borrow<auth(HybridCustody.Manage) &HybridCustody.Manager>(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<FungibleTokenMetadataViews.FTVaultData>()) 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<FungibleTokenMetadataViews.FTVaultData>()) as! FungibleTokenMetadataViews.FTVaultData?
?? panic("Could not get the vault data view for vault ".concat(vaultIdentifier))

//get Ft cap from child account
let capType = Type<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>()
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<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>
assert(providerCap.check(), message: "invalid provider capability")

// Get a reference to the child's stored vault
let vaultRef = providerCap.borrow()!

// Withdraw tokens from the signer's stored vault
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
let recipient = getAccount(to)

// 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)
}
}

85 changes: 85 additions & 0 deletions transactions/hybrid-custody/send_child_nfts_with_parent.cdc
Original file line number Diff line number Diff line change
@@ -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.<ADDRESS>.<CONTRACT_NAME>.<OBJECT_NAME> 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.<ADDRESS>.<CONTRACT_NAME>.<OBJECT_NAME> 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<auth(HybridCustody.Manage) &HybridCustody.Manager>(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<MetadataViews.NFTCollectionData>()) as! MetadataViews.NFTCollectionData?
?? panic("Could not get the vault data view for NFT ".concat(nftIdentifier))

// get Provider capability from child account
let capType = Type<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>()
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<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>
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
)
}
}
}
Loading