Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): add integration with gateway backend for notifications feature #4878

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50af7ca
feat: add dedicated hook for delegation
Jonathansoufer Feb 4, 2025
67d6a07
chore: move constants to global
Jonathansoufer Feb 4, 2025
d75da8e
fix: typo
Jonathansoufer Feb 4, 2025
07e4b56
feat: create delegate hook
Jonathansoufer Feb 5, 2025
178d4d0
refactor: sending signature
Jonathansoufer Feb 5, 2025
34dc5e7
chore: add siwe lib and utils
Jonathansoufer Feb 7, 2025
ef8b296
fix: add cockie auth on RTK
Jonathansoufer Feb 7, 2025
df34acc
chore: add debug logger
Jonathansoufer Feb 7, 2025
497cce7
feat: add siwe hook
Jonathansoufer Feb 7, 2025
81d76fa
feat: add useDelegate hook
Jonathansoufer Feb 7, 2025
3e83d6f
Merge branch 'dev' into feat/notifications-gtw-integration
Jonathansoufer Feb 7, 2025
6166897
chore: add support to random numbers generation
Jonathansoufer Feb 13, 2025
dcece0e
chore: adds sign w ethereum hook
Jonathansoufer Feb 13, 2025
9259893
chore: adds specific function to return signer
Jonathansoufer Feb 13, 2025
bdb6c84
chore: removes console.log
Jonathansoufer Feb 13, 2025
6a7264e
chore: adds hook to hold interaction w/ gateway
Jonathansoufer Feb 13, 2025
8158986
chore: adds redux structure for delegators
Jonathansoufer Feb 13, 2025
fd1ca3f
chore: adds timeout wrapper
Jonathansoufer Feb 13, 2025
f5d09ab
chore: adds userId
Jonathansoufer Feb 13, 2025
7bf5d31
refactor: useDelegator hook
Jonathansoufer Feb 13, 2025
8109e9b
refactor: fixes key generation and granted rights
Jonathansoufer Feb 18, 2025
8364bb4
Merge branch 'dev' into feat/notifications-gtw-integration
Jonathansoufer Feb 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions apps/mobile/src/hooks/useDelegateKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useState, useCallback } from 'react'
import { ethers } from 'ethers'
import DeviceCrypto from 'react-native-device-crypto'
import * as Keychain from 'react-native-keychain'

import Logger from '../utils/logger'
import { Address } from '@/src/types/address'
import GatewayService from '../services/gateway/GatewayService'
import { useSign } from './useSign'
import { asymmetricKey, keychainGenericPassword } from '@/src/store/constants'

export function useDelegateKey() {
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<Error | null>(null)
const { getPrivateKey, createMnemonicAccount } = useSign()

const createDelegate = useCallback(async (ownerAddress: Address) => {
setLoading(true)
setError(null)

try {
if (!ownerAddress) {
throw Logger.info('OwnerKeyNotFoundForDelegate')
}

// Step 0: Get users private key from keychain. TODO: need to pass ownerAddress to getPrivateKey
const privateKey = await getPrivateKey()

if (!privateKey) {
throw Logger.error('useDelegateKey: Something went wrong', error)
}

// Step 1: Generate delegate key
const delegateMnemonic = ethers.Wallet.createRandom().mnemonic?.phrase
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved

if (!delegateMnemonic) {
throw Logger.error('useDelegateKey: Something went wrong', error)
}

const delegatedAccount = await createMnemonicAccount(delegateMnemonic)

if (!delegatedAccount) {
throw Logger.error('useDelegateKey: Something went wrong', error)
}

// Step 2: Generate message to sign
const time = Math.floor(Date.now() / 3600000).toString()
const messageToSign = delegatedAccount.address + time
const hashToSign = ethers.hashMessage(messageToSign)

// Step 3: Sign message
const signature = await signMessage({
signer: delegatedAccount,
message: hashToSign,
})

// Step 4: Send to backend
await createOnBackEnd(ownerAddress, delegatedAccount, signature)

// Step 5: Store delegate key in keychain
const encryptedDelegateKey = await DeviceCrypto.encrypt(asymmetricKey, delegatedAccount.privateKey, {
biometryTitle: 'Authenticate',
biometrySubTitle: 'Saving delegated key',
biometryDescription: 'Please authenticate yourself',
})
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved

await Keychain.setGenericPassword(
keychainGenericPassword,
JSON.stringify({
encryptedPassword: encryptedDelegateKey.encryptedText,
iv: encryptedDelegateKey.iv,
}),
)
} catch (err) {
throw Logger.error('useDelegateKey: Something went wrong', err)
} finally {
setLoading(false)
}
}, [])

const deleteDelegate = useCallback(async () => {
setLoading(true)
setError(null)
}, [])

const signMessage = useCallback(async ({ signer, message }: { signer: ethers.HDNodeWallet; message: string }) => {
const signature = await signer.signMessage(message)
return signature
}, [])

const createOnBackEnd = async (ownerAddress: Address, delegatedAccount: ethers.HDNodeWallet, signature: string) => {
console.log({ createOnBackEnd: delegatedAccount, signature })
// TODO 1: call backend endpoints to create delegate into database
// TODO 2: check if we need to loop through all chains and create delegate for each chain
// Chain.all.forEach(async (chain) => {
try {
await GatewayService.createDelegate({
delegatorAddress: ownerAddress,
delegatedAddress: delegatedAccount.address,
signature,
description: 'iOS Device Delegate',
chainId: 'chain.id', //TODO: need to pass chainId
})
} catch (err) {
throw Logger.error('CreateDelegateFailed', err)
}
}

// const deleteOnBackEnd = async (delegateAddress: Address, signature: string) => {}

return {
loading,
error,
createDelegate,
deleteDelegate,
}
}
4 changes: 2 additions & 2 deletions apps/mobile/src/hooks/useSign/useSign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('useSign', () => {
})
expect(spy).toHaveBeenCalledWith(
keychainGenericPassword,
JSON.stringify({ encryptyedPassword: 'encryptedText', iv: `${privateKey}000` }),
JSON.stringify({ encryptedPassword: 'encryptedText', iv: `${privateKey}000` }),
)
})

