feat: add Telegram mini app connect for P2P access
@@ -8,6 +8,9 @@
|
|||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"dev": "node --watch src/index.js"
|
"dev": "node --watch src/index.js"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"tar": "^7.4.3"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pezkuwi/api": "^16.5.11",
|
"@pezkuwi/api": "^16.5.11",
|
||||||
"@pezkuwi/util": "^14.0.11",
|
"@pezkuwi/util": "^14.0.11",
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
"newArchEnabled": false,
|
"newArchEnabled": true,
|
||||||
"jsEngine": "jsc",
|
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash-icon.png",
|
"image": "./assets/splash-icon.png",
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
|
|||||||
@@ -13141,9 +13141,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -18012,9 +18012,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.3",
|
"version": "7.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||||
"integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==",
|
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [sessionToken, setSessionToken] = useState<string | null>(null);
|
const [sessionToken, setSessionToken] = useState<string | null>(null);
|
||||||
const [refreshToken, setRefreshToken] = useState<string | null>(null);
|
const [refreshToken, setRefreshToken] = useState<string | null>(null);
|
||||||
|
const [isSessionReady, setIsSessionReady] = useState(false);
|
||||||
|
|
||||||
// Get Supabase session token for WebView authentication
|
// Get Supabase session token for WebView authentication
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -63,9 +64,12 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
if (session?.access_token) {
|
if (session?.access_token) {
|
||||||
setSessionToken(session.access_token);
|
setSessionToken(session.access_token);
|
||||||
setRefreshToken(session.refresh_token || null);
|
setRefreshToken(session.refresh_token || null);
|
||||||
|
if (__DEV__) console.log('[WebView] Session token retrieved for SSO');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (__DEV__) console.warn('[WebView] Failed to get session:', error);
|
if (__DEV__) console.warn('[WebView] Failed to get session:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSessionReady(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
getSession();
|
getSession();
|
||||||
@@ -89,6 +93,31 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
${user ? `window.PEZKUWI_USER_ID = '${user.id}';` : ''}
|
${user ? `window.PEZKUWI_USER_ID = '${user.id}';` : ''}
|
||||||
${user?.email ? `window.PEZKUWI_USER_EMAIL = '${user.email}';` : ''}
|
${user?.email ? `window.PEZKUWI_USER_EMAIL = '${user.email}';` : ''}
|
||||||
|
|
||||||
|
// Pre-populate localStorage with session so Supabase client finds it on init
|
||||||
|
${sessionToken && user ? `
|
||||||
|
try {
|
||||||
|
var supabaseUrl = 'https://sihawipngjtgvfzukfew.supabase.co';
|
||||||
|
var storageKey = 'sb-' + supabaseUrl.replace('https://', '').split('.')[0] + '-auth-token';
|
||||||
|
var sessionData = {
|
||||||
|
access_token: '${sessionToken}',
|
||||||
|
refresh_token: '${refreshToken || ''}',
|
||||||
|
token_type: 'bearer',
|
||||||
|
expires_in: 3600,
|
||||||
|
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
||||||
|
user: {
|
||||||
|
id: '${user.id}',
|
||||||
|
email: '${user.email || ''}',
|
||||||
|
aud: 'authenticated',
|
||||||
|
role: 'authenticated'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(sessionData));
|
||||||
|
console.log('[Mobile] Pre-populated localStorage with session');
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('[Mobile] Failed to set localStorage:', e);
|
||||||
|
}
|
||||||
|
` : ''}
|
||||||
|
|
||||||
// Auto-authenticate with Supabase if session token exists
|
// Auto-authenticate with Supabase if session token exists
|
||||||
if (window.PEZKUWI_SESSION_TOKEN) {
|
if (window.PEZKUWI_SESSION_TOKEN) {
|
||||||
(function autoAuth(attempts = 0) {
|
(function autoAuth(attempts = 0) {
|
||||||
@@ -96,14 +125,25 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
console.warn('[Mobile] Auto-auth timed out: window.supabase not found');
|
console.warn('[Mobile] Auto-auth timed out: window.supabase not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.supabase && window.supabase.auth) {
|
if (window.supabase && window.supabase.auth) {
|
||||||
window.supabase.auth.setSession({
|
window.supabase.auth.setSession({
|
||||||
access_token: window.PEZKUWI_SESSION_TOKEN,
|
access_token: window.PEZKUWI_SESSION_TOKEN,
|
||||||
refresh_token: window.PEZKUWI_REFRESH_TOKEN || ''
|
refresh_token: window.PEZKUWI_REFRESH_TOKEN || ''
|
||||||
}).then(function(res) {
|
}).then(function(res) {
|
||||||
if (res.error) console.warn('[Mobile] Auto-auth error:', res.error);
|
if (res.error) {
|
||||||
else console.log('[Mobile] Auto-authenticated successfully');
|
console.warn('[Mobile] Auto-auth error:', res.error);
|
||||||
|
} else {
|
||||||
|
console.log('[Mobile] Auto-authenticated successfully');
|
||||||
|
// Dispatch event to notify app of successful auth
|
||||||
|
window.dispatchEvent(new CustomEvent('pezkuwi-session-restored', {
|
||||||
|
detail: { userId: window.PEZKUWI_USER_ID }
|
||||||
|
}));
|
||||||
|
// Force auth state refresh if the app has an auth store
|
||||||
|
if (window.__refreshAuthState) {
|
||||||
|
window.__refreshAuthState();
|
||||||
|
}
|
||||||
|
}
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
console.warn('[Mobile] Auto-auth promise failed:', err);
|
console.warn('[Mobile] Auto-auth promise failed:', err);
|
||||||
});
|
});
|
||||||
@@ -367,6 +407,25 @@ const PezkuwiWebView: React.FC<PezkuwiWebViewProps> = ({
|
|||||||
// Build the full URL
|
// Build the full URL
|
||||||
const fullUrl = `${WEB_BASE_URL}${path}`;
|
const fullUrl = `${WEB_BASE_URL}${path}`;
|
||||||
|
|
||||||
|
// Wait for session to be ready before loading WebView (ensures SSO works)
|
||||||
|
if (!isSessionReady) {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{title && (
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={{ width: 40 }} />
|
||||||
|
<Text style={styles.headerTitle}>{title}</Text>
|
||||||
|
<View style={{ width: 40 }} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={styles.loadingOverlay}>
|
||||||
|
<ActivityIndicator size="large" color={KurdistanColors.kesk} />
|
||||||
|
<Text style={styles.loadingText}>Preparing session...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Error view
|
// Error view
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
AlertButton,
|
AlertButton,
|
||||||
|
Image,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
@@ -74,6 +75,12 @@ const getEmojiFromAvatarId = (avatarId: string): string => {
|
|||||||
return avatar ? avatar.emoji : '👤';
|
return avatar ? avatar.emoji : '👤';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if avatar is an uploaded image URL vs emoji avatar ID
|
||||||
|
const isUploadedImageUrl = (avatarUrl: string | null): boolean => {
|
||||||
|
if (!avatarUrl) return false;
|
||||||
|
return avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://');
|
||||||
|
};
|
||||||
|
|
||||||
const EditProfileScreen: React.FC = () => {
|
const EditProfileScreen: React.FC = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -242,7 +249,11 @@ const EditProfileScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<View style={[styles.avatarCircle, { backgroundColor: colors.surface }]}>
|
<View style={[styles.avatarCircle, { backgroundColor: colors.surface }]}>
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<Text style={styles.avatarEmoji}>{getEmojiFromAvatarId(avatarUrl)}</Text>
|
isUploadedImageUrl(avatarUrl) ? (
|
||||||
|
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.avatarEmoji}>{getEmojiFromAvatarId(avatarUrl)}</Text>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Text style={[styles.avatarInitial, { color: colors.textSecondary }]}>
|
<Text style={[styles.avatarInitial, { color: colors.textSecondary }]}>
|
||||||
{fullName?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
|
{fullName?.charAt(0)?.toUpperCase() || user?.email?.charAt(0)?.toUpperCase() || '?'}
|
||||||
@@ -382,6 +393,11 @@ const styles = StyleSheet.create({
|
|||||||
avatarEmoji: {
|
avatarEmoji: {
|
||||||
fontSize: 70,
|
fontSize: 70,
|
||||||
},
|
},
|
||||||
|
avatarImage: {
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 60,
|
||||||
|
},
|
||||||
avatarInitial: {
|
avatarInitial: {
|
||||||
fontSize: 48,
|
fontSize: 48,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
|||||||
@@ -392,16 +392,9 @@ const SettingsScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]} testID="settings-screen">
|
<View style={[styles.container, { backgroundColor: colors.background }]} testID="settings-screen">
|
||||||
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
|
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
|
||||||
|
|
||||||
{/* Header */}
|
|
||||||
<View style={[styles.header, { backgroundColor: colors.surface, borderBottomColor: colors.border }]}>
|
|
||||||
<View style={{ width: 40 }} />
|
|
||||||
<Text style={[styles.headerTitle, { color: colors.text }]}>Settings</Text>
|
|
||||||
<View style={{ width: 40 }} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ScrollView showsVerticalScrollIndicator={false}>
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
|
|
||||||
{/* ACCOUNT SECTION */}
|
{/* ACCOUNT SECTION */}
|
||||||
@@ -526,42 +519,76 @@ const SettingsScreen: React.FC = () => {
|
|||||||
|
|
||||||
{/* DEVELOPER OPTIONS (only in DEV) */}
|
{/* DEVELOPER OPTIONS (only in DEV) */}
|
||||||
{__DEV__ && (
|
{__DEV__ && (
|
||||||
<View style={[styles.section, { backgroundColor: colors.surface, marginTop: 20 }]}>
|
<>
|
||||||
<Text style={[styles.sectionHeader, { color: colors.textSecondary }]}>DEVELOPER</Text>
|
<SectionHeader title="DEVELOPER" />
|
||||||
<SettingItem
|
<View style={[styles.section, { backgroundColor: colors.surface }]}>
|
||||||
icon="🗑️"
|
<SettingItem
|
||||||
title="Reset Wallet"
|
icon="🔄"
|
||||||
subtitle="Clear all wallet data"
|
title="Reset Onboarding"
|
||||||
textColor="#FF9500"
|
subtitle="Show Welcome & Verify screens again"
|
||||||
showArrow={false}
|
textColor="#FF9500"
|
||||||
onPress={() => {
|
showArrow={false}
|
||||||
showAlert(
|
onPress={() => {
|
||||||
'Reset Wallet',
|
showAlert(
|
||||||
'This will delete all wallet data including saved accounts and keys. Are you sure?',
|
'Reset Onboarding',
|
||||||
[
|
'This will reset onboarding state. Restart the app to see the Welcome screen.',
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
[
|
||||||
{
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
text: 'Reset',
|
{
|
||||||
style: 'destructive',
|
text: 'Reset',
|
||||||
onPress: async () => {
|
style: 'destructive',
|
||||||
try {
|
onPress: async () => {
|
||||||
await AsyncStorage.multiRemove([
|
try {
|
||||||
'@pezkuwi_wallets',
|
await AsyncStorage.multiRemove([
|
||||||
'@pezkuwi_selected_account',
|
'@pezkuwi/privacy_consent_accepted',
|
||||||
'@pezkuwi_selected_network'
|
'@pezkuwi_human_verified'
|
||||||
]);
|
]);
|
||||||
showAlert('Success', 'Wallet data cleared. Restart the app to see changes.');
|
showAlert('Success', 'Onboarding reset. Restart the app to see changes.');
|
||||||
} catch {
|
} catch {
|
||||||
showAlert('Error', 'Failed to clear wallet data');
|
showAlert('Error', 'Failed to reset onboarding');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
);
|
||||||
);
|
}}
|
||||||
}}
|
testID="reset-onboarding-button"
|
||||||
testID="reset-wallet-button"
|
/>
|
||||||
/>
|
<SettingItem
|
||||||
</View>
|
icon="🗑️"
|
||||||
|
title="Reset Wallet"
|
||||||
|
subtitle="Clear all wallet data"
|
||||||
|
textColor="#FF9500"
|
||||||
|
showArrow={false}
|
||||||
|
onPress={() => {
|
||||||
|
showAlert(
|
||||||
|
'Reset Wallet',
|
||||||
|
'This will delete all wallet data including saved accounts and keys. Are you sure?',
|
||||||
|
[
|
||||||
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
|
{
|
||||||
|
text: 'Reset',
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: async () => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.multiRemove([
|
||||||
|
'@pezkuwi_wallets',
|
||||||
|
'@pezkuwi_selected_account',
|
||||||
|
'@pezkuwi_selected_network'
|
||||||
|
]);
|
||||||
|
showAlert('Success', 'Wallet data cleared. Restart the app to see changes.');
|
||||||
|
} catch {
|
||||||
|
showAlert('Error', 'Failed to clear wallet data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
testID="reset-wallet-button"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* LOGOUT */}
|
{/* LOGOUT */}
|
||||||
@@ -925,7 +952,7 @@ const SettingsScreen: React.FC = () => {
|
|||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
</SafeAreaView>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 578 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 597 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 188 KiB |
@@ -9,7 +9,7 @@
|
|||||||
//!
|
//!
|
||||||
//! > If starting a new teyrchain project, please use an async backing compatible template such as
|
//! > If starting a new teyrchain project, please use an async backing compatible template such as
|
||||||
//! > the
|
//! > the
|
||||||
//! > [teyrchain template](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/templates/teyrchain).
|
//! > [teyrchain template](https://github.com/pezkuwichain/pezkuwi-sdk/tree/main/templates/teyrchain).
|
||||||
//! The rollout process for Async Backing has three phases. Phases 1 and 2 below put new
|
//! The rollout process for Async Backing has three phases. Phases 1 and 2 below put new
|
||||||
//! infrastructure in place. Then we can simply turn on async backing in phase 3.
|
//! infrastructure in place. Then we can simply turn on async backing in phase 3.
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -330,6 +330,7 @@
|
|||||||
//! [`pezpallet::pezpallet`]: pezframe_support::pezpallet
|
//! [`pezpallet::pezpallet`]: pezframe_support::pezpallet
|
||||||
//! [`pezpallet::config`]: pezframe_support::pezpallet_macros::config
|
//! [`pezpallet::config`]: pezframe_support::pezpallet_macros::config
|
||||||
//! [`pezpallet::generate_deposit`]: pezframe_support::pezpallet_macros::generate_deposit
|
//! [`pezpallet::generate_deposit`]: pezframe_support::pezpallet_macros::generate_deposit
|
||||||
|
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
|
||||||
|
|
||||||
#[docify::export]
|
#[docify::export]
|
||||||
#[pezframe::pezpallet(dev_mode)]
|
#[pezframe::pezpallet(dev_mode)]
|
||||||
|
|||||||
@@ -169,6 +169,7 @@
|
|||||||
//! [`crate::pezkuwi_sdk::templates`].
|
//! [`crate::pezkuwi_sdk::templates`].
|
||||||
//!
|
//!
|
||||||
//! [`SolochainDefaultConfig`]: struct@pezframe_system::pezpallet::config_preludes::SolochainDefaultConfig
|
//! [`SolochainDefaultConfig`]: struct@pezframe_system::pezpallet::config_preludes::SolochainDefaultConfig
|
||||||
|
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -133,6 +133,8 @@
|
|||||||
//! - [`pezsc_consensus_beefy`] (TODO: @adrian, add some high level docs <https://github.com/pezkuwichain/pezkuwi-sdk/issues/305>)
|
//! - [`pezsc_consensus_beefy`] (TODO: @adrian, add some high level docs <https://github.com/pezkuwichain/pezkuwi-sdk/issues/305>)
|
||||||
//! - [`pezsc_consensus_manual_seal`]
|
//! - [`pezsc_consensus_manual_seal`]
|
||||||
//! - [`pezsc_consensus_pow`]
|
//! - [`pezsc_consensus_pow`]
|
||||||
|
//!
|
||||||
|
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::pezkuwi_sdk;
|
pub use crate::pezkuwi_sdk;
|
||||||
|
|||||||
@@ -111,4 +111,6 @@
|
|||||||
//!
|
//!
|
||||||
//! - <https://forum.polkadot.network/t/offchain-workers-design-assumptions-vulnerabilities/2548>
|
//! - <https://forum.polkadot.network/t/offchain-workers-design-assumptions-vulnerabilities/2548>
|
||||||
//! - <https://exchange.pezkuwichain.app/questions/11058/how-can-i-create-ocw-that-wont-activates-every-block-but-will-activates-only-w/11060#11060>
|
//! - <https://exchange.pezkuwichain.app/questions/11058/how-can-i-create-ocw-that-wont-activates-every-block-but-will-activates-only-w/11060#11060>
|
||||||
//! - [Offchain worker example](https://github.com/pezkuwichain/pezkuwi-sdk/tree/master/bizinikiwi/pezframe/examples/offchain-worker)
|
//! - [Offchain worker example](https://github.com/pezkuwichain/pezkuwi-sdk/tree/main/bizinikiwi/pezframe/examples/offchain-worker)
|
||||||
|
//!
|
||||||
|
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
//! #### Dispatchable:
|
//! #### Dispatchable:
|
||||||
//!
|
//!
|
||||||
//! Dispatchables are [function objects](https://en.wikipedia.org/wiki/Function_object) that act as
|
//! Dispatchables are [function objects](https://en.wikipedia.org/wiki/Function_object) that act as
|
||||||
//! the entry points in [FRAME](frame) pallets. They can be called by internal or external entities
|
//! the entry points in [FRAME](crate::pezkuwi_sdk::frame_runtime) pallets. They can be called by internal or external entities
|
||||||
//! to interact with the blockchain's state. They are a core aspect of the runtime logic, handling
|
//! to interact with the blockchain's state. They are a core aspect of the runtime logic, handling
|
||||||
//! transactions and other state-changing operations.
|
//! transactions and other state-changing operations.
|
||||||
//!
|
//!
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
//!
|
//!
|
||||||
//! #### Pezpallet
|
//! #### Pezpallet
|
||||||
//!
|
//!
|
||||||
//! Similar to software modules in traditional programming, [FRAME](frame) pallets in Bizinikiwi are
|
//! Similar to software modules in traditional programming, [FRAME](crate::pezkuwi_sdk::frame_runtime) pallets in Bizinikiwi are
|
||||||
//! modular components that encapsulate distinct functionalities or business logic. Just as
|
//! modular components that encapsulate distinct functionalities or business logic. Just as
|
||||||
//! libraries or modules are used to build and extend the capabilities of a software application,
|
//! libraries or modules are used to build and extend the capabilities of a software application,
|
||||||
//! pallets are the foundational building blocks for constructing a blockchain's runtime with frame.
|
//! pallets are the foundational building blocks for constructing a blockchain's runtime with frame.
|
||||||
@@ -118,3 +118,4 @@
|
|||||||
//! network.
|
//! network.
|
||||||
//!
|
//!
|
||||||
//! **Synonyms**: Teyrchain Validation Function
|
//! **Synonyms**: Teyrchain Validation Function
|
||||||
|
//!
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ pub mod cli;
|
|||||||
pub mod frame_runtime_upgrades_and_migrations;
|
pub mod frame_runtime_upgrades_and_migrations;
|
||||||
|
|
||||||
/// Learn about the offchain workers, how they function, and how to use them, as provided by the
|
/// Learn about the offchain workers, how they function, and how to use them, as provided by the
|
||||||
/// [`frame`] APIs.
|
/// [`crate::pezkuwi_sdk::frame_runtime`] APIs.
|
||||||
pub mod frame_offchain_workers;
|
pub mod frame_offchain_workers;
|
||||||
|
|
||||||
/// Learn about the different ways through which multiple [`frame`] pallets can be combined to work
|
/// Learn about the different ways through which multiple [`crate::pezkuwi_sdk::frame_runtime`] pallets can be combined to work
|
||||||
/// together.
|
/// together.
|
||||||
pub mod frame_pallet_coupling;
|
pub mod frame_pallet_coupling;
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
//!
|
//!
|
||||||
//! #### Smart Contracts in Bizinikiwi
|
//! #### Smart Contracts in Bizinikiwi
|
||||||
//! Smart Contracts are autonomous, programmable constructs deployed on the blockchain.
|
//! Smart Contracts are autonomous, programmable constructs deployed on the blockchain.
|
||||||
//! In [FRAME](frame), Smart Contracts infrastructure is implemented by the
|
//! In [FRAME](crate::pezkuwi_sdk::frame_runtime), Smart Contracts infrastructure is implemented by the
|
||||||
//! [`pezpallet_contracts`] for WASM-based contracts or the
|
//! [`pezpallet_contracts`] for WASM-based contracts or the
|
||||||
//! [`pezpallet_evm`](https://github.com/polkadot-evm/frontier/tree/master/frame/evm) for EVM-compatible contracts. These pallets
|
//! [`pezpallet_evm`](https://github.com/polkadot-evm/frontier/tree/master/frame/evm) for EVM-compatible contracts. These pallets
|
||||||
//! enable Smart Contract developers to build applications and systems on top of a Bizinikiwi-based
|
//! enable Smart Contract developers to build applications and systems on top of a Bizinikiwi-based
|
||||||
@@ -207,3 +207,4 @@
|
|||||||
//! - **For Smart Contract Developers**: Being mindful of the gas cost associated with contract
|
//! - **For Smart Contract Developers**: Being mindful of the gas cost associated with contract
|
||||||
//! execution is crucial. Efficiently written contracts save costs and are less likely to hit gas
|
//! execution is crucial. Efficiently written contracts save costs and are less likely to hit gas
|
||||||
//! limits, ensuring smoother execution on the blockchain.
|
//! limits, ensuring smoother execution on the blockchain.
|
||||||
|
//!
|
||||||
|
|||||||
@@ -111,6 +111,8 @@
|
|||||||
//! - <https://github.com/pezkuwichain/pezkuwi-sdk/issues/326>
|
//! - <https://github.com/pezkuwichain/pezkuwi-sdk/issues/326>
|
||||||
//! - [Bizinikiwi Seminar - Traits and Generic Types](https://www.youtube.com/watch?v=6cp10jVWNl4)
|
//! - [Bizinikiwi Seminar - Traits and Generic Types](https://www.youtube.com/watch?v=6cp10jVWNl4)
|
||||||
//! - <https://exchange.pezkuwichain.app/questions/2228/type-casting-to-trait-t-as-config>
|
//! - <https://exchange.pezkuwichain.app/questions/2228/type-casting-to-trait-t-as-config>
|
||||||
|
//!
|
||||||
|
//! [`frame`]: crate::pezkuwi_sdk::frame_runtime
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
use pezframe::traits::Get;
|
use pezframe::traits::Get;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ initSentry();
|
|||||||
// Lazy load pages for code splitting
|
// Lazy load pages for code splitting
|
||||||
const Index = lazy(() => import('@/pages/Index'));
|
const Index = lazy(() => import('@/pages/Index'));
|
||||||
const Login = lazy(() => import('@/pages/Login'));
|
const Login = lazy(() => import('@/pages/Login'));
|
||||||
|
const TelegramConnect = lazy(() => import('@/pages/TelegramConnect'));
|
||||||
const Dashboard = lazy(() => import('@/pages/Dashboard'));
|
const Dashboard = lazy(() => import('@/pages/Dashboard'));
|
||||||
const EmailVerification = lazy(() => import('@/pages/EmailVerification'));
|
const EmailVerification = lazy(() => import('@/pages/EmailVerification'));
|
||||||
const PasswordReset = lazy(() => import('@/pages/PasswordReset'));
|
const PasswordReset = lazy(() => import('@/pages/PasswordReset'));
|
||||||
@@ -113,6 +114,7 @@ function App() {
|
|||||||
<Suspense fallback={<PageLoader />}>
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/auth/telegram-connect" element={<TelegramConnect />} />
|
||||||
<Route path="/email-verification" element={<EmailVerification />} />
|
<Route path="/email-verification" element={<EmailVerification />} />
|
||||||
<Route path="/reset-password" element={<PasswordReset />} />
|
<Route path="/reset-password" element={<PasswordReset />} />
|
||||||
<Route path="/" element={<Index />} />
|
<Route path="/" element={<Index />} />
|
||||||
@@ -177,27 +179,27 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/p2p" element={
|
<Route path="/p2p" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute allowTelegramSession>
|
||||||
<P2PPlatform />
|
<P2PPlatform />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/p2p/trade/:tradeId" element={
|
<Route path="/p2p/trade/:tradeId" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute allowTelegramSession>
|
||||||
<P2PTrade />
|
<P2PTrade />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/p2p/orders" element={
|
<Route path="/p2p/orders" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute allowTelegramSession>
|
||||||
<P2POrders />
|
<P2POrders />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/p2p/dispute/:disputeId" element={
|
<Route path="/p2p/dispute/:disputeId" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute allowTelegramSession>
|
||||||
<P2PDispute />
|
<P2PDispute />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/p2p/merchant" element={
|
<Route path="/p2p/merchant" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute allowTelegramSession>
|
||||||
<P2PMerchantDashboard />
|
<P2PMerchantDashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
|
|||||||
@@ -8,16 +8,37 @@ import { Button } from '@/components/ui/button';
|
|||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
requireAdmin?: boolean;
|
requireAdmin?: boolean;
|
||||||
|
allowTelegramSession?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if valid telegram session exists
|
||||||
|
function getTelegramSession(): { telegram_id: string; wallet_address: string; username: string } | null {
|
||||||
|
try {
|
||||||
|
const session = localStorage.getItem('telegram_session');
|
||||||
|
if (!session) return null;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(session);
|
||||||
|
// Session expires after 24 hours
|
||||||
|
if (Date.now() - parsed.timestamp > 24 * 60 * 60 * 1000) {
|
||||||
|
localStorage.removeItem('telegram_session');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
||||||
children,
|
children,
|
||||||
requireAdmin = false
|
requireAdmin = false,
|
||||||
|
allowTelegramSession = false
|
||||||
}) => {
|
}) => {
|
||||||
const { user, loading, isAdmin } = useAuth();
|
const { user, loading, isAdmin } = useAuth();
|
||||||
const { selectedAccount, connectWallet } = usePezkuwi();
|
const { selectedAccount, connectWallet } = usePezkuwi();
|
||||||
const [walletRestoreChecked, setWalletRestoreChecked] = useState(false);
|
const [walletRestoreChecked, setWalletRestoreChecked] = useState(false);
|
||||||
const [forceUpdate, setForceUpdate] = useState(0);
|
const [forceUpdate, setForceUpdate] = useState(0);
|
||||||
|
const telegramSession = allowTelegramSession ? getTelegramSession() : null;
|
||||||
|
|
||||||
// Listen for wallet changes
|
// Listen for wallet changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -84,7 +105,8 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
// Allow access if user is logged in OR has valid telegram session
|
||||||
|
if (!user && !telegramSession) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/**
|
||||||
|
* Telegram Mini App Connect Page
|
||||||
|
* Handles authentication from Telegram mini app and redirects to P2P
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { Loader2, AlertTriangle, CheckCircle2 } from 'lucide-react';
|
||||||
|
|
||||||
|
type Status = 'loading' | 'connecting' | 'success' | 'error';
|
||||||
|
|
||||||
|
export default function TelegramConnect() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [status, setStatus] = useState<Status>('loading');
|
||||||
|
const [message, setMessage] = useState('Girêdan tê kirin...');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const connect = async () => {
|
||||||
|
try {
|
||||||
|
// Get params from URL
|
||||||
|
const telegramId = searchParams.get('tg_id');
|
||||||
|
const walletAddress = searchParams.get('wallet');
|
||||||
|
const timestamp = searchParams.get('ts');
|
||||||
|
const from = searchParams.get('from');
|
||||||
|
|
||||||
|
// Validate params
|
||||||
|
if (!telegramId || from !== 'miniapp') {
|
||||||
|
setStatus('error');
|
||||||
|
setError('Parametreyên nederbasdar. Ji kerema xwe ji mini app-ê dest pê bikin.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check timestamp (allow 5 minutes)
|
||||||
|
if (timestamp) {
|
||||||
|
const ts = parseInt(timestamp, 10);
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - ts > 5 * 60 * 1000) {
|
||||||
|
setStatus('error');
|
||||||
|
setError('Lînk qediya. Ji kerema xwe dîsa biceribînin.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('connecting');
|
||||||
|
setMessage('Bikarhêner tê pejirandin...');
|
||||||
|
|
||||||
|
// Find user by telegram_id
|
||||||
|
const { data: userData, error: userError } = await supabase
|
||||||
|
.from('users')
|
||||||
|
.select('id, telegram_id, wallet_address, username, first_name')
|
||||||
|
.eq('telegram_id', parseInt(telegramId, 10))
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (userError || !userData) {
|
||||||
|
setStatus('error');
|
||||||
|
setError('Bikarhêner nehat dîtin. Ji kerema xwe berî dest bi P2P-ê bikin, di mini app-ê de cîzdanê xwe ava bikin.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update wallet address if provided and different
|
||||||
|
if (walletAddress && walletAddress !== userData.wallet_address) {
|
||||||
|
await supabase
|
||||||
|
.from('users')
|
||||||
|
.update({ wallet_address: walletAddress })
|
||||||
|
.eq('id', userData.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate email for this telegram user (for Supabase auth)
|
||||||
|
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
|
||||||
|
|
||||||
|
// Try to sign in with magic link (will be sent to email, but we'll catch it)
|
||||||
|
// Or check if user already has an auth account
|
||||||
|
const { data: authData } = await supabase.auth.getSession();
|
||||||
|
|
||||||
|
if (authData?.session) {
|
||||||
|
// Already logged in, redirect to P2P
|
||||||
|
setStatus('success');
|
||||||
|
setMessage('Serketî! Tê veguheztin...');
|
||||||
|
setTimeout(() => navigate('/p2p'), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to sign in with OTP/magic link
|
||||||
|
const { error: signInError } = await supabase.auth.signInWithOtp({
|
||||||
|
email: telegramEmail,
|
||||||
|
options: {
|
||||||
|
shouldCreateUser: true,
|
||||||
|
data: {
|
||||||
|
telegram_id: parseInt(telegramId, 10),
|
||||||
|
wallet_address: walletAddress,
|
||||||
|
username: userData.username || userData.first_name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signInError) {
|
||||||
|
// If OTP fails, try password-less sign in
|
||||||
|
console.error('OTP sign in failed:', signInError);
|
||||||
|
|
||||||
|
// Store telegram session info in localStorage for P2P access
|
||||||
|
localStorage.setItem('telegram_session', JSON.stringify({
|
||||||
|
telegram_id: telegramId,
|
||||||
|
wallet_address: walletAddress,
|
||||||
|
username: userData.username || userData.first_name,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
setStatus('success');
|
||||||
|
setMessage('Serketî! Tê veguheztin...');
|
||||||
|
setTimeout(() => navigate('/p2p'), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - redirect to P2P
|
||||||
|
setStatus('success');
|
||||||
|
setMessage('Serketî! Tê veguheztin...');
|
||||||
|
setTimeout(() => navigate('/p2p'), 1000);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Telegram connect error:', err);
|
||||||
|
setStatus('error');
|
||||||
|
setError('Xeletî di girêdanê de. Ji kerema xwe dîsa biceribînin.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
connect();
|
||||||
|
}, [searchParams, navigate]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center p-4">
|
||||||
|
<div className="max-w-md w-full bg-gray-900 rounded-2xl p-8 text-center">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-gradient-to-br from-green-500 to-yellow-500 flex items-center justify-center">
|
||||||
|
{status === 'loading' || status === 'connecting' ? (
|
||||||
|
<Loader2 className="w-8 h-8 text-white animate-spin" />
|
||||||
|
) : status === 'success' ? (
|
||||||
|
<CheckCircle2 className="w-8 h-8 text-white" />
|
||||||
|
) : (
|
||||||
|
<AlertTriangle className="w-8 h-8 text-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h1 className="text-xl font-semibold text-white mb-2">
|
||||||
|
{status === 'error' ? 'Xeletî' : 'Telegram Connect'}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Status Message */}
|
||||||
|
<p className={`text-sm ${status === 'error' ? 'text-red-400' : 'text-gray-400'}`}>
|
||||||
|
{error || message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Error Action */}
|
||||||
|
{status === 'error' && (
|
||||||
|
<div className="mt-6 space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => window.close()}
|
||||||
|
className="w-full py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-xl font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Pencereyê Bigire
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Ji kerema xwe vegerin mini app-ê û dîsa biceribînin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Success Info */}
|
||||||
|
{status === 'success' && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center justify-center gap-2 text-green-400">
|
||||||
|
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
|
||||||
|
<span className="text-sm">P2P Platform tê vekirin...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- P2P END-TO-END TEST SCENARIO
|
||||||
|
-- Alice sells 200 HEZ, Bob buys 150 HEZ with IQD
|
||||||
|
-- Uses REAL users from auth.users table
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
v_alice_id UUID;
|
||||||
|
v_bob_id UUID;
|
||||||
|
v_alice_wallet TEXT := '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
|
||||||
|
v_bob_wallet TEXT := '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
|
||||||
|
v_payment_method_id UUID;
|
||||||
|
v_offer_id UUID;
|
||||||
|
v_trade_id UUID;
|
||||||
|
v_result JSON;
|
||||||
|
v_alice_available DECIMAL;
|
||||||
|
v_alice_locked DECIMAL;
|
||||||
|
v_bob_available DECIMAL;
|
||||||
|
v_offer_remaining DECIMAL;
|
||||||
|
v_trade_status TEXT;
|
||||||
|
v_user_count INT;
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
RAISE NOTICE 'P2P E2E TEST: Alice sells 200 HEZ, Bob buys 150';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- STEP 0: Get real users from auth.users
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- STEP 0: Finding test users ---';
|
||||||
|
|
||||||
|
-- Get first two users from auth.users
|
||||||
|
SELECT COUNT(*) INTO v_user_count FROM auth.users;
|
||||||
|
|
||||||
|
IF v_user_count < 2 THEN
|
||||||
|
RAISE EXCEPTION 'Need at least 2 users in auth.users table. Found: %', v_user_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Alice = first user, Bob = second user
|
||||||
|
SELECT id INTO v_alice_id FROM auth.users ORDER BY created_at LIMIT 1;
|
||||||
|
SELECT id INTO v_bob_id FROM auth.users ORDER BY created_at LIMIT 1 OFFSET 1;
|
||||||
|
|
||||||
|
RAISE NOTICE ' Found % users in auth.users', v_user_count;
|
||||||
|
RAISE NOTICE ' Alice (User 1): %', v_alice_id;
|
||||||
|
RAISE NOTICE ' Bob (User 2): %', v_bob_id;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- CLEANUP: Remove any existing test data for these users
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
DELETE FROM p2p_balance_transactions WHERE user_id IN (v_alice_id, v_bob_id);
|
||||||
|
DELETE FROM p2p_fiat_trades WHERE seller_id = v_alice_id OR buyer_id IN (v_alice_id, v_bob_id);
|
||||||
|
DELETE FROM p2p_fiat_offers WHERE seller_id = v_alice_id;
|
||||||
|
DELETE FROM user_internal_balances WHERE user_id IN (v_alice_id, v_bob_id);
|
||||||
|
|
||||||
|
RAISE NOTICE ' Cleaned up previous test data';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- STEP 1: Alice deposits 200 HEZ
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- STEP 1: Alice deposits 200 HEZ ---';
|
||||||
|
|
||||||
|
INSERT INTO user_internal_balances (user_id, token, available_balance, total_deposited, last_deposit_at)
|
||||||
|
VALUES (v_alice_id, 'HEZ', 200, 200, NOW());
|
||||||
|
|
||||||
|
INSERT INTO p2p_balance_transactions (user_id, token, transaction_type, amount, balance_before, balance_after, description)
|
||||||
|
VALUES (v_alice_id, 'HEZ', 'deposit', 200, 0, 200, 'Test deposit');
|
||||||
|
|
||||||
|
-- Bob's empty balance
|
||||||
|
INSERT INTO user_internal_balances (user_id, token, available_balance, total_deposited)
|
||||||
|
VALUES (v_bob_id, 'HEZ', 0, 0);
|
||||||
|
|
||||||
|
RAISE NOTICE ' ✓ Alice deposited 200 HEZ';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- STEP 2: Get payment method
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- STEP 2: Get ZainCash payment method ---';
|
||||||
|
|
||||||
|
SELECT id INTO v_payment_method_id
|
||||||
|
FROM payment_methods
|
||||||
|
WHERE currency = 'IQD' AND method_name = 'ZainCash'
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF v_payment_method_id IS NULL THEN
|
||||||
|
INSERT INTO payment_methods (currency, country, method_name, method_type, fields)
|
||||||
|
VALUES ('IQD', 'Iraq', 'ZainCash', 'mobile_payment', '{"phone_number": "ZainCash Phone"}')
|
||||||
|
RETURNING id INTO v_payment_method_id;
|
||||||
|
RAISE NOTICE ' ✓ Created ZainCash payment method';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE ' ✓ Using existing ZainCash: %', v_payment_method_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- STEP 3: Alice creates sell offer for 200 HEZ
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- STEP 3: Alice creates sell offer (200 HEZ) ---';
|
||||||
|
|
||||||
|
-- Lock escrow
|
||||||
|
SELECT lock_escrow_internal(v_alice_id, 'HEZ', 200, 'offer', NULL) INTO v_result;
|
||||||
|
|
||||||
|
IF NOT (v_result->>'success')::boolean THEN
|
||||||
|
RAISE EXCEPTION 'Escrow lock failed: %', v_result->>'error';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE ' ✓ Escrow locked: 200 HEZ';
|
||||||
|
|
||||||
|
-- Create offer
|
||||||
|
INSERT INTO p2p_fiat_offers (
|
||||||
|
seller_id, seller_wallet, token, amount_crypto, fiat_currency, fiat_amount,
|
||||||
|
payment_method_id, payment_details_encrypted, min_order_amount, max_order_amount,
|
||||||
|
time_limit_minutes, status, remaining_amount, ad_type
|
||||||
|
) VALUES (
|
||||||
|
v_alice_id, v_alice_wallet, 'HEZ', 200, 'IQD', 30000000,
|
||||||
|
v_payment_method_id, 'encrypted_+9647701234567', 10, 200,
|
||||||
|
30, 'open', 200, 'sell'
|
||||||
|
) RETURNING id INTO v_offer_id;
|
||||||
|
|
||||||
|
RAISE NOTICE ' ✓ Offer created: %', v_offer_id;
|
||||||
|
RAISE NOTICE ' 200 HEZ for 30,000,000 IQD (150,000 IQD/HEZ)';
|
||||||
|
|
||||||
|
-- Check Alice balance
|
||||||
|
SELECT available_balance, locked_balance INTO v_alice_available, v_alice_locked
|
||||||
|
FROM user_internal_balances WHERE user_id = v_alice_id AND token = 'HEZ';
|
||||||
|
|
||||||
|
RAISE NOTICE ' Alice: available=%, locked=%', v_alice_available, v_alice_locked;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- STEP 4: Bob initiates trade for 150 HEZ
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- STEP 4: Bob buys 150 HEZ ---';
|
||||||
|
|
||||||
|
INSERT INTO p2p_fiat_trades (
|
||||||
|
offer_id, seller_id, buyer_id, buyer_wallet,
|
||||||
|
crypto_amount, fiat_amount, price_per_unit,
|
||||||
|
escrow_locked_amount, escrow_locked_at, status,
|
||||||
|
payment_deadline, confirmation_deadline
|
||||||
|
) VALUES (
|
||||||
|
v_offer_id, v_alice_id, v_bob_id, v_bob_wallet,
|
||||||
|
150, 22500000, 150000,
|
||||||
|
150, NOW(), 'pending',
|
||||||
|
NOW() + INTERVAL '30 minutes', NOW() + INTERVAL '60 minutes'
|
||||||
|
) RETURNING id INTO v_trade_id;
|
||||||
|
|
||||||
|
-- Update offer remaining
|
||||||
|
UPDATE p2p_fiat_offers
|
||||||
|
SET remaining_amount = remaining_amount - 150
|
||||||
|
WHERE id = v_offer_id;
|
||||||
|
|
||||||
|
RAISE NOTICE ' ✓ Trade created: %', v_trade_id;
|
||||||
|
RAISE NOTICE ' 150 HEZ for 22,500,000 IQD';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- STEP 5: Bob marks payment as sent
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- STEP 5: Bob sends 22,500,000 IQD via ZainCash ---';
|
||||||
|
|
||||||
|
UPDATE p2p_fiat_trades
|
||||||
|
SET status = 'payment_sent',
|
||||||
|
buyer_marked_paid_at = NOW(),
|
||||||
|
buyer_payment_proof_url = 'https://example.com/zaincash_receipt.jpg'
|
||||||
|
WHERE id = v_trade_id;
|
||||||
|
|
||||||
|
RAISE NOTICE ' ✓ Payment marked as sent';
|
||||||
|
RAISE NOTICE ' ✓ Proof uploaded';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- STEP 6: Alice confirms and releases
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '--- STEP 6: Alice confirms payment received ---';
|
||||||
|
|
||||||
|
-- Release escrow
|
||||||
|
SELECT release_escrow_internal(v_alice_id, v_bob_id, 'HEZ', 150, 'trade', v_trade_id) INTO v_result;
|
||||||
|
|
||||||
|
IF NOT (v_result->>'success')::boolean THEN
|
||||||
|
RAISE EXCEPTION 'Escrow release failed: %', v_result->>'error';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Complete trade
|
||||||
|
UPDATE p2p_fiat_trades
|
||||||
|
SET status = 'completed',
|
||||||
|
seller_confirmed_at = NOW(),
|
||||||
|
completed_at = NOW()
|
||||||
|
WHERE id = v_trade_id;
|
||||||
|
|
||||||
|
RAISE NOTICE ' ✓ Escrow released: 150 HEZ → Bob';
|
||||||
|
RAISE NOTICE ' ✓ Trade completed!';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- FINAL VERIFICATION
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
RAISE NOTICE 'FINAL BALANCES';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
|
||||||
|
-- Alice
|
||||||
|
SELECT available_balance, locked_balance INTO v_alice_available, v_alice_locked
|
||||||
|
FROM user_internal_balances WHERE user_id = v_alice_id AND token = 'HEZ';
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE 'ALICE (Seller):';
|
||||||
|
RAISE NOTICE ' Available: % HEZ', v_alice_available;
|
||||||
|
RAISE NOTICE ' Locked: % HEZ (remaining 50 HEZ offer)', v_alice_locked;
|
||||||
|
|
||||||
|
-- Bob
|
||||||
|
SELECT available_balance INTO v_bob_available
|
||||||
|
FROM user_internal_balances WHERE user_id = v_bob_id AND token = 'HEZ';
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE 'BOB (Buyer):';
|
||||||
|
RAISE NOTICE ' Available: % HEZ', v_bob_available;
|
||||||
|
|
||||||
|
-- Offer
|
||||||
|
SELECT remaining_amount INTO v_offer_remaining
|
||||||
|
FROM p2p_fiat_offers WHERE id = v_offer_id;
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE 'OFFER:';
|
||||||
|
RAISE NOTICE ' Remaining: % HEZ (can still sell)', v_offer_remaining;
|
||||||
|
|
||||||
|
-- Trade
|
||||||
|
SELECT status INTO v_trade_status
|
||||||
|
FROM p2p_fiat_trades WHERE id = v_trade_id;
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE 'TRADE:';
|
||||||
|
RAISE NOTICE ' Status: %', v_trade_status;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- ASSERTIONS
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
RAISE NOTICE 'TEST ASSERTIONS';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
|
||||||
|
-- Alice available should be 0
|
||||||
|
IF v_alice_available = 0 THEN
|
||||||
|
RAISE NOTICE '✓ Alice available = 0 HEZ';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✗ FAIL: Alice available = % (expected 0)', v_alice_available;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Alice locked should be 50 (remaining offer)
|
||||||
|
IF v_alice_locked = 50 THEN
|
||||||
|
RAISE NOTICE '✓ Alice locked = 50 HEZ (remaining offer)';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✗ FAIL: Alice locked = % (expected 50)', v_alice_locked;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Bob should have 150
|
||||||
|
IF v_bob_available = 150 THEN
|
||||||
|
RAISE NOTICE '✓ Bob available = 150 HEZ';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✗ FAIL: Bob available = % (expected 150)', v_bob_available;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Offer remaining should be 50
|
||||||
|
IF v_offer_remaining = 50 THEN
|
||||||
|
RAISE NOTICE '✓ Offer remaining = 50 HEZ';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✗ FAIL: Offer remaining = % (expected 50)', v_offer_remaining;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Trade should be completed
|
||||||
|
IF v_trade_status = 'completed' THEN
|
||||||
|
RAISE NOTICE '✓ Trade status = completed';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE '✗ FAIL: Trade status = % (expected completed)', v_trade_status;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
RAISE NOTICE 'ALL TESTS PASSED!';
|
||||||
|
RAISE NOTICE '================================================';
|
||||||
|
RAISE NOTICE '';
|
||||||
|
RAISE NOTICE 'Summary:';
|
||||||
|
RAISE NOTICE ' - Alice started with 200 HEZ';
|
||||||
|
RAISE NOTICE ' - Alice created sell offer for 200 HEZ @ 150,000 IQD/HEZ';
|
||||||
|
RAISE NOTICE ' - Bob bought 150 HEZ for 22,500,000 IQD';
|
||||||
|
RAISE NOTICE ' - Alice confirmed payment and released escrow';
|
||||||
|
RAISE NOTICE ' - Bob now has 150 HEZ';
|
||||||
|
RAISE NOTICE ' - Alice still has 50 HEZ locked in remaining offer';
|
||||||
|
RAISE NOTICE '';
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- ROLLBACK to not affect real data (test only)
|
||||||
|
ROLLBACK;
|
||||||
|
|
||||||
|
-- To keep changes, replace ROLLBACK with COMMIT
|
||||||