diff --git a/example/src/App.js b/example/src/App.js new file mode 100644 index 0000000..099b8b2 --- /dev/null +++ b/example/src/App.js @@ -0,0 +1,51 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { HomeScreen } from './screens/HomeScreen'; +import { PoolServiceDemo } from './screens/PoolServiceDemo'; +import { MPCKeyServiceDemo } from './screens/MPCKeyServiceDemo'; +import { MPCWalletServiceDemo } from './screens/MPCWalletServiceDemo'; +import { MPCSignatureDemo } from './screens/MPCSignatureDemo'; +import { MPCKeyExportDemo } from './screens/MPCKeyExportDemo'; +import AppContext from './components/AppContext'; +import { DeviceBackupDemo } from './screens/DeviceBackupDemo'; +import { DeviceAdditionDemo } from './screens/DeviceAdditionDemo'; +import { ModeSelectionScreen } from './screens/ModeSelectionScreen'; +import { ModeContext } from './utils/ModeProvider'; +import { ModeProvider } from './utils/ModeProvider'; +/** The navigation stack. */ +const Stack = createNativeStackNavigator(); +function SetupApp() { + const [apiKeyData, setApiKeyData] = useState({}); + const [proxyUrlData, setProxyUrlData] = useState(''); + const { selectedMode } = useContext(ModeContext); + useEffect(() => { + if (selectedMode === 'direct-mode') { + const cloudAPIKey = require('./.coinbase_cloud_api_key.json'); + setApiKeyData(cloudAPIKey); + } + else { + const config = require('./config.json'); + setProxyUrlData(config.proxyUrl); + } + }, [selectedMode]); + const apiKeyName = apiKeyData.name || ''; + const privateKey = apiKeyData.privateKey || ''; + const proxyUrl = proxyUrlData || ''; + return (React.createElement(AppContext.Provider, { value: { apiKeyName, privateKey, proxyUrl } }, + React.createElement(NavigationContainer, null, + React.createElement(Stack.Navigator, { initialRouteName: "ModeSelectionScreen" }, + React.createElement(Stack.Screen, { name: "ModeSelectionScreen", component: ModeSelectionScreen }), + React.createElement(Stack.Screen, { name: "Home", component: HomeScreen }), + React.createElement(Stack.Screen, { name: "PoolServiceDemo", component: PoolServiceDemo }), + React.createElement(Stack.Screen, { name: "MPCKeyServiceDemo", component: MPCKeyServiceDemo }), + React.createElement(Stack.Screen, { name: "MPCWalletServiceDemo", component: MPCWalletServiceDemo }), + React.createElement(Stack.Screen, { name: "MPCSignatureDemo", component: MPCSignatureDemo }), + React.createElement(Stack.Screen, { name: "MPCKeyExportDemo", component: MPCKeyExportDemo }), + React.createElement(Stack.Screen, { name: "DeviceBackupDemo", component: DeviceBackupDemo }), + React.createElement(Stack.Screen, { name: "DeviceAdditionDemo", component: DeviceAdditionDemo }))))); +} +export default function App() { + return (React.createElement(ModeProvider, null, + React.createElement(SetupApp, null))); +} diff --git a/example/src/components/AppContext.js b/example/src/components/AppContext.js new file mode 100644 index 0000000..427fe6c --- /dev/null +++ b/example/src/components/AppContext.js @@ -0,0 +1,4 @@ +// components/AppContext.js +import React from 'react'; +const AppContext = React.createContext({}); +export default AppContext; diff --git a/example/src/components/ContinueButton.js b/example/src/components/ContinueButton.js new file mode 100644 index 0000000..1522216 --- /dev/null +++ b/example/src/components/ContinueButton.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { StyleSheet, Button, View } from 'react-native'; +/** + * A component for a continue button. + * @param onPress The function to call when the button is pressed. + * @returns The continue button component. + */ +export const ContinueButton = ({ onPress }) => { + return (React.createElement(View, { style: styles.continueButtonContainer }, + React.createElement(Button, { title: "Continue", onPress: onPress }))); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + continueButtonContainer: { + marginTop: 2, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/example/src/components/CopyButton.js b/example/src/components/CopyButton.js new file mode 100644 index 0000000..c390ddd --- /dev/null +++ b/example/src/components/CopyButton.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { StyleSheet, Button, View } from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; +/** + * A component for a copy button. + * @param text The text to copy. + * @returns The copy button component. + */ +export const CopyButton = ({ text }) => { + return (React.createElement(View, { style: styles.copyButtonContainer }, + React.createElement(Button, { title: "Copy", onPress: () => Clipboard.setString(text) }))); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + copyButtonContainer: { + marginTop: 2, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/example/src/components/DemoStep.js b/example/src/components/DemoStep.js new file mode 100644 index 0000000..5329b2c --- /dev/null +++ b/example/src/components/DemoStep.js @@ -0,0 +1,28 @@ +import React, { useState, useEffect } from 'react'; +import { Animated, StyleSheet } from 'react-native'; +/** + * A component for a single step in a demo. + * @param children The children of the demo step. + * @returns The demo step component. + */ +export const DemoStep = ({ children }) => { + const [fade] = useState(new Animated.Value(0)); + useEffect(() => { + Animated.timing(fade, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }).start(); + }, [fade]); + return (React.createElement(Animated.View, { style: [styles.demoStepContainer, { opacity: fade }] }, children)); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + demoStepContainer: { + background: 'white', + flexDirection: 'column', + margin: 10, + }, +}); diff --git a/example/src/components/DemoText.js b/example/src/components/DemoText.js new file mode 100644 index 0000000..3a753cb --- /dev/null +++ b/example/src/components/DemoText.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { StyleSheet, Text } from 'react-native'; +/** + * A component for demo text. + * @param children The text representing the step. + * @returns The demo text component. + */ +export const DemoText = ({ children }) => { + return React.createElement(Text, { style: styles.demoText }, children); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + demoText: { + fontSize: 14, + fontWeight: '600', + marginVertical: 10, + }, +}); diff --git a/example/src/components/ErrorText.js b/example/src/components/ErrorText.js new file mode 100644 index 0000000..2f757eb --- /dev/null +++ b/example/src/components/ErrorText.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { StyleSheet, Text } from 'react-native'; +/** + * A component for error text. + * @param children The text representing the error. + * @returns The error text component. + */ +export const ErrorText = ({ children }) => { + return React.createElement(Text, { style: styles.demoText }, children); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + demoText: { + color: 'red', + fontSize: 14, + fontWeight: '600', + }, +}); diff --git a/example/src/components/InputText.js b/example/src/components/InputText.js new file mode 100644 index 0000000..1bb5502 --- /dev/null +++ b/example/src/components/InputText.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { StyleSheet, TextInput } from 'react-native'; +/** + * A component for input text. + * @param onTextChange The function to call when the text changes. + * @param editable Whether the input text is editable. + * @param secret Whether the input text should be considered secret. + * @param placeholderText The placeholder text to display in the input field. + * @returns The input text component. + */ +export const InputText = ({ onTextChange, editable, secret, placeholderText }) => { + return (React.createElement(TextInput, { style: styles.inputText, autoCapitalize: "none", onChangeText: onTextChange, secureTextEntry: !!secret, editable: !!editable, placeholder: placeholderText, placeholderTextColor: "#999" })); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + inputText: { + height: 40, + margin: 12, + borderWidth: 1, + padding: 10, + }, +}); diff --git a/example/src/components/LargeInputText.js b/example/src/components/LargeInputText.js new file mode 100644 index 0000000..97e0796 --- /dev/null +++ b/example/src/components/LargeInputText.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { StyleSheet, TextInput } from 'react-native'; +/** + * A component for large input text. + * @param onTextChange The function to call when the text changes. + * @param initialText The initial text. + * @returns The large input text component. + */ +export const LargeInputText = ({ onTextChange, initialText }) => { + return (React.createElement(TextInput, { style: styles.inputText, multiline: true, numberOfLines: 15, autoCapitalize: "none", onChangeText: onTextChange, defaultValue: initialText })); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + inputText: { + fontFamily: 'Courier New', + height: 200, + margin: 12, + borderWidth: 1, + padding: 10, + }, +}); diff --git a/example/src/components/MonospaceText.js b/example/src/components/MonospaceText.js new file mode 100644 index 0000000..96a6cc1 --- /dev/null +++ b/example/src/components/MonospaceText.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +/** + * A component for displaying text in monospace font. + * @param verticalMargin The amount of margin to apply above and below the text. + * @returns The text component. + */ +export const MonospaceText = ({ children, verticalMargin = 0, }) => { + return (React.createElement(View, { style: styles.container }, + React.createElement(Text, { style: [styles.monospaceText, { marginVertical: verticalMargin }] }, children))); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#f5f5f5', + }, + monospaceText: { + fontFamily: 'Courier', + fontSize: 12, + lineHeight: 24, + backgroundColor: '#f5f5f5', + }, +}); diff --git a/example/src/components/Note.js b/example/src/components/Note.js new file mode 100644 index 0000000..4186e0c --- /dev/null +++ b/example/src/components/Note.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +/** + * A component for displaying a note section. + * @param children The content to display in the note. + * @param items An optional array of items to render in a numbered list. + * @returns The note component. + */ +export const Note = ({ children, items }) => { + const renderItem = (item, index) => (React.createElement(View, { key: index, style: styles.listItem }, + React.createElement(Text, { style: [styles.listItemNumber, styles.noteText] }, + index + 1, + "."), + React.createElement(Text, { style: [styles.listItemText, styles.noteText] }, item))); + const renderContent = () => { + if (typeof children === 'string') { + return (React.createElement(React.Fragment, null, + React.createElement(Text, { style: styles.noteText }, children), + items && (React.createElement(View, { style: styles.listContainer }, items.map((item, index) => renderItem(item, index)))))); + } + else if (Array.isArray(children)) { + return (React.createElement(React.Fragment, null, + children.map((child, index) => (React.createElement(Text, { key: index, style: styles.noteText }, child))), + items && (React.createElement(View, { style: styles.listContainer }, items.map((item, index) => renderItem(item, index)))))); + } + else { + return children; + } + }; + return React.createElement(View, { style: styles.container }, renderContent()); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + container: { + backgroundColor: '#fff8dc', + borderColor: '#ffeb9c', + borderWidth: 1, + borderRadius: 4, + padding: 8, + marginBottom: 16, + }, + noteText: { + fontSize: 16, + color: '#333', + fontStyle: 'italic', + }, + listContainer: { + marginTop: 8, + }, + listItem: { + flexDirection: 'row', + alignItems: 'flex-start', + marginBottom: 4, + }, + listItemNumber: { + marginRight: 4, + fontSize: 14, + color: '#333', + }, + listItemText: { + flex: 1, + fontSize: 14, + color: '#333', + marginLeft: 4, + }, +}); diff --git a/example/src/components/PageTitle.js b/example/src/components/PageTitle.js new file mode 100644 index 0000000..1960093 --- /dev/null +++ b/example/src/components/PageTitle.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { StyleSheet, Text, useColorScheme, View } from 'react-native'; +import { Colors } from 'react-native/Libraries/NewAppScreen'; +/** + * A component for a page title. + * @param title The page title. + * @returns The page title component. + */ +export const PageTitle = ({ title }) => { + const isDarkMode = useColorScheme() === 'dark'; + return (React.createElement(View, { style: styles.pageTitleContainer }, + React.createElement(Text, { style: [ + styles.pageTitle, + { + color: isDarkMode ? Colors.white : Colors.black, + }, + ] }, title))); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + pageTitleContainer: { + marginVertical: 20, + }, + pageTitle: { + fontSize: 32, + fontWeight: '600', + textAlign: 'center', + }, +}); diff --git a/example/src/components/Section.js b/example/src/components/Section.js new file mode 100644 index 0000000..c18f485 --- /dev/null +++ b/example/src/components/Section.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { Button, StyleSheet, Text, useColorScheme, View } from 'react-native'; +import { Colors } from 'react-native/Libraries/NewAppScreen'; +import { useNavigation } from '@react-navigation/native'; +/** + * A component for a section. + * @param title The title of the section. + * @param children The child nodes of the component. + * @param runDestination The screen to navigate to when the run button is clicked. + */ +export const Section = ({ children, title, runDestination, disabled }) => { + const isDarkMode = useColorScheme() === 'dark'; + const navigation = useNavigation(); + const stylesToApply = [styles.sectionContainer]; + if (disabled) { + stylesToApply.push(styles.disabled); + } + return (React.createElement(View, { style: stylesToApply }, + React.createElement(Text, { style: [ + styles.sectionTitle, + { + color: isDarkMode ? Colors.white : Colors.black, + }, + ] }, title), + React.createElement(Text, { style: [ + styles.sectionDescription, + { + color: isDarkMode ? Colors.light : Colors.dark, + }, + ] }, children), + runDestination !== undefined && (React.createElement(View, { style: styles.runButton }, + React.createElement(Button, { title: "Run", onPress: () => navigation.navigate(runDestination) }))))); +}; +/** + * Styles for the component. + */ +const styles = StyleSheet.create({ + sectionContainer: { + borderColor: 'gray', + borderWidth: 2, + marginHorizontal: 12, + marginTop: 32, + paddingBottom: 16, + paddingHorizontal: 24, + paddingTop: 12, + }, + sectionTitle: { + fontSize: 24, + fontWeight: '600', + }, + sectionDescription: { + marginTop: 8, + fontSize: 18, + fontWeight: '400', + }, + runButton: { + marginTop: 8, + }, + disabled: { + backgroundColor: '#bebebe', + }, +}); diff --git a/example/src/constants.js b/example/src/constants.js new file mode 100644 index 0000000..74cc9a7 --- /dev/null +++ b/example/src/constants.js @@ -0,0 +1,4 @@ +// The operating mode where the app connects directly to the WaaS API. +export const directMode = 'direct-mode'; +// The operating mode where the app connects to the WaaS API via a proxy server. +export const proxyMode = 'proxy-mode'; diff --git a/example/src/screens/DeviceAdditionDemo.js b/example/src/screens/DeviceAdditionDemo.js new file mode 100644 index 0000000..6463c02 --- /dev/null +++ b/example/src/screens/DeviceAdditionDemo.js @@ -0,0 +1,122 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import { addDevice, computeAddDeviceMPCOperation, initMPCKeyService, initMPCSdk, initMPCWalletService, pollForPendingDevices, } from '@coinbase/waas-sdk-react-native'; +import { ContinueButton } from '../components/ContinueButton'; +import { DemoStep } from '../components/DemoStep'; +import { DemoText } from '../components/DemoText'; +import { ErrorText } from '../components/ErrorText'; +import { InputText } from '../components/InputText'; +import { PageTitle } from '../components/PageTitle'; +import AppContext from '../components/AppContext'; +import { Note } from '../components/Note'; +import { ModeContext } from '../utils/ModeProvider'; +import { directMode, proxyMode } from '../constants'; +export const DeviceAdditionDemo = () => { + const [deviceGroupName, setDeviceGroupName] = React.useState(''); + const [deviceGroupEditable, setDeviceGroupEditable] = React.useState(true); + const [deviceName, setDeviceName] = React.useState(''); + const [deviceEditable, setDeviceEditable] = React.useState(true); + const [passcode, setPasscode] = React.useState(''); + const [passcodeEditable, setPasscodeEditable] = React.useState(true); + const [deviceBackup, setDeviceBackup] = React.useState(''); + const [deviceBackupEditable, setDeviceBackupEditable] = React.useState(true); + const [resultError, setResultError] = React.useState(); + const [showStep2, setShowStep2] = React.useState(); + const [showStep5, setShowStep5] = React.useState(); + const [showStep6, setShowStep6] = React.useState(); + const [showError, setShowError] = React.useState(); + const { selectedMode } = React.useContext(ModeContext); + const { apiKeyName: apiKeyName, privateKey: privateKey, proxyUrl: proxyUrl, } = React.useContext(AppContext); + // Runs the DeviceAdditionDemo. + React.useEffect(() => { + let demoFn = async function () { + if (!showStep2 || showStep6 || deviceGroupName) { + return; + } + if (selectedMode === directMode && + (apiKeyName === '' || privateKey === '')) { + return; + } + try { + const apiKey = selectedMode === proxyMode ? '' : apiKeyName; + const privKey = selectedMode === proxyMode ? '' : privateKey; + // Initialize the MPCKeyService and MPCSdk. + await initMPCSdk(true); + await initMPCKeyService(apiKey, privKey, proxyUrl); + await initMPCWalletService(apiKey, privKey, proxyUrl); + if (!showStep5) { + const operationName = await addDevice(deviceGroupName, deviceName); + setShowStep5(true); + // Process operation. + const pendingDeviceOperations = await pollForPendingDevices(deviceGroupName); + for (let i = pendingDeviceOperations.length - 1; i >= 0; i--) { + const pendingOperation = pendingDeviceOperations[i]; + if (pendingOperation?.Operation === operationName) { + await computeAddDeviceMPCOperation(pendingOperation.MPCData, passcode, deviceBackup); + setShowStep6(true); + return; + } + } + throw new Error(`could not find operation with name ${operationName}`); + } + } + catch (error) { + setResultError(error); + setShowError(true); + } + }; + demoFn(); + }, // eslint-disable-next-line react-hooks/exhaustive-deps + [ + deviceGroupName, + apiKeyName, + privateKey, + proxyUrl, + showStep2, + deviceName, + deviceBackup, + passcode, + selectedMode, + ]); + const requiredDemos = [ + 'Pool Creation', + 'Device Registration', + 'Address Generation', + 'Device Backup', + ]; + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Device Restore" }), + React.createElement(Note, { items: requiredDemos }, "Note: This demo requires that you initialize a new Device (i.e. simulator), and run the Device Registration demo with it, before you run this Demo with the new Device. The old Device should have run the following demos already:"), + React.createElement(DemoStep, null, + React.createElement(DemoText, null, "1. Input your DeviceGroup resource name below:"), + React.createElement(InputText, { onTextChange: setDeviceGroupName, editable: deviceGroupEditable, placeholderText: "pools/{pool_id}/deviceGroups/{device_group_id}" }), + React.createElement(DemoText, null, "2. Input the resource name of your newly registered Device (i.e. not your old Device) below:"), + React.createElement(InputText, { onTextChange: setDeviceName, editable: deviceEditable, placeholderText: "devices/{device_id}" }), + React.createElement(DemoText, null, "3. Input your passcode below:"), + React.createElement(InputText, { onTextChange: setPasscode, editable: passcodeEditable, secret: true }), + React.createElement(DemoText, null, "4. Input the Device backup created from an existing Device using the Device Backup demo. This will be a long hexadecimal string:"), + React.createElement(InputText, { onTextChange: setDeviceBackup, editable: deviceBackupEditable }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep2(true); + setDeviceGroupEditable(false); + setDeviceEditable(false); + setPasscodeEditable(false); + setDeviceBackupEditable(false); + } })), + showStep5 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "5. Initiated the Device Restore operation. Processing MPC Operation - this may take a while..."))), + showStep6 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "6. Successfully added the new Device to the DeviceGroup, and thereby restored the access of the old Device. Now, run the Transaction Signing demo with the new Device to confirm access."))), + showError && (React.createElement(DemoStep, null, + React.createElement(ErrorText, null, + "ERROR: ", + resultError?.message))))); +}; +/** + * The styles for the App container. + */ +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + }, +}); diff --git a/example/src/screens/DeviceBackupDemo.js b/example/src/screens/DeviceBackupDemo.js new file mode 100644 index 0000000..0b66da5 --- /dev/null +++ b/example/src/screens/DeviceBackupDemo.js @@ -0,0 +1,134 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import { computePrepareDeviceBackupMPCOperation, exportDeviceBackup, initMPCKeyService, initMPCSdk, initMPCWalletService, pollForPendingDeviceBackups, prepareDeviceBackup, } from '@coinbase/waas-sdk-react-native'; +import { ContinueButton } from '../components/ContinueButton'; +import { DemoStep } from '../components/DemoStep'; +import { DemoText } from '../components/DemoText'; +import { ErrorText } from '../components/ErrorText'; +import { InputText } from '../components/InputText'; +import { PageTitle } from '../components/PageTitle'; +import AppContext from '../components/AppContext'; +import { CopyButton } from '../components/CopyButton'; +import { Note } from '../components/Note'; +import { ModeContext } from '../utils/ModeProvider'; +import { directMode, proxyMode } from '../constants'; +export const DeviceBackupDemo = () => { + const [deviceGroupName, setDeviceGroupName] = React.useState(''); + const [deviceGroupEditable, setDeviceGroupEditable] = React.useState(true); + const [deviceEditable, setDeviceEditable] = React.useState(true); + const [passcodeEditable, setPasscodeEditable] = React.useState(true); + const [resultError, setResultError] = React.useState(); + const [deviceName, setDeviceName] = React.useState(''); + const [passcode, setPasscode] = React.useState(''); + const [deviceBackup, setDeviceBackup] = React.useState(''); + const [showStep4, setShowStep4] = React.useState(); + const [showStep5, setShowStep5] = React.useState(); + const [showStep6, setShowStep6] = React.useState(); + const [showError, setShowError] = React.useState(); + const { selectedMode } = React.useContext(ModeContext); + const { apiKeyName: apiKeyName, privateKey: privateKey, proxyUrl: proxyUrl, } = React.useContext(AppContext); + // Runs the DeviceBackupDemo. + React.useEffect(() => { + let demoFn = async function () { + if (deviceGroupName === '' || + deviceName === '' || + passcode === '' || + !showStep4 || + showStep5) { + return; + } + if (selectedMode === directMode && + (apiKeyName === '' || privateKey === '')) { + return; + } + try { + const apiKey = selectedMode === proxyMode ? '' : apiKeyName; + const privKey = selectedMode === proxyMode ? '' : privateKey; + // Initialize the MPCKeyService and MPCSdk. + await initMPCSdk(true); + await initMPCKeyService(apiKey, privKey, proxyUrl); + await initMPCWalletService(apiKey, privKey, proxyUrl); + let operationName = await prepareDeviceBackup(deviceGroupName, deviceName); + // Process operation. + const pendingDeviceBackupOperations = await pollForPendingDeviceBackups(deviceGroupName); + let pendingOperation; + for (let i = pendingDeviceBackupOperations.length - 1; i >= 0; i--) { + if (pendingDeviceBackupOperations[i]?.Operation === operationName) { + pendingOperation = pendingDeviceBackupOperations[i]; + } + } + if (!pendingOperation) { + throw new Error(`could not find operation with name ${operationName}`); + } + await computePrepareDeviceBackupMPCOperation(pendingOperation.MPCData, passcode); + setShowStep5(true); + } + catch (error) { + setResultError(error); + setShowError(true); + } + }; + demoFn(); + let waitForBackupExport = async function () { + if (!showStep5) { + return; + } + let result = await exportDeviceBackup(); + setDeviceBackup(result); + setShowStep6(true); + }; + waitForBackupExport(); + }, [ + deviceGroupName, + apiKeyName, + privateKey, + proxyUrl, + showStep4, + showStep5, + deviceName, + passcode, + selectedMode, + ]); + const requiredDemos = [ + 'Pool Creation', + 'Device Registration', + 'Address Generation', + ]; + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Device Backup" }), + React.createElement(Note, { items: requiredDemos }, "Note: Ensure you have run the following demos before this one:"), + React.createElement(DemoStep, null, + React.createElement(DemoText, null, "1. Input your DeviceGroup resource name below:"), + React.createElement(InputText, { onTextChange: setDeviceGroupName, editable: deviceGroupEditable, placeholderText: "pools/{pool_id}/deviceGroups/{device_group_id}" }), + React.createElement(DemoText, null, "2. Input your Device resource name below:"), + React.createElement(InputText, { onTextChange: setDeviceName, editable: deviceEditable, placeholderText: "devices/{device_id}" }), + React.createElement(DemoText, null, "3. Input your passcode below:"), + React.createElement(InputText, { onTextChange: setPasscode, editable: passcodeEditable, secret: true }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep4(true); + setDeviceGroupEditable(false); + setDeviceEditable(false); + setPasscodeEditable(false); + } })), + showStep4 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "4. Preparing your Device backup. This may take some time..."))), + showStep5 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "5. Successfully created the backup for this Device and DeviceGroup."))), + showStep6 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "6. Retrieved the Device backup. It is a long hexadecimal string we do not render here."), + React.createElement(DemoText, null, "Copy the Device backup and paste it into a notepad before proceeding to the next demo."), + React.createElement(CopyButton, { text: deviceBackup }), + React.createElement(Note, null, "This data is sensitive, do not share!"))), + showError && (React.createElement(DemoStep, null, + React.createElement(ErrorText, null, + "ERROR: ", + resultError?.message))))); +}; +/** + * The styles for the App container. + */ +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + }, +}); diff --git a/example/src/screens/HomeScreen.js b/example/src/screens/HomeScreen.js new file mode 100644 index 0000000..b9944f4 --- /dev/null +++ b/example/src/screens/HomeScreen.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { SafeAreaView, ScrollView, StatusBar, useColorScheme, View, } from 'react-native'; +import { Colors } from 'react-native/Libraries/NewAppScreen'; +import { PageTitle } from '../components/PageTitle'; +import { Section } from '../components/Section'; +/** + * The home screen. + */ +export const HomeScreen = () => { + const isDarkMode = useColorScheme() === 'dark'; + const backgroundStyle = { + backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, + }; + return (React.createElement(SafeAreaView, { style: backgroundStyle }, + React.createElement(StatusBar, { barStyle: isDarkMode ? 'light-content' : 'dark-content' }), + React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: backgroundStyle }, + React.createElement(View, { style: { + backgroundColor: isDarkMode ? Colors.black : Colors.white, + } }, + React.createElement(PageTitle, { title: "WaaS SDK Demos" }), + React.createElement(Section, { title: "Pool Creation", runDestination: "PoolServiceDemo" }, "Create a Pool resource."), + React.createElement(Section, { title: "Device Registration", runDestination: "MPCKeyServiceDemo" }, "Generate registration data for the Device and register the Device with WaaS."), + React.createElement(Section, { title: "Address Generation", runDestination: "MPCWalletServiceDemo" }, "Create an MPCWallet with an associated DeviceGroup and generate an Ethereum Address in the MPCWallet."), + React.createElement(Section, { title: "Transaction Signing", runDestination: "MPCSignatureDemo" }, "Compute a signed transaction for the Ethereum Address."), + React.createElement(Section, { title: "Key Export", runDestination: "MPCKeyExportDemo" }, "Export the private keys corresponding to the MPCKeys in the DeviceGroup."), + React.createElement(Section, { title: "Device Backup", runDestination: "DeviceBackupDemo" }, "Export a Device backup that can be used to restore a Device within a DeviceGroup."), + React.createElement(Section, { title: "Device Restore", runDestination: "DeviceAdditionDemo" }, "Restore an old Device by adding a new Device to an existing DeviceGroup using the backup prepared by the old Device."))))); +}; diff --git a/example/src/screens/MPCKeyExportDemo.js b/example/src/screens/MPCKeyExportDemo.js new file mode 100644 index 0000000..e8f3715 --- /dev/null +++ b/example/src/screens/MPCKeyExportDemo.js @@ -0,0 +1,167 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import { initMPCWalletService, prepareDeviceArchive, exportPrivateKeys, initMPCKeyService, initMPCSdk, computePrepareDeviceArchiveMPCOperation, getDeviceGroup, pollForPendingDeviceArchives, } from '@coinbase/waas-sdk-react-native'; +import { ContinueButton } from '../components/ContinueButton'; +import { DemoStep } from '../components/DemoStep'; +import { DemoText } from '../components/DemoText'; +import { ErrorText } from '../components/ErrorText'; +import { InputText } from '../components/InputText'; +import { PageTitle } from '../components/PageTitle'; +import AppContext from '../components/AppContext'; +import { CopyButton } from '../components/CopyButton'; +import { Note } from '../components/Note'; +import { MonospaceText } from '../components/MonospaceText'; +import { ModeContext } from '../utils/ModeProvider'; +import { directMode, proxyMode } from '../constants'; +export const MPCKeyExportDemo = () => { + const [deviceGroupName, setDeviceGroupName] = React.useState(''); + const [deviceGroupEditable, setDeviceGroupEditable] = React.useState(true); + const [deviceEditable, setDeviceEditable] = React.useState(true); + const [passcodeEditable, setPasscodeEditable] = React.useState(true); + const [resultError, setResultError] = React.useState(); + const [deviceName, setDeviceName] = React.useState(''); + const [passcode, setPasscode] = React.useState(''); + const [mpcKeyExportMetadata, setMpcKeyExportMetadata] = React.useState(''); + const [mpcKeyExportMetadataInput, setMpcKeyExportMetadataInput] = React.useState(''); + const [mpcKeyExportMetadataInputEditable, setmpcKeyExportMetadataInputEditable,] = React.useState(true); + const [exportedKeys, setExportedKeys] = React.useState(''); + const [showStep2, setShowStep2] = React.useState(); + const [showStep3, setShowStep3] = React.useState(); + const [showStep4, setShowStep4] = React.useState(); + const [showStep5, setShowStep5] = React.useState(); + const [showStep6, setShowStep6] = React.useState(); + const [showStep7, setShowStep7] = React.useState(); + const [showError, setShowError] = React.useState(); + const { selectedMode } = React.useContext(ModeContext); + const { apiKeyName: apiKeyName, privateKey: privateKey, proxyUrl: proxyUrl, } = React.useContext(AppContext); + // Runs the MPCKeyExportDemo. + React.useEffect(() => { + let demoFn = async function () { + if (deviceGroupName === '' || !showStep2 || showStep7) { + return; + } + if (selectedMode === directMode && + (apiKeyName === '' || privateKey === '')) { + return; + } + try { + const apiKey = selectedMode === proxyMode ? '' : apiKeyName; + const privKey = selectedMode === proxyMode ? '' : privateKey; + // Initialize the MPCKeyService and MPCSdk. + await initMPCSdk(true); + await initMPCKeyService(apiKey, privKey, proxyUrl); + await initMPCWalletService(apiKey, privKey, proxyUrl); + if (!showStep3) { + const operationName = (await prepareDeviceArchive(deviceGroupName, deviceName)); + setShowStep3(true); + const pendingDeviceArchiveOperations = await pollForPendingDeviceArchives(deviceGroupName); + let pendingOperation; + for (let i = pendingDeviceArchiveOperations.length - 1; i >= 0; i--) { + if (pendingDeviceArchiveOperations[i]?.Operation === operationName) { + pendingOperation = pendingDeviceArchiveOperations[i]; + break; + } + } + if (!pendingOperation) { + throw new Error(`could not find operation with name ${operationName}`); + } + await computePrepareDeviceArchiveMPCOperation(pendingOperation.MPCData, passcode); + const retrievedDeviceGroup = await getDeviceGroup(deviceGroupName); + setMpcKeyExportMetadata(retrievedDeviceGroup.MPCKeyExportMetadata); + setShowStep4(true); + } + } + catch (error) { + setResultError(error); + setShowError(true); + } + }; + demoFn(); + let waitForKeyExport = async function () { + if (!showStep7) { + return; + } + let result = await exportPrivateKeys(mpcKeyExportMetadataInput, passcode); + setExportedKeys((result[0]?.Address + + ' -> ' + + result[0]?.PrivateKey)); + setShowStep7(true); + }; + waitForKeyExport(); + }, [ + deviceGroupName, + apiKeyName, + privateKey, + proxyUrl, + showStep2, + showStep3, + showStep4, + showStep7, + deviceName, + mpcKeyExportMetadata, + passcode, + mpcKeyExportMetadataInput, + selectedMode, + ]); + const requiredDemos = [ + 'Pool Creation', + 'Device Registration', + 'Address Generation', + ]; + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Key Export" }), + React.createElement(Note, { items: requiredDemos }, "Note: Ensure you have run the following demos before this one:"), + React.createElement(DemoStep, null, + React.createElement(DemoText, null, "1. Input your DeviceGroup resource name below:"), + React.createElement(InputText, { onTextChange: setDeviceGroupName, editable: deviceGroupEditable, placeholderText: "pools/{pool_id}/deviceGroups/{device_group_id}" }), + React.createElement(DemoText, null, "2. Input your Device resource name below:"), + React.createElement(InputText, { onTextChange: setDeviceName, editable: deviceEditable, placeholderText: "devices/{device_id}" }), + React.createElement(DemoText, null, "3. Input your passcode below:"), + React.createElement(InputText, { onTextChange: setPasscode, editable: passcodeEditable, secret: true }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep2(true); + setDeviceGroupEditable(false); + setDeviceEditable(false); + } })), + showStep3 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "3. Preparing your Device archive..."))), + showStep4 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "4. Successfully created a Device archive for this Device and DeviceGroup. The archive's base64-encoded key export metadata is:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, mpcKeyExportMetadata), + React.createElement(DemoText, null, "Copy your archive's key export metadata and paste it into a notepad before proceeding to the next step."), + React.createElement(CopyButton, { text: mpcKeyExportMetadata }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep5(true); + } }))), + showStep5 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "5. Input your passcode again to export the private keys in your MPCWallet:"), + React.createElement(InputText, { onTextChange: setPasscode, editable: passcodeEditable, secret: true }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep6(true); + setPasscodeEditable(false); + } }))), + showStep6 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "6. Input the mpcKeyExportMetadata from Step 4:"), + React.createElement(InputText, { onTextChange: setMpcKeyExportMetadataInput, editable: mpcKeyExportMetadataInputEditable }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep7(true); + setmpcKeyExportMetadataInputEditable(false); + } }))), + showStep7 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "7. Successfully exported your private keys. The list below maps your addresses to their corresponding private keys:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, exportedKeys), + React.createElement(CopyButton, { text: exportedKeys }), + React.createElement(Note, null, "This data is sensitive, do not share!"))), + showError && (React.createElement(DemoStep, null, + React.createElement(ErrorText, null, + "ERROR: ", + resultError?.message))))); +}; +/** + * The styles for the App container. + */ +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + }, +}); diff --git a/example/src/screens/MPCKeyServiceDemo.js b/example/src/screens/MPCKeyServiceDemo.js new file mode 100644 index 0000000..ec8532e --- /dev/null +++ b/example/src/screens/MPCKeyServiceDemo.js @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import { getRegistrationData, bootstrapDevice, registerDevice, initMPCKeyService, initMPCSdk, } from '@coinbase/waas-sdk-react-native'; +import { ContinueButton } from '../components/ContinueButton'; +import { DemoStep } from '../components/DemoStep'; +import { DemoText } from '../components/DemoText'; +import { ErrorText } from '../components/ErrorText'; +import { PageTitle } from '../components/PageTitle'; +import { CopyButton } from '../components/CopyButton'; +import { InputText } from '../components/InputText'; +import AppContext from '../components/AppContext'; +import { MonospaceText } from '../components/MonospaceText'; +import { ModeContext } from '../utils/ModeProvider'; +import { directMode, proxyMode } from '../constants'; +export const MPCKeyServiceDemo = () => { + const [registrationData, setRegistrationData] = React.useState(''); + const [passcode, setPasscode] = React.useState(''); + const [resultError, setResultError] = React.useState(); + const [passcodeEditable, setPasscodeEditable] = React.useState(true); + const [device, setDevice] = React.useState(); + const [showStep2, setShowStep2] = React.useState(); + const [showStep3, setShowStep3] = React.useState(); + const [showStep4, setShowStep4] = React.useState(); + const [showStep5, setShowStep5] = React.useState(); + const [showError, setShowError] = React.useState(); + const { apiKeyName: apiKeyName, privateKey: privateKey, proxyUrl: proxyUrl, } = React.useContext(AppContext); + const { selectedMode } = React.useContext(ModeContext); + // Runs the MPCKeyService demo. + React.useEffect(() => { + let demoFn = async function () { + if (!showStep2) { + return; + } + if (selectedMode === directMode && + (apiKeyName === '' || privateKey === '')) { + return; + } + try { + const apiKey = selectedMode === proxyMode ? '' : apiKeyName; + const privKey = selectedMode === proxyMode ? '' : privateKey; + // Initialize the MPCKeyService and MPCSdk. + await initMPCSdk(true); + await initMPCKeyService(apiKey, privKey, proxyUrl); + await bootstrapDevice(passcode); + const regData = await getRegistrationData(); + setRegistrationData(regData); + setShowStep3(true); + setShowStep4(true); + const registeredDevice = await registerDevice(); + setDevice(registeredDevice); + setShowStep5(true); + } + catch (error) { + setResultError(error); + setShowError(true); + } + }; + demoFn(); + }, [showStep2, apiKeyName, passcode, privateKey, proxyUrl, selectedMode]); + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Device Registration" }), + React.createElement(DemoStep, null, + React.createElement(DemoText, null, "1. Enter a passcode of at least 6 digits. This passcode will be used to encrypt your device archive and backup materials. Remember your passcode!"), + React.createElement(InputText, { onTextChange: setPasscode, editable: passcodeEditable, secret: true }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep2(true); + setPasscodeEditable(false); + } })), + showStep2 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "2. Generating Device registration data..."))), + showStep3 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "3. Your base64-encoded Device registration data is below:"), + React.createElement(MonospaceText, { verticalMargin: 5 }, registrationData), + React.createElement(DemoText, null, "Typically, you would use this data to call RegisterDevice from your proxy server; however, for convenience, we'll do that directly here."))), + showStep4 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "4. Registering Device..."))), + showStep5 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "5. Successfully registered Device with resource name:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, device?.Name), + React.createElement(DemoText, null, "Copy your Device resource name and paste it into a notepad before proceeding to the next demo."), + React.createElement(CopyButton, { text: device?.Name }))), + showError && (React.createElement(DemoStep, null, + React.createElement(ErrorText, null, + "ERROR: ", + resultError?.message))))); +}; +/** + * The styles for the App container. + */ +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + }, +}); diff --git a/example/src/screens/MPCSignatureDemo.js b/example/src/screens/MPCSignatureDemo.js new file mode 100644 index 0000000..8647a8d --- /dev/null +++ b/example/src/screens/MPCSignatureDemo.js @@ -0,0 +1,171 @@ +import { computeMPCOperation, createSignatureFromTx, getAddress, getSignedTransaction, initMPCKeyService, initMPCSdk, initMPCWalletService, pollForPendingSignatures, waitPendingSignature, } from '@coinbase/waas-sdk-react-native'; +import * as React from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import AppContext from '../components/AppContext'; +import { ContinueButton } from '../components/ContinueButton'; +import { CopyButton } from '../components/CopyButton'; +import { DemoStep } from '../components/DemoStep'; +import { DemoText } from '../components/DemoText'; +import { ErrorText } from '../components/ErrorText'; +import { InputText } from '../components/InputText'; +import { LargeInputText } from '../components/LargeInputText'; +import { Note } from '../components/Note'; +import { PageTitle } from '../components/PageTitle'; +import { MonospaceText } from '../components/MonospaceText'; +import { ModeContext } from '../utils/ModeProvider'; +import { directMode, proxyMode } from '../constants'; +export const MPCSignatureDemo = () => { + // The initial transaction text. + const initialTx = `{ + "ChainID": "0x5", + "Nonce": 0, + "MaxPriorityFeePerGas": "0x400", + "MaxFeePerGas": "0x400", + "Gas": 63000, + "To": "0xd8ddbfd00b958e94a024fb8c116ae89c70c60257", + "Value": "0x1000", + "Data": "" + }`; + const [deviceGroupName, setDeviceGroupName] = React.useState(''); + const [deviceGroupEditable, setDeviceGroupEditable] = React.useState(true); + const [addressName, setAddressName] = React.useState(''); + const [addressNameEditable, setAddressNameEditable] = React.useState(true); + const [tx, setTx] = React.useState(initialTx); + const [pendingSignature, setPendingSignature] = React.useState(); + const [signature, setSignature] = React.useState(); + const [signedTx, setSignedTx] = React.useState(); + const [resultError, setResultError] = React.useState(); + const [showStep2, setShowStep2] = React.useState(); + const [showStep3, setShowStep3] = React.useState(); + const [showStep4, setShowStep4] = React.useState(); + const [showStep5, setShowStep5] = React.useState(); + const [showStep6, setShowStep6] = React.useState(); + const [showStep7, setShowStep7] = React.useState(); + const [showStep8, setShowStep8] = React.useState(); + const [showStep9, setShowStep9] = React.useState(); + const [showError, setShowError] = React.useState(); + const { selectedMode } = React.useContext(ModeContext); + const { apiKeyName: apiKeyName, privateKey: privateKey, proxyUrl: proxyUrl, } = React.useContext(AppContext); + // Runs the Signature demo. + React.useEffect(() => { + let demoFn = async function () { + if (addressName === '' || deviceGroupName === '' || !showStep3) { + return; + } + if (selectedMode === directMode && + (apiKeyName === '' || privateKey === '')) { + return; + } + try { + const apiKey = selectedMode === proxyMode ? '' : apiKeyName; + const privKey = selectedMode === proxyMode ? '' : privateKey; + // Initialize the MPCSdk, MPCKeyService and MPCWalletService. + await initMPCSdk(true); + await initMPCKeyService(apiKey, privKey, proxyUrl); + await initMPCWalletService(apiKey, privKey, proxyUrl); + const retrievedAddress = await getAddress(addressName); + const keyName = retrievedAddress.MPCKeys[0]; + // Initiate the operation to create a signature. + const resultTx = JSON.parse(tx); + const operationName = await createSignatureFromTx(keyName, resultTx); + setShowStep4(true); + // Poll for pending signatures. + setShowStep5(true); + const pendingSignatures = await pollForPendingSignatures(deviceGroupName); + let pendingSignatureOp; + for (let i = 0; i < pendingSignatures.length; i++) { + if (pendingSignatures[i]?.Operation === operationName) { + pendingSignatureOp = pendingSignatures[i]; + } + } + if (!pendingSignatureOp) { + throw new Error(`could not find operation with name ${operationName}`); + } + setPendingSignature(pendingSignatureOp); + setShowStep6(true); + // Process the pending signature. + setShowStep7(true); + await computeMPCOperation(pendingSignatureOp.MPCData); + // Get Signature from MPCKeyService. + let signatureResult = await waitPendingSignature(pendingSignatureOp.Operation); + setSignature(signatureResult); + setShowStep8(true); + const signedTxResult = await getSignedTransaction(resultTx, signatureResult); + setSignedTx(signedTxResult); + setShowStep9(true); + } + catch (error) { + setResultError(error); + setShowError(true); + } + }; + demoFn(); + }, [ + addressName, + deviceGroupName, + apiKeyName, + privateKey, + proxyUrl, + tx, + initialTx, + showStep3, + selectedMode, + ]); + const requiredDemos = [ + 'Pool Creation', + 'Device Registration', + 'Address Generation', + ]; + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Transaction Signing" }), + React.createElement(Note, { items: requiredDemos }, "Note: Ensure you have run the following demos before this one:"), + React.createElement(DemoStep, null, + React.createElement(DemoText, null, "1. Input your Address resource name below:"), + React.createElement(InputText, { onTextChange: setAddressName, editable: addressNameEditable, placeholderText: "networks/{network_id}/addresses/{address_id}" }), + React.createElement(DemoText, null, "Input your DeviceGroup resource name below:"), + React.createElement(InputText, { onTextChange: setDeviceGroupName, editable: deviceGroupEditable, placeholderText: "pools/{pool_id}/deviceGroups/{device_group_id}" }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep2(true); + setAddressNameEditable(false); + setDeviceGroupEditable(false); + } })), + showStep2 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "2. Input your Transaction information below. The default values should suffice for the Goerli Network."), + React.createElement(LargeInputText, { onTextChange: setTx, initialText: initialTx }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep3(true); + } }))), + showStep3 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "3. Initiating Signature creation..."))), + showStep4 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "4. Successfully initiated Signature creation."))), + showStep5 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "5. Polling for pending Signatures..."))), + showStep6 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "6. Found pending Signature with resource name:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, pendingSignature?.MPCOperation), + React.createElement(DemoText, null, "with hexadecimal payload:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, pendingSignature?.Payload))), + showStep7 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "7. Processing pending Signature..."))), + showStep8 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "8. Got Signature with signed hexadecimal payload:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, signature?.SignedPayload), + React.createElement(CopyButton, { text: signature?.SignedPayload }))), + showStep9 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "9. Got signed transaction:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, signedTx?.RawTransaction), + React.createElement(CopyButton, { text: signedTx?.RawTransaction }), + React.createElement(DemoText, null, "You can broadcast this value on-chain if it is a valid transaction."), + React.createElement(Note, null, "You will need to fund your address with the native currency (e.g. ETH) for the broadcast to be successful."))), + showError && (React.createElement(DemoStep, null, + React.createElement(ErrorText, null, resultError?.message))))); +}; +/** + * The styles for the App container. + */ +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + }, +}); diff --git a/example/src/screens/MPCWalletServiceDemo.js b/example/src/screens/MPCWalletServiceDemo.js new file mode 100644 index 0000000..bd0c140 --- /dev/null +++ b/example/src/screens/MPCWalletServiceDemo.js @@ -0,0 +1,142 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import { createMPCWallet, generateAddress, initMPCKeyService, initMPCSdk, initMPCWalletService, computeMPCWallet, pollForPendingDeviceGroup, computeMPCOperation, waitPendingMPCWallet, } from '@coinbase/waas-sdk-react-native'; +import { ContinueButton } from '../components/ContinueButton'; +import { DemoStep } from '../components/DemoStep'; +import { DemoText } from '../components/DemoText'; +import { ErrorText } from '../components/ErrorText'; +import { InputText } from '../components/InputText'; +import { PageTitle } from '../components/PageTitle'; +import AppContext from '../components/AppContext'; +import { CopyButton } from '../components/CopyButton'; +import { MonospaceText } from '../components/MonospaceText'; +import { Note } from '../components/Note'; +import { ModeContext } from '../utils/ModeProvider'; +import { directMode, proxyMode } from '../constants'; +export const MPCWalletServiceDemo = () => { + const [deviceName, setDeviceName] = React.useState(''); + const [poolName, setPoolName] = React.useState(''); + const [deviceGroupName, setDeviceGroupName] = React.useState(''); + const [deviceEditable, setDeviceEditable] = React.useState(true); + const [poolEditable, setPoolEditable] = React.useState(true); + const [wallet, setWallet] = React.useState(); + const [address, setAddress] = React.useState(); + const [resultError, setResultError] = React.useState(); + const [passcode, setPasscode] = React.useState(''); + const [passcodeEditable, setPasscodeEditable] = React.useState(true); + const [showStep4, setShowStep4] = React.useState(); + const [showStep5, setShowStep5] = React.useState(); + const [showStep6, setShowStep6] = React.useState(); + const [showStep7, setShowStep7] = React.useState(); + const [showError, setShowError] = React.useState(); + const { apiKeyName: apiKeyName, privateKey: privateKey, proxyUrl: proxyUrl, } = React.useContext(AppContext); + const { selectedMode } = React.useContext(ModeContext); + const prepareDeviceArchiveEnforced = true; + // Runs the WalletService demo. + React.useEffect(() => { + let demoFn = async function () { + if (poolName === '' || deviceName === '' || !showStep4) { + return; + } + if (selectedMode === directMode && + (apiKeyName === '' || privateKey === '')) { + return; + } + try { + const apiKey = selectedMode === proxyMode ? '' : apiKeyName; + const privKey = selectedMode === proxyMode ? '' : privateKey; + // Initialize the MPCSdk, MPCKeyService and MPCWalletService. + await initMPCSdk(true); + await initMPCKeyService(apiKey, privKey, proxyUrl); + await initMPCWalletService(apiKey, privKey, proxyUrl); + // Create MPCWallet if Device Group is not set. + if (deviceGroupName === '') { + const createMpcWalletResponse = await createMPCWallet(poolName, deviceName); + setDeviceGroupName(createMpcWalletResponse.DeviceGroup); + setShowStep5(true); + if (prepareDeviceArchiveEnforced) { + await computeMPCWallet(createMpcWalletResponse.DeviceGroup, passcode); + } + else { + const pendingDeviceGroup = await pollForPendingDeviceGroup(createMpcWalletResponse.DeviceGroup); + for (let i = pendingDeviceGroup.length - 1; i >= 0; i--) { + const deviceGroupOperation = pendingDeviceGroup[i]; + await computeMPCOperation(deviceGroupOperation?.MPCData); + } + } + setShowStep4(true); + const walletCreated = await waitPendingMPCWallet(createMpcWalletResponse.Operation); + setWallet(walletCreated); + setShowStep6(true); + const addressCreated = await generateAddress(walletCreated.Name, 'networks/ethereum-goerli'); + setAddress(addressCreated); + setShowStep7(true); + } + } + catch (error) { + console.error(error); + setResultError(error); + setShowError(true); + } + }; + demoFn(); + }, [ + deviceName, + apiKeyName, + privateKey, + proxyUrl, + showStep4, + deviceGroupName, + passcode, + poolName, + prepareDeviceArchiveEnforced, + selectedMode, + ]); + const requiredDemos = ['Pool Creation', 'Device Registration']; + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Address Generation" }), + React.createElement(Note, { items: requiredDemos }, "Note: Ensure you have run the following demos before this one:"), + React.createElement(DemoStep, null, + React.createElement(DemoText, null, "1. Input your Pool resource name below:"), + React.createElement(InputText, { onTextChange: setPoolName, editable: poolEditable, placeholderText: "pools/{pool_id}" }), + React.createElement(DemoText, null, "2. Input your Device resource name below:"), + React.createElement(InputText, { onTextChange: setDeviceName, editable: deviceEditable, placeholderText: "devices/{device_id}" }), + React.createElement(DemoText, null, "3. Input the passcode of the registered Device below:"), + React.createElement(InputText, { onTextChange: setPasscode, editable: passcodeEditable, secret: true }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep4(true); + setDeviceEditable(false); + setPoolEditable(false); + setPasscodeEditable(false); + } })), + showStep4 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "4. Creating your MPCWallet..."))), + showStep5 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "5. Initiated DeviceGroup creation with resource name:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, deviceGroupName), + React.createElement(DemoText, null, "Copy your DeviceGroup resource name and paste it into a notepad before proceeding to the next step."), + React.createElement(CopyButton, { text: deviceGroupName }), + React.createElement(DemoText, null, "Creating MPCWallet. This may take some time (1 min)..."))), + showStep6 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "6. Created MPCWallet with resource name:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, wallet?.Name), + React.createElement(DemoText, null, "Copy your MPCWallet resource name and paste it into a notepad before proceeding to the next step."), + React.createElement(CopyButton, { text: wallet?.Name }))), + showStep7 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "7. Generated Ethereum Address with resource name:"), + React.createElement(MonospaceText, { verticalMargin: 10 }, address?.Name), + React.createElement(DemoText, null, "Copy your Address resource name and paste it into a notepad before proceeding to the next demo."), + React.createElement(CopyButton, { text: address?.Name }))), + showError && (React.createElement(DemoStep, null, + React.createElement(ErrorText, null, + "ERROR: ", + resultError?.message))))); +}; +/** + * The styles for the App container. + */ +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + }, +}); diff --git a/example/src/screens/ModeSelectionScreen.js b/example/src/screens/ModeSelectionScreen.js new file mode 100644 index 0000000..12734be --- /dev/null +++ b/example/src/screens/ModeSelectionScreen.js @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet, Button, View, Text } from 'react-native'; +import { PageTitle } from '../components/PageTitle'; +import { DemoStep } from '../components/DemoStep'; +import { ModeContext } from '../utils/ModeProvider'; +import { useNavigation } from '@react-navigation/native'; +/** + * Prompts users to choose between 'Direct Mode' and 'Proxy Mode'. + * Navigates to the 'Home' screen after a mode is selected. + */ +export const ModeSelectionScreen = () => { + const { setSelectedMode } = React.useContext(ModeContext); + const navigation = useNavigation(); + const handleModeSelection = (mode) => { + setSelectedMode(mode); + navigation.navigate('Home'); + }; + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Mode Selection." }), + React.createElement(DemoStep, null, + React.createElement(View, { style: styles.buttonContainer }, + React.createElement(Button, { title: "Direct Mode", onPress: () => handleModeSelection('direct-mode') }), + React.createElement(Text, { style: styles.modeDescription }, "API credentials are required.")), + React.createElement(Button, { title: "Proxy Mode", onPress: () => handleModeSelection('proxy-mode') }), + React.createElement(Text, { style: styles.modeDescription }, "No API credentials. Assumes that API keys are stored in the proxy server.")))); +}; +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: 'white', + paddingHorizontal: 90, + paddingTop: 0, + }, + buttonContainer: { + marginVertical: 15, + alignItems: 'center', + }, + modeDescription: { + marginVertical: 10, + textAlign: 'center', + color: '#555', + }, +}); diff --git a/example/src/screens/PoolServiceDemo.js b/example/src/screens/PoolServiceDemo.js new file mode 100644 index 0000000..2bdabd1 --- /dev/null +++ b/example/src/screens/PoolServiceDemo.js @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import { initPoolService, createPool, } from '@coinbase/waas-sdk-react-native'; +import { ContinueButton } from '../components/ContinueButton'; +import { DemoStep } from '../components/DemoStep'; +import { DemoText } from '../components/DemoText'; +import { ErrorText } from '../components/ErrorText'; +import { InputText } from '../components/InputText'; +import { PageTitle } from '../components/PageTitle'; +import { CopyButton } from '../components/CopyButton'; +import AppContext from '../components/AppContext'; +import { MonospaceText } from '../components/MonospaceText'; +import { ModeContext } from '../utils/ModeProvider'; +import { directMode, proxyMode } from '../constants'; +export const PoolServiceDemo = () => { + const [poolDisplayName, setPoolDisplayName] = React.useState(''); + const [displayNameEditable, setDisplayNameEditable] = React.useState(true); + const [resultPool, setResultPool] = React.useState(); + const [resultError, setResultError] = React.useState(); + const [showStep2, setShowStep2] = React.useState(); + const [showStep3, setShowStep3] = React.useState(); + const [showError, setShowError] = React.useState(); + const { selectedMode } = React.useContext(ModeContext); + const { apiKeyName: apiKeyName, privateKey: privateKey, proxyUrl: proxyUrl, } = React.useContext(AppContext); + // Creates a Pool once the API key, API secret, and Pool display name are defined. + React.useEffect(() => { + let createPoolFn = async function () { + if (!showStep2 || poolDisplayName === '') { + return; + } + if (selectedMode === directMode && + (apiKeyName === '' || privateKey === '')) { + return; + } + try { + const apiKey = selectedMode === proxyMode ? '' : apiKeyName; + const privKey = selectedMode === proxyMode ? '' : privateKey; + await initPoolService(apiKey, privKey, proxyUrl); + const createdPool = await createPool(poolDisplayName); + setResultPool(createdPool); + setShowStep3(true); + } + catch (error) { + setResultError(error); + setShowError(true); + } + }; + createPoolFn(); + }, [ + apiKeyName, + privateKey, + poolDisplayName, + proxyUrl, + showStep2, + selectedMode, + ]); + return (React.createElement(ScrollView, { contentInsetAdjustmentBehavior: "automatic", style: styles.container }, + React.createElement(PageTitle, { title: "Pool Creation" }), + React.createElement(DemoStep, null, + React.createElement(DemoText, null, "1. Input your Pool's desired display name:"), + React.createElement(InputText, { onTextChange: setPoolDisplayName, editable: displayNameEditable }), + React.createElement(ContinueButton, { onPress: () => { + setShowStep2(true); + setDisplayNameEditable(false); + } })), + showStep2 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, "2. Creating your Pool..."))), + showStep3 && (React.createElement(DemoStep, null, + React.createElement(DemoText, null, + "3. Successfully created and got Pool resource with display name \"", + resultPool?.displayName, + "\":"), + React.createElement(MonospaceText, { verticalMargin: 10 }, resultPool?.name), + React.createElement(DemoText, null, "Copy your Pool resource name and paste it into a notepad before proceeding to the next demo."), + React.createElement(CopyButton, { text: resultPool?.name }))), + showError && (React.createElement(DemoStep, null, + React.createElement(ErrorText, null, + "ERROR: ", + resultError?.message))))); +}; +const styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + }, +}); diff --git a/example/src/utils/ModeProvider.js b/example/src/utils/ModeProvider.js new file mode 100644 index 0000000..368d5ef --- /dev/null +++ b/example/src/utils/ModeProvider.js @@ -0,0 +1,15 @@ +import React from 'react'; +// Default values for the ModeContext. +const defaultModeContext = { + selectedMode: '', + setSelectedMode: () => { }, +}; +// Creates a React context for managing mode selection in the application. +export const ModeContext = React.createContext(defaultModeContext); +// ModeProvider is a context provider component for `ModeContext`. +// It maintains the state of the selected mode and provides the ability to change it. +// Any components wrapped inside ModeProvider will have access to the current mode and the function to set it. +export const ModeProvider = ({ children }) => { + const [selectedMode, setSelectedMode] = React.useState(''); + return (React.createElement(ModeContext.Provider, { value: { selectedMode, setSelectedMode } }, children)); +}; diff --git a/example/src/utils/sleep.js b/example/src/utils/sleep.js new file mode 100644 index 0000000..0259bb2 --- /dev/null +++ b/example/src/utils/sleep.js @@ -0,0 +1,7 @@ +/** + * Waits for the provided number of milliseconds. + * @param ms The number of milliseconds. + */ +export const sleep = async function (ms) { + await new Promise((r) => setTimeout(r, ms)); +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..5ce99ab --- /dev/null +++ b/src/index.js @@ -0,0 +1,473 @@ +// Copyright (c) 2018-2023 Coinbase, Inc. +// Licensed under the Apache License, version 2.0 +import { NativeModules, Platform } from 'react-native'; +const LINKING_ERROR = `The package 'react-native-waas-sdk' doesn't seem to be linked. Make sure: \n\n` + + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + + '- You rebuilt the app after installing the package\n' + + '- You are not using Expo Go\n'; +/** + * The native hook into the WaaS MPC SDK. + */ +const MPCSdk = NativeModules.MPCSdk + ? NativeModules.MPCSdk + : new Proxy({}, { + get() { + throw new Error(LINKING_ERROR); + }, + }); +/** + * Initializes the MPC SDK. This function must be invoked before + * any MPC SDK methods are called. + * @returns A void promise, that either succeeds or rejects. + * otherwise. + */ +export function initMPCSdk(isSimulator) { + return MPCSdk.initialize(isSimulator); +} +/** + Bootstraps the Device with the given passcode. The passcode is used to generate a private/public key pair + that encrypts the backup and archive for the DeviceGroups containing this Device. This function should be called + exactly once per Device per application, and should be called before the Device is registered with GetRegistrationData. + It is the responsibility of the application to track whether bootstrapDevice has been called for the Device. + * @param passcode Passcode to protect all key materials in the secure enclave. + * @returns A void promise, that either succeeds or rejects.. + */ +export function bootstrapDevice(passcode) { + return MPCSdk.bootstrapDevice(passcode); +} +/** + * Resets the passcode used to encrypt the backups and archives of the DeviceGroups containing this Device. + * While there is no need to call bootstrapDevice again, it is the client's responsibility to call and participate in + * PrepareDeviceArchive and PrepareDeviceBackup operations afterwards for each DeviceGroup the Device was in. + * This function can be used when/if the end user forgets their old passcode. + * @param newPasscode The new passcode to use to encrypt backups and archives associated with the Device. + * @returns A void promise, that either succeeds or rejects. + */ +export function resetPasscode(newPasscode) { + return MPCSdk.resetPasscode(newPasscode); +} +/** + * Retrieves the data required to call RegisterDeviceAPI on MPCKeyService. + * @returns A promise with the RegistrationData on success; a rejection otherwise. + */ +export function getRegistrationData() { + return MPCSdk.getRegistrationData(); +} +/** + * Computes an MPC operation, given mpcData from the response of ListMPCOperations API on MPCKeyService. + * This function can be used to compute MPCOperations of types: CreateDeviceGroup and CreateSignature. + * @param mpcData The mpcData from ListMPCOperationsResponse on MPCKeyService. + * @returns A void promise, that either succeeds or rejects. + */ +export function computeMPCOperation(mpcData) { + return MPCSdk.computeMPCOperation(mpcData); +} +/** + * Computes a PrepareDeviceArchive MPCOperation, + * given mpcData from the response of ListMPCOperations API on MPCKeyService and passcode for the Device. + * @param mpcData The mpcData from ListMPCOperationsResponse on MPCKeyService. + * @param passcode The passcode set for the Device on BootstrapDevice call. + * @returns A void promise, that either succeeds or rejects. + */ +export function computePrepareDeviceArchiveMPCOperation(mpcData, passcode) { + return MPCSdk.computePrepareDeviceArchiveMPCOperation(mpcData, passcode); +} +/** + * Exports private keys corresponding to MPCKeys derived from a particular DeviceGroup. This method only supports + * exporting private keys that back EVM addresses. This function is recommended to be called while the Device is + * on airplane mode. + * @param mpcKeyExportMetadata The metadata to be used to export MPCKeys. This metadata is obtained from the response + * of GetDeviceGroup RPC in MPCKeyService. This metadata is a dynamic value, ensure you pass the most recent value of + * this metadata. + * @param passcode Passcode protecting key materials in the device, set during the call to BootstrapDevice. + * @returns A promise with the ExportPrivateKeysResponse on success; a rejection otherwise. + */ +export function exportPrivateKeys(mpcKeyExportMetadata, passcode) { + return MPCSdk.exportPrivateKeys(mpcKeyExportMetadata, passcode); +} +/** + * Computes a PrepareDeviceBackup MPCOperation, + * given mpcData from the response of ListMPCOperations API on MPCKeyService and passcode for the Device. + * @param mpcData The mpcData from ListMPCOperationsResponse on MPCKeyService. + * @param passcode The passcode set for the Device on BootstrapDevice call. + * @returns A void promise, that either succeeds or rejects. + */ +export function computePrepareDeviceBackupMPCOperation(mpcData, passcode) { + return MPCSdk.computePrepareDeviceBackupMPCOperation(mpcData, passcode); +} +/** + * Exports the device backup that is created after successfully computing a PrepareDeviceBackup MPCOperation. + * It is recommended to store this backup securely in a storage provider of your choice. If the existing Device is lost, + * follow the below steps: + * 1. Bootstrap the new Device with the same passcode as the old Device. + * 2. Register the new Device. + * 3. Initiate AddDevice MPCOperation using the AddDevice RPC in the MPCKeyService. + * 4. Compute AddDevice MPCOperation with the computeAddDeviceMPCOperation method using this exported device backup. + * @returns A promise with the backup as hex-encoded string; a rejection otherwise. + */ +export function exportDeviceBackup() { + return MPCSdk.exportDeviceBackup(); +} +/** + * Computes an AddDevice MPCOperation, + * given mpcData from the response of ListMPCOperations API on MPCKeyService and passcode for the Device. + * @param mpcData The mpcData from ListMPCOperationsResponse on MPCKeyService. + * @param passcode The passcode set for the Device on BootstrapDevice call. + * @param deviceBackup The backup retrieved from the exportDeviceBackup call after successful computation of a + * PrepareDeviceBackup MPCOperation. + * @returns A void promise, that either succeeds or rejects. + */ +export function computeAddDeviceMPCOperation(mpcData, passcode, deviceBackup) { + return MPCSdk.computeAddDeviceMPCOperation(mpcData, passcode, deviceBackup); +} +/** + * The native hook into the WaaS PoolService. + */ +const PoolService = NativeModules.PoolService + ? NativeModules.PoolService + : new Proxy({}, { + get() { + throw new Error(LINKING_ERROR); + }, + }); +/** + * Initializes the PoolService with Cloud API Key. This function must be invoked before + * any PoolService functions are called. + * @param apiKeyName The API key name. + * @param privateKey The private key. + * @param proxyUrl The URL of the proxy service. Required when in proxy mode and not needed in direct mode. + * @returns A void promise, that either succeeds or rejects. + * otherwise. + */ +export function initPoolService(apiKeyName, privateKey, proxyUrl) { + return PoolService.initialize(apiKeyName, privateKey, proxyUrl); +} +/** + * Creates a Pool. Call this method before creating any resources scoped to a Pool. + * @param displayName A user-readable name for the Pool. + * @param poolID The ID to use for the Pool, which will become the final component of + * the resource name. If not provided, the server will assign a Pool ID automatically. + * @returns A promise with the Pool on success; a rejection otherwise. + */ +export function createPool(displayName, poolID) { + let poolIDString = poolID; + if (poolIDString === undefined) { + poolIDString = ''; + } + return PoolService.createPool(displayName, poolIDString); +} +/** + * The native hook into the WaaS MPCKeyService. + */ +const MPCKeyService = NativeModules.MPCKeyService + ? NativeModules.MPCKeyService + : new Proxy({}, { + get() { + throw new Error(LINKING_ERROR); + }, + }); +/** + * Initializes the MPCKeyService. + * This function must be invoked before any MPCKeyService functions are called. + * @param apiKeyName The API key name. + * @param privateKey The private key. + * @param proxyUrl The URL of the proxy service. Required when in proxy mode and not needed in direct mode. + * @returns A void promise, that either succeeds or rejects. + * otherwise. + */ +export function initMPCKeyService(apiKeyName, privateKey, proxyUrl) { + return MPCKeyService.initialize(apiKeyName, privateKey, proxyUrl); +} +/** + * Registers the current Device. + * @returns A promise with the registered Device on success; a rejection otherwise. + */ +export function registerDevice() { + return MPCKeyService.registerDevice(); +} +/** + * Polls for pending DeviceGroup (i.e. CreateDeviceGroup), and returns the first set that materializes. + * Only one DeviceGroup can be polled at a time; thus, this function must return (by calling either + * stopPollingForPendingDeviceGroup or computeMPCOperation) before another call is made to this function. + * @param deviceGroup The resource name of the DeviceGroup for which to poll the pending + * CreateDeviceGroupOperation. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param pollInterval The interval at which to poll for the pending operation in milliseconds. + * If not provided, a reasonable default will be used. + * @returns A promise with a list of the pending CreateDeviceGroupOperations on success; a rejection otherwise. + */ +export function pollForPendingDeviceGroup(deviceGroup, pollInterval) { + const pollIntervalToUse = pollInterval === undefined ? 200 : pollInterval; + return MPCKeyService.pollForPendingDeviceGroup(deviceGroup, pollIntervalToUse); +} +/** + * Stops polling for pending DeviceGroup. This function should be called, e.g., before your app exits, + * screen changes, etc. This function is a no-op if the SDK is not currently polling for a pending DeviceGroup. + * @returns A promise with string "stopped polling for pending DeviceGroup" if polling is stopped successfully; + * a promise with the empty string otherwise. + */ +export function stopPollingForPendingDeviceGroup() { + return MPCKeyService.stopPollingForPendingDeviceGroup(); +} +/** + * Initiates an operation to create a Signature resource from the given Transaction using + * the given parent Key. + * @param parent The resource name of the parent Key. + * Format: pools/{pool_id}/deviceGroups/{device_group_id}/mpcKeys/{mpc_key_id} + * @param tx The transaction to sign. + * @returns A promise with the resource name of the WaaS operation creating the Signature on successful initiation; + * a rejection otherwise. + */ +export function createSignatureFromTx(parent, tx) { + return MPCKeyService.createSignatureFromTx(parent, tx); +} +/** + * Polls for pending Signatures (i.e. CreateSignatureOperations), and returns the first set that materializes. + * Only one DeviceGroup can be polled at a time; thus, this function must return (by calling either + * stopPollingForPendingSignatures or processPendingSignature) before another call is made to this function. + * @param deviceGroup The resource name of the DeviceGroup for which to poll the pending + * CreateSignatureOperation. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param pollInterval The interval at which to poll for the pending operation in milliseconds. + * If not provided, a reasonable default will be used. + * @returns A promise with a list of the pending Signatures on success; a rejection otherwise. + */ +export function pollForPendingSignatures(deviceGroup, pollInterval) { + const pollIntervalToUse = pollInterval === undefined ? 200 : pollInterval; + return MPCKeyService.pollForPendingSignatures(deviceGroup, pollIntervalToUse); +} +/** + * Stops polling for pending Signatures. This function should be called, e.g., before your app exits, + * screen changes, etc. This function is a no-op if the SDK is not currently polling for a pending Signature. + * @returns A promise with string "stopped polling for pending Signatures" if polling is stopped successfully; + * a promise with the empty string otherwise. + */ +export function stopPollingForPendingSignatures() { + return MPCKeyService.stopPollingForPendingSignatures(); +} +/** + * Waits for a pending Signature. + * @param wallet The name of operation that created the Signature. + * @returns A promise with the Signature on success; a rejection otherwise. + */ +export function waitPendingSignature(operation) { + return MPCKeyService.waitPendingSignature(operation); +} +/** + * Obtains the signed transaction object based on the given inputs. + * @param unsignedTx The unsigned Transaction object. + * @param signature The Signature object obtained from the CreateSignature flow. + */ +export function getSignedTransaction(unsignedTx, signature) { + return MPCKeyService.getSignedTransaction(unsignedTx, signature); +} +/** + * Gets a DeviceGroup. + * @param name The resource name of the DeviceGroup. + * @returns A promise with the Address on success; a rejection otherwise. + */ +export function getDeviceGroup(name) { + return MPCKeyService.getDeviceGroup(name); +} +/** + * Initiates an operation to prepare device archive for MPCKey export. Ensure this operation is run prior to any attempts + * generate Addresses for the DeviceGroup. The prepared archive will include cryptographic materials to export the + * private keys corresponding to each of the MPCKey in the DeviceGroup. Once the device archive is prepared, utilize + * ExportPrivateKeys function to export private keys for to your MPCKeys. + * @param deviceGroup The resource name of the DeviceGroup. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param device The resource name of the Device that prepares the archive. + * Format: devices/{device_id} + * @returns A promise with the resource name of the WaaS operation creating the Device Archive on successful initiation; + * a rejection otherwise. + */ +export function prepareDeviceArchive(deviceGroup, device) { + return MPCKeyService.prepareDeviceArchive(deviceGroup, device); +} +/** + * Polls for pending DeviceArchives (i.e. PrepareDeviceArchiveOperation), and returns the first set that materializes. + * Only one DeviceGroup can be polled at a time; thus, this function must return (by calling either + * stopPollingForPendingDeviceArchives or computePrepareDeviceArchiveMPCOperation) + * before another call is made to this function. + * @param deviceGroup The resource name of the DeviceGroup for which to poll the pending + * PrepareDeviceArchiveOperation. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param pollInterval The interval at which to poll for the pending operation in milliseconds. + * If not provided, a reasonable default will be used. + * @returns A promise with a list of the pending operations on success; a rejection otherwise. + */ +export function pollForPendingDeviceArchives(deviceGroup, pollInterval) { + const pollIntervalToUse = pollInterval === undefined ? 200 : pollInterval; + return MPCKeyService.pollForPendingDeviceArchives(deviceGroup, pollIntervalToUse); +} +/** + * Stops polling for pending device archive operations. This function should be called, e.g., before your app exits, + * screen changes, etc. This function is a no-op if the SDK is not currently polling for a pending DeviceArchive. + * @returns A promise with string "stopped polling for pending Device Archives" if polling is + * stopped successfully; a promise with the empty string otherwise. + */ +export function stopPollingForPendingDeviceArchives() { + return MPCKeyService.stopPollingForPendingDeviceArchives(); +} +/** + * Initiates an operation to prepare a device backup for the given Device. The backup contains certain cryptographic + * materials that can be used to restore MPCKeys, which have the given DeviceGroup as their parent, on a new Device. + * The Device must retrieve the resulting MPCOperation using pollForPendingDeviceBackups and compute with + * computePrepareDeviceBackupMPCOperation method. + * @param deviceGroup The resource name of the DeviceGroup. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param device The resource name of the Device that is preparing the device backup. + * Format: devices/{device_id} + * @returns A promise with the resource name of the WaaS operation creating the Device Backup; a rejection + * otherwise. + */ +export function prepareDeviceBackup(deviceGroup, device) { + return MPCKeyService.prepareDeviceBackup(deviceGroup, device); +} +/** + * Polls for pending DeviceBackups (i.e. PrepareDeviceBackupOperation), and returns the first set that materializes. + * Only one DeviceGroup can be polled at a time; thus, this function must return (by calling either + * stopPollingForPendingDeviceBackups or computePrepareDeviceBackupMPCOperation) before another call is made + * to this function. + * @param deviceGroup The resource name of the DeviceGroup for which to poll the pending + * PrepareDeviceBackupOperation. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param pollInterval The interval at which to poll for the pending operation in milliseconds. + * If not provided, a reasonable default will be used. + * @returns A promise with a list of the pending operations on success; a rejection otherwise. + */ +export function pollForPendingDeviceBackups(deviceGroup, pollInterval) { + const pollIntervalToUse = pollInterval === undefined ? 200 : pollInterval; + return MPCKeyService.pollForPendingDeviceBackups(deviceGroup, pollIntervalToUse); +} +/** + * Stops polling for pending DeviceBackup operations. This function should be called, e.g., before your app exits, + * screen changes, etc. This function is a no-op if the SDK is not currently polling for a pending DeviceBackup. + * @returns A promise with string "stopped polling for pending Device Backups" if polling is stopped successfully; + * a promise with the empty string otherwise. + */ +export function stopPollingForPendingDeviceBackups() { + return MPCKeyService.stopPollingForPendingDeviceBackups(); +} +/** + * Initiates an operation to add a Device to the DeviceGroup, + * using a device backup prepared with PrepareDeviceBackupOperation. + * The Device must retrieve the resulting MPCOperation using pollForPendingDevices and compute with + * computeAddDeviceMPCOperation method. + * @param deviceGroup The resource name of the DeviceGroup to which the Device is to be added. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param device The resource name of the Device that has to be added to the DeviceGroup. + * Format: devices/{device_id} + * @returns A void promise, that either succeeds or rejects. + * otherwise. + */ +export function addDevice(deviceGroup, device) { + return MPCKeyService.addDevice(deviceGroup, device); +} +/** + * Polls for pending Devices (i.e. AddDeviceOperations), and returns the first set that materializes. + * Only one DeviceGroup can be polled at a time; thus, this function must return (by calling either + * stopPollingForPendingDevices or computeAddDeviceMPCOperation) before another call is made + * to this function. + * @param deviceGroup The resource name of the deviceGroup for which to poll the pending + * AddDeviceOperation. + * Format: pools/{pool_id}/deviceGroups/{device_group_id} + * @param pollInterval The interval at which to poll for the pending operation in milliseconds. + * If not provided, a reasonable default will be used. + * @returns A promise with a list of the pending operations on success; a rejection otherwise. + */ +export function pollForPendingDevices(deviceGroup, pollInterval) { + const pollIntervalToUse = pollInterval === undefined ? 200 : pollInterval; + return MPCKeyService.pollForPendingDevices(deviceGroup, pollIntervalToUse); +} +/** + * Stops polling for pending Device operations. This function should be called, e.g., before your app exits, + * screen changes, etc. This function is a no-op if the SDK is not currently polling for a pending Device. + * @returns A promise with string "stopped polling for pending Devices" if polling is stopped successfully; + * a promise with the empty string otherwise. + */ +export function stopPollingForPendingDevices() { + return MPCKeyService.stopPollingForPendingDevices(); +} +/** + * The native hook into the WaaS MPCWalletService. + */ +const MPCWalletService = NativeModules.MPCWalletService + ? NativeModules.MPCWalletService + : new Proxy({}, { + get() { + throw new Error(LINKING_ERROR); + }, + }); +/** + * Initializes the MPCWalletService with Cloud API Key. This function must be invoked before + * any MPCWalletService functions are called. + * @param apiKeyName The API key name. + * @param privateKey The private key. + * @param proxyUrl The URL of the proxy service. Required when in proxy mode and not needed in direct mode. + * @returns A void promise, that either succeeds or rejects. + * otherwise. + */ +export function initMPCWalletService(apiKeyName, privateKey, proxyUrl) { + return MPCWalletService.initialize(apiKeyName, privateKey, proxyUrl); +} +/** + * Creates an MPCWallet. + * @param parent The resource name of the parent Pool. + * @param device The resource name of the Device. + * @returns A promise with the response on success; a rejection otherwise. + */ +export function createMPCWallet(parent, device) { + return MPCWalletService.createMPCWallet(parent, device); +} +/** + * Computes an MPCWallet. + * Computing an MPCWallet consists of two steps: + * 1. Compute the MPC operation to create the DeviceGroup. + * 2. Compute the MPC operation to prepare device archive for the DeviceGroup. + * Users are provided with this convenience API to compute both MPC operations using one single API call. + * Users have the choices to compute the two MPC operations separately. + * @param deviceGroup The resource name of the DeviceGroup from the createMPCWallet response. + * @param passcode Passcode protecting key materials in the device, set during the call to BootstrapDevice. + * @param pollInterval The interval at which to poll for the pending operation in milliseconds. + * If not provided, a reasonable default will be used. + * @returns A void promise, that either succeeds or rejects. + */ +export async function computeMPCWallet(deviceGroup, passcode, pollInterval) { + const pendingDeviceGroup = await pollForPendingDeviceGroup(deviceGroup, pollInterval); + for (let i = pendingDeviceGroup.length - 1; i >= 0; i--) { + const deviceGroupOperation = pendingDeviceGroup[i]; + await computeMPCOperation(deviceGroupOperation?.MPCData); + } + const pendingDeviceArchiveOperations = await pollForPendingDeviceArchives(deviceGroup, pollInterval); + for (let i = pendingDeviceArchiveOperations.length - 1; i >= 0; i--) { + const pendingOperation = pendingDeviceArchiveOperations[i]; + await computePrepareDeviceArchiveMPCOperation(pendingOperation.MPCData, passcode); + } + return; +} +/** + * Waits for a pending MPCWallet. + * @param wallet The name of operation that created the MPCWallet. + * @returns A promise with the MPCWallet on success; a rejection otherwise. + */ +export function waitPendingMPCWallet(operation) { + return MPCWalletService.waitPendingMPCWallet(operation); +} +/** + * Generates an Address. + * @param wallet The resource name of the MPCWallet to create the Address in. + * @param network The resource name of Network to create the Address for. + * @returns A promise with the Address on success; a rejection otherwise. + */ +export function generateAddress(wallet, network) { + return MPCWalletService.generateAddress(wallet, network); +} +/** + * Gets an Address. + * @param name The resource name of the Address. + * @returns A promise with the Address on success; a rejection otherwise. + */ +export function getAddress(name) { + return MPCWalletService.getAddress(name); +}