From 3d02e9a495836742c6b028aeb0645759c50fb978 Mon Sep 17 00:00:00 2001 From: lamdanghoang Date: Sun, 18 Jan 2026 03:44:53 +0700 Subject: [PATCH 1/4] add swap sui for wal --- app/marketplace/my-data/page.tsx | 93 ++++++++++---- bun.lock | 3 + config/walrus.ts | 12 ++ hooks/useWalrusPayment.ts | 206 +++++++++++++++++++++++++++++++ lib/walrus-payment.ts | 79 ++++++++++++ package.json | 1 + 6 files changed, 372 insertions(+), 22 deletions(-) create mode 100644 config/walrus.ts create mode 100644 hooks/useWalrusPayment.ts create mode 100644 lib/walrus-payment.ts diff --git a/app/marketplace/my-data/page.tsx b/app/marketplace/my-data/page.tsx index a711696..22d7aec 100644 --- a/app/marketplace/my-data/page.tsx +++ b/app/marketplace/my-data/page.tsx @@ -11,6 +11,7 @@ import { Transaction } from '@mysten/sui/transactions'; import { marketplaceConfig, MIST_PER_SUI } from '@/config/marketplace'; import { getMarketplaceTarget } from '@/lib/marketplace'; import { encryptForMarketplace } from '@/lib/seal'; +import { useWalrusPayment } from '@/hooks/useWalrusPayment'; // Type for Walrus write blob flow (raw bytes, no file metadata) interface WalrusBlobFlow { @@ -59,6 +60,7 @@ export default function MyDataPage() { const { data: listings, isLoading, refetch } = useOwnedListings(account?.address); const { data: balance } = useAccountBalance(); const { data: purchases, isLoading: isPurchasesLoading } = usePurchasedDatasets(account?.address); + const { ensureWalBalance, getStorageCost, checkBalance, formatWal, formatSui, getPrices, isLoading: isWalrusLoading } = useWalrusPayment(); const [activeTab, setActiveTab] = useState('uploads'); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); @@ -95,6 +97,13 @@ export default function MyDataPage() { }]); }; + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + const addLogWithDelay = async (step: string, status: TransactionLog['status'], message: string, details?: string, delayMs: number = 300) => { + await delay(delayMs); + addLog(step, status, message, details); + }; + const createFlow = useCallback(async (data: Uint8Array, _identifier: string) => { const walrus = await getWalrus(); @@ -167,25 +176,65 @@ export default function MyDataPage() { encryptedObj = encryptResult.encryptedHex.slice(0, 128); // Store first 128 chars as reference console.log('Encrypted data size:', dataToUpload.length); - addLog('encode', 'success', 'File encrypted with Seal'); + await addLogWithDelay('encode', 'success', 'File encrypted with Seal'); } catch (encryptError) { console.warn('Seal encryption failed, uploading unencrypted:', encryptError); // Fallback: upload unencrypted if Seal fails dataToUpload = uint8Array; encryptedObj = bytesToHex(uint8Array).slice(0, 128); - addLog('encode', 'success', 'File prepared (unencrypted fallback)'); + await addLogWithDelay('encode', 'success', 'File prepared (unencrypted fallback)'); } setEncryptedObject(encryptedObj); + // Step 0.5: Check WAL balance for Walrus storage cost (auto-swap SUI → WAL if needed) + setCurrentStep('register'); + const storageCost = getStorageCost(dataToUpload.length, 3); + const walCostFormatted = formatWal(storageCost.totalCostFrost); + + // Get swap prices to show SUI equivalent + let suiNeeded = 0; + let suiEquivalent = ''; + try { + const prices = await getPrices(); + if (prices) { + const walAmount = Number(storageCost.totalCostFrost) / 1e9; + suiNeeded = walAmount * (prices.walUsd / prices.suiUsd) * 1.1; + suiEquivalent = ` (~${suiNeeded.toFixed(6)} SUI)`; + } + } catch { + // Prices unavailable + } + + // Check current WAL balance first + const balanceCheck = await checkBalance(dataToUpload.length, 3); + const hasEnoughWal = balanceCheck?.sufficient ?? false; + + if (hasEnoughWal) { + await addLogWithDelay('register', 'processing', `Storage cost: ${walCostFormatted} WAL (paying with WAL)`); + } else { + await addLogWithDelay('register', 'processing', `Storage cost: ${walCostFormatted} WAL → Swapping ${suiNeeded.toFixed(6)} SUI to WAL...`); + } + + const walResult = await ensureWalBalance(dataToUpload.length, 3); + if (!walResult.success) { + await addLogWithDelay('register', 'error', `Need ${walCostFormatted} WAL${suiEquivalent}. ${walResult.error}`); + setUploadError(`Insufficient balance. Need ${walCostFormatted} WAL${suiEquivalent}.`); + setIsProcessing(false); + return; + } + + if (!hasEnoughWal) { + await addLogWithDelay('register', 'success', `Swapped SUI → WAL. Balance: ${formatWal(walResult.walBalance)} WAL`); + } + // Step 1: Create flow with encrypted data const flow = await createFlow(dataToUpload, file.name); flowRef.current = flow; await flow.encode(); // Step 2: Register blob on-chain - setCurrentStep('register'); - addLog('register', 'processing', 'Registering blob on Sui...'); + await addLogWithDelay('register', 'processing', 'Registering blob on Sui...'); const registerTx = flow.register({ epochs: 3, @@ -198,18 +247,18 @@ export default function MyDataPage() { { onSuccess: async (result) => { try { - addLog('register', 'success', 'Blob registered', `TX: ${result.digest.slice(0, 16)}...`); + await addLogWithDelay('register', 'success', 'Blob registered', `TX: ${result.digest.slice(0, 16)}...`); // Step 3: Upload to Walrus storage nodes setCurrentStep('upload'); - addLog('upload', 'processing', 'Uploading to Walrus...'); + await addLogWithDelay('upload', 'processing', 'Uploading to Walrus...'); await flow.upload({ digest: result.digest }); - addLog('upload', 'success', 'Uploaded to Walrus storage nodes'); + await addLogWithDelay('upload', 'success', 'Uploaded to Walrus storage nodes'); // Step 4: Certify blob setCurrentStep('certify'); - addLog('certify', 'processing', 'Certifying blob on Sui...'); + await addLogWithDelay('certify', 'processing', 'Certifying blob on Sui...'); const certifyTx = flow.certify(); @@ -217,7 +266,7 @@ export default function MyDataPage() { { transaction: certifyTx }, { onSuccess: async (certifyResult) => { - addLog('certify', 'success', 'Blob certified', `TX: ${certifyResult.digest.slice(0, 16)}...`); + await addLogWithDelay('certify', 'success', 'Blob certified', `TX: ${certifyResult.digest.slice(0, 16)}...`); // Step 5: Get blobId from getBlob (for raw blob upload) let walrusBlobId = ''; @@ -227,7 +276,7 @@ export default function MyDataPage() { walrusBlobId = blobInfo.blobId || ''; if (walrusBlobId) { setBlobId(walrusBlobId); - addLog('complete', 'success', 'Blob ID obtained', `ID: ${walrusBlobId.slice(0, 20)}...`); + await addLogWithDelay('complete', 'success', 'Blob ID obtained', `ID: ${walrusBlobId.slice(0, 20)}...`); } } catch (listError) { console.error('[Walrus] getBlob error:', listError); @@ -236,8 +285,8 @@ export default function MyDataPage() { // Step 6: Create listing await handleCreateListing(price, walrusBlobId, encryptedObj); }, - onError: (err) => { - addLog('certify', 'error', err.message || 'Certification failed'); + onError: async (err) => { + await addLogWithDelay('certify', 'error', err.message || 'Certification failed'); setUploadError(err.message || 'Certification failed'); setIsProcessing(false); }, @@ -245,13 +294,13 @@ export default function MyDataPage() { ); } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Upload failed'; - addLog('upload', 'error', errorMsg); + await addLogWithDelay('upload', 'error', errorMsg); setUploadError(errorMsg); setIsProcessing(false); } }, - onError: (err) => { - addLog('register', 'error', err.message || 'Registration failed'); + onError: async (err) => { + await addLogWithDelay('register', 'error', err.message || 'Registration failed'); setUploadError(err.message || 'Registration failed'); setIsProcessing(false); }, @@ -259,7 +308,7 @@ export default function MyDataPage() { ); } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Failed to prepare upload'; - addLog('encode', 'error', errorMsg); + await addLogWithDelay('encode', 'error', errorMsg); setUploadError(errorMsg); setIsProcessing(false); } @@ -270,7 +319,7 @@ export default function MyDataPage() { try { setCurrentStep('listing'); - addLog('listing', 'processing', 'Creating listing on marketplace...'); + await addLogWithDelay('listing', 'processing', 'Creating listing on marketplace...'); const priceInMIST = BigInt(Math.floor(price * Number(MIST_PER_SUI))); const registryId = marketplaceConfig.registryId; @@ -310,14 +359,14 @@ export default function MyDataPage() { signAndExecute( { transaction: tx }, { - onSuccess: (result) => { + onSuccess: async (result) => { console.log('=== CREATE LISTING SUCCESS ==='); console.log('Result:', result); const effects = result.effects as { created?: Array<{ reference: { objectId: string } }> } | undefined; const newListingId = effects?.created?.[0]?.reference?.objectId || result.digest; console.log('New listing ID:', newListingId); - addLog('listing', 'success', 'Listing created!', `ID: ${newListingId.slice(0, 16)}...`); + await addLogWithDelay('listing', 'success', 'Listing created!', `ID: ${newListingId.slice(0, 16)}...`); setListingId(newListingId); setCurrentStep('complete'); @@ -325,10 +374,10 @@ export default function MyDataPage() { setProcessingType('create'); setIsProcessing(false); }, - onError: (err) => { + onError: async (err) => { console.error('=== CREATE LISTING ERROR ==='); console.error('Error:', err); - addLog('listing', 'error', err.message || 'Failed to create listing'); + await addLogWithDelay('listing', 'error', err.message || 'Failed to create listing'); setUploadError(err.message || 'Failed to create listing'); setIsProcessing(false); }, @@ -338,7 +387,7 @@ export default function MyDataPage() { console.error('=== CREATE LISTING CATCH ERROR ==='); console.error('Error:', err); const errorMsg = err instanceof Error ? err.message : 'Failed to create listing'; - addLog('listing', 'error', errorMsg); + await addLogWithDelay('listing', 'error', errorMsg); setUploadError(errorMsg); setIsProcessing(false); } diff --git a/bun.lock b/bun.lock index e01b566..b9bc38a 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "datex", "dependencies": { + "@lamdanghoang/sui-pay-wal": "^0.1.1", "@mysten/bcs": "^1.9.2", "@mysten/dapp-kit": "^0.20.0", "@mysten/enoki": "^0.13.0", @@ -200,6 +201,8 @@ "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@lamdanghoang/sui-pay-wal": ["@lamdanghoang/sui-pay-wal@0.1.1", "", { "peerDependencies": { "@mysten/sui": "^1.0.0" } }, "sha512-CqlLIkEzb5DfNqBN5fOOoKHDlDsfecuGrjPTMC82yTgftCkSn5SYpzpBdHL+RTE4dzG2W68/4wG7Ah9dkQsY5w=="], + "@ledgerhq/devices": ["@ledgerhq/devices@8.9.0", "", { "dependencies": { "@ledgerhq/errors": "^6.28.0", "@ledgerhq/logs": "^6.13.0", "rxjs": "7.8.2", "semver": "7.7.3" } }, "sha512-5U5kl7oR4NdqSS4Wr6Ix74ruXLYhjKK62SqJyAwXM9qJog7CqSHw4bH1psMAveOVvOFMOnHbmMlytcz6+Imw7A=="], "@ledgerhq/errors": ["@ledgerhq/errors@6.28.0", "", {}, "sha512-Rx6GN801GP/3gCfVmmiXFVZWmiaEGMuXVwjM6WOCX0dzw4v7KcB1nj4vrNC1plDI/xkPt/clYJPG7LgSt0mxlw=="], diff --git a/config/walrus.ts b/config/walrus.ts new file mode 100644 index 0000000..e83e44c --- /dev/null +++ b/config/walrus.ts @@ -0,0 +1,12 @@ +// Walrus & SUI-Pay configuration +export const walrusConfig = { + network: (process.env.NEXT_PUBLIC_SUI_NETWORK || 'testnet') as 'testnet' | 'mainnet', + + // WAL Exchange contract addresses + exchangePackageId: process.env.NEXT_PUBLIC_WAL_EXCHANGE_PACKAGE_ID || '', + exchangeObjectId: process.env.NEXT_PUBLIC_WAL_EXCHANGE_OBJECT_ID || '', + + // Default storage settings + defaultEpochs: 3, + defaultBufferPercent: 0, // 0% buffer for price fluctuations - exact amount +}; \ No newline at end of file diff --git a/hooks/useWalrusPayment.ts b/hooks/useWalrusPayment.ts new file mode 100644 index 0000000..ff968f2 --- /dev/null +++ b/hooks/useWalrusPayment.ts @@ -0,0 +1,206 @@ +'use client'; + +import { useState, useCallback, useMemo } from 'react'; +import { useCurrentAccount, useSignAndExecuteTransaction } from '@mysten/dapp-kit'; +import { + WalrusSuiPay, + calculateStorageCost, + calculateStorageCostLive, + estimateWalNeeded, + getSwapPrices, + formatWal, + formatSui, + type StorageCost, + type SwapPrices, + type BalanceCheckResult, +} from '@lamdanghoang/sui-pay-wal'; +import { walrusConfig } from '@/config/walrus'; + +export function useWalrusPayment() { + const account = useCurrentAccount(); + const { mutateAsync: signAndExecute } = useSignAndExecuteTransaction(); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Initialize WalrusSuiPay client + const client = useMemo(() => { + if (!walrusConfig.exchangePackageId || !walrusConfig.exchangeObjectId) { + return null; + } + return new WalrusSuiPay({ + network: walrusConfig.network, + exchangePackageId: walrusConfig.exchangePackageId, + exchangeObjectId: walrusConfig.exchangeObjectId, + }); + }, []); + + // Calculate storage cost (offline, fast) + const getStorageCost = useCallback((fileSizeBytes: number, epochs: number = walrusConfig.defaultEpochs): StorageCost => { + return calculateStorageCost(fileSizeBytes, epochs); + }, []); + + // Calculate storage cost with live on-chain pricing + const getStorageCostLive = useCallback(async (fileSizeBytes: number, epochs: number = walrusConfig.defaultEpochs) => { + setIsLoading(true); + setError(null); + try { + const cost = await calculateStorageCostLive(fileSizeBytes, epochs, walrusConfig.network); + return cost; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to get live storage cost'); + throw err; + } finally { + setIsLoading(false); + } + }, []); + + // Estimate WAL needed with buffer + const estimateWal = useCallback((fileSizeBytes: number, epochs: number = walrusConfig.defaultEpochs, bufferPercent: number = walrusConfig.defaultBufferPercent): bigint => { + return estimateWalNeeded(fileSizeBytes, epochs, bufferPercent); + }, []); + + // Get current swap prices from Pyth Oracle + const getPrices = useCallback(async (): Promise => { + setIsLoading(true); + setError(null); + try { + const prices = await getSwapPrices(); + return prices; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to get swap prices'); + throw err; + } finally { + setIsLoading(false); + } + }, []); + + // Check if user has enough WAL balance + const checkBalance = useCallback(async ( + fileSizeBytes: number, + epochs: number = walrusConfig.defaultEpochs, + bufferPercent: number = walrusConfig.defaultBufferPercent + ): Promise => { + if (!client || !account?.address) { + setError('Client not initialized or wallet not connected'); + return null; + } + + setIsLoading(true); + setError(null); + try { + const result = await client.checkBalance( + account.address, + fileSizeBytes, + epochs, + bufferPercent + ); + return result; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to check balance'); + throw err; + } finally { + setIsLoading(false); + } + }, [client, account?.address]); + + // Ensure user has enough WAL (auto-swap if needed) + const ensureWalBalance = useCallback(async ( + fileSizeBytes: number, + epochs: number = walrusConfig.defaultEpochs, + bufferPercent: number = walrusConfig.defaultBufferPercent + ) => { + if (!client || !account?.address) { + setError('Client not initialized or wallet not connected'); + return { success: false, walBalance: BigInt(0), error: 'Not initialized' }; + } + + setIsLoading(true); + setError(null); + try { + const result = await client.ensureWalBalance( + fileSizeBytes, + epochs, + account.address, + signAndExecute, + bufferPercent + ); + return result; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Failed to ensure WAL balance'; + setError(errorMsg); + return { success: false, walBalance: BigInt(0), error: errorMsg }; + } finally { + setIsLoading(false); + } + }, [client, account?.address, signAndExecute]); + + // Swap SUI to WAL directly + const swapSuiToWal = useCallback(async (suiAmount: bigint, slippageBps: number = 100) => { + if (!client || !account?.address) { + setError('Client not initialized or wallet not connected'); + return null; + } + + setIsLoading(true); + setError(null); + try { + const result = await client.swapSuiToWal(suiAmount, signAndExecute, slippageBps); + return result; + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to swap SUI to WAL'); + throw err; + } finally { + setIsLoading(false); + } + }, [client, account?.address, signAndExecute]); + + // Get WAL balance + const getWalBalance = useCallback(async (): Promise => { + if (!client || !account?.address) return null; + try { + return await client.getWalBalance(account.address); + } catch { + return null; + } + }, [client, account?.address]); + + // Get SUI balance + const getSuiBalance = useCallback(async (): Promise => { + if (!client || !account?.address) return null; + try { + return await client.getSuiBalance(account.address); + } catch { + return null; + } + }, [client, account?.address]); + + return { + // State + isLoading, + error, + isInitialized: !!client, + address: account?.address, + + // Cost estimation + getStorageCost, + getStorageCostLive, + estimateWal, + + // Pricing + getPrices, + + // Balance operations + checkBalance, + ensureWalBalance, + getWalBalance, + getSuiBalance, + + // Swap + swapSuiToWal, + + // Utilities + formatWal, + formatSui, + }; +} \ No newline at end of file diff --git a/lib/walrus-payment.ts b/lib/walrus-payment.ts new file mode 100644 index 0000000..4d26e29 --- /dev/null +++ b/lib/walrus-payment.ts @@ -0,0 +1,79 @@ +// Walrus Payment utilities - standalone functions for server-side or non-hook usage +import { + WalrusSuiPay, + calculateStorageCost, + calculateStorageCostLive, + estimateWalNeeded, + estimateWalNeededLive, + getWalrusSystemInfo, + getSwapPrices, + getSuiUsdPrice, + getWalUsdPrice, + formatWal, + formatSui, +} from '@lamdanghoang/sui-pay-wal'; +import { walrusConfig } from '@/config/walrus'; + +// Re-export utilities +export { + calculateStorageCost, + calculateStorageCostLive, + estimateWalNeeded, + estimateWalNeededLive, + getWalrusSystemInfo, + getSwapPrices, + getSuiUsdPrice, + getWalUsdPrice, + formatWal, + formatSui, +}; + +// Create a singleton client instance +let clientInstance: WalrusSuiPay | null = null; + +export function getWalrusSuiPayClient(): WalrusSuiPay | null { + if (!walrusConfig.exchangePackageId || !walrusConfig.exchangeObjectId) { + console.warn('WAL Exchange contract not configured. Set NEXT_PUBLIC_WAL_EXCHANGE_PACKAGE_ID and NEXT_PUBLIC_WAL_EXCHANGE_OBJECT_ID'); + return null; + } + + if (!clientInstance) { + clientInstance = new WalrusSuiPay({ + network: walrusConfig.network, + exchangePackageId: walrusConfig.exchangePackageId, + exchangeObjectId: walrusConfig.exchangeObjectId, + }); + } + + return clientInstance; +} + +// Helper to format file size +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// Helper to calculate and format storage cost summary +export async function getStorageCostSummary(fileSizeBytes: number, epochs: number = 3) { + const cost = calculateStorageCost(fileSizeBytes, epochs); + + let prices = null; + try { + prices = await getSwapPrices(); + } catch { + // Prices unavailable + } + + return { + fileSize: formatFileSize(fileSizeBytes), + epochs, + walCost: cost.totalWal, + walCostRaw: cost.totalCostFrost, + storageUnits: cost.storageUnits, + usdEstimate: prices ? (Number(cost.totalCostFrost) / 1e9 * prices.walUsd).toFixed(4) : null, + }; +} \ No newline at end of file diff --git a/package.json b/package.json index c4d6b11..145862a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@lamdanghoang/sui-pay-wal": "^0.1.1", "@mysten/bcs": "^1.9.2", "@mysten/dapp-kit": "^0.20.0", "@mysten/enoki": "^0.13.0", From 14fb3bbfa81711d31bd5402f2bafeee6f39cda2c Mon Sep 17 00:00:00 2001 From: lamdanghoang Date: Sun, 18 Jan 2026 03:50:44 +0700 Subject: [PATCH 2/4] add auto-scroll for logs, update close modal logic --- app/marketplace/my-data/page.tsx | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/app/marketplace/my-data/page.tsx b/app/marketplace/my-data/page.tsx index 22d7aec..1678d94 100644 --- a/app/marketplace/my-data/page.tsx +++ b/app/marketplace/my-data/page.tsx @@ -86,6 +86,14 @@ export default function MyDataPage() { }); const flowRef = useRef> | null>(null); + const logContainerRef = useRef(null); + + // Auto-scroll log panel when new logs are added + React.useEffect(() => { + if (logContainerRef.current) { + logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; + } + }, [logs]); const addLog = (step: string, status: TransactionLog['status'], message: string, details?: string) => { setLogs(prev => [...prev, { @@ -749,8 +757,14 @@ export default function MyDataPage() { {isCreateModalOpen && ( -
-
+
+
e.stopPropagation()} + >

Create New Listing

@@ -1009,7 +1023,7 @@ export default function MyDataPage() {
{/* Right Panel - Transaction Logs */} -
+

terminal Transaction Log @@ -1095,7 +1109,6 @@ export default function MyDataPage() {

))} From bd0dfe995176dd5981dd2d39b8dea6d7c2f5c394 Mon Sep 17 00:00:00 2001 From: tpSpace Date: Sun, 18 Jan 2026 07:22:33 +0700 Subject: [PATCH 4/4] update lib to bun lock --- bun.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/bun.lock b/bun.lock index b9bc38a..0bac467 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "datex",