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