Expand All @@ -45,7 +45,7 @@ describe('useSign', () => {

expect(spy).toHaveBeenCalledWith(
'safeuser',
JSON.stringify({ encryptyedPassword: 'encryptedText', iv: `${privateKey}000` }),
JSON.stringify({ encryptedPassword: 'encryptedText', iv: `${privateKey}000` }),
)
expect(returnedKey).toBe(privateKey)
})
Expand Down
14 changes: 6 additions & 8 deletions apps/mobile/src/hooks/useSign/useSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import DeviceCrypto from 'react-native-device-crypto'
import * as Keychain from 'react-native-keychain'
import DeviceInfo from 'react-native-device-info'
import { Wallet } from 'ethers'

export const asymmetricKey = 'safe'
export const keychainGenericPassword = 'safeuser'
import { asymmetricKey, keychainGenericPassword } from '@/src/store/constants'

export function useSign() {
// TODO: move it to a global context or reduce
Expand All @@ -17,7 +15,7 @@ export function useSign() {
invalidateOnNewBiometry: true,
})

const encryptyedPrivateKey = await DeviceCrypto.encrypt(asymmetricKey, privateKey, {
const encryptedPrivateKey = await DeviceCrypto.encrypt(asymmetricKey, privateKey, {
biometryTitle: 'Authenticate',
biometrySubTitle: 'Saving key',
biometryDescription: 'Please authenticate yourself',
Expand All @@ -26,8 +24,8 @@ export function useSign() {
await Keychain.setGenericPassword(
keychainGenericPassword,
JSON.stringify({
encryptyedPassword: encryptyedPrivateKey.encryptedText,
iv: encryptyedPrivateKey.iv,
encryptedPassword: encryptedPrivateKey.encryptedText,
iv: encryptedPrivateKey.iv,
}),
)
} catch (err) {
Expand All @@ -43,8 +41,8 @@ export function useSign() {
throw 'user password not found'
}

const { encryptyedPassword, iv } = JSON.parse(user.password)
const decryptedKey = await DeviceCrypto.decrypt(asymmetricKey, encryptyedPassword, iv, {
const { encryptedPassword, iv } = JSON.parse(user.password)
const decryptedKey = await DeviceCrypto.decrypt(asymmetricKey, encryptedPassword, iv, {
biometryTitle: 'Authenticate',
biometrySubTitle: 'Signing',
biometryDescription: 'Authenticate yourself to sign the text',
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/store/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,6 @@ export enum PressActionId {
}

export const LAUNCH_ACTIVITY = 'global.safe.mobileapp.ui.MainActivity'

export const asymmetricKey = 'safe'
export const keychainGenericPassword = 'safeuser'
compojoom marked this conversation as resolved.
Show resolved Hide resolved