Skip to content

Commit 7963d7a

Browse files
authored
Add useBridgeNftFromEvm hook (#2624)
* Implemented cross-vm-receive-nft hook and updated demo * Added changeset * Fixed prettier check * Fixed demo after master merge conflict * Minor spacing fix * Renamed hook * Improved changeset * Implemented playground cards for new hook and the opposite one for testing * Prettier fix * Renamed crossvm nft hooks * Prettier fix --------- Co-authored-by: mfbz <[email protected]>
1 parent 52688c3 commit 7963d7a

12 files changed

+1225
-0
lines changed

.changeset/mighty-rocks-smell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@onflow/react-sdk": minor
3+
"@onflow/demo": minor
4+
---
5+
6+
Added `useCrossVmBridgeNftFromEvm` hook for bridging NFTs from Flow EVM to Cadence. This hook withdraws an NFT from the signer's Cadence-Owned Account (COA) in EVM and deposits it into their Cadence collection, automatically configuring the collection if needed.

packages/demo/src/components/content-section.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {UseFlowMutateCard} from "./hook-cards/use-flow-mutate-card"
1212
import {UseFlowEventsCard} from "./hook-cards/use-flow-events-card"
1313
import {UseFlowTransactionStatusCard} from "./hook-cards/use-flow-transaction-status-card"
1414
import {UseFlowRevertibleRandomCard} from "./hook-cards/use-flow-revertible-random-card"
15+
import {UseCrossVmBridgeNftFromEvmCard} from "./hook-cards/use-cross-vm-bridge-nft-from-evm-card"
16+
import {UseCrossVmBridgeNftToEvmCard} from "./hook-cards/use-cross-vm-bridge-nft-to-evm-card"
1517
import {UseCrossVmBridgeTokenFromEvmCard} from "./hook-cards/use-cross-vm-bridge-token-from-evm-card"
1618
import {UseCrossVmBridgeTokenToEvmCard} from "./hook-cards/use-cross-vm-bridge-token-to-evm-card"
1719
import {UseFlowNftMetadataCard} from "./hook-cards/use-flow-nft-metadata-card"
@@ -88,6 +90,8 @@ export function ContentSection() {
8890
<UseFlowEventsCard />
8991
<UseFlowRevertibleRandomCard />
9092
<UseFlowTransactionStatusCard />
93+
<UseCrossVmBridgeNftFromEvmCard />
94+
<UseCrossVmBridgeNftToEvmCard />
9195
<UseCrossVmBridgeTokenFromEvmCard />
9296
<UseCrossVmBridgeTokenToEvmCard />
9397
<UseFlowNftMetadataCard />

packages/demo/src/components/content-sidebar.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ const sidebarItems: SidebarItem[] = [
110110
category: "hooks",
111111
description: "Track transaction status",
112112
},
113+
{
114+
id: "usecrossvmbridgenftfromevm",
115+
label: "Bridge NFT from EVM",
116+
category: "hooks",
117+
description: "Bridge NFTs from EVM to Cadence",
118+
},
119+
{
120+
id: "usecrossvmbridgenfttoevm",
121+
label: "Bridge NFT to EVM",
122+
category: "hooks",
123+
description: "Bridge NFTs from Cadence to EVM",
124+
},
113125
{
114126
id: "usecrossvmbridgetokenfromevm",
115127
label: "Bridge Token from EVM",
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {useCrossVmBridgeNftFromEvm, useFlowConfig} from "@onflow/react-sdk"
2+
import {useState, useMemo} from "react"
3+
import {useDarkMode} from "../flow-provider-wrapper"
4+
import {DemoCard} from "../ui/demo-card"
5+
import {ResultsSection} from "../ui/results-section"
6+
import {getContractAddress} from "../../constants"
7+
import {PlusGridIcon} from "../ui/plus-grid"
8+
9+
const IMPLEMENTATION_CODE = `import { useCrossVmBridgeNftFromEvm } from "@onflow/react-sdk"
10+
11+
const {
12+
crossVmBridgeNftFromEvm,
13+
isPending,
14+
error,
15+
data: txId
16+
} = useCrossVmBridgeNftFromEvm()
17+
18+
crossVmBridgeNftFromEvm({
19+
nftIdentifier: "A.dfc20aee650fcbdf.ExampleNFT.NFT",
20+
nftId: "1"
21+
})`
22+
23+
export function UseCrossVmBridgeNftFromEvmCard() {
24+
const {darkMode} = useDarkMode()
25+
const config = useFlowConfig()
26+
const currentNetwork = config.flowNetwork || "emulator"
27+
const [nftIdentifier, setNftIdentifier] = useState("")
28+
const [nftId, setNftId] = useState("1")
29+
30+
const {
31+
crossVmBridgeNftFromEvm,
32+
isPending,
33+
data: transactionId,
34+
error,
35+
} = useCrossVmBridgeNftFromEvm()
36+
37+
const exampleNftData = useMemo(() => {
38+
if (currentNetwork !== "testnet") return null
39+
40+
const exampleNftAddress = getContractAddress("ExampleNFT", currentNetwork)
41+
return {
42+
name: "Example NFT",
43+
nftIdentifier: `A.${exampleNftAddress.replace("0x", "")}.ExampleNFT.NFT`,
44+
nftId: "1",
45+
}
46+
}, [currentNetwork])
47+
48+
// Set default NFT identifier when network changes to testnet
49+
useMemo(() => {
50+
if (exampleNftData && !nftIdentifier) {
51+
setNftIdentifier(exampleNftData.nftIdentifier)
52+
}
53+
}, [exampleNftData, nftIdentifier])
54+
55+
const handleBridgeNft = () => {
56+
crossVmBridgeNftFromEvm({
57+
nftIdentifier,
58+
nftId,
59+
})
60+
}
61+
62+
return (
63+
<DemoCard
64+
id="usecrossvmbridgenftfromevm"
65+
title="useCrossVmBridgeNftFromEvm"
66+
description="Bridge NFTs from Flow EVM to Cadence by withdrawing from the signer's Cadence-Owned Account (COA) in EVM."
67+
code={IMPLEMENTATION_CODE}
68+
docsUrl="https://developers.flow.com/build/tools/react-sdk/hooks#usecrossvmbridgenftfromevm"
69+
>
70+
<div className="space-y-6">
71+
{exampleNftData && (
72+
<div
73+
className={`relative p-4 rounded-lg border ${
74+
darkMode
75+
? "bg-blue-500/10 border-blue-500/20"
76+
: "bg-blue-50 border-blue-200"
77+
}`}
78+
>
79+
<PlusGridIcon placement="top left" className="absolute" />
80+
<p
81+
className={`text-sm ${darkMode ? "text-blue-300" : "text-blue-800"}`}
82+
>
83+
<strong>Note:</strong> Example prefilled with ExampleNFT type
84+
identifier for testnet
85+
</p>
86+
</div>
87+
)}
88+
89+
<div className="space-y-3">
90+
<label
91+
className={`block text-sm font-medium ${darkMode ? "text-gray-300" : "text-gray-700"}`}
92+
>
93+
NFT Identifier
94+
</label>
95+
<input
96+
type="text"
97+
value={nftIdentifier}
98+
onChange={e => setNftIdentifier(e.target.value)}
99+
placeholder={
100+
exampleNftData
101+
? exampleNftData.nftIdentifier
102+
: "e.g., A.dfc20aee650fcbdf.ExampleNFT.NFT"
103+
}
104+
className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200
105+
${
106+
darkMode
107+
? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500
108+
focus:border-flow-primary/50`
109+
: `bg-white border-black/10 text-black placeholder-gray-400
110+
focus:border-flow-primary/50`
111+
} outline-none`}
112+
/>
113+
</div>
114+
115+
<div className="space-y-3">
116+
<label
117+
className={`block text-sm font-medium ${darkMode ? "text-gray-300" : "text-gray-700"}`}
118+
>
119+
NFT ID (UInt256)
120+
</label>
121+
<input
122+
type="text"
123+
value={nftId}
124+
onChange={e => setNftId(e.target.value)}
125+
placeholder="e.g., 1"
126+
className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200
127+
${
128+
darkMode
129+
? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500
130+
focus:border-flow-primary/50`
131+
: `bg-white border-black/10 text-black placeholder-gray-400
132+
focus:border-flow-primary/50`
133+
} outline-none`}
134+
/>
135+
</div>
136+
137+
<div className="flex justify-start">
138+
<button
139+
onClick={handleBridgeNft}
140+
disabled={isPending || !nftIdentifier || !nftId}
141+
className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${
142+
isPending || !nftIdentifier || !nftId
143+
? "bg-gray-300 text-gray-500 cursor-not-allowed"
144+
: "bg-flow-primary text-black hover:bg-flow-primary/80"
145+
}`}
146+
>
147+
{isPending ? "Bridging..." : "Bridge NFT from EVM"}
148+
</button>
149+
</div>
150+
151+
<ResultsSection
152+
data={transactionId || error}
153+
darkMode={darkMode}
154+
show={!!transactionId || !!error}
155+
title={
156+
transactionId
157+
? "NFT bridged successfully!"
158+
: error
159+
? "Bridge failed"
160+
: undefined
161+
}
162+
/>
163+
</div>
164+
</DemoCard>
165+
)
166+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import {useCrossVmBridgeNftToEvm, useFlowConfig} from "@onflow/react-sdk"
2+
import {useState, useMemo} from "react"
3+
import {useDarkMode} from "../flow-provider-wrapper"
4+
import {DemoCard} from "../ui/demo-card"
5+
import {ResultsSection} from "../ui/results-section"
6+
import {getContractAddress} from "../../constants"
7+
import {PlusGridIcon} from "../ui/plus-grid"
8+
9+
const IMPLEMENTATION_CODE = `import { useCrossVmBridgeNftToEvm } from "@onflow/react-sdk"
10+
11+
const {
12+
crossVmBridgeNftToEvm,
13+
isPending,
14+
error,
15+
data: txId
16+
} = useCrossVmBridgeNftToEvm()
17+
18+
crossVmBridgeNftToEvm({
19+
nftIdentifier: "A.012e4d204a60ac6f.ExampleNFT.NFT",
20+
nftIds: ["1", "2", "3"],
21+
calls: []
22+
})`
23+
24+
export function UseCrossVmBridgeNftToEvmCard() {
25+
const {darkMode} = useDarkMode()
26+
const config = useFlowConfig()
27+
const currentNetwork = config.flowNetwork || "emulator"
28+
const [nftIdentifier, setNftIdentifier] = useState("")
29+
const [nftIds, setNftIds] = useState("1")
30+
31+
const {
32+
crossVmBridgeNftToEvm,
33+
isPending,
34+
data: transactionId,
35+
error,
36+
} = useCrossVmBridgeNftToEvm()
37+
38+
const exampleNftData = useMemo(() => {
39+
if (currentNetwork !== "testnet") return null
40+
41+
const exampleNftAddress = getContractAddress("ExampleNFT", currentNetwork)
42+
return {
43+
name: "Example NFT",
44+
nftIdentifier: `A.${exampleNftAddress.replace("0x", "")}.ExampleNFT.NFT`,
45+
nftIds: "1",
46+
}
47+
}, [currentNetwork])
48+
49+
// Set default NFT identifier when network changes to testnet
50+
useMemo(() => {
51+
if (exampleNftData && !nftIdentifier) {
52+
setNftIdentifier(exampleNftData.nftIdentifier)
53+
setNftIds(exampleNftData.nftIds)
54+
}
55+
}, [exampleNftData, nftIdentifier])
56+
57+
const handleBridgeNft = () => {
58+
const nftIdArray = nftIds.split(",").map(id => id.trim())
59+
60+
crossVmBridgeNftToEvm({
61+
nftIdentifier,
62+
nftIds: nftIdArray,
63+
calls: [], // No EVM calls, just bridging
64+
})
65+
}
66+
67+
return (
68+
<DemoCard
69+
id="usecrossvmbridgenfttoevm"
70+
title="useCrossVmBridgeNftToEvm"
71+
description="Bridge NFTs from Cadence to Flow EVM by depositing them into the signer's Cadence-Owned Account (COA)."
72+
code={IMPLEMENTATION_CODE}
73+
docsUrl="https://developers.flow.com/build/tools/react-sdk/hooks#usecrossvmbridgenfttoevm"
74+
>
75+
<div className="space-y-6">
76+
{exampleNftData && (
77+
<div
78+
className={`relative p-4 rounded-lg border ${
79+
darkMode
80+
? "bg-blue-500/10 border-blue-500/20"
81+
: "bg-blue-50 border-blue-200"
82+
}`}
83+
>
84+
<PlusGridIcon placement="top right" className="absolute" />
85+
<p
86+
className={`text-sm ${darkMode ? "text-blue-300" : "text-blue-800"}`}
87+
>
88+
<strong>Note:</strong> Example prefilled with ExampleNFT type
89+
identifier for testnet
90+
</p>
91+
</div>
92+
)}
93+
94+
<div className="space-y-3">
95+
<label
96+
className={`block text-sm font-medium ${darkMode ? "text-gray-300" : "text-gray-700"}`}
97+
>
98+
NFT Identifier
99+
</label>
100+
<input
101+
type="text"
102+
value={nftIdentifier}
103+
onChange={e => setNftIdentifier(e.target.value)}
104+
placeholder={
105+
exampleNftData
106+
? exampleNftData.nftIdentifier
107+
: "e.g., A.012e4d204a60ac6f.ExampleNFT.NFT"
108+
}
109+
className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200
110+
${
111+
darkMode
112+
? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500
113+
focus:border-flow-primary/50`
114+
: `bg-white border-black/10 text-black placeholder-gray-400
115+
focus:border-flow-primary/50`
116+
} outline-none`}
117+
/>
118+
</div>
119+
120+
<div className="space-y-3">
121+
<label
122+
className={`block text-sm font-medium ${darkMode ? "text-gray-300" : "text-gray-700"}`}
123+
>
124+
NFT IDs (UInt64, comma-separated)
125+
</label>
126+
<input
127+
type="text"
128+
value={nftIds}
129+
onChange={e => setNftIds(e.target.value)}
130+
placeholder="e.g., 1,2,3"
131+
className={`w-full px-4 py-3 rounded-lg border font-mono text-sm transition-all duration-200
132+
${
133+
darkMode
134+
? `bg-gray-900/50 border-white/10 text-white placeholder-gray-500
135+
focus:border-flow-primary/50`
136+
: `bg-white border-black/10 text-black placeholder-gray-400
137+
focus:border-flow-primary/50`
138+
} outline-none`}
139+
/>
140+
</div>
141+
142+
<div className="flex justify-start">
143+
<button
144+
onClick={handleBridgeNft}
145+
disabled={isPending || !nftIdentifier || !nftIds}
146+
className={`px-6 py-3 rounded-lg font-medium transition-all duration-200 ${
147+
isPending || !nftIdentifier || !nftIds
148+
? "bg-gray-300 text-gray-500 cursor-not-allowed"
149+
: "bg-flow-primary text-black hover:bg-flow-primary/80"
150+
}`}
151+
>
152+
{isPending ? "Bridging..." : "Bridge NFT to EVM"}
153+
</button>
154+
</div>
155+
156+
<ResultsSection
157+
data={transactionId || error}
158+
darkMode={darkMode}
159+
show={!!transactionId || !!error}
160+
title={
161+
transactionId
162+
? "NFTs bridged successfully!"
163+
: error
164+
? "Bridge failed"
165+
: undefined
166+
}
167+
/>
168+
</div>
169+
</DemoCard>
170+
)
171+
}

packages/demo/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export const CONTRACT_ADDRESSES: Record<string, Record<any, string>> = {
2121
testnet: "0x7e60df042a9c0868",
2222
mainnet: "0x1654653399040a61",
2323
},
24+
ExampleNFT: {
25+
testnet: "0x012e4d204a60ac6f",
26+
},
2427
ClickToken: {
2528
testnet: "0xdfc20aee650fcbdf",
2629
},

0 commit comments

Comments
 (0)