TransactionSEALED
■!~▓*▓▫?*╱▪●○*╳◆○╳@@▫█▪$▓░!╳^█▫╲?█^▪!◇╳^╲#*%^!!█▪▒~╲░!*□!◆#~●~^▪
Transaction ID
Execution Error
Error Code: 1009
error caused by: 1 error occurred:
Raw Error
[Error Code: 1009] error caused by: 1 error occurred: * transaction verification failed: [Error Code: 1006] invalid proposal key: public key 6 on account f380b22ef386ac7e does not have a valid signature: [Error Code: 1009] invalid envelope key: public key 6 on account f380b22ef386ac7e does not have a valid signature: signature is not valid
Transaction Summary
TransactionScript Arguments
0nftIdentifierString
A.2d4c3caffbeab845.FLOAT.NFT
1childAddress
2ids[UInt64]
[ "239693535195987" ]
Cadence Script
1import MetadataViews from 0x1d7e57aa55817448
2import ViewResolver from 0x1d7e57aa55817448
3import NonFungibleToken from 0x1d7e57aa55817448
4import FungibleToken from 0xf233dcee88fe0abe
5import FlowToken from 0x1654653399040a61
6import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
7import ScopedFTProviders from 0x1e4aa0b87d10b141
8import EVM from 0xe467b9dd11fa00df
9import FlowEVMBridgeUtils from 0x1e4aa0b87d10b141
10import FlowEVMBridge from 0x1e4aa0b87d10b141
11import FlowEVMBridgeConfig from 0x1e4aa0b87d10b141
12import HybridCustody from 0xd8a7e05a7ac670c0
13import CapabilityFilter from 0xd8a7e05a7ac670c0
14import CrossVMMetadataViews from 0x1d7e57aa55817448
15
16transaction(nftIdentifier: String, child: Address, ids: [UInt64]) {
17 prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, Capabilities, SaveValue) &Account, payer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {
18 /* --- Reference the signer's CadenceOwnedAccount --- */
19 //
20 // Borrow a reference to the signer's COA
21 let coa = signer.storage.borrow<auth(EVM.Call, EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)
22 ?? panic("Could not borrow COA from provided gateway address")
23
24 let m = signer.storage.borrow<auth(HybridCustody.Manage) &HybridCustody.Manager>(from: HybridCustody.ManagerStoragePath)
25 ?? panic("manager does not exist")
26 let childAcct = m.borrowAccount(addr: child) ?? panic("child account not found")
27
28 // Construct the NFT type from the provided identifier
29 let nftType = CompositeType(nftIdentifier)
30 ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))
31 let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: nftType)
32 ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))
33 let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: nftType)
34 ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))
35
36 /* --- Retrieve the NFT --- */
37 //
38 // Borrow a reference to the NFT collection, configuring if necessary
39 let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)
40 ?? panic("Could not borrow ViewResolver from NFT contract")
41 let collectionData = viewResolver.resolveContractView(
42 resourceType: nil,
43 viewType: Type<MetadataViews.NFTCollectionData>()
44 ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view")
45 var collection = signer.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(
46 from: collectionData.storagePath
47 )
48
49 // Create collection if it doesn't exist
50 if collection == nil {
51 signer.storage.save(<- collectionData.createEmptyCollection(), to: collectionData.storagePath)
52 signer.capabilities.unpublish(collectionData.publicPath)
53 signer.capabilities.publish(
54 signer.capabilities.storage.issue<&{NonFungibleToken.Collection}>(collectionData.storagePath),
55 at: collectionData.publicPath
56 )
57
58 // Borrow authorized withdraw reference to the signer's collection
59 collection = signer.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(
60 from: collectionData.storagePath
61 )!
62 }
63
64 let capType = Type<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>()
65 let controllerID = childAcct.getControllerIDForType(type: capType, forPath: collectionData.storagePath)
66 ?? panic("no controller found for capType")
67
68 let cap = childAcct.getCapability(controllerID: controllerID, type: capType) ?? panic("no cap found")
69 let providerCap = cap as! Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider}>
70 assert(providerCap.check(), message: "invalid provider capability")
71
72 let id = ids[0]
73 // Get a reference to the child's stored vault
74 let collectionRef = providerCap.borrow()!
75 let childNft <- collectionRef.withdraw(withdrawID: id)
76 collection!.deposit(token: <-childNft)
77 // // Withdraw tokens from the signer's stored vault
78 let currentStorageUsage = signer.storage.used
79 let nft <- collection!.withdraw(withdrawID: id)
80 let withdrawnStorageUsage = signer.storage.used
81 let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(
82 bytes: 400_000
83 ) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length))
84
85 /* --- Configure a ScopedFTProvider --- */
86 //
87 // Issue and store bridge-dedicated Provider Capability in storage if necessary
88 if payer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {
89 let providerCap = payer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
90 /storage/flowTokenVault
91 )
92 payer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
93 }
94 // Copy the stored Provider capability and create a ScopedFTProvider
95 let providerCapCopy = payer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
96 from: FlowEVMBridgeConfig.providerCapabilityStoragePath
97 ) ?? panic("Invalid Provider Capability found in storage.")
98 let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)
99 let scopedProvider <- ScopedFTProviders.createScopedFTProvider(
100 provider: providerCapCopy,
101 filters: [ providerFilter ],
102 expiration: getCurrentBlock().timestamp + 1.0
103 )
104
105 // Execute the bridge
106 coa.depositNFT(
107 nft: <- nft,
108 feeProvider: &scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
109 )
110 var idx = 0
111 for nftId in ids {
112 if idx == 0 {
113 idx = idx + 1
114 continue
115 }
116
117 let nft <- collectionRef.withdraw(withdrawID: nftId)
118 coa.depositNFT(
119 nft: <- nft,
120 feeProvider: &scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
121 )
122 idx = idx + 1
123 }
124 // Destroy the ScopedFTProvider
125 destroy scopedProvider
126
127 // Wrap NFTs if applicable
128 wrapAndTransferNFTsIfApplicable(coa,
129 nftIDs: ids,
130 nftType: nftType,
131 viewResolver: viewResolver,
132 recipientIfNotCoa: nil
133 )
134 }
135}
136
137/// Wraps and transfers bridged NFTs into a project's custom ERC721 wrapper contract on EVM, if applicable.
138/// Enables projects to use their own ERC721 contract while leveraging the bridge's underlying contract,
139/// until direct custom contract support is added to the bridge.
140///
141/// @param coa: The COA of the signer
142/// @param nftIDs: The IDs of the NFTs to wrap
143/// @param nftType: The type of the NFTs to wrap
144/// @param viewResolver: The ViewResolver of the NFT contract
145/// @param recipientIfNotCoa: The EVM address to transfer the wrapped NFTs to, nil if the NFTs should stay in signer's COA
146///
147access(all) fun wrapAndTransferNFTsIfApplicable(
148 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
149 nftIDs: [UInt64],
150 nftType: Type,
151 viewResolver: &{ViewResolver},
152 recipientIfNotCoa: EVM.EVMAddress?
153) {
154 // Get the project-defined ERC721 address if it exists
155 if let crossVMPointer = viewResolver.resolveContractView(
156 resourceType: nftType,
157 viewType: Type<CrossVMMetadataViews.EVMPointer>()
158 ) as! CrossVMMetadataViews.EVMPointer? {
159 // Get the underlying ERC721 address if it exists
160 if let underlyingAddress = getUnderlyingERC721Address(coa, crossVMPointer.evmContractAddress) {
161 // Wrap NFTs if underlying ERC721 address matches bridge's associated address for NFT type
162 if underlyingAddress.equals(FlowEVMBridgeConfig.getEVMAddressAssociated(with: nftType)!) {
163 // Approve contract to withdraw underlying NFTs from signer's coa
164 mustCall(coa, underlyingAddress,
165 functionSig: "setApprovalForAll(address,bool)",
166 args: [crossVMPointer.evmContractAddress, true]
167 )
168
169 // Wrap NFTs with provided IDs, and check if the call was successful
170 let res = mustCall(coa, crossVMPointer.evmContractAddress,
171 functionSig: "depositFor(address,uint256[])",
172 args: [coa.address(), nftIDs]
173 )
174 let decodedRes = EVM.decodeABI(types: [Type<Bool>()], data: res.data)
175 assert(decodedRes.length == 1, message: "Invalid response length")
176 assert(decodedRes[0] as! Bool, message: "Failed to wrap NFTs")
177
178 // Transfer NFTs to recipient if provided
179 if let to = recipientIfNotCoa {
180 mustTransferNFTs(coa, crossVMPointer.evmContractAddress, nftIDs: nftIDs, to: to)
181 }
182
183 // Revoke approval for contract to withdraw underlying NFTs from signer's coa
184 mustCall(coa, underlyingAddress,
185 functionSig: "setApprovalForAll(address,bool)",
186 args: [crossVMPointer.evmContractAddress, false]
187 )
188 }
189 }
190 }
191}
192
193/// Gets the underlying ERC721 address if it exists (i.e. if the ERC721 is a wrapper)
194///
195access(all) fun getUnderlyingERC721Address(
196 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
197 _ wrapperAddress: EVM.EVMAddress
198): EVM.EVMAddress? {
199 let res = coa.call(
200 to: wrapperAddress,
201 data: EVM.encodeABIWithSignature("underlying()", []),
202 gasLimit: 100_000,
203 value: EVM.Balance(attoflow: 0)
204 )
205
206 // If the call fails, return nil
207 if res.status != EVM.Status.successful || res.data.length == 0 {
208 return nil
209 }
210
211 // Decode and return the underlying ERC721 address
212 let decodedResult = EVM.decodeABI(
213 types: [Type<EVM.EVMAddress>()],
214 data: res.data
215 )
216 assert(decodedResult.length == 1, message: "Invalid response length")
217 return decodedResult[0] as! EVM.EVMAddress
218}
219
220/// Checks if the provided NFT is owned by the provided EVM address
221///
222access(all) fun isOwner(
223 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
224 _ erc721Address: EVM.EVMAddress,
225 _ nftID: UInt64,
226 _ ownerToCheck: EVM.EVMAddress
227): Bool {
228 let res = coa.call(
229 to: erc721Address,
230 data: EVM.encodeABIWithSignature("ownerOf(uint256)", [nftID]),
231 gasLimit: 100_000,
232 value: EVM.Balance(attoflow: 0)
233 )
234 assert(res.status == EVM.Status.successful, message: "Call to ERC721.ownerOf(uint256) failed")
235 let decodedRes = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: res.data)
236 if decodedRes.length == 1 {
237 let actualOwner = decodedRes[0] as! EVM.EVMAddress
238 return actualOwner.equals(ownerToCheck)
239 }
240 return false
241}
242
243/// Transfers NFTs from the provided COA to the provided EVM address
244///
245access(all) fun mustTransferNFTs(
246 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
247 _ erc721Address: EVM.EVMAddress,
248 nftIDs: [UInt64],
249 to: EVM.EVMAddress
250) {
251 for id in nftIDs {
252 assert(isOwner(coa, erc721Address, id, coa.address()), message: "NFT not owned by signer's COA")
253 mustCall(coa, erc721Address,
254 functionSig: "safeTransferFrom(address,address,uint256)",
255 args: [coa.address(), to, id]
256 )
257 assert(isOwner(coa, erc721Address, id, to), message: "NFT not transferred to recipient")
258 }
259}
260
261/// Calls a function on an EVM contract from provided coa
262///
263access(all) fun mustCall(
264 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
265 _ contractAddr: EVM.EVMAddress,
266 functionSig: String,
267 args: [AnyStruct]
268): EVM.Result {
269 let res = coa.call(
270 to: contractAddr,
271 data: EVM.encodeABIWithSignature(functionSig, args),
272 gasLimit: 4_000_000,
273 value: EVM.Balance(attoflow: 0)
274 )
275
276 assert(res.status == EVM.Status.successful,
277 message: "Failed to call '".concat(functionSig)
278 .concat("\n\t error code: ").concat(res.errorCode.toString())
279 .concat("\n\t error message: ").concat(res.errorMessage)
280 .concat("\n\t gas used: ").concat(res.gasUsed.toString())
281 .concat("\n\t caller address: 0x").concat(coa.address().toString())
282 .concat("\n\t contract address: 0x").concat(contractAddr.toString())
283 )
284
285 return res
286}