diff --git a/cadence/contracts/bridge/FlowEVMBridgeNFTEscrow.cdc b/cadence/contracts/bridge/FlowEVMBridgeNFTEscrow.cdc index f8de4b72..4be73e3b 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeNFTEscrow.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeNFTEscrow.cdc @@ -63,19 +63,16 @@ access(all) contract FlowEVMBridgeNFTEscrow { return self.borrowLocker(forType: type)?.getEVMID(from: cadenceID) ?? nil } - /// Resolves the requested view type for the given NFT type if it is locked and supports the requested view type + /// Returns the metadata view types supported by a given NFT if it is in escrow, nil otherwise /// /// @param nftType: Type of the locked NFT - /// @param viewType: Type of the view to resolve /// @param id: ID of the locked NFT /// - /// @returns The resolved view as AnyStruct if the NFT is locked and the view is supported, otherwise returns nil + /// @returns The metadata view types supported by the locked NFT if it is in escrow, nil otherwise /// - access(all) fun resolveLockedNFTView(nftType: Type, id: UInt256, viewType: Type): AnyStruct? { - if let locker = self.borrowLocker(forType: nftType) { - if let cadenceID = locker.getCadenceID(from: id) { - return locker.borrowViewResolver(id: cadenceID)?.resolveView(viewType) ?? nil - } + access(all) view fun getViews(nftType: Type, id: UInt64): [Type]? { + if let nft = self.borrowLockedNFT(type: nftType, id: id) { + return nft.getViews() } return nil } @@ -88,9 +85,9 @@ access(all) contract FlowEVMBridgeNFTEscrow { /// access(account) fun initializeEscrow(forType: Type, name: String, symbol: String, erc721Address: EVM.EVMAddress) { let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: forType) - ?? panic("Problem deriving locker path") + ?? panic("Problem deriving Locker path for NFT type identifier=".concat(forType.identifier)) if self.account.storage.type(at: lockerPath) != nil { - panic("Locker already stored at storage path: ".concat(lockerPath.toString())) + panic("NFT Locker already stored at storage path=".concat(lockerPath.toString())) } let locker <- create Locker(name: name, symbol: symbol, lockedType: forType, erc721Address: erc721Address) @@ -101,7 +98,7 @@ access(all) contract FlowEVMBridgeNFTEscrow { /// access(account) fun lockNFT(_ nft: @{NonFungibleToken.NFT}): UInt64 { let locker = self.borrowLocker(forType: nft.getType()) - ?? panic("Problem deriving locker path") + ?? panic("Problem borrowing reference to Locker for NFT type identifier=".concat(nft.getType().identifier)) let preStorageSnapshot = self.account.storage.used locker.deposit(token: <-nft) @@ -114,7 +111,7 @@ access(all) contract FlowEVMBridgeNFTEscrow { /// access(account) fun unlockNFT(type: Type, id: UInt64): @{NonFungibleToken.NFT} { let locker = self.borrowLocker(forType: type) - ?? panic("Problem deriving locker path") + ?? panic("Problem borrowing reference to Locker for NFT type identifier=".concat(type.identifier)) return <- locker.withdraw(withdrawID: id) } @@ -270,7 +267,9 @@ access(all) contract FlowEVMBridgeNFTEscrow { access(all) fun deposit(token: @{NonFungibleToken.NFT}) { pre { - self.borrowNFT(token.id) == nil: "NFT with this ID already exists in the Locker" + self.borrowNFT(token.id) == nil: + "NFT type=".concat(token.getType().identifier).concat(" with id=").concat(token.id.toString()) + .concat(" already exists in the Locker") } if let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) { self.evmIDToFlowID[evmID] = token.id @@ -284,7 +283,11 @@ access(all) contract FlowEVMBridgeNFTEscrow { access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} { // Should not happen, but prevent potential underflow - assert(self.lockedNFTCount > 0, message: "No NFTs to withdraw") + assert( + self.lockedNFTCount > 0, + message: "Attempting to withdraw NFT id=".concat(withdrawID.toString()) + .concat(" - no NFTs of type=").concat(self.lockedType.identifier).concat(" to withdraw") + ) self.lockedNFTCount = self.lockedNFTCount - 1 let token <- self.ownedNFTs.remove(key: withdrawID)! if let evmID = CrossVMNFT.getEVMID(from: &token as &{NonFungibleToken.NFT}) { diff --git a/cadence/contracts/bridge/FlowEVMBridgeTokenEscrow.cdc b/cadence/contracts/bridge/FlowEVMBridgeTokenEscrow.cdc index a8c9eebf..a407b279 100644 --- a/cadence/contracts/bridge/FlowEVMBridgeTokenEscrow.cdc +++ b/cadence/contracts/bridge/FlowEVMBridgeTokenEscrow.cdc @@ -51,18 +51,6 @@ access(all) contract FlowEVMBridgeTokenEscrow { return self.borrowLocker(forType: tokenType)?.getViews() ?? [] } - /// Resolves the requested view type for the given FT type if it is locked and supports the requested view type - /// - /// @param tokenType: Type of the locked fungible tokens - /// @param viewType: Type of the view to resolve - /// - /// @returns The resolved view as AnyStruct if the vault is locked and the view is supported, otherwise returns nil - /// - access(all) fun resolveLockedTokenView(tokenType: Type, viewType: Type): AnyStruct? { - // The Locker implements Resolver, which has basic resolveView functionality - return self.borrowLocker(forType: tokenType)?.resolveView(viewType) ?? nil - } - /********************** Bridge Methods ***********************/ @@ -77,13 +65,15 @@ access(all) contract FlowEVMBridgeTokenEscrow { evmTokenAddress: EVM.EVMAddress ) { pre { - vault.balance == 0.0: "Can only initialize Escrow with an empty vault" + vault.balance == 0.0: + "Vault contains a balance=".concat(vault.balance.toString()) + .concat(" - can only initialize Escrow with an empty vault") } let lockedType = vault.getType() let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: lockedType) - ?? panic("Problem deriving locker path") + ?? panic("Problem deriving Locker path for Vault type identifier=".concat(lockedType.identifier)) if self.account.storage.type(at: lockerPath) != nil { - panic("Collision at derived Locker path for type: ".concat(lockedType.identifier)) + panic("Token Locker already stored at storage path=".concat(lockedType.identifier)) } // Create the Locker, lock a new vault of given type and save at the derived path @@ -100,7 +90,8 @@ access(all) contract FlowEVMBridgeTokenEscrow { /// Locks the fungible tokens in escrow returning the storage used by locking the Vault /// access(account) fun lockTokens(_ vault: @{FungibleToken.Vault}): UInt64 { - let locker = self.borrowLocker(forType: vault.getType()) ?? panic("Locker doesn't exist for given type") + let locker = self.borrowLocker(forType: vault.getType()) + ?? panic("Locker doesn't exist for given type=".concat(vault.getType().identifier)) let preStorageSnapshot = self.account.storage.used locker.deposit(from: <-vault) @@ -112,7 +103,8 @@ access(all) contract FlowEVMBridgeTokenEscrow { /// Unlocks the tokens of the given type and amount, reverting if it isn't in escrow /// access(account) fun unlockTokens(type: Type, amount: UFix64): @{FungibleToken.Vault} { - let locker = self.borrowLocker(forType: type) ?? panic("Locker doesn't exist for given type") + let locker = self.borrowLocker(forType: type) + ?? panic("Locker doesn't exist for given type=".concat(type.identifier)) return <- locker.withdraw(amount: amount) } @@ -158,7 +150,7 @@ access(all) contract FlowEVMBridgeTokenEscrow { // Locked Vaults must accept their own type as Lockers escrow Vaults on a 1:1 type basis assert( self.lockedVault.isSupportedVaultType(type: self.lockedVault.getType()), - message: "Locked Vault does not accept its own type" + message: "Locked Vault does not accept its own type=".concat(self.lockedVault.getType().identifier) ) } diff --git a/cadence/scripts/escrow/get_locked_token_balance.cdc b/cadence/scripts/escrow/get_locked_token_balance.cdc new file mode 100644 index 00000000..170e1f61 --- /dev/null +++ b/cadence/scripts/escrow/get_locked_token_balance.cdc @@ -0,0 +1,11 @@ +import "FlowEVMBridgeTokenEscrow" + +/// Returns the balance of a given FungibleToken Vault type locked in escrow or nil if a vault of the given type is not +/// locked in escrow +/// +/// @param vaultTypeIdentifier: The type identifier of the FungibleToken Vault +/// +access(all) fun main(vaultTypeIdentifier: String): UFix64? { + let tokenType = CompositeType(vaultTypeIdentifier) ?? panic("Malformed Vault type identifier=".concat(vaultTypeIdentifier)) + return FlowEVMBridgeTokenEscrow.getLockedTokenBalance(tokenType: tokenType) +} diff --git a/cadence/scripts/escrow/get_nft_views.cdc b/cadence/scripts/escrow/get_nft_views.cdc new file mode 100644 index 00000000..5d3a14f6 --- /dev/null +++ b/cadence/scripts/escrow/get_nft_views.cdc @@ -0,0 +1,16 @@ +import "NonFungibleToken" + +import "FlowEVMBridgeNFTEscrow" +import "FlowEVMBridge" + +/// Returns the views supported by an escrowed NFT or nil if the NFT is not locked in escrow +/// +/// @param nftTypeIdentifier: The type identifier of the NFT +/// @param id: The ID of the NFT +/// +/// @return The metadata view types supported by the escrowed NFT or nil if the NFT is not locked in escrow +/// +access(all) fun main(nftTypeIdentifier: String, id: UInt64): [Type]? { + let type = CompositeType(nftTypeIdentifier) ?? panic("Malformed NFT type identifier=".concat(nftTypeIdentifier)) + return FlowEVMBridgeNFTEscrow.getViews(nftType: type, id: id) +} diff --git a/cadence/scripts/escrow/get_vault_views.cdc b/cadence/scripts/escrow/get_vault_views.cdc new file mode 100644 index 00000000..a7e50e9e --- /dev/null +++ b/cadence/scripts/escrow/get_vault_views.cdc @@ -0,0 +1,16 @@ +import "NonFungibleToken" + +import "FlowEVMBridgeTokenEscrow" +import "FlowEVMBridge" + +/// Returns the views supported by an escrowed FungibleToken Vault or nil if there is no Vault of the given type locked +/// in escrow +/// +/// @param vaultTypeIdentifier: The type identifier of the NFT +/// +/// @return The metadata view types supported by the escrowed FT Vault or nil if there is not Vault locked in escrow +/// +access(all) fun main(vaultTypeIdentifier: String, id: UInt64): [Type]? { + let type = CompositeType(vaultTypeIdentifier) ?? panic("Malformed Vault type identifier=".concat(vaultTypeIdentifier)) + return FlowEVMBridgeTokenEscrow.getViews(tokenType: type) +} diff --git a/cadence/scripts/escrow/is_locked.cdc b/cadence/scripts/escrow/is_nft_locked.cdc similarity index 90% rename from cadence/scripts/escrow/is_locked.cdc rename to cadence/scripts/escrow/is_nft_locked.cdc index 6db07dad..b45934f1 100644 --- a/cadence/scripts/escrow/is_locked.cdc +++ b/cadence/scripts/escrow/is_nft_locked.cdc @@ -11,6 +11,6 @@ import "FlowEVMBridge" /// @return true if the NFT is locked in escrow and false otherwise /// access(all) fun main(nftTypeIdentifier: String, id: UInt64): Bool { - let type = CompositeType(nftTypeIdentifier) ?? panic("Malformed type identifier") + let type = CompositeType(nftTypeIdentifier) ?? panic("Malformed NFT type identifier=".concat(nftTypeIdentifier)) return FlowEVMBridgeNFTEscrow.isLocked(type: type, id: id) } diff --git a/cadence/scripts/escrow/resolve_locked_nft_metadata.cdc b/cadence/scripts/escrow/resolve_locked_nft_metadata.cdc index d29677e8..cc7a2bf2 100644 --- a/cadence/scripts/escrow/resolve_locked_nft_metadata.cdc +++ b/cadence/scripts/escrow/resolve_locked_nft_metadata.cdc @@ -2,19 +2,40 @@ import "NonFungibleToken" import "MetadataViews" import "FlowEVMBridgeNFTEscrow" -import "FlowEVMBridge" +import "FlowEVMBridgeUtils" /// Resolves the view for the requested locked NFT or nil if the NFT is not locked +/// NOTE: This functionality is not available via the escrow contract as `resolveView` is not a `view` method, but the +/// escrow contract does provide the necessary functionality to resolve the view from the context of a script /// +/// @param bridgeAddress: The address of the bridge contract (included as the VM bridge address varies across networks) /// @param nftTypeIdentifier: The identifier of the NFT type /// @param id: The ERC721 id of the escrowed NFT /// @param viewIdentifier: The identifier of the view to resolve /// /// @return The resolved view if the NFT is escrowed & the view is resolved by it or nil if the NFT is not locked /// -access(all) fun main(nftTypeIdentifier: String, id: UInt256, viewIdentifier: String): AnyStruct? { - let nftType: Type = CompositeType(nftTypeIdentifier) ?? panic("Malformed nft type identifier") - let view: Type = CompositeType(viewIdentifier) ?? panic("Malformed view type identifier") +access(all) fun main(bridgeAddress: Address, nftTypeIdentifier: String, id: UInt256, viewIdentifier: String): AnyStruct? { + // Construct runtime types from provided identifiers + let nftType: Type = CompositeType(nftTypeIdentifier) ?? panic("Malformed NFT type identifier=".concat(nftTypeIdentifier)) + let view: Type = CompositeType(viewIdentifier) ?? panic("Malformed view type identifier=".concat(viewIdentifier)) - return FlowEVMBridgeNFTEscrow.resolveLockedNFTView(nftType: nftType, id: id, viewType: view) + // Derive the Locker path for the given NFT type + let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: nftType) + ?? panic("Problem deriving Locker path for NFT type identifier=".concat(nftTypeIdentifier)) + + // Borrow the locker from the bridge account's storage + if let locker = getAuthAccount(bridgeAddress).storage.borrow<&FlowEVMBridgeNFTEscrow.Locker>( + from: lockerPath + ) { + // Retrieve the NFT type's cadence ID from the locker + if let cadenceID = locker.getCadenceID(from: id) { + // Resolve the requested view for the given NFT type, returning nil if the view is not supported or the NFT + // is not locked in escrow + return locker.borrowViewResolver(id: cadenceID)?.resolveView(view) + } + } + + // Return nil if no locker was found for the given NFT type + return nil } diff --git a/cadence/scripts/escrow/resolve_locked_vault_metadata.cdc b/cadence/scripts/escrow/resolve_locked_vault_metadata.cdc new file mode 100644 index 00000000..9c4808ea --- /dev/null +++ b/cadence/scripts/escrow/resolve_locked_vault_metadata.cdc @@ -0,0 +1,34 @@ +import "NonFungibleToken" +import "MetadataViews" + +import "FlowEVMBridgeTokenEscrow" +import "FlowEVMBridgeUtils" + +/// Resolves the view for the requested locked Vault or nil if the Vault is not locked in escrow +/// NOTE: This functionality is not available via the escrow contract as `resolveView` is not a `view` method, but the +/// escrow contract does provide the necessary functionality to resolve the view from the context of a script +/// +/// @param bridgeAddress: The address of the bridge contract (included as the VM bridge address varies across networks) +/// @param vaultTypeIdentifier: The identifier of the Vault type +/// @param viewIdentifier: The identifier of the view to resolve +/// +/// @return The resolved view if the Vault is escrowed & the view is resolved by it or nil if the Vault is not locked +/// +access(all) fun main(bridgeAddress: Address, vaultTypeIdentifier: String, viewIdentifier: String): AnyStruct? { + // Construct runtime types from provided identifiers + let vaultType: Type = CompositeType(vaultTypeIdentifier) ?? panic("Malformed vault type identifier=".concat(vaultTypeIdentifier)) + let view: Type = CompositeType(viewIdentifier) ?? panic("Malformed view type identifier=".concat(viewIdentifier)) + + // Derive the Locker path for the given Vault type + let lockerPath = FlowEVMBridgeUtils.deriveEscrowStoragePath(fromType: vaultType) + ?? panic("Problem deriving Locker path for NFT type identifier=".concat(vaultTypeIdentifier)) + + // Borrow the locker from the bridge account's storage & return the requested view if the locker exists + if let locker = getAuthAccount(bridgeAddress).storage.borrow<&FlowEVMBridgeTokenEscrow.Locker>( + from: lockerPath + ) { + return locker.resolveView(view) + } + + return nil +} diff --git a/cadence/tests/flow_evm_bridge_tests.cdc b/cadence/tests/flow_evm_bridge_tests.cdc index 0a7748e2..25c02f24 100644 --- a/cadence/tests/flow_evm_bridge_tests.cdc +++ b/cadence/tests/flow_evm_bridge_tests.cdc @@ -3,6 +3,8 @@ import BlockchainHelpers import "FungibleToken" import "NonFungibleToken" +import "MetadataViews" +import "FungibleTokenMetadataViews" import "ExampleNFT" import "ExampleToken" import "FlowStorageFees" @@ -1053,6 +1055,12 @@ fun testBridgeCadenceNativeNFTToEVMSucceeds() { ) Test.expect(isOwnerResult, Test.beSucceeded()) Test.assertEqual(true, isOwnerResult.returnValue as! Bool? ?? panic("Problem getting owner status")) + + let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID) + Test.assertEqual(true, isNFTLocked) + + let metadata = resolveLockedNFTView(bridgeAddress: bridgeAccount.address, nftTypeIdentifier: exampleNFTIdentifier, id: UInt256(mintedNFTID), viewIdentifier: Type().identifier) + Test.assert(metadata != nil, message: "Expected NFT metadata to be resolved from escrow but none was returned") } access(all) @@ -1091,6 +1099,9 @@ fun testCrossVMTransferCadenceNativeNFTFromEVMSucceeds() { let bobOwnedIDs = getIDs(ownerAddr: bob.address, storagePathIdentifier: "cadenceExampleNFTCollection") Test.assertEqual(1, bobOwnedIDs.length) Test.assertEqual(mintedNFTID, bobOwnedIDs[0]) + + let isNFTLocked = isNFTLocked(nftTypeIdentifier: exampleNFTIdentifier, id: mintedNFTID) + Test.assertEqual(false, isNFTLocked) } access(all) @@ -1260,6 +1271,13 @@ fun testBridgeCadenceNativeTokenToEVMSucceeds() { let expectedEVMBalance = ufix64ToUInt256(exampleTokenMintAmount, decimals: decimals) let evmBalance = balanceOf(evmAddressHex: aliceCOAAddressHex, erc20AddressHex: associatedEVMAddressHex) Test.assertEqual(expectedEVMBalance, evmBalance) + + // Confirm the token is locked + let lockedBalance = getLockedTokenBalance(vaultTypeIdentifier: exampleTokenIdentifier) ?? panic("Problem getting locked balance") + Test.assertEqual(exampleTokenMintAmount, lockedBalance) + + let metadata = resolveLockedTokenView(bridgeAddress: bridgeAccount.address, vaultTypeIdentifier: exampleTokenIdentifier, viewIdentifier: Type().identifier) + Test.assert(metadata != nil, message: "Expected Vault metadata to be resolved from escrow but none was returned") } access(all) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index cd586013..307a1efb 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -351,6 +351,46 @@ fun evmAddressRequiresOnboarding(_ addressHex: String): Bool? { return onboardingRequiredResult.returnValue as! Bool? ?? panic("Problem getting onboarding requirement") } +access(all) +fun isNFTLocked(nftTypeIdentifier: String, id: UInt64): Bool { + let isLockedResult = _executeScript( + "../scripts/escrow/is_nft_locked.cdc", + [nftTypeIdentifier, id] + ) + Test.expect(isLockedResult, Test.beSucceeded()) + return isLockedResult.returnValue as! Bool? ?? panic("Problem getting locked status") +} + +access(all) +fun getLockedTokenBalance(vaultTypeIdentifier: String): UFix64? { + let balanceResult = _executeScript( + "../scripts/escrow/get_locked_token_balance.cdc", + [vaultTypeIdentifier] + ) + Test.expect(balanceResult, Test.beSucceeded()) + return balanceResult.returnValue as! UFix64? +} + +access(all) +fun resolveLockedNFTView(bridgeAddress: Address, nftTypeIdentifier: String, id: UInt256, viewIdentifier: String): AnyStruct? { + let resolvedViewResult = _executeScript( + "../scripts/escrow/resolve_locked_nft_metadata.cdc", + [bridgeAddress, nftTypeIdentifier, id, viewIdentifier] + ) + Test.expect(resolvedViewResult, Test.beSucceeded()) + return resolvedViewResult.returnValue as! AnyStruct? +} + +access(all) +fun resolveLockedTokenView(bridgeAddress: Address, vaultTypeIdentifier: String, viewIdentifier: String): AnyStruct? { + let resolvedViewResult = _executeScript( + "../scripts/escrow/resolve_locked_vault_metadata.cdc", + [bridgeAddress, vaultTypeIdentifier, viewIdentifier] + ) + Test.expect(resolvedViewResult, Test.beSucceeded()) + return resolvedViewResult.returnValue as! AnyStruct? +} + /* --- Transaction Helpers --- */ access(all)