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.f1ab99c82dee3526.FlowtyDrops.NFT
1childAddress
2ids[UInt256]
[ "11111", "22222" ]
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: [UInt256]) {
17
18 prepare(signer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account, payer: auth(BorrowValue, CopyValue, IssueStorageCapabilityController, PublishCapability, SaveValue, UnpublishCapability) &Account) {
19 /* --- Reference the signer's CadenceOwnedAccount --- */
20 //
21 // Borrow a reference to the signer's COA
22 let coa = signer.storage.borrow<auth(EVM.Call, EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)
23 ?? panic("Could not borrow COA from provided gateway address")
24
25 // Construct the NFT type from the provided identifier
26 let nftType = CompositeType(nftIdentifier)
27 ?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))
28 let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: nftType)
29 ?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))
30 let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: nftType)
31 ?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))
32
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 /* --- Retrieve the NFT --- */
38 //
39 // Borrow a reference to the NFT collection, configuring if necessary
40 let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)
41 ?? panic("Could not borrow ViewResolver from NFT contract")
42 let collectionData = viewResolver.resolveContractView(
43 resourceType: nil,
44 viewType: Type<MetadataViews.NFTCollectionData>()
45 ) as! MetadataViews.NFTCollectionData? ?? panic("Could not resolve NFTCollectionData view")
46
47 let capType = Type<&{NonFungibleToken.CollectionPublic}>()
48 let controllerID = childAcct.getControllerIDForType(type: capType, forPath: collectionData.storagePath)
49 ?? panic("no controller found for capType")
50
51 let cap = childAcct.getCapability(controllerID: controllerID, type: capType) ?? panic("no cap found")
52 let publicCap = cap as! Capability<&{NonFungibleToken.CollectionPublic}>
53 assert(publicCap.check(), message: "invalid public capability")
54
55 // Get a reference to the child's stored vault
56 let collectionRef = publicCap.borrow()!
57
58 // Calculate the approximate fee for the bridge
59 let approxFee = FlowEVMBridgeUtils.calculateBridgeFee(bytes: 400_000) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length))
60
61 /* --- Configure a ScopedFTProvider --- */
62 //
63 // Issue and store bridge-dedicated Provider Capability in storage if necessary
64 if payer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {
65 let providerCap = payer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
66 /storage/flowTokenVault
67 )
68 payer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
69 }
70 // Copy the stored Provider capability and create a ScopedFTProvider
71 let providerCapCopy = payer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
72 from: FlowEVMBridgeConfig.providerCapabilityStoragePath
73 ) ?? panic("Invalid Provider Capability found in storage.")
74 let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)
75 let scopedProvider <- ScopedFTProviders.createScopedFTProvider(
76 provider: providerCapCopy,
77 filters: [ providerFilter ],
78 expiration: getCurrentBlock().timestamp + 1.0
79 )
80
81 // Unwrap NFTs if applicable
82 unwrapNFTsIfApplicable(coa, nftIDs: ids, nftType: nftType, viewResolver: viewResolver)
83
84 // Bridge NFTs from EVM to child flow account
85 for id in ids {
86 let nft: @{NonFungibleToken.NFT} <- coa.withdrawNFT(
87 type: nftType,
88 id: id,
89 feeProvider: &scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
90 )
91
92 assert(
93 nft.getType() == nftType,
94 message: "Bridged nft type mismatch - requested: ".concat(nftType.identifier)
95 .concat(", received: ").concat(nft.getType().identifier)
96 )
97
98 collectionRef.deposit(token: <- nft)
99 }
100
101 // Destroy the ScopedFTProvider
102 destroy scopedProvider
103 }
104}
105
106/// Unwraps NFTs from a project's custom ERC721 wrapper contract to bridged NFTs on EVM, if applicable.
107/// Enables projects to use their own ERC721 contract while leveraging the bridge's underlying contract,
108/// until direct custom contract support is added to the bridge.
109///
110/// @param coa: The COA of the signer
111/// @param nftIDs: The IDs of the NFTs to wrap
112/// @param nftType: The type of the NFTs to wrap
113/// @param viewResolver: The ViewResolver of the NFT contract
114///
115access(all) fun unwrapNFTsIfApplicable(
116 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
117 nftIDs: [UInt256],
118 nftType: Type,
119 viewResolver: &{ViewResolver}
120) {
121 // Get the project-defined ERC721 address if it exists
122 if let crossVMPointer = viewResolver.resolveContractView(
123 resourceType: nftType,
124 viewType: Type<CrossVMMetadataViews.EVMPointer>()
125 ) as! CrossVMMetadataViews.EVMPointer? {
126 // Get the underlying ERC721 address if it exists
127 if let underlyingAddress = getUnderlyingERC721Address(coa, crossVMPointer.evmContractAddress) {
128 for id in nftIDs {
129 // Unwrap NFT if it is wrapped
130 if isNFTWrapped(coa,
131 nftID: id,
132 underlying: underlyingAddress,
133 wrapper: crossVMPointer.evmContractAddress
134 ) {
135 let res = mustCall(coa, crossVMPointer.evmContractAddress,
136 functionSig: "withdrawTo(address,uint256[])",
137 args: [coa.address(), [id]]
138 )
139 let decodedRes = EVM.decodeABI(types: [Type<Bool>()], data: res.data)
140 assert(decodedRes.length == 1, message: "Invalid response length")
141 assert(decodedRes[0] as! Bool, message: "Failed to unwrap NFT")
142 }
143 }
144 }
145 }
146}
147
148/// Gets the underlying ERC721 address if it exists (i.e. if the ERC721 is a wrapper)
149///
150access(all) fun getUnderlyingERC721Address(
151 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
152 _ wrapperAddress: EVM.EVMAddress
153): EVM.EVMAddress? {
154 let res = coa.call(
155 to: wrapperAddress,
156 data: EVM.encodeABIWithSignature("underlying()", []),
157 gasLimit: 100_000,
158 value: EVM.Balance(attoflow: 0)
159 )
160
161 // If the call fails, return nil
162 if res.status != EVM.Status.successful || res.data.length == 0 {
163 return nil
164 }
165
166 // Decode and return the underlying ERC721 address
167 let decodedResult = EVM.decodeABI(
168 types: [Type<EVM.EVMAddress>()],
169 data: res.data
170 )
171 assert(decodedResult.length == 1, message: "Invalid response length")
172 return decodedResult[0] as! EVM.EVMAddress
173}
174
175/// Checks if the provided NFT is wrapped in the underlying ERC721 contract
176///
177access(all) fun isNFTWrapped(
178 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
179 nftID: UInt256,
180 underlying: EVM.EVMAddress,
181 wrapper: EVM.EVMAddress
182): Bool {
183 let res = coa.call(
184 to: underlying,
185 data: EVM.encodeABIWithSignature("ownerOf(uint256)", [nftID]),
186 gasLimit: 100_000,
187 value: EVM.Balance(attoflow: 0)
188 )
189
190 // If the call fails, return false
191 if res.status != EVM.Status.successful || res.data.length == 0{
192 return false
193 }
194
195 // Decode and compare the addresses
196 let decodedResult = EVM.decodeABI(
197 types: [Type<EVM.EVMAddress>()],
198 data: res.data
199 )
200 assert(decodedResult.length == 1, message: "Invalid response length")
201 let owner = decodedResult[0] as! EVM.EVMAddress
202 return owner.toString() == wrapper.toString()
203}
204
205/// Calls a function on an EVM contract from provided coa
206///
207access(all) fun mustCall(
208 _ coa: auth(EVM.Call) &EVM.CadenceOwnedAccount,
209 _ contractAddr: EVM.EVMAddress,
210 functionSig: String,
211 args: [AnyStruct]
212): EVM.Result {
213 let res = coa.call(
214 to: contractAddr,
215 data: EVM.encodeABIWithSignature(functionSig, args),
216 gasLimit: 4_000_000,
217 value: EVM.Balance(attoflow: 0)
218 )
219
220 assert(res.status == EVM.Status.successful,
221 message: "Failed to call '".concat(functionSig)
222 .concat("\n\t error code: ").concat(res.errorCode.toString())
223 .concat("\n\t error message: ").concat(res.errorMessage)
224 .concat("\n\t gas used: ").concat(res.gasUsed.toString())
225 .concat("\n\t caller address: 0x").concat(coa.address().toString())
226 .concat("\n\t contract address: 0x").concat(contractAddr.toString())
227 )
228
229 return res
230}