diff --git a/cadence/transactions/bridge/nft/batch_bridge_nft_from_evm.cdc b/cadence/transactions/bridge/nft/batch_bridge_nft_from_evm.cdc new file mode 100644 index 00000000..958247b7 --- /dev/null +++ b/cadence/transactions/bridge/nft/batch_bridge_nft_from_evm.cdc @@ -0,0 +1,111 @@ +import "FungibleToken" +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" +import "FlowToken" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// This transaction bridges an NFT from EVM to Cadence assuming it has already been onboarded to the FlowEVMBridge +/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method +/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress) +/// +/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier +/// @param ids: The ERC721 ids of the NFTs to bridge to Cadence from EVM +/// +transaction(nftIdentifier: String, ids: [UInt256]) { + + let nftType: Type + let collection: &{NonFungibleToken.Collection} + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount + + prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) { + /* --- Reference the signer's CadenceOwnedAccount --- */ + // + // Borrow a reference to the signer's COA + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA from provided gateway address") + + /* --- Construct the NFT type --- */ + // + // Construct the NFT type from the provided identifier + self.nftType = CompositeType(nftIdentifier) + ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier)) + // Parse the NFT identifier into its components + let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType) + ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier)) + let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType) + ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier)) + + /* --- Reference the signer's NFT Collection --- */ + // + // Borrow a reference to the NFT collection, configuring if necessary + let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName) + ?? panic("Could not borrow ViewResolver from NFT contract") + let collectionData = viewResolver.resolveContractView( + resourceType: self.nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + if signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) == nil { + signer.storage.save(<-collectionData.createEmptyCollection(), to: collectionData.storagePath) + signer.capabilities.unpublish(collectionData.publicPath) + let collectionCap = signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(collectionData.storagePath) + signer.capabilities.publish(collectionCap, at: collectionData.publicPath) + } + self.collection = signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) + ?? panic("Could not borrow collection from storage path") + + /* --- Configure a ScopedFTProvider --- */ + // + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 200_000 // 200 kB as upper bound on movable storage used in a single transaction + ) + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Iterate over the provided ids + for id in ids { + // Execute the bridge + let nft: @{NonFungibleToken.NFT} <- self.coa.withdrawNFT( + type: self.nftType, + id: id, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + // Ensure the bridged nft is the correct type + assert( + nft.getType() == self.nftType, + message: "Bridged nft type mismatch - requeswted: ".concat(self.nftType.identifier) + .concat(", received: ").concat(nft.getType().identifier) + ) + // Deposit the bridged NFT into the signer's collection + self.collection.deposit(token: <-nft) + } + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +} diff --git a/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_cadence_address.cdc b/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_cadence_address.cdc new file mode 100644 index 00000000..c334d031 --- /dev/null +++ b/cadence/transactions/bridge/nft/batch_bridge_nft_to_any_cadence_address.cdc @@ -0,0 +1,114 @@ +import "FungibleToken" +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" + +import "ScopedFTProviders" + +import "EVM" + +import "FlowEVMBridge" +import "FlowEVMBridgeConfig" +import "FlowEVMBridgeUtils" + +/// This transaction bridges an NFT from EVM to Cadence assuming it has already been onboarded to the FlowEVMBridge. +/// Also know that the recipient Flow account must have a Receiver capable of receiving the this bridged NFT accessible +/// via published Capability at the token's standard path. +/// NOTE: The ERC721 must have first been onboarded to the bridge. This can be checked via the method +/// FlowEVMBridge.evmAddressRequiresOnboarding(address: self.evmContractAddress) +/// +/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier +/// @param ids: The ERC721 ids of the NFTs to bridge to Cadence from EVM +/// @param recipient: The Flow account address to receive the bridged NFT +/// +transaction(nftIdentifier: String, ids: [UInt256], recipient: Address) { + + let nftType: Type + let receiver: &{NonFungibleToken.Receiver} + let scopedProvider: @ScopedFTProviders.ScopedFTProvider + let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount + + prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) { + /* --- Reference the signer's CadenceOwnedAccount --- */ + // + // Borrow a reference to the signer's COA + self.coa = signer.storage.borrow(from: /storage/evm) + ?? panic("Could not borrow COA from provided gateway address") + + /* --- Construct the NFT type --- */ + // + // Construct the NFT type from the provided identifier + self.nftType = CompositeType(nftIdentifier) + ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier)) + // Parse the NFT identifier into its components + let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType) + ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier)) + let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType) + ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier)) + + /* --- Reference the recipient's NFT Receiver --- */ + // + // Borrow a reference to the NFT collection, configuring if necessary + let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName) + ?? panic("Could not borrow ViewResolver from NFT contract") + let collectionData = viewResolver.resolveContractView( + resourceType: self.nftType, + viewType: Type() + ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view") + // Configure the signer's account for this NFT + if signer.storage.borrow<&{NonFungibleToken.Collection}>(from: collectionData.storagePath) == nil { + signer.storage.save(<-collectionData.createEmptyCollection(), to: collectionData.storagePath) + signer.capabilities.unpublish(collectionData.publicPath) + let collectionCap = signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(collectionData.storagePath) + signer.capabilities.publish(collectionCap, at: collectionData.publicPath) + } + self.receiver = getAccount(recipient).capabilities.borrow<&{NonFungibleToken.Receiver}>(collectionData.publicPath) + ?? panic("Could not borrow Receiver from recipient's public capability path") + + /* --- Configure a ScopedFTProvider --- */ + // + // Set a cap on the withdrawable bridge fee + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( + bytes: 200_000 // 200 kB as upper bound on movable storage used in a single transaction + ) + // Issue and store bridge-dedicated Provider Capability in storage if necessary + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { + let providerCap = signer.capabilities.storage.issue( + /storage/flowTokenVault + ) + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) + } + // Copy the stored Provider capability and create a ScopedFTProvider + let providerCapCopy = signer.storage.copy>( + from: FlowEVMBridgeConfig.providerCapabilityStoragePath + ) ?? panic("Invalid Provider Capability found in storage.") + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( + provider: providerCapCopy, + filters: [ providerFilter ], + expiration: getCurrentBlock().timestamp + 1.0 + ) + } + + execute { + // Iterate over the provided ids + for id in ids { + // Execute the bridge + let nft: @{NonFungibleToken.NFT} <- self.coa.withdrawNFT( + type: self.nftType, + id: id, + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} + ) + // Ensure the bridged nft is the correct type + assert( + nft.getType() == self.nftType, + message: "Bridged nft type mismatch - requeswted: ".concat(self.nftType.identifier) + .concat(", received: ").concat(nft.getType().identifier) + ) + // Deposit the bridged NFT into the signer's collection + self.receiver.deposit(token: <-nft) + } + // Destroy the ScopedFTProvider + destroy self.scopedProvider + } +}