diff --git a/mobile/src/components/PezkuwiWebView.tsx b/mobile/src/components/PezkuwiWebView.tsx index 47896b5e..ce3bffa2 100644 --- a/mobile/src/components/PezkuwiWebView.tsx +++ b/mobile/src/components/PezkuwiWebView.tsx @@ -7,6 +7,7 @@ import { TouchableOpacity, BackHandler, Platform, + Alert, } from 'react-native'; import { WebView, WebViewMessageEvent } from 'react-native-webview'; import { useFocusEffect } from '@react-navigation/native'; @@ -40,7 +41,7 @@ const PezkuwiWebView: React.FC = ({ const [error, setError] = useState(null); const [canGoBack, setCanGoBack] = useState(false); - const { selectedAccount, getKeyPair } = usePezkuwi(); + const { selectedAccount, getKeyPair, api, isApiReady } = usePezkuwi(); // JavaScript to inject into the WebView // This creates a bridge between the web app and native app @@ -66,12 +67,12 @@ const PezkuwiWebView: React.FC = ({ // Create native bridge for wallet operations window.PezkuwiNativeBridge = { - // Request transaction signing from native wallet - signTransaction: function(extrinsicHex, callback) { + // Request transaction signing and submission from native wallet + signTransaction: function(payload, callback) { window.__pendingSignCallback = callback; window.ReactNativeWebView?.postMessage(JSON.stringify({ type: 'SIGN_TRANSACTION', - payload: { extrinsicHex } + payload: payload })); }, @@ -119,9 +120,8 @@ const PezkuwiWebView: React.FC = ({ switch (message.type) { case 'SIGN_TRANSACTION': - // Handle transaction signing + // Handle transaction signing and submission if (!selectedAccount) { - // Send error back to WebView webViewRef.current?.injectJavaScript(` if (window.__pendingSignCallback) { window.__pendingSignCallback(null, 'Wallet not connected'); @@ -131,29 +131,82 @@ const PezkuwiWebView: React.FC = ({ return; } - try { - const { extrinsicHex } = message.payload as { extrinsicHex: string }; - const keyPair = await getKeyPair(selectedAccount.address); + if (!api || !isApiReady) { + webViewRef.current?.injectJavaScript(` + if (window.__pendingSignCallback) { + window.__pendingSignCallback(null, 'Blockchain not connected'); + delete window.__pendingSignCallback; + } + `); + return; + } + try { + const payload = message.payload as { + section: string; + method: string; + args: unknown[]; + }; + + const keyPair = await getKeyPair(selectedAccount.address); if (!keyPair) { throw new Error('Could not retrieve key pair'); } - // Sign the transaction - const signature = keyPair.sign(extrinsicHex); - const signatureHex = Buffer.from(signature).toString('hex'); + // Build the transaction using native API + const { section, method, args } = payload; - // Send signature back to WebView + if (__DEV__) { + console.log('[WebView] Building transaction:', { section, method, args }); + } + + // Get the transaction method from API + const txModule = api.tx[section]; + if (!txModule) { + throw new Error(`Unknown section: ${section}`); + } + + const txMethod = txModule[method]; + if (!txMethod) { + throw new Error(`Unknown method: ${section}.${method}`); + } + + // Create the transaction + const tx = txMethod(...args); + + // Sign and send transaction + const txHash = await new Promise((resolve, reject) => { + tx.signAndSend(keyPair, { nonce: -1 }, (result: { status: { isInBlock?: boolean; isFinalized?: boolean; asInBlock?: { toString: () => string }; asFinalized?: { toString: () => string } }; dispatchError?: unknown }) => { + if (result.status.isInBlock) { + const hash = result.status.asInBlock?.toString() || ''; + if (__DEV__) { + console.log('[WebView] Transaction included in block:', hash); + } + resolve(hash); + } else if (result.status.isFinalized) { + const hash = result.status.asFinalized?.toString() || ''; + if (__DEV__) { + console.log('[WebView] Transaction finalized:', hash); + } + } + if (result.dispatchError) { + reject(new Error('Transaction failed')); + } + }).catch(reject); + }); + + // Send success back to WebView webViewRef.current?.injectJavaScript(` if (window.__pendingSignCallback) { - window.__pendingSignCallback('${signatureHex}', null); + window.__pendingSignCallback('${txHash}', null); delete window.__pendingSignCallback; } `); } catch (signError) { + const errorMessage = (signError as Error).message.replace(/'/g, "\\'"); webViewRef.current?.injectJavaScript(` if (window.__pendingSignCallback) { - window.__pendingSignCallback(null, '${(signError as Error).message}'); + window.__pendingSignCallback(null, '${errorMessage}'); delete window.__pendingSignCallback; } `); diff --git a/web/src/contexts/WalletContext.tsx b/web/src/contexts/WalletContext.tsx index f3f14f0d..61e9394e 100644 --- a/web/src/contexts/WalletContext.tsx +++ b/web/src/contexts/WalletContext.tsx @@ -10,7 +10,7 @@ import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@pezkuwi/lib/wallet'; import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types'; import type { Signer } from '@pezkuwi/api/types'; import { web3FromAddress } from '@pezkuwi/extension-dapp'; -import { isMobileApp, getNativeWalletAddress, signTransactionNative } from '@/lib/mobile-bridge'; +import { isMobileApp, getNativeWalletAddress, signTransactionNative, type TransactionPayload } from '@/lib/mobile-bridge'; interface TokenBalances { HEZ: string; @@ -170,16 +170,38 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr if (isMobileApp()) { if (import.meta.env.DEV) console.log('[Mobile] Using native bridge for transaction signing'); - // Get extrinsic hex for native signing - const extrinsicHex = (tx as { toHex?: () => string }).toHex?.() || ''; + // Extract transaction details from the tx object + const txAny = tx as { + method: { + section: string; + method: string; + args: unknown[]; + toHuman?: () => { args?: Record }; + }; + }; - // Sign via native bridge - const signature = await signTransactionNative(extrinsicHex); + // Get section, method and args from the transaction + const section = txAny.method.section; + const method = txAny.method.method; - // Submit the signed transaction - // Note: The native app signs and may also submit, so we return the signature as hash - // In production, the mobile app handles the full submit flow - return signature; + // Extract args - convert to array format + const argsHuman = txAny.method.toHuman?.()?.args || {}; + const args = Object.values(argsHuman); + + if (import.meta.env.DEV) { + console.log('[Mobile] Transaction details:', { section, method, args }); + } + + const payload: TransactionPayload = { section, method, args }; + + // Sign and send via native bridge + const blockHash = await signTransactionNative(payload); + + if (import.meta.env.DEV) { + console.log('[Mobile] Transaction submitted, block hash:', blockHash); + } + + return blockHash; } // Desktop: Use browser extension for signing diff --git a/web/src/lib/mobile-bridge.ts b/web/src/lib/mobile-bridge.ts index 31744ad3..68951af3 100644 --- a/web/src/lib/mobile-bridge.ts +++ b/web/src/lib/mobile-bridge.ts @@ -19,7 +19,7 @@ declare global { postMessage: (message: string) => void; }; PezkuwiNativeBridge?: { - signTransaction: (extrinsicHex: string, callback: (signature: string | null, error: string | null) => void) => void; + signTransaction: (payload: { section: string; method: string; args: unknown[] }, callback: (hash: string | null, error: string | null) => void) => void; connectWallet: () => void; goBack: () => void; isWalletConnected: () => boolean; @@ -83,29 +83,57 @@ export function requestNativeWalletConnection(): void { window.PezkuwiNativeBridge?.connectWallet(); } +export interface TransactionPayload { + section: string; + method: string; + args: unknown[]; +} + /** - * Sign transaction using native wallet - * Returns a promise that resolves with the signature or rejects with error + * Sign and submit transaction using native wallet + * Returns a promise that resolves with the block hash or rejects with error */ -export function signTransactionNative(extrinsicHex: string): Promise { +export function signTransactionNative(payload: TransactionPayload): Promise { return new Promise((resolve, reject) => { if (!isMobileApp() || !window.PezkuwiNativeBridge) { reject(new Error('Native bridge not available')); return; } - window.PezkuwiNativeBridge.signTransaction(extrinsicHex, (signature, error) => { + window.PezkuwiNativeBridge.signTransaction(payload, (hash, error) => { if (error) { reject(new Error(error)); - } else if (signature) { - resolve(signature); + } else if (hash) { + resolve(hash); } else { - reject(new Error('No signature returned')); + reject(new Error('No transaction hash returned')); } }); }); } +/** + * Legacy: Sign transaction using native wallet (raw hex - deprecated) + * @deprecated Use signTransactionNative with TransactionPayload instead + */ +export function signTransactionNativeHex(extrinsicHex: string): Promise { + return new Promise((resolve, reject) => { + if (!isMobileApp() || !window.ReactNativeWebView) { + reject(new Error('Native bridge not available')); + return; + } + + // For backwards compatibility, send as legacy format + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'SIGN_TRANSACTION_LEGACY', + payload: { extrinsicHex } + })); + + // Note: This won't work without proper callback handling + reject(new Error('Legacy signing not supported - use TransactionPayload')); + }); +} + /** * Navigate back in native app */