mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 05:31:01 +00:00
fix(p2p): complete mobile bridge transaction signing flow
- PezkuwiWebView now uses signAndSend via native API - Transactions are signed AND submitted on native side - Returns actual block hash instead of raw signature - Web sends section/method/args payload format - WalletContext extracts tx details for mobile bridge
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Platform,
|
Platform,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
@@ -40,7 +41,7 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [canGoBack, setCanGoBack] = useState(false);
|
const [canGoBack, setCanGoBack] = useState(false);
|
||||||
|
|
||||||
const { selectedAccount, getKeyPair } = usePezkuwi();
|
const { selectedAccount, getKeyPair, api, isApiReady } = usePezkuwi();
|
||||||
|
|
||||||
// JavaScript to inject into the WebView
|
// JavaScript to inject into the WebView
|
||||||
// This creates a bridge between the web app and native app
|
// This creates a bridge between the web app and native app
|
||||||
@@ -66,12 +67,12 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
|
|
||||||
// Create native bridge for wallet operations
|
// Create native bridge for wallet operations
|
||||||
window.PezkuwiNativeBridge = {
|
window.PezkuwiNativeBridge = {
|
||||||
// Request transaction signing from native wallet
|
// Request transaction signing and submission from native wallet
|
||||||
signTransaction: function(extrinsicHex, callback) {
|
signTransaction: function(payload, callback) {
|
||||||
window.__pendingSignCallback = callback;
|
window.__pendingSignCallback = callback;
|
||||||
window.ReactNativeWebView?.postMessage(JSON.stringify({
|
window.ReactNativeWebView?.postMessage(JSON.stringify({
|
||||||
type: 'SIGN_TRANSACTION',
|
type: 'SIGN_TRANSACTION',
|
||||||
payload: { extrinsicHex }
|
payload: payload
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -119,9 +120,8 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'SIGN_TRANSACTION':
|
case 'SIGN_TRANSACTION':
|
||||||
// Handle transaction signing
|
// Handle transaction signing and submission
|
||||||
if (!selectedAccount) {
|
if (!selectedAccount) {
|
||||||
// Send error back to WebView
|
|
||||||
webViewRef.current?.injectJavaScript(`
|
webViewRef.current?.injectJavaScript(`
|
||||||
if (window.__pendingSignCallback) {
|
if (window.__pendingSignCallback) {
|
||||||
window.__pendingSignCallback(null, 'Wallet not connected');
|
window.__pendingSignCallback(null, 'Wallet not connected');
|
||||||
@@ -131,29 +131,82 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (!api || !isApiReady) {
|
||||||
const { extrinsicHex } = message.payload as { extrinsicHex: string };
|
webViewRef.current?.injectJavaScript(`
|
||||||
const keyPair = await getKeyPair(selectedAccount.address);
|
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) {
|
if (!keyPair) {
|
||||||
throw new Error('Could not retrieve key pair');
|
throw new Error('Could not retrieve key pair');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign the transaction
|
// Build the transaction using native API
|
||||||
const signature = keyPair.sign(extrinsicHex);
|
const { section, method, args } = payload;
|
||||||
const signatureHex = Buffer.from(signature).toString('hex');
|
|
||||||
|
|
||||||
// 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<string>((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(`
|
webViewRef.current?.injectJavaScript(`
|
||||||
if (window.__pendingSignCallback) {
|
if (window.__pendingSignCallback) {
|
||||||
window.__pendingSignCallback('${signatureHex}', null);
|
window.__pendingSignCallback('${txHash}', null);
|
||||||
delete window.__pendingSignCallback;
|
delete window.__pendingSignCallback;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
} catch (signError) {
|
} catch (signError) {
|
||||||
|
const errorMessage = (signError as Error).message.replace(/'/g, "\\'");
|
||||||
webViewRef.current?.injectJavaScript(`
|
webViewRef.current?.injectJavaScript(`
|
||||||
if (window.__pendingSignCallback) {
|
if (window.__pendingSignCallback) {
|
||||||
window.__pendingSignCallback(null, '${(signError as Error).message}');
|
window.__pendingSignCallback(null, '${errorMessage}');
|
||||||
delete window.__pendingSignCallback;
|
delete window.__pendingSignCallback;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@pezkuwi/lib/wallet';
|
|||||||
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
||||||
import type { Signer } from '@pezkuwi/api/types';
|
import type { Signer } from '@pezkuwi/api/types';
|
||||||
import { web3FromAddress } from '@pezkuwi/extension-dapp';
|
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 {
|
interface TokenBalances {
|
||||||
HEZ: string;
|
HEZ: string;
|
||||||
@@ -170,16 +170,38 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
if (isMobileApp()) {
|
if (isMobileApp()) {
|
||||||
if (import.meta.env.DEV) console.log('[Mobile] Using native bridge for transaction signing');
|
if (import.meta.env.DEV) console.log('[Mobile] Using native bridge for transaction signing');
|
||||||
|
|
||||||
// Get extrinsic hex for native signing
|
// Extract transaction details from the tx object
|
||||||
const extrinsicHex = (tx as { toHex?: () => string }).toHex?.() || '';
|
const txAny = tx as {
|
||||||
|
method: {
|
||||||
|
section: string;
|
||||||
|
method: string;
|
||||||
|
args: unknown[];
|
||||||
|
toHuman?: () => { args?: Record<string, unknown> };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Sign via native bridge
|
// Get section, method and args from the transaction
|
||||||
const signature = await signTransactionNative(extrinsicHex);
|
const section = txAny.method.section;
|
||||||
|
const method = txAny.method.method;
|
||||||
|
|
||||||
// Submit the signed transaction
|
// Extract args - convert to array format
|
||||||
// Note: The native app signs and may also submit, so we return the signature as hash
|
const argsHuman = txAny.method.toHuman?.()?.args || {};
|
||||||
// In production, the mobile app handles the full submit flow
|
const args = Object.values(argsHuman);
|
||||||
return signature;
|
|
||||||
|
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
|
// Desktop: Use browser extension for signing
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ declare global {
|
|||||||
postMessage: (message: string) => void;
|
postMessage: (message: string) => void;
|
||||||
};
|
};
|
||||||
PezkuwiNativeBridge?: {
|
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;
|
connectWallet: () => void;
|
||||||
goBack: () => void;
|
goBack: () => void;
|
||||||
isWalletConnected: () => boolean;
|
isWalletConnected: () => boolean;
|
||||||
@@ -83,29 +83,57 @@ export function requestNativeWalletConnection(): void {
|
|||||||
window.PezkuwiNativeBridge?.connectWallet();
|
window.PezkuwiNativeBridge?.connectWallet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TransactionPayload {
|
||||||
|
section: string;
|
||||||
|
method: string;
|
||||||
|
args: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign transaction using native wallet
|
* Sign and submit transaction using native wallet
|
||||||
* Returns a promise that resolves with the signature or rejects with error
|
* Returns a promise that resolves with the block hash or rejects with error
|
||||||
*/
|
*/
|
||||||
export function signTransactionNative(extrinsicHex: string): Promise<string> {
|
export function signTransactionNative(payload: TransactionPayload): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!isMobileApp() || !window.PezkuwiNativeBridge) {
|
if (!isMobileApp() || !window.PezkuwiNativeBridge) {
|
||||||
reject(new Error('Native bridge not available'));
|
reject(new Error('Native bridge not available'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.PezkuwiNativeBridge.signTransaction(extrinsicHex, (signature, error) => {
|
window.PezkuwiNativeBridge.signTransaction(payload, (hash, error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(new Error(error));
|
reject(new Error(error));
|
||||||
} else if (signature) {
|
} else if (hash) {
|
||||||
resolve(signature);
|
resolve(hash);
|
||||||
} else {
|
} 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<string> {
|
||||||
|
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
|
* Navigate back in native app
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user