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