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]
[
  "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}