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