@quantabit/nft-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1200 @@
1
+ import { BaseApiClient } from '@quantabit/sdk-config';
2
+ import React, { useState, useCallback, useEffect, useMemo } from 'react';
3
+
4
+ /**
5
+ * NFT SDK - API 客户端
6
+ * NFT 系统后端接口封装
7
+ *
8
+ * 使用 BaseApiClient 基类简化代码
9
+ */
10
+
11
+
12
+ /**
13
+ * NFT API 客户端
14
+ */
15
+ class NftApiClient extends BaseApiClient {
16
+ constructor(config = {}) {
17
+ super('/nft', config);
18
+ }
19
+
20
+ // ============ NFT 查询 ============
21
+
22
+ /**
23
+ * 获取 NFT 列表
24
+ * @param {Object} params - 查询参数
25
+ */
26
+ async getNFTs(params = {}) {
27
+ return this.get('/list', params);
28
+ }
29
+
30
+ /**
31
+ * 兼容旧 Hook 命名
32
+ */
33
+ async getNfts(params = {}) {
34
+ return this.getNFTs(params);
35
+ }
36
+
37
+ /**
38
+ * 获取 NFT 详情
39
+ * @param {string} nftId - NFT ID
40
+ */
41
+ async getNFT(nftId) {
42
+ return this.get(`/${nftId}`);
43
+ }
44
+
45
+ /**
46
+ * 兼容旧 Hook 命名
47
+ */
48
+ async getNftDetail(nftId) {
49
+ return this.getNFT(nftId);
50
+ }
51
+
52
+ /**
53
+ * 获取 NFT 元数据
54
+ * @param {string} nftId - NFT ID
55
+ */
56
+ async getMetadata(nftId) {
57
+ return this.get(`/${nftId}/metadata`);
58
+ }
59
+
60
+ /**
61
+ * 获取 NFT 历史
62
+ * @param {string} nftId - NFT ID
63
+ */
64
+ async getHistory(nftId) {
65
+ return this.get(`/${nftId}/history`);
66
+ }
67
+
68
+ // ============ 我的 NFT ============
69
+
70
+ /**
71
+ * 获取我的 NFT
72
+ * @param {Object} params - 查询参数
73
+ */
74
+ async getMyNFTs(params = {}) {
75
+ return this.get('/my', params);
76
+ }
77
+
78
+ /**
79
+ * 兼容旧 Hook 命名
80
+ */
81
+ async getMyNfts(params = {}) {
82
+ return this.getMyNFTs(params);
83
+ }
84
+
85
+ /**
86
+ * 获取收藏的 NFT
87
+ * @param {Object} params - 查询参数
88
+ */
89
+ async getFavorites(params = {}) {
90
+ return this.get('/my/favorites', params);
91
+ }
92
+
93
+ /**
94
+ * 收藏 NFT
95
+ * @param {string} nftId - NFT ID
96
+ */
97
+ async favorite(nftId) {
98
+ return this.post(`/${nftId}/favorite`);
99
+ }
100
+ async addFavorite(nftId) {
101
+ return this.favorite(nftId);
102
+ }
103
+
104
+ /**
105
+ * 取消收藏
106
+ * @param {string} nftId - NFT ID
107
+ */
108
+ async unfavorite(nftId) {
109
+ return this.delete(`/${nftId}/favorite`);
110
+ }
111
+ async removeFavorite(nftId) {
112
+ return this.unfavorite(nftId);
113
+ }
114
+
115
+ // ============ NFT 铸造 ============
116
+
117
+ /**
118
+ * 铸造 NFT
119
+ * @param {Object} data - 铸造数据
120
+ */
121
+ async mint(data) {
122
+ return this.post('/mint', data);
123
+ }
124
+ async mintNft(data) {
125
+ return this.mint(data);
126
+ }
127
+
128
+ /**
129
+ * 批量铸造
130
+ * @param {Object[]} items - NFT 数据列表
131
+ */
132
+ async batchMint(items) {
133
+ return this.post('/mint/batch', {
134
+ items
135
+ });
136
+ }
137
+
138
+ /**
139
+ * 获取铸造费用估算
140
+ * @param {Object} data - 铸造数据
141
+ */
142
+ async estimateMintFee(data) {
143
+ return this.post('/mint/estimate', data);
144
+ }
145
+
146
+ // ============ NFT 转移 ============
147
+
148
+ /**
149
+ * 转移 NFT
150
+ * @param {string} nftId - NFT ID
151
+ * @param {string} toAddress - 目标地址
152
+ */
153
+ async transfer(nftId, toAddress) {
154
+ return this.post(`/${nftId}/transfer`, {
155
+ to_address: toAddress
156
+ });
157
+ }
158
+
159
+ /**
160
+ * 批量转移
161
+ * @param {Object[]} transfers - 转移列表
162
+ */
163
+ async batchTransfer(transfers) {
164
+ return this.post('/transfer/batch', {
165
+ transfers
166
+ });
167
+ }
168
+
169
+ // ============ 合集 ============
170
+
171
+ /**
172
+ * 获取合集列表
173
+ * @param {Object} params - 查询参数
174
+ */
175
+ async getCollections(params = {}) {
176
+ return this.get('/collections', params);
177
+ }
178
+
179
+ /**
180
+ * 获取合集详情
181
+ * @param {string} collectionId - 合集 ID
182
+ */
183
+ async getCollection(collectionId) {
184
+ return this.get(`/collections/${collectionId}`);
185
+ }
186
+
187
+ /**
188
+ * 获取合集中的 NFT
189
+ * @param {string} collectionId - 合集 ID
190
+ * @param {Object} params - 查询参数
191
+ */
192
+ async getCollectionNFTs(collectionId, params = {}) {
193
+ return this.get(`/collections/${collectionId}/nfts`, params);
194
+ }
195
+
196
+ /**
197
+ * 创建合集
198
+ * @param {Object} data - 合集数据
199
+ */
200
+ async createCollection(data) {
201
+ return this.post('/collections', data);
202
+ }
203
+
204
+ // ============ 市场 ============
205
+
206
+ /**
207
+ * 上架 NFT
208
+ * @param {string} nftId - NFT ID
209
+ * @param {Object} listing - 上架信息
210
+ */
211
+ async list(nftId, listing) {
212
+ return this.post(`/${nftId}/list`, listing);
213
+ }
214
+
215
+ /**
216
+ * 下架 NFT
217
+ * @param {string} nftId - NFT ID
218
+ */
219
+ async unlist(nftId) {
220
+ return this.post(`/${nftId}/unlist`);
221
+ }
222
+
223
+ /**
224
+ * 购买 NFT
225
+ * @param {string} nftId - NFT ID
226
+ */
227
+ async buy(nftId) {
228
+ return this.post(`/${nftId}/buy`);
229
+ }
230
+ async buyNft(nftId, price, options = {}) {
231
+ return this.post(`/${nftId}/buy`, {
232
+ price,
233
+ ...options
234
+ });
235
+ }
236
+
237
+ /**
238
+ * 出价
239
+ * @param {string} nftId - NFT ID
240
+ * @param {Object} offer - 出价信息
241
+ */
242
+ async makeOffer(nftId, offer) {
243
+ return this.post(`/${nftId}/offer`, offer);
244
+ }
245
+
246
+ /**
247
+ * 接受出价
248
+ * @param {string} nftId - NFT ID
249
+ * @param {string} offerId - 出价 ID
250
+ */
251
+ async acceptOffer(nftId, offerId) {
252
+ return this.post(`/${nftId}/offer/${offerId}/accept`);
253
+ }
254
+
255
+ // ============ 链上证明 ============
256
+
257
+ /**
258
+ * 提交 NFT 链上交易哈希
259
+ */
260
+ async submitChainTx(nftId, txHash, data = {}) {
261
+ return this.post(`/${nftId}/chain/submit`, {
262
+ tx_hash: txHash,
263
+ chain: 'qbit',
264
+ network: 'mainnet',
265
+ ...data
266
+ });
267
+ }
268
+
269
+ /**
270
+ * 查询 NFT 链上确认状态
271
+ */
272
+ async getChainStatus(nftId) {
273
+ return this.get(`/${nftId}/chain/status`);
274
+ }
275
+ async getAuction(nftId) {
276
+ return this.get(`/${nftId}/auction`);
277
+ }
278
+ async placeBid(nftId, amount, options = {}) {
279
+ return this.post(`/${nftId}/bid`, {
280
+ amount,
281
+ ...options
282
+ });
283
+ }
284
+ }
285
+
286
+ // 创建默认实例
287
+ const nftApi = new NftApiClient();
288
+
289
+ const NFT_ANCHOR_NAMESPACE = 'qbit_nft';
290
+ function pickNftId(nft) {
291
+ return nft?.id || nft?.nft_id || nft?.nftId || nft?.token_id || nft?.tokenId || nft?.mint;
292
+ }
293
+ function pickContentHash(nft, options) {
294
+ return options.contentHash || nft?.content_hash || nft?.metadata_hash || nft?.image_hash || nft?.hash || null;
295
+ }
296
+ async function buildNftAnchorMemo(nft, options = {}) {
297
+ const {
298
+ buildQBitAnchorMemo
299
+ } = await import('@quantabit/qbit-chain-sdk');
300
+ const nftId = options.nftId || pickNftId(nft);
301
+ const contentHash = pickContentHash(nft, options);
302
+ return buildQBitAnchorMemo({
303
+ action: options.action || 'nft_anchor',
304
+ subject: nftId ? `nft:${nftId}` : 'nft',
305
+ resource_id: nftId,
306
+ content_hash: contentHash,
307
+ version: options.version,
308
+ timestamp: options.timestamp,
309
+ extra: {
310
+ collection_id: nft?.collection_id || nft?.collectionId || nft?.collection?.id,
311
+ owner: nft?.owner || nft?.owner_address,
312
+ creator_did: nft?.creator_did,
313
+ ...options.extra
314
+ }
315
+ }, {
316
+ namespace: options.namespace || NFT_ANCHOR_NAMESPACE,
317
+ maxBytes: options.maxBytes
318
+ });
319
+ }
320
+ function buildNftChainSubmit(nftId, txHash, options = {}) {
321
+ return {
322
+ nft_id: nftId,
323
+ tx_hash: txHash,
324
+ chain: options.chain || 'qbit',
325
+ network: options.network || 'mainnet',
326
+ action: options.action || 'nft_anchor',
327
+ memo: options.memo,
328
+ content_hash: options.contentHash,
329
+ ...options.extra
330
+ };
331
+ }
332
+
333
+ /**
334
+ * NFT SDK - 类型定义
335
+ */
336
+
337
+ const NftRarity = {};
338
+ const NftStatus = {};
339
+
340
+ /**
341
+ * NFT SDK - 国际化
342
+ * NFT系统多语言支持
343
+ */
344
+
345
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
346
+ const messages = {
347
+ zh: {
348
+ // NFT
349
+ nft: 'NFT',
350
+ nfts: 'NFT',
351
+ myNfts: '我的NFT',
352
+ allNfts: '全部NFT',
353
+ // 详情
354
+ name: '名称',
355
+ description: '描述',
356
+ collection: '系列',
357
+ creator: '创作者',
358
+ owner: '拥有者',
359
+ tokenId: 'Token ID',
360
+ contractAddress: '合约地址',
361
+ contract: '合约地址',
362
+ blockchain: '区块链',
363
+ chain: '区块链',
364
+ history: '交易历史',
365
+ // 属性
366
+ attributes: '属性',
367
+ properties: '属性',
368
+ rarity: '稀有度',
369
+ common: '普通',
370
+ rare: '稀有',
371
+ epic: '史诗',
372
+ legendary: '传说',
373
+ // 交易
374
+ price: '价格',
375
+ lastSale: '最近成交',
376
+ buy: '购买',
377
+ sell: '出售',
378
+ transfer: '转移',
379
+ list: '上架',
380
+ delist: '下架',
381
+ unlist: '下架',
382
+ // 竞拍
383
+ auction: '拍卖',
384
+ bid: '出价',
385
+ currentBid: '当前出价',
386
+ minBid: '最低出价',
387
+ placeBid: '出价',
388
+ endTime: '结束时间',
389
+ // 铸造
390
+ mint: '铸造',
391
+ minting: '铸造中...',
392
+ mintSuccess: '铸造成功',
393
+ mintFailed: '铸造失败',
394
+ supply: '发行量',
395
+ // 筛选
396
+ filter: '筛选',
397
+ sort: '排序',
398
+ priceHighToLow: '价格从高到低',
399
+ priceLowToHigh: '价格从低到高',
400
+ newest: '最新',
401
+ oldest: '最早',
402
+ // 状态
403
+ loading: '加载中...',
404
+ noNfts: '暂无NFT',
405
+ error: '加载失败',
406
+ // 其他
407
+ viewOnExplorer: '在浏览器中查看',
408
+ share: '分享',
409
+ favorite: '收藏',
410
+ favorites: '收藏'
411
+ },
412
+ en: {
413
+ nft: 'NFT',
414
+ nfts: 'NFTs',
415
+ myNfts: 'My NFTs',
416
+ allNfts: 'All NFTs',
417
+ name: 'Name',
418
+ description: 'Description',
419
+ collection: 'Collection',
420
+ creator: 'Creator',
421
+ owner: 'Owner',
422
+ tokenId: 'Token ID',
423
+ contractAddress: 'Contract',
424
+ contract: 'Contract',
425
+ blockchain: 'Blockchain',
426
+ chain: 'Blockchain',
427
+ history: 'History',
428
+ attributes: 'Attributes',
429
+ properties: 'Properties',
430
+ rarity: 'Rarity',
431
+ common: 'Common',
432
+ rare: 'Rare',
433
+ epic: 'Epic',
434
+ legendary: 'Legendary',
435
+ price: 'Price',
436
+ lastSale: 'Last Sale',
437
+ buy: 'Buy',
438
+ sell: 'Sell',
439
+ transfer: 'Transfer',
440
+ list: 'List',
441
+ delist: 'Delist',
442
+ unlist: 'Delist',
443
+ auction: 'Auction',
444
+ bid: 'Bid',
445
+ currentBid: 'Current Bid',
446
+ minBid: 'Min Bid',
447
+ placeBid: 'Place Bid',
448
+ endTime: 'End Time',
449
+ mint: 'Mint',
450
+ minting: 'Minting...',
451
+ mintSuccess: 'Minted!',
452
+ mintFailed: 'Mint Failed',
453
+ supply: 'Supply',
454
+ filter: 'Filter',
455
+ sort: 'Sort',
456
+ priceHighToLow: 'Price: High to Low',
457
+ priceLowToHigh: 'Price: Low to High',
458
+ newest: 'Newest',
459
+ oldest: 'Oldest',
460
+ loading: 'Loading...',
461
+ noNfts: 'No NFTs',
462
+ error: 'Error',
463
+ viewOnExplorer: 'View on Explorer',
464
+ share: 'Share',
465
+ favorite: 'Favorite',
466
+ favorites: 'Favorites'
467
+ },
468
+ ja: {
469
+ nft: 'NFT',
470
+ nfts: 'NFT',
471
+ myNfts: 'マイNFT',
472
+ allNfts: 'すべてのNFT',
473
+ name: '名前',
474
+ description: '説明',
475
+ collection: 'コレクション',
476
+ creator: 'クリエイター',
477
+ owner: 'オーナー',
478
+ tokenId: 'トークンID',
479
+ contractAddress: 'コントラクト',
480
+ contract: 'コントラクト',
481
+ blockchain: 'ブロックチェーン',
482
+ chain: 'ブロックチェーン',
483
+ history: '取引履歴',
484
+ attributes: '属性',
485
+ properties: 'プロパティ',
486
+ rarity: 'レアリティ',
487
+ common: 'コモン',
488
+ rare: 'レア',
489
+ epic: 'エピック',
490
+ legendary: 'レジェンダリー',
491
+ price: '価格',
492
+ lastSale: '最終取引',
493
+ buy: '購入',
494
+ sell: '売却',
495
+ transfer: '転送',
496
+ list: '出品',
497
+ delist: '取り下げ',
498
+ unlist: '取り下げ',
499
+ auction: 'オークション',
500
+ bid: '入札',
501
+ currentBid: '現在の入札',
502
+ minBid: '最低入札',
503
+ placeBid: '入札する',
504
+ endTime: '終了時間',
505
+ mint: 'ミント',
506
+ minting: 'ミント中...',
507
+ mintSuccess: 'ミント完了',
508
+ mintFailed: 'ミント失敗',
509
+ supply: '発行数',
510
+ filter: 'フィルター',
511
+ sort: '並び替え',
512
+ priceHighToLow: '価格: 高い順',
513
+ priceLowToHigh: '価格: 安い順',
514
+ newest: '新着順',
515
+ oldest: '古い順',
516
+ loading: '読み込み中...',
517
+ noNfts: 'NFTなし',
518
+ error: 'エラー',
519
+ viewOnExplorer: 'エクスプローラーで見る',
520
+ share: '共有',
521
+ favorite: 'お気に入り',
522
+ favorites: 'お気に入り'
523
+ },
524
+ ko: {
525
+ nft: 'NFT',
526
+ nfts: 'NFT',
527
+ myNfts: '내 NFT',
528
+ allNfts: '모든 NFT',
529
+ name: '이름',
530
+ description: '설명',
531
+ collection: '컬렉션',
532
+ creator: '제작자',
533
+ owner: '소유자',
534
+ tokenId: '토큰 ID',
535
+ contractAddress: '컨트랙트',
536
+ contract: '컨트랙트',
537
+ blockchain: '블록체인',
538
+ chain: '블록체인',
539
+ history: '거래 내역',
540
+ attributes: '속성',
541
+ properties: '속성',
542
+ rarity: '희귀도',
543
+ common: '일반',
544
+ rare: '레어',
545
+ epic: '에픽',
546
+ legendary: '전설',
547
+ price: '가격',
548
+ lastSale: '최근 거래',
549
+ buy: '구매',
550
+ sell: '판매',
551
+ transfer: '전송',
552
+ list: '등록',
553
+ delist: '등록 취소',
554
+ unlist: '등록 취소',
555
+ auction: '경매',
556
+ bid: '입찰',
557
+ currentBid: '현재 입찰',
558
+ minBid: '최소 입찰',
559
+ placeBid: '입찰하기',
560
+ endTime: '종료 시간',
561
+ mint: '민팅',
562
+ minting: '민팅 중...',
563
+ mintSuccess: '민팅 완료',
564
+ mintFailed: '민팅 실패',
565
+ supply: '발행량',
566
+ filter: '필터',
567
+ sort: '정렬',
568
+ priceHighToLow: '가격: 높은 순',
569
+ priceLowToHigh: '가격: 낮은 순',
570
+ newest: '최신순',
571
+ oldest: '오래된 순',
572
+ loading: '로딩 중...',
573
+ noNfts: 'NFT 없음',
574
+ error: '오류',
575
+ viewOnExplorer: '탐색기에서 보기',
576
+ share: '공유',
577
+ favorite: '즐겨찾기',
578
+ favorites: '즐겨찾기'
579
+ }
580
+ };
581
+ let currentLanguage = 'zh';
582
+ function setLanguage(lang) {
583
+ if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
584
+ }
585
+ function getLanguage() {
586
+ return currentLanguage;
587
+ }
588
+ function t(key) {
589
+ return (messages[currentLanguage] || messages.en)[key] || key;
590
+ }
591
+
592
+ /**
593
+ * NFT SDK - React Hooks
594
+ * NFT系统相关的状态管理
595
+ */
596
+
597
+
598
+ /**
599
+ * 获取NFT列表
600
+ */
601
+ function useNfts(options = {}) {
602
+ const [nfts, setNfts] = useState([]);
603
+ const [loading, setLoading] = useState(true);
604
+ const [error, setError] = useState(null);
605
+ const [pagination, setPagination] = useState({
606
+ page: 1,
607
+ total: 0,
608
+ hasMore: true
609
+ });
610
+ const {
611
+ collection,
612
+ owner,
613
+ sort = 'newest',
614
+ limit = 20
615
+ } = options;
616
+ const fetchNfts = useCallback(async (page = 1) => {
617
+ try {
618
+ setLoading(true);
619
+ const response = await nftApi.getNfts({
620
+ collection,
621
+ owner,
622
+ sort,
623
+ page,
624
+ limit
625
+ });
626
+ if (page === 1) {
627
+ setNfts(response.data || []);
628
+ } else {
629
+ setNfts(prev => [...prev, ...(response.data || [])]);
630
+ }
631
+ setPagination({
632
+ page,
633
+ total: response.total || 0,
634
+ hasMore: response.hasMore || false
635
+ });
636
+ setError(null);
637
+ } catch (err) {
638
+ setError(err.message);
639
+ } finally {
640
+ setLoading(false);
641
+ }
642
+ }, [collection, owner, sort, limit]);
643
+ useEffect(() => {
644
+ fetchNfts(1);
645
+ }, [fetchNfts]);
646
+ const loadMore = useCallback(() => {
647
+ if (!loading && pagination.hasMore) {
648
+ fetchNfts(pagination.page + 1);
649
+ }
650
+ }, [loading, pagination, fetchNfts]);
651
+ return {
652
+ nfts,
653
+ loading,
654
+ error,
655
+ pagination,
656
+ loadMore,
657
+ refresh: () => fetchNfts(1)
658
+ };
659
+ }
660
+
661
+ /**
662
+ * 获取NFT详情
663
+ */
664
+ function useNftDetail(tokenId) {
665
+ const [nft, setNft] = useState(null);
666
+ const [loading, setLoading] = useState(true);
667
+ const [error, setError] = useState(null);
668
+ const fetchDetail = useCallback(async () => {
669
+ if (!tokenId) return;
670
+ try {
671
+ setLoading(true);
672
+ const response = await nftApi.getNftDetail(tokenId);
673
+ setNft(response);
674
+ setError(null);
675
+ } catch (err) {
676
+ setError(err.message);
677
+ } finally {
678
+ setLoading(false);
679
+ }
680
+ }, [tokenId]);
681
+ useEffect(() => {
682
+ fetchDetail();
683
+ }, [fetchDetail]);
684
+ return {
685
+ nft,
686
+ loading,
687
+ error,
688
+ refresh: fetchDetail
689
+ };
690
+ }
691
+
692
+ /**
693
+ * 我的NFT
694
+ */
695
+ function useMyNfts() {
696
+ const [nfts, setNfts] = useState([]);
697
+ const [loading, setLoading] = useState(true);
698
+ const [error, setError] = useState(null);
699
+ const fetchNfts = useCallback(async () => {
700
+ try {
701
+ setLoading(true);
702
+ const response = await nftApi.getMyNfts();
703
+ setNfts(response.data || []);
704
+ setError(null);
705
+ } catch (err) {
706
+ setError(err.message);
707
+ } finally {
708
+ setLoading(false);
709
+ }
710
+ }, []);
711
+ useEffect(() => {
712
+ fetchNfts();
713
+ }, [fetchNfts]);
714
+
715
+ // 按系列分组
716
+ const grouped = useMemo(() => {
717
+ const groups = {};
718
+ nfts.forEach(nft => {
719
+ const collection = nft.collection?.name || 'Others';
720
+ if (!groups[collection]) groups[collection] = [];
721
+ groups[collection].push(nft);
722
+ });
723
+ return groups;
724
+ }, [nfts]);
725
+ return {
726
+ nfts,
727
+ grouped,
728
+ totalCount: nfts.length,
729
+ loading,
730
+ error,
731
+ refresh: fetchNfts
732
+ };
733
+ }
734
+
735
+ /**
736
+ * NFT收藏
737
+ */
738
+ function useFavoriteNfts() {
739
+ const [favorites, setFavorites] = useState([]);
740
+ const [loading, setLoading] = useState(true);
741
+ const fetchFavorites = useCallback(async () => {
742
+ try {
743
+ setLoading(true);
744
+ const response = await nftApi.getFavorites();
745
+ setFavorites(response.data || []);
746
+ } catch (err) {
747
+ console.error('Get favorites error:', err);
748
+ } finally {
749
+ setLoading(false);
750
+ }
751
+ }, []);
752
+ useEffect(() => {
753
+ fetchFavorites();
754
+ }, [fetchFavorites]);
755
+ const toggleFavorite = useCallback(async tokenId => {
756
+ try {
757
+ const isFav = favorites.some(f => f.tokenId === tokenId);
758
+ if (isFav) {
759
+ await nftApi.removeFavorite(tokenId);
760
+ setFavorites(prev => prev.filter(f => f.tokenId !== tokenId));
761
+ } else {
762
+ await nftApi.addFavorite(tokenId);
763
+ fetchFavorites(); // 重新加载
764
+ }
765
+ } catch (err) {
766
+ console.error('Toggle favorite error:', err);
767
+ }
768
+ }, [favorites, fetchFavorites]);
769
+ const isFavorite = useCallback(tokenId => {
770
+ return favorites.some(f => f.tokenId === tokenId);
771
+ }, [favorites]);
772
+ return {
773
+ favorites,
774
+ toggleFavorite,
775
+ isFavorite,
776
+ loading,
777
+ refresh: fetchFavorites
778
+ };
779
+ }
780
+
781
+ /**
782
+ * 购买NFT
783
+ */
784
+ function useBuyNft() {
785
+ const [loading, setLoading] = useState(false);
786
+ const [error, setError] = useState(null);
787
+ const [result, setResult] = useState(null);
788
+ const buy = useCallback(async (tokenId, price) => {
789
+ try {
790
+ setLoading(true);
791
+ setError(null);
792
+ const response = await nftApi.buyNft(tokenId, price);
793
+ setResult(response);
794
+ return response;
795
+ } catch (err) {
796
+ setError(err.message);
797
+ throw err;
798
+ } finally {
799
+ setLoading(false);
800
+ }
801
+ }, []);
802
+ const reset = useCallback(() => {
803
+ setResult(null);
804
+ setError(null);
805
+ }, []);
806
+ return {
807
+ buy,
808
+ loading,
809
+ error,
810
+ result,
811
+ reset
812
+ };
813
+ }
814
+
815
+ /**
816
+ * 铸造NFT
817
+ */
818
+ function useMintNft() {
819
+ const [loading, setLoading] = useState(false);
820
+ const [error, setError] = useState(null);
821
+ const [result, setResult] = useState(null);
822
+ const [progress, setProgress] = useState(0);
823
+ const mint = useCallback(async metadata => {
824
+ try {
825
+ setLoading(true);
826
+ setError(null);
827
+ setProgress(0);
828
+
829
+ // 上传资源
830
+ setProgress(30);
831
+ const response = await nftApi.mintNft(metadata);
832
+ setProgress(100);
833
+ setResult(response);
834
+ return response;
835
+ } catch (err) {
836
+ setError(err.message);
837
+ throw err;
838
+ } finally {
839
+ setLoading(false);
840
+ }
841
+ }, []);
842
+ const reset = useCallback(() => {
843
+ setResult(null);
844
+ setError(null);
845
+ setProgress(0);
846
+ }, []);
847
+ return {
848
+ mint,
849
+ loading,
850
+ error,
851
+ result,
852
+ progress,
853
+ reset
854
+ };
855
+ }
856
+
857
+ /**
858
+ * NFT拍卖
859
+ */
860
+ function useNftAuction(tokenId) {
861
+ const [auction, setAuction] = useState(null);
862
+ const [loading, setLoading] = useState(true);
863
+ const [bidding, setBidding] = useState(false);
864
+ const fetchAuction = useCallback(async () => {
865
+ if (!tokenId) return;
866
+ try {
867
+ setLoading(true);
868
+ const response = await nftApi.getAuction(tokenId);
869
+ setAuction(response);
870
+ } catch (err) {
871
+ console.error('Get auction error:', err);
872
+ } finally {
873
+ setLoading(false);
874
+ }
875
+ }, [tokenId]);
876
+ useEffect(() => {
877
+ fetchAuction();
878
+ }, [fetchAuction]);
879
+ const placeBid = useCallback(async amount => {
880
+ try {
881
+ setBidding(true);
882
+ await nftApi.placeBid(tokenId, amount);
883
+ await fetchAuction();
884
+ } catch (err) {
885
+ console.error('Place bid error:', err);
886
+ throw err;
887
+ } finally {
888
+ setBidding(false);
889
+ }
890
+ }, [tokenId, fetchAuction]);
891
+ return {
892
+ auction,
893
+ placeBid,
894
+ loading,
895
+ bidding,
896
+ refresh: fetchAuction
897
+ };
898
+ }
899
+
900
+ /**
901
+ * NFT SDK - React 组件
902
+ * NFT系统可视化组件
903
+ */
904
+
905
+
906
+ /**
907
+ * NFT卡片
908
+ */
909
+ function NftCard({
910
+ nft,
911
+ onClick,
912
+ showPrice = true
913
+ }) {
914
+ const {
915
+ isFavorite,
916
+ toggleFavorite
917
+ } = useFavoriteNfts();
918
+ const isFav = isFavorite(nft.tokenId);
919
+ return /*#__PURE__*/React.createElement("div", {
920
+ className: "eco-nft-card",
921
+ onClick: () => onClick?.(nft)
922
+ }, /*#__PURE__*/React.createElement("div", {
923
+ className: "eco-nft-image"
924
+ }, /*#__PURE__*/React.createElement("img", {
925
+ src: nft.image,
926
+ alt: nft.name
927
+ }), nft.rarity && /*#__PURE__*/React.createElement("span", {
928
+ className: `eco-nft-rarity ${nft.rarity}`
929
+ }, t(nft.rarity)), /*#__PURE__*/React.createElement("button", {
930
+ className: `eco-nft-fav ${isFav ? 'active' : ''}`,
931
+ onClick: e => {
932
+ e.stopPropagation();
933
+ toggleFavorite(nft.tokenId);
934
+ }
935
+ }, isFav ? '❤️' : '🤍')), /*#__PURE__*/React.createElement("div", {
936
+ className: "eco-nft-info"
937
+ }, /*#__PURE__*/React.createElement("span", {
938
+ className: "eco-nft-collection"
939
+ }, nft.collection?.name), /*#__PURE__*/React.createElement("h4", {
940
+ className: "eco-nft-name"
941
+ }, nft.name), showPrice && nft.price && /*#__PURE__*/React.createElement("div", {
942
+ className: "eco-nft-price"
943
+ }, /*#__PURE__*/React.createElement("span", {
944
+ className: "eco-nft-price-value"
945
+ }, nft.price, " ETH"))));
946
+ }
947
+
948
+ /**
949
+ * NFT网格列表
950
+ */
951
+ function NftGrid({
952
+ nfts,
953
+ onNftClick,
954
+ loading
955
+ }) {
956
+ if (loading && nfts.length === 0) {
957
+ return /*#__PURE__*/React.createElement("div", {
958
+ className: "eco-nft-grid eco-nft-loading"
959
+ }, /*#__PURE__*/React.createElement("div", {
960
+ className: "eco-nft-spinner"
961
+ }));
962
+ }
963
+ if (nfts.length === 0) {
964
+ return /*#__PURE__*/React.createElement("div", {
965
+ className: "eco-nft-grid eco-nft-empty"
966
+ }, /*#__PURE__*/React.createElement("span", {
967
+ className: "eco-nft-empty-icon"
968
+ }, "\uD83D\uDDBC\uFE0F"), /*#__PURE__*/React.createElement("span", null, t('noNfts')));
969
+ }
970
+ return /*#__PURE__*/React.createElement("div", {
971
+ className: "eco-nft-grid"
972
+ }, nfts.map(nft => /*#__PURE__*/React.createElement(NftCard, {
973
+ key: nft.tokenId,
974
+ nft: nft,
975
+ onClick: onNftClick
976
+ })));
977
+ }
978
+
979
+ /**
980
+ * NFT详情
981
+ */
982
+ function NftDetail({
983
+ tokenId,
984
+ onClose
985
+ }) {
986
+ const {
987
+ nft,
988
+ loading
989
+ } = useNftDetail(tokenId);
990
+ const {
991
+ buy,
992
+ loading: buying
993
+ } = useBuyNft();
994
+ const handleBuy = useCallback(async () => {
995
+ try {
996
+ await buy(tokenId, nft.price);
997
+ onClose?.();
998
+ } catch (err) {
999
+ console.error('Buy error:', err);
1000
+ }
1001
+ }, [buy, tokenId, nft, onClose]);
1002
+ if (loading) {
1003
+ return /*#__PURE__*/React.createElement("div", {
1004
+ className: "eco-nft-detail eco-nft-loading"
1005
+ }, /*#__PURE__*/React.createElement("div", {
1006
+ className: "eco-nft-spinner"
1007
+ }));
1008
+ }
1009
+ if (!nft) return null;
1010
+ return /*#__PURE__*/React.createElement("div", {
1011
+ className: "eco-nft-detail"
1012
+ }, /*#__PURE__*/React.createElement("div", {
1013
+ className: "eco-nft-detail-image"
1014
+ }, /*#__PURE__*/React.createElement("img", {
1015
+ src: nft.image,
1016
+ alt: nft.name
1017
+ })), /*#__PURE__*/React.createElement("div", {
1018
+ className: "eco-nft-detail-content"
1019
+ }, /*#__PURE__*/React.createElement("span", {
1020
+ className: "eco-nft-detail-collection"
1021
+ }, nft.collection?.name), /*#__PURE__*/React.createElement("h2", {
1022
+ className: "eco-nft-detail-name"
1023
+ }, nft.name), /*#__PURE__*/React.createElement("div", {
1024
+ className: "eco-nft-detail-owner"
1025
+ }, /*#__PURE__*/React.createElement("span", null, t('owner')), /*#__PURE__*/React.createElement("span", null, nft.owner?.slice(0, 8), "...")), /*#__PURE__*/React.createElement("p", {
1026
+ className: "eco-nft-detail-desc"
1027
+ }, nft.description), nft.attributes?.length > 0 && /*#__PURE__*/React.createElement("div", {
1028
+ className: "eco-nft-attributes"
1029
+ }, /*#__PURE__*/React.createElement("h4", null, t('attributes')), /*#__PURE__*/React.createElement("div", {
1030
+ className: "eco-nft-attributes-grid"
1031
+ }, nft.attributes.map((attr, qbit) => /*#__PURE__*/React.createElement("div", {
1032
+ key: qbit,
1033
+ className: "eco-nft-attribute"
1034
+ }, /*#__PURE__*/React.createElement("span", {
1035
+ className: "eco-nft-attribute-type"
1036
+ }, attr.trait_type), /*#__PURE__*/React.createElement("span", {
1037
+ className: "eco-nft-attribute-value"
1038
+ }, attr.value))))), nft.isForSale && /*#__PURE__*/React.createElement("div", {
1039
+ className: "eco-nft-detail-buy"
1040
+ }, /*#__PURE__*/React.createElement("div", {
1041
+ className: "eco-nft-detail-price"
1042
+ }, /*#__PURE__*/React.createElement("span", null, t('price')), /*#__PURE__*/React.createElement("span", {
1043
+ className: "eco-nft-detail-price-value"
1044
+ }, nft.price, " ETH")), /*#__PURE__*/React.createElement("button", {
1045
+ className: "eco-nft-buy-btn",
1046
+ onClick: handleBuy,
1047
+ disabled: buying
1048
+ }, buying ? '...' : t('buy')))));
1049
+ }
1050
+
1051
+ /**
1052
+ * 我的NFT收藏页
1053
+ */
1054
+ function MyNftsPage() {
1055
+ const {
1056
+ nfts,
1057
+ grouped,
1058
+ totalCount,
1059
+ loading
1060
+ } = useMyNfts();
1061
+ const [selectedNft, setSelectedNft] = useState(null);
1062
+ return /*#__PURE__*/React.createElement("div", {
1063
+ className: "eco-nft-my-page"
1064
+ }, /*#__PURE__*/React.createElement("div", {
1065
+ className: "eco-nft-my-header"
1066
+ }, /*#__PURE__*/React.createElement("h2", null, t('myNfts')), /*#__PURE__*/React.createElement("span", {
1067
+ className: "eco-nft-count"
1068
+ }, totalCount, " NFTs")), Object.entries(grouped).map(([collection, items]) => /*#__PURE__*/React.createElement("div", {
1069
+ key: collection,
1070
+ className: "eco-nft-collection-section"
1071
+ }, /*#__PURE__*/React.createElement("h3", null, collection), /*#__PURE__*/React.createElement(NftGrid, {
1072
+ nfts: items,
1073
+ loading: loading,
1074
+ onNftClick: setSelectedNft
1075
+ }))), selectedNft && /*#__PURE__*/React.createElement("div", {
1076
+ className: "eco-nft-modal"
1077
+ }, /*#__PURE__*/React.createElement("div", {
1078
+ className: "eco-nft-modal-overlay",
1079
+ onClick: () => setSelectedNft(null)
1080
+ }), /*#__PURE__*/React.createElement("div", {
1081
+ className: "eco-nft-modal-content"
1082
+ }, /*#__PURE__*/React.createElement("button", {
1083
+ className: "eco-nft-modal-close",
1084
+ onClick: () => setSelectedNft(null)
1085
+ }, "\xD7"), /*#__PURE__*/React.createElement(NftDetail, {
1086
+ tokenId: selectedNft.tokenId,
1087
+ onClose: () => setSelectedNft(null)
1088
+ }))));
1089
+ }
1090
+
1091
+ /**
1092
+ * 拍卖卡片
1093
+ */
1094
+ function AuctionCard({
1095
+ tokenId
1096
+ }) {
1097
+ const {
1098
+ auction,
1099
+ placeBid,
1100
+ bidding,
1101
+ loading
1102
+ } = useNftAuction(tokenId);
1103
+ const [bidAmount, setBidAmount] = useState('');
1104
+ const handleBid = useCallback(async () => {
1105
+ if (!bidAmount) return;
1106
+ try {
1107
+ await placeBid(parseFloat(bidAmount));
1108
+ setBidAmount('');
1109
+ } catch (err) {
1110
+ console.error('Bid error:', err);
1111
+ }
1112
+ }, [placeBid, bidAmount]);
1113
+ if (loading) {
1114
+ return /*#__PURE__*/React.createElement("div", {
1115
+ className: "eco-nft-auction eco-nft-loading"
1116
+ }, /*#__PURE__*/React.createElement("div", {
1117
+ className: "eco-nft-spinner"
1118
+ }));
1119
+ }
1120
+ if (!auction) return null;
1121
+ return /*#__PURE__*/React.createElement("div", {
1122
+ className: "eco-nft-auction"
1123
+ }, /*#__PURE__*/React.createElement("h4", null, t('auction')), /*#__PURE__*/React.createElement("div", {
1124
+ className: "eco-nft-auction-info"
1125
+ }, /*#__PURE__*/React.createElement("div", {
1126
+ className: "eco-nft-auction-item"
1127
+ }, /*#__PURE__*/React.createElement("span", null, t('currentBid')), /*#__PURE__*/React.createElement("span", {
1128
+ className: "eco-nft-auction-value"
1129
+ }, auction.currentBid, " ETH")), /*#__PURE__*/React.createElement("div", {
1130
+ className: "eco-nft-auction-item"
1131
+ }, /*#__PURE__*/React.createElement("span", null, t('endTime')), /*#__PURE__*/React.createElement("span", null, new Date(auction.endTime).toLocaleString()))), /*#__PURE__*/React.createElement("div", {
1132
+ className: "eco-nft-auction-bid"
1133
+ }, /*#__PURE__*/React.createElement("input", {
1134
+ type: "number",
1135
+ value: bidAmount,
1136
+ onChange: e => setBidAmount(e.target.value),
1137
+ placeholder: `${t('minBid')}: ${auction.minBid} ETH`,
1138
+ step: "0.01"
1139
+ }), /*#__PURE__*/React.createElement("button", {
1140
+ className: "eco-nft-bid-btn",
1141
+ onClick: handleBid,
1142
+ disabled: bidding
1143
+ }, bidding ? '...' : t('placeBid'))));
1144
+ }
1145
+
1146
+ /**
1147
+ * NFT市场首页
1148
+ */
1149
+ function NftMarketplace({
1150
+ onNftClick
1151
+ }) {
1152
+ const [sort, setSort] = useState('newest');
1153
+ const {
1154
+ nfts,
1155
+ loading,
1156
+ pagination,
1157
+ loadMore
1158
+ } = useNfts({
1159
+ sort
1160
+ });
1161
+ return /*#__PURE__*/React.createElement("div", {
1162
+ className: "eco-nft-marketplace"
1163
+ }, /*#__PURE__*/React.createElement("div", {
1164
+ className: "eco-nft-toolbar"
1165
+ }, /*#__PURE__*/React.createElement("h2", null, t('allNfts')), /*#__PURE__*/React.createElement("select", {
1166
+ className: "eco-nft-sort",
1167
+ value: sort,
1168
+ onChange: e => setSort(e.target.value)
1169
+ }, /*#__PURE__*/React.createElement("option", {
1170
+ value: "newest"
1171
+ }, t('newest')), /*#__PURE__*/React.createElement("option", {
1172
+ value: "oldest"
1173
+ }, t('oldest')), /*#__PURE__*/React.createElement("option", {
1174
+ value: "priceHighToLow"
1175
+ }, t('priceHighToLow')), /*#__PURE__*/React.createElement("option", {
1176
+ value: "priceLowToHigh"
1177
+ }, t('priceLowToHigh')))), /*#__PURE__*/React.createElement(NftGrid, {
1178
+ nfts: nfts,
1179
+ loading: loading,
1180
+ onNftClick: onNftClick
1181
+ }), pagination.hasMore && /*#__PURE__*/React.createElement("button", {
1182
+ className: "eco-nft-load-more",
1183
+ onClick: loadMore,
1184
+ disabled: loading
1185
+ }, loading ? '...' : t('loading')));
1186
+ }
1187
+
1188
+ /**
1189
+ * @quantabit/nft-sdk
1190
+ * NFT System SDK - Full Version
1191
+ */
1192
+
1193
+ const getNfts = (...args) => nftApi.getNfts(...args);
1194
+ const getNftDetail = (...args) => nftApi.getNftDetail(...args);
1195
+ const getMyNfts = (...args) => nftApi.getMyNfts(...args);
1196
+ const buyNft = (...args) => nftApi.buyNft(...args);
1197
+ const mintNft = (...args) => nftApi.mintNft(...args);
1198
+
1199
+ export { AuctionCard, MyNftsPage, NFT_ANCHOR_NAMESPACE, NftApiClient, NftCard, NftDetail, NftGrid, NftMarketplace, NftRarity, NftStatus, SUPPORTED_LANGUAGES, buildNftAnchorMemo, buildNftChainSubmit, buyNft, getLanguage, getMyNfts, getNftDetail, getNfts, messages, mintNft, nftApi, setLanguage, t, useBuyNft, useFavoriteNfts, useMintNft, useMyNfts, useNftAuction, useNftDetail, useNfts };
1200
+ //# sourceMappingURL=index.esm.js.map