Skip to content

Commit ba8919a

Browse files
committed
add separate transactions for batching nfts and tokens
1 parent cf87da0 commit ba8919a

File tree

4 files changed

+367
-43
lines changed

4 files changed

+367
-43
lines changed

cadence/tests/flow_evm_bridge_tests.cdc

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,23 @@ fun testOnboardAndBridgeTokensToEVMSucceeds() {
489489
Test.assertEqual(expectedEVMBalance, evmBalance)
490490
}
491491

492+
access(all)
493+
fun testBridgeTokensToEVMandTxsSucceeds() {
494+
// Revert to state before ExampleNFT was onboarded
495+
Test.reset(to: snapshot)
496+
497+
var cadenceBalance = getBalance(ownerAddr: alice.address, storagePathIdentifier: "exampleTokenVault")
498+
?? panic("Could not get ExampleToken balance")
499+
500+
// Execute bridge to EVM - should also onboard the token type
501+
let bridgeResult = executeTransaction(
502+
"../transactions/bridge/tokens/bridge_tokens_to_evm_and_txs.cdc",
503+
[ exampleTokenIdentifier, cadenceBalance, [], [], [], []],
504+
alice
505+
)
506+
Test.expect(bridgeResult, Test.beSucceeded())
507+
}
508+
492509
access(all)
493510
fun testOnboardAndCrossVMTransferTokensToEVMSucceeds() {
494511
// Revert to state before ExampleNFT was onboarded
@@ -762,7 +779,7 @@ fun testBatchBridgeCadenceNativeNFTToEVMSucceeds() {
762779
// Execute bridge to EVM
763780
let bridgeResult = executeTransaction(
764781
"../transactions/bridge/nft/batch_bridge_nft_to_evm.cdc",
765-
[ exampleNFTIdentifier, aliceOwnedIDs, [], [], [], [] ],
782+
[ exampleNFTIdentifier, aliceOwnedIDs ],
766783
alice
767784
)
768785
Test.expect(bridgeResult, Test.beSucceeded())
@@ -791,6 +808,26 @@ fun testBatchBridgeCadenceNativeNFTToEVMSucceeds() {
791808
Test.assert(metadata2 != nil, message: "Expected NFT metadata to be resolved from escrow but none was returned")
792809
}
793810

811+
access(all)
812+
fun testBatchBridgeCadenceNativeNFTToEVMAndTxsSucceeds() {
813+
let tmp = snapshot
814+
Test.reset(to: snapshot)
815+
snapshot = tmp
816+
817+
var aliceOwnedIDs = getIDs(ownerAddr: alice.address, storagePathIdentifier: "cadenceExampleNFTCollection")
818+
Test.assertEqual(2, aliceOwnedIDs.length)
819+
820+
var aliceCOAAddressHex = getCOAAddressHex(atFlowAddress: alice.address)
821+
822+
// Execute bridge to EVM
823+
let bridgeResult = executeTransaction(
824+
"../transactions/bridge/nft/batch_bridge_nft_to_evm_and_txs.cdc",
825+
[ exampleNFTIdentifier, aliceOwnedIDs, [], [], [], []],
826+
alice
827+
)
828+
Test.expect(bridgeResult, Test.beSucceeded())
829+
}
830+
794831
access(all)
795832
fun testBatchBridgeCadenceNativeNFTFromEVMSucceeds() {
796833
snapshot = getCurrentBlockHeight()

cadence/transactions/bridge/nft/batch_bridge_nft_to_evm.cdc

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,26 @@ import "FlowEVMBridgeConfig"
1313
import "FlowEVMBridgeUtils"
1414

1515
/// Bridges NFTs (from the same collection) from the signer's collection in Cadence to the signer's COA in FlowEVM
16-
/// and then performs an arbitrary number of calls afterwards to potentially do things
17-
/// with the bridged NFTs
1816
///
19-
/// This transaction assumes that the NFT has already been onboarded to the bridge
17+
/// NOTE: This transaction also onboards the NFT to the bridge if necessary which may incur additional fees
18+
/// than bridging an asset that has already been onboarded.
2019
///
2120
/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier
2221
/// @param ids: The Cadence NFT.id of the NFTs to bridge to EVM
23-
/// @params evmContractAddressHexes, calldatas, gasLimits, values: An array of calldata
24-
/// to be included in transaction calls to Flow EVM from the signer's COA.
25-
/// The arrays are all expected to be of the same length
2622
///
27-
transaction(
28-
nftIdentifier: String,
29-
ids: [UInt64],
30-
evmContractAddressHexes: [String],
31-
calldatas: [String],
32-
gasLimits: [UInt64],
33-
values: [UFix64]
34-
) {
23+
transaction(nftIdentifier: String, ids: [UInt64]) {
3524

3625
let nftType: Type
3726
let collection: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}
38-
let coa: auth(EVM.Bridge, EVM.Call) &EVM.CadenceOwnedAccount
27+
let coa: auth(EVM.Bridge) &EVM.CadenceOwnedAccount
3928
let requiresOnboarding: Bool
4029
let scopedProvider: @ScopedFTProviders.ScopedFTProvider
4130

4231
prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {
43-
pre {
44-
(evmContractAddressHexes.length == calldatas.length)
45-
&& (calldatas.length == gasLimits.length)
46-
&& (gasLimits.length == values.length):
47-
"Calldata array lengths must all be the same!"
48-
}
49-
5032
/* --- Reference the signer's CadenceOwnedAccount --- */
5133
//
5234
// Borrow a reference to the signer's COA
53-
self.coa = signer.storage.borrow<auth(EVM.Bridge, EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
35+
self.coa = signer.storage.borrow<auth(EVM.Bridge) &EVM.CadenceOwnedAccount>(from: /storage/evm)
5436
?? panic("Could not borrow COA signer's account at path /storage/evm")
5537

5638
/* --- Construct the NFT type --- */
@@ -116,7 +98,6 @@ transaction(
11698
}
11799

118100
execute {
119-
120101
if self.requiresOnboarding {
121102
// Onboard the NFT to the bridge
122103
FlowEVMBridge.onboardByType(
@@ -143,22 +124,5 @@ transaction(
143124

144125
// Destroy the ScopedFTProvider
145126
destroy self.scopedProvider
146-
147-
// Perform all the calls
148-
for index, evmAddressHex in evmContractAddressHexes {
149-
let evmAddress = EVM.addressFromString(evmAddressHex)
150-
151-
let valueBalance = EVM.Balance(attoflow: 0)
152-
valueBalance.setFLOW(flow: values[index])
153-
let callResult = self.coa.call(
154-
to: evmAddress,
155-
data: calldatas[index].decodeHex(),
156-
gasLimit: gasLimits[index],
157-
value: valueBalance
158-
)
159-
assert(
160-
callResult.status == EVM.Status.successful,
161-
message: "Call failed with address \(evmAddressHex) and calldata \(calldatas[index]) with error \(callResult.errorMessage)")
162-
}
163127
}
164-
}
128+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import "FungibleToken"
2+
import "NonFungibleToken"
3+
import "ViewResolver"
4+
import "MetadataViews"
5+
import "FlowToken"
6+
7+
import "ScopedFTProviders"
8+
9+
import "EVM"
10+
11+
import "FlowEVMBridge"
12+
import "FlowEVMBridgeConfig"
13+
import "FlowEVMBridgeUtils"
14+
15+
/// Bridges NFTs (from the same collection) from the signer's collection in Cadence to the signer's COA in FlowEVM
16+
/// and then performs an arbitrary number of calls afterwards to potentially do things
17+
/// with the bridged NFTs
18+
///
19+
/// NOTE: This transaction also onboards the NFT to the bridge if necessary which may incur additional fees
20+
/// than bridging an asset that has already been onboarded.
21+
///
22+
/// @param nftIdentifier: The Cadence type identifier of the NFT to bridge - e.g. nft.getType().identifier
23+
/// @param ids: The Cadence NFT.id of the NFTs to bridge to EVM
24+
/// @params evmContractAddressHexes, calldatas, gasLimits, values: Arrays of calldata
25+
/// to be included in transaction calls to Flow EVM from the signer's COA.
26+
/// The arrays are all expected to be of the same length
27+
///
28+
transaction(
29+
nftIdentifier: String,
30+
ids: [UInt64],
31+
evmContractAddressHexes: [String],
32+
calldatas: [String],
33+
gasLimits: [UInt64],
34+
values: [UFix64]
35+
) {
36+
37+
let nftType: Type
38+
let collection: auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}
39+
let coa: auth(EVM.Bridge, EVM.Call) &EVM.CadenceOwnedAccount
40+
let requiresOnboarding: Bool
41+
let scopedProvider: @ScopedFTProviders.ScopedFTProvider
42+
43+
prepare(signer: auth(CopyValue, BorrowValue, IssueStorageCapabilityController, PublishCapability, SaveValue) &Account) {
44+
pre {
45+
(evmContractAddressHexes.length == calldatas.length)
46+
&& (calldatas.length == gasLimits.length)
47+
&& (gasLimits.length == values.length):
48+
"Calldata array lengths must all be the same!"
49+
}
50+
51+
/* --- Reference the signer's CadenceOwnedAccount --- */
52+
//
53+
// Borrow a reference to the signer's COA
54+
self.coa = signer.storage.borrow<auth(EVM.Bridge, EVM.Call) &EVM.CadenceOwnedAccount>(from: /storage/evm)
55+
?? panic("Could not borrow COA signer's account at path /storage/evm")
56+
57+
/* --- Construct the NFT type --- */
58+
//
59+
// Construct the NFT type from the provided identifier
60+
self.nftType = CompositeType(nftIdentifier)
61+
?? panic("Could not construct NFT type from identifier: ".concat(nftIdentifier))
62+
// Parse the NFT identifier into its components
63+
let nftContractAddress = FlowEVMBridgeUtils.getContractAddress(fromType: self.nftType)
64+
?? panic("Could not get contract address from identifier: ".concat(nftIdentifier))
65+
let nftContractName = FlowEVMBridgeUtils.getContractName(fromType: self.nftType)
66+
?? panic("Could not get contract name from identifier: ".concat(nftIdentifier))
67+
68+
/* --- Retrieve the NFT --- */
69+
//
70+
// Borrow a reference to the NFT collection, configuring if necessary
71+
let viewResolver = getAccount(nftContractAddress).contracts.borrow<&{ViewResolver}>(name: nftContractName)
72+
?? panic("Could not borrow ViewResolver from NFT contract with name "
73+
.concat(nftContractName).concat(" and address ")
74+
.concat(nftContractAddress.toString()))
75+
let collectionData = viewResolver.resolveContractView(
76+
resourceType: self.nftType,
77+
viewType: Type<MetadataViews.NFTCollectionData>()
78+
) as! MetadataViews.NFTCollectionData?
79+
?? panic("Could not resolve NFTCollectionData view for NFT type ".concat(self.nftType.identifier))
80+
self.collection = signer.storage.borrow<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>(
81+
from: collectionData.storagePath
82+
) ?? panic("Could not borrow a NonFungibleToken Collection from the signer's storage path "
83+
.concat(collectionData.storagePath.toString()))
84+
85+
// Withdraw the requested NFT & set a cap on the withdrawable bridge fee
86+
var approxFee = FlowEVMBridgeUtils.calculateBridgeFee(
87+
bytes: 400_000 // 400 kB as upper bound on movable storage used in a single transaction
88+
) + (FlowEVMBridgeConfig.baseFee * UFix64(ids.length))
89+
// Determine if the NFT requires onboarding - this impacts the fee required
90+
self.requiresOnboarding = FlowEVMBridge.typeRequiresOnboarding(self.nftType)
91+
?? panic("Bridge does not support the requested asset type ".concat(nftIdentifier))
92+
// Add the onboarding fee if onboarding is necessary
93+
if self.requiresOnboarding {
94+
approxFee = approxFee + FlowEVMBridgeConfig.onboardFee
95+
}
96+
97+
/* --- Configure a ScopedFTProvider --- */
98+
//
99+
// Issue and store bridge-dedicated Provider Capability in storage if necessary
100+
if signer.storage.type(at: FlowEVMBridgeConfig.providerCapabilityStoragePath) == nil {
101+
let providerCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(
102+
/storage/flowTokenVault
103+
)
104+
signer.storage.save(providerCap, to: FlowEVMBridgeConfig.providerCapabilityStoragePath)
105+
}
106+
// Copy the stored Provider capability and create a ScopedFTProvider
107+
let providerCapCopy = signer.storage.copy<Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>>(
108+
from: FlowEVMBridgeConfig.providerCapabilityStoragePath
109+
) ?? panic("Invalid FungibleToken Provider Capability found in storage at path "
110+
.concat(FlowEVMBridgeConfig.providerCapabilityStoragePath.toString()))
111+
let providerFilter = ScopedFTProviders.AllowanceFilter(approxFee)
112+
self.scopedProvider <- ScopedFTProviders.createScopedFTProvider(
113+
provider: providerCapCopy,
114+
filters: [ providerFilter ],
115+
expiration: getCurrentBlock().timestamp + 1.0
116+
)
117+
}
118+
119+
execute {
120+
121+
if self.requiresOnboarding {
122+
// Onboard the NFT to the bridge
123+
FlowEVMBridge.onboardByType(
124+
self.nftType,
125+
feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
126+
)
127+
}
128+
129+
// Iterate over requested IDs and bridge each NFT to the signer's COA in EVM
130+
for id in ids {
131+
// Withdraw the NFT & ensure it's the correct type
132+
let nft <-self.collection.withdraw(withdrawID: id)
133+
assert(
134+
nft.getType() == self.nftType,
135+
message: "Bridged nft type mismatch - requested: ".concat(self.nftType.identifier)
136+
.concat(", received: ").concat(nft.getType().identifier)
137+
)
138+
// Execute the bridge to EVM for the current ID
139+
self.coa.depositNFT(
140+
nft: <-nft,
141+
feeProvider: &self.scopedProvider as auth(FungibleToken.Withdraw) &{FungibleToken.Provider}
142+
)
143+
}
144+
145+
// Destroy the ScopedFTProvider
146+
destroy self.scopedProvider
147+
148+
// Perform all the calls
149+
for index, evmAddressHex in evmContractAddressHexes {
150+
let evmAddress = EVM.addressFromString(evmAddressHex)
151+
152+
let valueBalance = EVM.Balance(attoflow: 0)
153+
valueBalance.setFLOW(flow: values[index])
154+
let callResult = self.coa.call(
155+
to: evmAddress,
156+
data: calldatas[index].decodeHex(),
157+
gasLimit: gasLimits[index],
158+
value: valueBalance
159+
)
160+
assert(
161+
callResult.status == EVM.Status.successful,
162+
message: "Call failed with address \(evmAddressHex) and calldata \(calldatas[index]) with error \(callResult.errorMessage)"
163+
)
164+
}
165+
}
166+
}

0 commit comments

Comments
 (0)