Skip to content

Commit

Permalink
feat(fiatconnect): Allow wallet to login with FC providers (#2612)
Browse files Browse the repository at this point in the history
* Test branch

* Workaround tslib import error

* Testing

* Remove unnecessary code

* Put metro config changes in different PR

* Merge

* tmp

* Test

* Remove unused import

* Fix tests

* tests

* Use clock sync version of SDK

* Update constructor

* Add test for signing function

* Unlock in signing function

Co-authored-by: Jean Regisser <jean.regisser@gmail.com>
  • Loading branch information
jophish and jeanregisser authored Jun 23, 2022
1 parent 849f199 commit 96720af
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@celo/utils": "^1.2.4",
"@celo/wallet-rpc": "~1.2.0",
"@crowdin/ota-client": "^0.4.0",
"@fiatconnect/fiatconnect-sdk": "0.3.1",
"@fiatconnect/fiatconnect-types": "^3.2.0",
"@google-cloud/storage": "^5.16.1",
"@komenci/contracts": "1.1.0",
Expand Down
117 changes: 116 additions & 1 deletion src/fiatconnect/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { FiatAccountSchema, FiatAccountType } from '@fiatconnect/fiatconnect-types'
import { FetchMock } from 'jest-fetch-mock'
import { Network } from '@fiatconnect/fiatconnect-types'
jest.mock('src/pincode/authentication')

import { CICOFlow } from 'src/fiatExchanges/utils'
import { LocalCurrencyCode } from 'src/localCurrency/consts'
import { CiCoCurrency } from 'src/utils/currencies'
Expand All @@ -18,7 +21,12 @@ import {
getFiatConnectProviders,
getFiatConnectQuotes,
QuotesInput,
loginWithFiatConnectProvider,
getSigningFunction,
} from './index'
import { FiatConnectClient } from '@fiatconnect/fiatconnect-sdk'
import { KeychainWallet } from 'src/web3/KeychainWallet'
import { getPassword } from 'src/pincode/authentication'

jest.mock('src/utils/Logger', () => ({
__esModule: true,
Expand All @@ -29,9 +37,28 @@ jest.mock('src/utils/Logger', () => ({
},
}))

jest.mock('src/web3/KeychainWallet', () => {
return {
KeychainWallet: () => {
return jest.fn(() => {
return {}
})
},
}
})

jest.mock('@fiatconnect/fiatconnect-sdk', () => {
return {
FiatConnectClient: () => {
return jest.fn(() => {
return {}
})
},
}
})

describe('FiatConnect helpers', () => {
const mockFetch = fetch as FetchMock

beforeEach(() => {
mockFetch.resetMocks()
jest.clearAllMocks()
Expand Down Expand Up @@ -134,4 +161,92 @@ describe('FiatConnect helpers', () => {
).rejects.toThrowError('Not implemented')
})
})
describe('getSigningFunction', () => {
const wallet = new KeychainWallet({
address: 'some address',
createdAt: new Date(),
})
beforeEach(() => {
wallet.getAccounts = jest.fn().mockReturnValue(['fakeAccount'])
wallet.isAccountUnlocked = jest.fn().mockReturnValue(true)
wallet.signPersonalMessage = jest.fn().mockResolvedValue('some signed message')
wallet.unlockAccount = jest.fn().mockResolvedValue(undefined)
})
it('returns a signing function that signs a message', async () => {
const signingFunction = getSigningFunction(wallet)
const signedMessage = await signingFunction('test')
expect(wallet.signPersonalMessage).toHaveBeenCalled()
expect(wallet.unlockAccount).not.toHaveBeenCalled()
expect(signedMessage).toEqual('some signed message')
})
it('returns a signing function that attempts to unlock accout if locked', async () => {
wallet.isAccountUnlocked = jest.fn().mockReturnValue(false)
const signingFunction = getSigningFunction(wallet)
const signedMessage = await signingFunction('test')
expect(wallet.signPersonalMessage).toHaveBeenCalled()
expect(wallet.unlockAccount).toHaveBeenCalled()
expect(getPassword).toHaveBeenCalled()
expect(signedMessage).toEqual('some signed message')
})
})
describe('loginWithFiatConnectProvider', () => {
const wallet = new KeychainWallet({
address: 'some address',
createdAt: new Date(),
})
const fiatConnectClient = new FiatConnectClient(
{
baseUrl: 'some url',
network: Network.Alfajores,
accountAddress: 'some address',
},
(message: string): Promise<string> => {
return Promise.resolve(message)
}
)

beforeEach(() => {
wallet.getAccounts = jest.fn().mockReturnValue(['fakeAccount'])
wallet.isAccountUnlocked = jest.fn().mockReturnValue(true)
wallet.unlockAccount = jest.fn().mockResolvedValue(undefined)

fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(true)
fiatConnectClient.login = jest.fn().mockResolvedValue({ isOk: true, value: 'success' })
})
it('Does not attempt to login if already logged in', async () => {
await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).resolves.toBeUndefined()
expect(fiatConnectClient.login).not.toHaveBeenCalled()
})
it('Forces login attempt if already logged in', async () => {
await expect(
loginWithFiatConnectProvider(wallet, fiatConnectClient, true)
).resolves.toBeUndefined()
expect(fiatConnectClient.login).toHaveBeenCalled()
})
it('Attempts to login and prompts PIN when account is locked', async () => {
wallet.isAccountUnlocked = jest.fn().mockReturnValue(false)
fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(false)
await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).resolves.toBeUndefined()
expect(wallet.unlockAccount).toHaveBeenCalled()
expect(getPassword).toHaveBeenCalled()
expect(fiatConnectClient.login).toHaveBeenCalled()
})
it('Attempts to login without prompting for PIN when account is unlocked', async () => {
wallet.isAccountUnlocked = jest.fn().mockReturnValue(true)
fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(false)
await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).resolves.toBeUndefined()
expect(wallet.unlockAccount).not.toHaveBeenCalled()
expect(getPassword).not.toHaveBeenCalled()
expect(fiatConnectClient.login).toHaveBeenCalled()
})
it('Throws an error when login fails', async () => {
wallet.isAccountUnlocked = jest.fn().mockReturnValue(true)
fiatConnectClient.isLoggedIn = jest.fn().mockReturnValue(false)
fiatConnectClient.login = jest.fn().mockResolvedValue({
isOk: false,
error: new Error('some error'),
})
await expect(loginWithFiatConnectProvider(wallet, fiatConnectClient)).rejects.toThrow()
})
})
})
42 changes: 42 additions & 0 deletions src/fiatconnect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import networkConfig from 'src/web3/networkConfig'
import { LocalCurrencyCode } from 'src/localCurrency/consts'
import { CiCoCurrency } from 'src/utils/currencies'
import Logger from 'src/utils/Logger'
import { FiatConnectApiClient } from '@fiatconnect/fiatconnect-sdk'
import { UnlockableWallet } from '@celo/wallet-base'
import { UNLOCK_DURATION } from 'src/web3/consts'
import { getPassword } from 'src/pincode/authentication'
import { ensureLeading0x } from '@celo/utils/lib/address'

