TransactionSEALED

○╳◆*@▪^■╳@@%▪●□╳$%╱◆○█○^▫^#^^?#~▒▪?╳╳▪&$▓▫#□▫░▪■$%▓╳█◇!░&■$#╲@%*

Transaction ID

Timestamp

Jan 19, 2026, 07:14:43 AM UTC
1mo ago

Block Height

139,361,035

Computation

0

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

Transaction

Script 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}