Skip to content

Commit bb56f54

Browse files
committed
Add transfer nft functionality
1 parent 5a2b900 commit bb56f54

File tree

6 files changed

+334
-11
lines changed

6 files changed

+334
-11
lines changed

hello/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ localnet.json
2121
test-ledger
2222

2323
lib
24+
dist
25+
26+
# Build artifacts
27+
build/
28+
*.log
29+
.DS_Store

hello/frontend/src/ConnectedContent.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ const DynamicConnectedContent = ({
5252
primaryWallet={primaryWallet}
5353
/>
5454
</div>
55-
<NFTList />
55+
<NFTList
56+
selectedProvider={selectedProvider || null}
57+
primaryWallet={primaryWallet || null}
58+
/>
5659
</div>
5760
<Footer />
5861
</div>
@@ -93,7 +96,10 @@ const Eip6963ConnectedContent = ({
9396
primaryWallet={primaryWallet}
9497
/>
9598
</div>
96-
<NFTList />
99+
<NFTList
100+
selectedProvider={selectedProvider || null}
101+
primaryWallet={primaryWallet || null}
102+
/>
97103
</div>
98104
<Footer />
99105
</div>

hello/frontend/src/components/NFTList.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@
108108
display: flex;
109109
gap: 10px;
110110
justify-content: space-between;
111+
align-items: center;
112+
}
113+
114+
.nft-transfer-section {
115+
display: flex;
116+
gap: 8px;
117+
align-items: center;
111118
}
112119

113120
.nft-link {
@@ -127,6 +134,26 @@
127134
text-decoration: none;
128135
}
129136

137+
.nft-destination-select {
138+
padding: 8px 12px;
139+
background: #2a2a2a;
140+
color: #ffffff;
141+
border: 1px solid #444;
142+
border-radius: 6px;
143+
font-size: 0.9em;
144+
min-width: 150px;
145+
}
146+
147+
.nft-destination-select:focus {
148+
outline: none;
149+
border-color: #007bff;
150+
}
151+
152+
.nft-destination-select option {
153+
background: #2a2a2a;
154+
color: #ffffff;
155+
}
156+
130157
.nft-transfer-btn {
131158
padding: 8px 16px;
132159
background: #6c757d;
@@ -161,6 +188,16 @@
161188

162189
.nft-actions {
163190
flex-direction: column;
191+
gap: 15px;
192+
}
193+
194+
.nft-transfer-section {
195+
flex-direction: column;
196+
gap: 10px;
197+
}
198+
199+
.nft-destination-select {
200+
min-width: 100%;
164201
}
165202

166203
.nft-link,

hello/frontend/src/components/NFTList.tsx

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import './NFTList.css';
22

3+
import type { PrimaryWallet } from '@zetachain/wallet';
34
import { useEffect, useState } from 'react';
45

6+
import { SUPPORTED_CHAINS } from '../constants/chains';
7+
import { NFT_CONTRACT_ADDRESSES } from '../constants/contracts';
8+
import type { EIP6963ProviderDetail } from '../types/wallet';
9+
import { getSignerAndProvider } from '../utils/ethersHelpers';
510
import { getAllNFTsForDisplay } from '../utils/nftDisplay';
11+
import { transferNFT } from '../utils/nftTransfer';
612

713
interface NFTDisplayData {
814
id: string;
@@ -19,9 +25,16 @@ interface NFTDisplayData {
1925
}
2026

2127

22-
export function NFTList() {
28+
interface NFTListProps {
29+
selectedProvider: EIP6963ProviderDetail | null;
30+
primaryWallet: PrimaryWallet | null;
31+
}
32+
33+
export function NFTList({ selectedProvider, primaryWallet }: NFTListProps) {
2334
const [nfts, setNfts] = useState<NFTDisplayData[]>([]);
2435
const [loading, setLoading] = useState(true);
36+
const [selectedDestinations, setSelectedDestinations] = useState<Record<string, string>>({});
37+
const [transferring, setTransferring] = useState<Record<string, boolean>>({});
2538

2639
useEffect(() => {
2740
const loadNFTs = () => {
@@ -43,6 +56,98 @@ export function NFTList() {
4356
return () => clearInterval(interval);
4457
}, []);
4558

59+
const handleDestinationChange = (nftId: string, destination: string) => {
60+
setSelectedDestinations(prev => ({
61+
...prev,
62+
[nftId]: destination
63+
}));
64+
};
65+
66+
const handleTransfer = async (nft: NFTDisplayData) => {
67+
const nftKey = `${nft.contractAddress}_${nft.id}`;
68+
const destination = selectedDestinations[nftKey];
69+
70+
if (!destination) {
71+
alert('Please select a destination chain');
72+
return;
73+
}
74+
75+
setTransferring(prev => ({ ...prev, [nftKey]: true }));
76+
77+
try {
78+
console.log(`Transferring NFT ${nft.id} from ${nft.currentChain} to ${destination}`);
79+
80+
// Get signer and provider
81+
const signerAndProvider = await getSignerAndProvider({
82+
selectedProvider,
83+
primaryWallet,
84+
});
85+
86+
if (!signerAndProvider) {
87+
throw new Error('Failed to get wallet connection');
88+
}
89+
90+
const { signer } = signerAndProvider;
91+
const userAddress = await signer.getAddress();
92+
93+
// Confirm transfer
94+
const confirmed = confirm(
95+
`Transfer NFT #${nft.id} from ${nft.currentChain} to ${destination}?\n\n` +
96+
`This will initiate a cross-chain transfer. The NFT will be moved to the destination chain.\n\n` +
97+
`You will need to approve two transactions:\n` +
98+
`1. Approve the contract to transfer your NFT\n` +
99+
`2. Execute the cross-chain transfer`
100+
);
101+
102+
if (confirmed) {
103+
// Get the contract address for the current chain
104+
const currentChainContractAddress = getNFTContractAddress(nft.currentChain);
105+
if (!currentChainContractAddress) {
106+
throw new Error(`Contract address not found for current chain: ${nft.currentChain}`);
107+
}
108+
109+
// Execute the actual transfer
110+
const result = await transferNFT({
111+
tokenId: nft.id,
112+
contractAddress: nft.contractAddress, // Original contract address for storage
113+
currentChainContractAddress: currentChainContractAddress, // Current chain contract for approve/transfer
114+
destinationChain: destination,
115+
receiverAddress: userAddress,
116+
signer: signer,
117+
fromChainName: nft.currentChain,
118+
});
119+
120+
alert(`Transfer successful! NFT #${nft.id} is being transferred to ${destination}.\n\nTransaction hash: ${result.hash}`);
121+
122+
// Refresh the NFT list to show updated state
123+
const nftData = getAllNFTsForDisplay();
124+
setNfts(nftData);
125+
}
126+
} catch (error) {
127+
console.error('Transfer failed:', error);
128+
alert(`Transfer failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
129+
} finally {
130+
setTransferring(prev => ({ ...prev, [nftKey]: false }));
131+
}
132+
};
133+
134+
const getAvailableDestinations = (currentChain: string) => {
135+
return SUPPORTED_CHAINS.filter(chain => chain.name !== currentChain);
136+
};
137+
138+
const getNFTContractAddress = (chainName: string): string | null => {
139+
switch (chainName) {
140+
case 'ZetaChain':
141+
return NFT_CONTRACT_ADDRESSES.ZETACHAIN;
142+
case 'Ethereum Sepolia':
143+
return NFT_CONTRACT_ADDRESSES.SEPOLIA;
144+
case 'Base Sepolia':
145+
return NFT_CONTRACT_ADDRESSES.BASE_SEPOLIA;
146+
default:
147+
return null;
148+
}
149+
};
150+
46151
if (loading) {
47152
return (
48153
<div className="nft-list-container">
@@ -87,14 +192,27 @@ export function NFTList() {
87192
>
88193
View Metadata
89194
</a>
90-
{/* Future: Add transfer button here */}
91-
<button
92-
className="nft-transfer-btn"
93-
disabled
94-
title="Cross-chain transfer coming soon"
95-
>
96-
Transfer
97-
</button>
195+
<div className="nft-transfer-section">
196+
<select
197+
className="nft-destination-select"
198+
value={selectedDestinations[`${nft.contractAddress}_${nft.id}`] || ''}
199+
onChange={(e) => handleDestinationChange(`${nft.contractAddress}_${nft.id}`, e.target.value)}
200+
>
201+
<option value="">Select destination</option>
202+
{getAvailableDestinations(nft.currentChain).map(chain => (
203+
<option key={chain.name} value={chain.name}>
204+
{chain.name}
205+
</option>
206+
))}
207+
</select>
208+
<button
209+
className="nft-transfer-btn"
210+
onClick={() => handleTransfer(nft)}
211+
disabled={transferring[`${nft.contractAddress}_${nft.id}`] || !selectedDestinations[`${nft.contractAddress}_${nft.id}`]}
212+
>
213+
{transferring[`${nft.contractAddress}_${nft.id}`] ? 'Transferring...' : 'Transfer'}
214+
</button>
215+
</div>
98216
</div>
99217
</div>
100218
</div>

hello/frontend/src/contracts/EVMUniversalNFT.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ export const EVMUniversalNFT_ABI = [
3434
"stateMutability": "nonpayable",
3535
"type": "function"
3636
},
37+
{
38+
"inputs": [
39+
{
40+
"internalType": "address",
41+
"name": "to",
42+
"type": "address"
43+
},
44+
{
45+
"internalType": "uint256",
46+
"name": "tokenId",
47+
"type": "uint256"
48+
}
49+
],
50+
"name": "approve",
51+
"outputs": [],
52+
"stateMutability": "nonpayable",
53+
"type": "function"
54+
},
3755
{
3856
"inputs": [
3957
{
@@ -71,6 +89,29 @@ export const EVMUniversalNFT_ABI = [
7189
],
7290
"stateMutability": "view",
7391
"type": "function"
92+
},
93+
{
94+
"inputs": [
95+
{
96+
"internalType": "uint256",
97+
"name": "tokenId",
98+
"type": "uint256"
99+
},
100+
{
101+
"internalType": "address",
102+
"name": "receiver",
103+
"type": "address"
104+
},
105+
{
106+
"internalType": "address",
107+
"name": "destination",
108+
"type": "address"
109+
}
110+
],
111+
"name": "transferCrossChain",
112+
"outputs": [],
113+
"stateMutability": "payable",
114+
"type": "function"
74115
}
75116
] as const;
76117

0 commit comments

Comments
 (0)