Skip to content

Commit

Permalink
#351 - WalletProvider refactor to move init logic into `contexts/…
Browse files Browse the repository at this point in the history
…*.tsx` (#380)

* quick save

* completed create pin page
completed verify pin (pending some text and styling)

* quick save

* complete pin creations UI implementation

* update OceanInterface queue api

* update unit test

* fix outdated jellyfish mnemonic usage

* derive account from wallet context when only needed

* add wallet context mock for oceaninterface unit test

* fix test, redux store auto append broadcast status on queue

* install rn scrypt, implement native ScryptProvider

* quick save

* add custom max-w tailwind

* add delay for pin input callback, allow render before complete (if any) promise

* delete accident swp

* fix merge, migrate to new custom button

* done deferred prompt promise (UI thread resolve context init promise)

* make standalone passcode prompt "page" rendered side by side with main AppNavigator

* remove pininput in oceaninterface

* quick save

* completed encrypted wallet implementation (refactored)
- ui implementation (page, pin input)
- tx queue (in store), split from broadcast queue
- retry mech + state management
- clear wallet after consecutive failure (persistent failure counter, reset upon valid pin)

* syntax and comment fixes

* rename scrypt native module file, to build correctly

* rename

* added loading after collect pin, before signing complete

* fix test snapshot

* try manual select module (by platform.os) for bundling

* require at runtime

* temp disable web scryptsy

* implement scryptsy for web locally, dep removed from jellyfish

* move pin creation screens under wallet/

* fix pin input not auto focused (race condition)

* bump dep jellyfish-wallet-encrypted 0.31

* fix outdated redux action usage, move files

* test mobile build without sestate at onpinchange

* manual select scryptsy (do not rely on extension)

* mock scrypt

* fix translate, replaceAll not working in RN

* fix auth UI not get dismissed correctly when wipe wallet

* minor syntax update, prevent less than 6 digit pin fire callback

* rollback changes that I don't need for this refactoring

* revert root navigator

* rename passcode_attempt.ts

* refactor WalletManagementContext.tsx to WalletPersistenceContext.tsx

* added deprecated notice for wallet state

* moved wallet init logic to WalletContext.tsx

* fixed playground with new wallet context setup

* updated package-lock.json

* update to support scrypt

Co-authored-by: ivan <[email protected]>
  • Loading branch information
fuxingloh and ivan authored Aug 2, 2021
1 parent 1b630e6 commit 5ebaf94
Show file tree
Hide file tree
Showing 24 changed files with 347 additions and 184 deletions.
8 changes: 4 additions & 4 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import * as SplashScreen from 'expo-splash-screen'
import React from 'react'
import { Provider as StoreProvider } from 'react-redux'
import './_shim'
import { Logging } from './app/api/logging'
import { Logging } from './app/api'
import { NetworkProvider } from './app/contexts/NetworkContext'
import { PlaygroundProvider, useConnectedPlayground } from './app/contexts/PlaygroundContext'
import { WalletManagementProvider } from './app/contexts/WalletManagementContext'
import { WalletPersistenceProvider } from './app/contexts/WalletPersistenceContext'
import { WhaleProvider } from './app/contexts/WhaleContext'
import { useCachedResources } from './app/hooks/useCachedResources'
import ErrorBoundary from './app/screens/ErrorBoundary/ErrorBoundary'
Expand Down Expand Up @@ -41,11 +41,11 @@ export default function App (): JSX.Element | null {
<NetworkProvider>
<PlaygroundProvider>
<WhaleProvider>
<WalletManagementProvider>
<WalletPersistenceProvider>
<StoreProvider store={store}>
<Main />
</StoreProvider>
</WalletManagementProvider>
</WalletPersistenceProvider>
</WhaleProvider>
</PlaygroundProvider>
</NetworkProvider>
Expand Down
1 change: 0 additions & 1 deletion app/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './wallet/persistence'
export * from './storage'
export * from './logging'
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { MnemonicProviderData } from "@defichain/jellyfish-wallet-mnemonic";
import { WhaleApiClient } from "@defichain/whale-api-client";
import { EnvironmentNetwork } from "../../../environment";
import { WalletPersistenceData, WalletType } from "../persistence";
import { EnvironmentNetwork } from "../../environment";
import { WalletPersistenceData, WalletType } from "./persistence";
import { initWhaleWallet } from "./index";
import { MnemonicUnprotected } from "./provider/mnemonic_unprotected";

beforeEach(async () => {
jest.clearAllMocks()
Expand All @@ -22,23 +23,11 @@ it('should initWhaleWallet', async () => {
}
}

const wallet = initWhaleWallet(data, network, client)
const provider = MnemonicUnprotected.initProvider(data, network)
const wallet = initWhaleWallet(provider, network, client)

expect(wallet.get(0).withTransactionBuilder().utxo).toBeDefined()
expect(wallet.get(0).withTransactionBuilder().dex).toBeDefined()
expect(wallet.get(0).withTransactionBuilder().account).toBeDefined()
expect(wallet.get(0).withTransactionBuilder().liqPool).toBeDefined()
});


it('should fail as wallet type not available', async () => {
const data: WalletPersistenceData<string> = {
version: "v1",
type: undefined as any,
raw: ""
}

expect(() => {
initWhaleWallet(data, network, client)
}).toThrow('wallet undefined not available')
})
21 changes: 21 additions & 0 deletions app/api/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { JellyfishWallet, WalletHdNode, WalletHdNodeProvider } from '@defichain/jellyfish-wallet'
import { WhaleApiClient } from '@defichain/whale-api-client'
import { WhaleWalletAccount, WhaleWalletAccountProvider } from '@defichain/whale-api-wallet'
import { EnvironmentNetwork } from '../../environment'
import { getJellyfishNetwork } from './network'

/**
* Whale JellyfishWallet connected to Whale APIs via the Ocean Infrastructure
*/
export type WhaleWallet = JellyfishWallet<WhaleWalletAccount, WalletHdNode>

export function initWhaleWallet (provider: WalletHdNodeProvider<WalletHdNode>, network: EnvironmentNetwork, client: WhaleApiClient): WhaleWallet {
const accountProvider = new WhaleWalletAccountProvider(client, getJellyfishNetwork(network))
return new JellyfishWallet(provider, accountProvider)
}

export * from './provider/mnemonic_encrypted'
export * from './provider/mnemonic_unprotected'
export * from './network'
export * from './passcode_attempt'
export * from './persistence'
20 changes: 20 additions & 0 deletions app/api/wallet/passcode_attempt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StorageAPI } from '../storage'

const KEY = 'PASSCODE_ATTEMPT.count'

async function get (): Promise<number> {
const str = await StorageAPI.getItem(KEY)
return str === undefined ? 0 : Number(str)
}

async function set (count: number): Promise<void> {
await StorageAPI.setItem(KEY, `${count}`)
}

/**
* Failed passcode input counter persistence layer
*/
export const PasscodeAttemptCounter = {
set,
get
}
3 changes: 2 additions & 1 deletion app/api/wallet/persistence.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { StorageAPI } from '../storage'

export enum WalletType {
MNEMONIC_UNPROTECTED = 'MNEMONIC_UNPROTECTED'
MNEMONIC_UNPROTECTED = 'MNEMONIC_UNPROTECTED',
MNEMONIC_ENCRYPTED = 'MNEMONIC_ENCRYPTED'
}

export interface WalletPersistenceData<T> {
Expand Down
35 changes: 0 additions & 35 deletions app/api/wallet/provider/index.ts

This file was deleted.

51 changes: 51 additions & 0 deletions app/api/wallet/provider/mnemonic_encrypted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
EncryptedHdNodeProvider,
EncryptedProviderData,
PrivateKeyEncryption,
PromptPassphrase,
Scrypt
} from '@defichain/jellyfish-wallet-encrypted'
import * as Random from 'expo-random'
import { EnvironmentNetwork } from '../../../environment'
import { getBip32Option } from '../network'
import { WalletPersistenceData, WalletType } from '../persistence'

const encryption = new PrivateKeyEncryption(new Scrypt(), numOfBytes => {
const bytes = Random.getRandomBytes(numOfBytes)
return Buffer.from(bytes)
})

function initProvider (
data: WalletPersistenceData<EncryptedProviderData>,
network: EnvironmentNetwork,
promptPassphrase: PromptPassphrase
): EncryptedHdNodeProvider {
if (data.type !== WalletType.MNEMONIC_ENCRYPTED || data.version !== 'v1') {
throw new Error('Unexpected WalletPersistenceData')
}

const bip32Options = getBip32Option(network)
return EncryptedHdNodeProvider.init(data.raw, bip32Options, encryption, promptPassphrase)
}

async function toData (mnemonic: string[], network: EnvironmentNetwork, passphrase: string): Promise<WalletPersistenceData<EncryptedProviderData>> {
const options = getBip32Option(network)
const data = await EncryptedHdNodeProvider.wordsToEncryptedData(mnemonic, options, encryption, passphrase)

return {
version: 'v1',
type: WalletType.MNEMONIC_ENCRYPTED,
raw: data
}
}

export const MnemonicEncrypted = {
initProvider,
toData,
/**
* Convenience Abandon23 Art on Playground Network Data
*/
Abandon23Playground: toData([
'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'abandon', 'art'
], EnvironmentNetwork.LocalPlayground, '123456')
}
97 changes: 77 additions & 20 deletions app/contexts/WalletContext.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,91 @@
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useDispatch } from 'react-redux'
import { Logging } from '../api/logging'
import { WhaleWallet } from '../api/wallet/provider'
import { wallet as store } from '../store/wallet'
import { useWalletManagementContext } from './WalletManagementContext'
import { EncryptedProviderData } from '@defichain/jellyfish-wallet-encrypted'
import { MnemonicProviderData } from '@defichain/jellyfish-wallet-mnemonic'
import { useNavigation } from '@react-navigation/native'
import React, { createContext, PropsWithChildren, useCallback, useContext, useMemo } from 'react'
import {
initWhaleWallet,
MnemonicEncrypted,
MnemonicUnprotected,
WalletPersistenceData,
WalletType,
WhaleWallet
} from '../api/wallet'
import { useNetworkContext } from './NetworkContext'
import { useWhaleApiClient } from './WhaleContext'

const WalletContext = createContext<WhaleWallet>(undefined as any)

export function useWallet (): WhaleWallet {
return useContext(WalletContext)
}

export function WalletProvider (props: React.PropsWithChildren<any>): JSX.Element | null {
const dispatch = useDispatch()
const management = useWalletManagementContext()
const wallet = management.wallets[0]

const [isLoaded, setLoaded] = useState(false)
interface WalletProviderProps<T> extends PropsWithChildren<any> {
data: WalletPersistenceData<T>
}

useEffect(() => {
wallet.get(0).getAddress().then(address => {
dispatch(store.actions.setAddress(address))
setLoaded(true)
}).catch(Logging.error)
}, [management.wallets])
/**
* Automatically determine the correct WalletProvider to use based on the wallet type.
*/
export function WalletProvider (props: WalletProviderProps<any>): JSX.Element | null {
if (props.data.type === WalletType.MNEMONIC_UNPROTECTED) {
return (
<MnemonicUnprotectedProvider {...props}>
{props.children}
</MnemonicUnprotectedProvider>
)
}

if (!isLoaded) {
return null
if (props.data.type === WalletType.MNEMONIC_ENCRYPTED) {
return (
<MnemonicEncryptedProvider {...props}>
{props.children}
</MnemonicEncryptedProvider>
)
}

throw new Error(`wallet type: ${props.data.type as string} not available`)
}

function MnemonicUnprotectedProvider (props: WalletProviderProps<MnemonicProviderData>): JSX.Element | null {
const { network } = useNetworkContext()
const client = useWhaleApiClient()

const wallet = useMemo(() => {
const provider = MnemonicUnprotected.initProvider(props.data, network)
return initWhaleWallet(provider, network, client)
}, [])

return (
<WalletContext.Provider value={wallet}>
{props.children}
</WalletContext.Provider>
)
}

function MnemonicEncryptedProvider (props: WalletProviderProps<EncryptedProviderData>): JSX.Element | null {
const { network } = useNetworkContext()
const client = useWhaleApiClient()
const navigation = useNavigation()

const promptPassphrase = useCallback(async () => {
return await new Promise<string>((resolve, reject) => {
// TODO(ivan): implementation
navigation.navigate('PromptPassphrase', {
resolve (passphrase: string) {
resolve(passphrase)
},
reject (error: Error) {
reject(error)
}
})
})
}, [])

const wallet = useMemo(() => {
const provider = MnemonicEncrypted.initProvider(props.data, network, promptPassphrase)
return initWhaleWallet(provider, network, client)
}, [])

return (
<WalletContext.Provider value={wallet}>
{props.children}
Expand Down
61 changes: 0 additions & 61 deletions app/contexts/WalletManagementContext.tsx

This file was deleted.

Loading

0 comments on commit 5ebaf94

Please sign in to comment.