|
| 1 | +import "FungibleToken" |
| 2 | +import "NonFungibleToken" |
| 3 | +import "ViewResolver" |
| 4 | +import "MetadataViews" |
| 5 | +import "FlowToken" |
| 6 | + |
| 7 | +import "ScopedFTProviders" |
| 8 | + |
| 9 | +import "EVM" |
| 10 | + |
| 11 | +import "FlowEVMBridge" |
| 12 | +import "FlowEVMBridgeConfig" |
| 13 | +import "FlowEVMBridgeUtils" |
| 14 | + |
| 15 | +/// Bridges NFTs (from the same collection) from the signer's collection in Cadence to the signer's COA in FlowEVM |
| 16 | +/// and then performs an arbitrary number of calls afterwards to potentially do things |
| 17 | +/// with the bridged NFTs |
| 18 | +/// |
| 19 | +/// NOTE: This transaction also onboards the NFT to the bridge if necessary which may incur additional fees |
| 20 | +/// than bridging an asset that has already been onboarded. |
| 21 | +/// |
| 22 | +/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier |
| 23 | +/// @param ids: The Cadence NFT.id of the NFTs to bridge to EVM |
| 24 | +/// @params evmContractAddressHexes, calldatas, gasLimits, values: Arrays of calldata |
| 25 | +/// to be included in transaction calls to Flow EVM from the signer's COA. |
| 26 | +/// The arrays are all expected to be of the same length |
| 27 | +/// |
| 28 | +transaction( |
| 29 | + nftIdentifier: String, |
| 30 | + ids: [UInt64], |
| 31 | + evmContractAddressHexes: [String], |
| 32 | + calldatas: [String], |
| 33 | + gasLimits: [UInt64], |
| 34 | + values: [UFix64] |
| 35 | +) { |
| 36 | + |
| 37 | + let nftType: Type |
| 38 | + let collection: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection} |
| 39 | + let coa: auth(EVM.Bridge, EVM.Call) &EVM.CadenceOwnedAccount |
| 40 | + let requiresOnboarding: Bool |
| 41 | + let scopedProvider: @ScopedFTProviders.ScopedFTProvider |
| 42 | + |
| 43 | + prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) { |
| 44 | + pre { |
| 45 | + (evmContractAddressHexes.length == calldatas.length) |
| 46 | + && (calldatas.length == gasLimits.length) |
| 47 | + && (gasLimits.length == values.length): |
| 48 | + "Calldata array lengths must all be the same!" |
| 49 | + } |
| 50 | + |
| 51 | + /* --- Reference the signer's CadenceOwnedAccount --- */ |
| 52 | + // |
| 53 | + // Borrow a reference to the signer's COA |
| 54 | + self.coa = signer.storage.borrow<auth(EVM.Bridge, EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm) |
| 55 | + ?? panic("Could not borrow COA signer's account at path /storage/evm") |
| 56 | + |
| 57 | + /* --- Construct the NFT type --- */ |
| 58 | + // |
| 59 | + // Construct the NFT type from the provided identifier |
| 60 | + self.nftType = CompositeType(nftIdentifier) |
| 61 | + ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier)) |
| 62 | + // Parse the NFT identifier into its components |
| 63 | + let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType) |
| 64 | + ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier)) |
| 65 | + let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType) |
| 66 | + ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier)) |
| 67 | + |
| 68 | + /* --- Retrieve the NFT --- */ |
| 69 | + // |
| 70 | + // Borrow a reference to the NFT collection, configuring if necessary |
| 71 | + let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName) |
| 72 | + ?? panic("Could not borrow ViewResolver from NFT contract with name " |
| 73 | + .concat(nftContractName).concat(" and address ") |
| 74 | + .concat(nftContractAddress.toString())) |
| 75 | + let collectionData = viewResolver.resolveContractView( |
| 76 | + resourceType: self.nftType, |
| 77 | + viewType: Type<MetadataViews.NFTCollectionData>() |
| 78 | + ) as! MetadataViews.NFTCollectionData? |
| 79 | + ?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier)) |
| 80 | + self.collection = signer.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>( |
| 81 | + from: collectionData.storagePath |
| 82 | + ) ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path " |
| 83 | + .concat(collectionData.storagePath.toString())) |
| 84 | + |
| 85 | + // Withdraw the requested NFT & set a cap on the withdrawable bridge fee |
| 86 | + var approxFee = FlowEVMBridgeUtils.calculateBridgeFee( |
| 87 | + bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction |
| 88 | + ) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length)) |
| 89 | + // Determine if the NFT requires onboarding - this impacts the fee required |
| 90 | + self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.nftType) |
| 91 | + ?? panic("Bridge does not support the requested asset type ".concat(nftIdentifier)) |
| 92 | + // Add the onboarding fee if onboarding is necessary |
| 93 | + if self.requiresOnboarding { |
| 94 | + approxFee = approxFee + FlowEVMBridgeConfig.onboardFee |
| 95 | + } |
| 96 | + |
| 97 | + /* --- Configure a ScopedFTProvider --- */ |
| 98 | + // |
| 99 | + // Issue and store bridge-dedicated Provider Capability in storage if necessary |
| 100 | + if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil { |
| 101 | + let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>( |
| 102 | + /storage/flowTokenVault |
| 103 | + ) |
| 104 | + signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath) |
| 105 | + } |
| 106 | + // Copy the stored Provider capability and create a ScopedFTProvider |
| 107 | + let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>( |
| 108 | + from: FlowEVMBridgeConfig.providerCapabilityStoragePath |
| 109 | + ) ?? panic("Invalid FungibleToken Provider Capability found in storage at path " |
| 110 | + .concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString())) |
| 111 | + let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee) |
| 112 | + self.scopedProvider <- ScopedFTProviders.createScopedFTProvider( |
| 113 | + provider: providerCapCopy, |
| 114 | + filters: [ providerFilter ], |
| 115 | + expiration: getCurrentBlock().timestamp + 1.0 |
| 116 | + ) |
| 117 | + } |
| 118 | + |
| 119 | + execute { |
| 120 | + |
| 121 | + if self.requiresOnboarding { |
| 122 | + // Onboard the NFT to the bridge |
| 123 | + FlowEVMBridge.onboardByType( |
| 124 | + self.nftType, |
| 125 | + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} |
| 126 | + ) |
| 127 | + } |
| 128 | + |
| 129 | + // Iterate over requested IDs and bridge each NFT to the signer's COA in EVM |
| 130 | + for id in ids { |
| 131 | + // Withdraw the NFT & ensure it's the correct type |
| 132 | + let nft <-self.collection.withdraw(withdrawID: id) |
| 133 | + assert( |
| 134 | + nft.getType() == self.nftType, |
| 135 | + message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier) |
| 136 | + .concat(", received: ").concat(nft.getType().identifier) |
| 137 | + ) |
| 138 | + // Execute the bridge to EVM for the current ID |
| 139 | + self.coa.depositNFT( |
| 140 | + nft: <-nft, |
| 141 | + feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider} |
| 142 | + ) |
| 143 | + } |
| 144 | + |
| 145 | + // Destroy the ScopedFTProvider |
| 146 | + destroy self.scopedProvider |
| 147 | + |
| 148 | + // Perform all the calls |
| 149 | + for index, evmAddressHex in evmContractAddressHexes { |
| 150 | + let evmAddress = EVM.addressFromString(evmAddressHex) |
| 151 | + |
| 152 | + let valueBalance = EVM.Balance(attoflow: 0) |
| 153 | + valueBalance.setFLOW(flow: values[index]) |
| 154 | + let callResult = self.coa.call( |
| 155 | + to: evmAddress, |
| 156 | + data: calldatas[index].decodeHex(), |
| 157 | + gasLimit: gasLimits[index], |
| 158 | + value: valueBalance |
| 159 | + ) |
| 160 | + assert( |
| 161 | + callResult.status == EVM.Status.successful, |
| 162 | + message: "Call failed with address \(evmAddressHex) and calldata \(calldatas[index]) with error \(callResult.errorMessage)" |
| 163 | + ) |
| 164 | + } |
| 165 | + } |
| 166 | +} |
0 commit comments