const TAG = 'FIATCONNECT'

Expand Down Expand Up @@ -60,6 +65,43 @@ export async function getFiatConnectProviders(
return providers
}

/**
* Logs in with a FiatConnect provider. Will not attempt to log in if an
* unexpired session already exists, unless the `forceLogin` flag is set to `true`.
* If the user's wallet is currently locked, will prompt for PIN entry.
*/
export async function loginWithFiatConnectProvider(
wallet: UnlockableWallet,
fiatConnectClient: FiatConnectApiClient,
forceLogin: boolean = false
): Promise<void> {
if (fiatConnectClient.isLoggedIn() && !forceLogin) {
return
}

const [account] = wallet.getAccounts()
if (!wallet.isAccountUnlocked(account)) {
await wallet.unlockAccount(account, await getPassword(account), UNLOCK_DURATION)
}

const response = await fiatConnectClient.login()
if (!response.isOk) {
Logger.error(TAG, `Failure logging in with FiatConnect provider: ${response.error}, throwing`)
throw response.error
}
}

export function getSigningFunction(wallet: UnlockableWallet): (message: string) => Promise<string> {
return async function (message: string): Promise<string> {
const [account] = wallet.getAccounts()
if (!wallet.isAccountUnlocked(account)) {
await wallet.unlockAccount(account, await getPassword(account), UNLOCK_DURATION)
}
const encodedMessage = ensureLeading0x(Buffer.from(message, 'utf8').toString('hex'))
return await wallet.signPersonalMessage(account, encodedMessage)
}
}

export type QuotesInput = {
fiatConnectProviders: FiatConnectProviderInfo[]
flow: CICOFlow
Expand Down
Loading

0 comments on commit 96720af

Please sign in to comment.