Skip to content

Commit

Permalink
fix: safe auth
Browse files Browse the repository at this point in the history
  • Loading branch information
JP Angelle committed Oct 20, 2023
1 parent 432b726 commit c895b9e
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 12 deletions.
86 changes: 78 additions & 8 deletions centrifuge-app/src/components/OnboardingAuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Centrifuge from '@centrifuge/centrifuge-js'
import { useCentrifuge, useCentrifugeUtils, useEvmProvider, useWallet } from '@centrifuge/centrifuge-react'
import { encodeAddress } from '@polkadot/util-crypto'
import { Wallet } from '@subwallet/wallet-connect/types'
import { BigNumber, ethers } from 'ethers'
import { hashMessage } from 'ethers/lib/utils'
import * as React from 'react'
import { useMutation, useQuery } from 'react-query'

Expand Down Expand Up @@ -222,20 +224,41 @@ Nonce: ${nonce}
Issued At: ${new Date().toISOString()}`

const signedMessage = await signer?.signMessage(message)
const tokenRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/authenticateWallet`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({

let body

if (signedMessage === '0x') {
const messageHash = hashMessage(message)

const isValid = await isValidSignature(signer, address, messageHash, evmChainId || 1)

if (isValid) {
body = JSON.stringify({
safeAddress: address,
messageHash,
evmChainId,
})
} else {
throw new Error('Invalid signature')
}
} else {
body = JSON.stringify({
message,
signature: signedMessage,
address,
nonce,
network: isEvmOnSubstrate ? 'evmOnSubstrate' : 'evm',
chainId: evmChainId || 1,
}),
})
}

const tokenRes = await fetch(`${import.meta.env.REACT_APP_ONBOARDING_API_URL}/authenticateWallet`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body,
})
if (tokenRes.status !== 200) {
throw new Error('Failed to authenticate wallet')
Expand All @@ -249,3 +272,50 @@ Issued At: ${new Date().toISOString()}`
)
}
}

const isValidSignature = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => {
const MAGIC_VALUE_BYTES = '0x20c13b0b'

const safeContract = new ethers.Contract(
safeAddress,
[
'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)',
'function getMessageHash(bytes memory message) public view returns (bytes32)',
'function getThreshold() public view returns (uint256)',
],
provider
)

const safeMessageHash = await safeContract.getMessageHash(messageHash)

const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId)

if (!safeMessage) {
throw new Error('Unable to fetch SafeMessage')
}

const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber()

if (!threshold || threshold > safeMessage.confirmations.length) {
throw new Error('Threshold has not been met')
}

const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature)

return response === MAGIC_VALUE_BYTES
}

const TX_SERVICE_URLS: Record<string, string> = {
'1': 'https://safe-transaction-mainnet.safe.global/api',
'5': 'https://safe-transaction-goerli.staging.5afe.dev/api',
}

const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => {
const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()]

const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, {
headers: { 'Content-Type': 'application/json' },
})

return response.json()
}
91 changes: 88 additions & 3 deletions onboarding-api/src/controllers/auth/authenticateWallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { InfuraProvider } from '@ethersproject/providers'
import { isAddress } from '@polkadot/util-crypto'
import { BigNumber, ethers } from 'ethers'
import { Request, Response } from 'express'
import * as jwt from 'jsonwebtoken'
import fetch from 'node-fetch'
import { InferType, number, object, string, StringSchema } from 'yup'
import { SupportedNetworks } from '../../database'
import { reportHttpError } from '../../utils/httpError'
Expand Down Expand Up @@ -33,13 +36,36 @@ const verifyWalletInput = object({
chainId: number().required(),
})

const verifySafeInput = object({
messageHash: string().required(),
safeAddress: string()
.required()
.test({
name: 'is-address',
test(value, ctx) {
if (isAddress(value)) return true
return ctx.createError({ message: 'Invalid address', path: ctx.path })
},
}),
evmChainId: number().required(),
})

export const authenticateWalletController = async (
req: Request<{}, {}, InferType<typeof verifyWalletInput>>,
req: Request<{}, {}, InferType<typeof verifyWalletInput | typeof verifySafeInput>>,
res: Response
) => {
try {
await validateInput(req.body, verifyWalletInput)
const payload = await new NetworkSwitch(req.body.network).verifiyWallet(req, res)
let payload

if ('safeAddress' in req.body) {
await validateInput(req.body, verifySafeInput)

const provider = new InfuraProvider(req.body.evmChainId, process.env.INFURA_KEY)
payload = await verifySafeWallet(provider, req.body.safeAddress, req.body.messageHash, req.body.evmChainId)
} else {
await validateInput(req.body, verifyWalletInput)
payload = await new NetworkSwitch(req.body.network).verifyWallet(req, res)
}

const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '8h',
Expand All @@ -51,3 +77,62 @@ export const authenticateWalletController = async (
return res.status(error.code).send({ error: error.message, e })
}
}

const verifySafeWallet = async (provider: any, safeAddress: string, messageHash: string, evmChainId: number) => {
try {
const MAGIC_VALUE_BYTES = '0x20c13b0b'

const safeContract = new ethers.Contract(
safeAddress,
[
'function isValidSignature(bytes calldata _data, bytes calldata _signature) public view returns (bytes4)',
'function getMessageHash(bytes memory message) public view returns (bytes32)',
'function getThreshold() public view returns (uint256)',
],
provider
)

const safeMessageHash = await safeContract.getMessageHash(messageHash)

const safeMessage = await fetchSafeMessage(safeMessageHash, evmChainId)

if (!safeMessage) {
throw new Error('Unable to fetch SafeMessage')
}

const threshold = BigNumber.from(await safeContract.getThreshold()).toNumber()

if (!threshold || threshold > safeMessage.confirmations.length) {
throw new Error('Threshold has not been met')
}

const response = await safeContract.isValidSignature(messageHash, safeMessage?.preparedSignature)

if (response === MAGIC_VALUE_BYTES) {
return {
address: safeAddress,
chainId: evmChainId,
network: 'evm',
}
}

throw new Error('Invalid signature')
} catch {
throw new Error('Something went wrong')
}
}

const TX_SERVICE_URLS: Record<string, string> = {
'1': 'https://safe-transaction-mainnet.safe.global/api',
'5': 'https://safe-transaction-goerli.staging.5afe.dev/api',
}

const fetchSafeMessage = async (safeMessageHash: string, chainId: number) => {
const TX_SERVICE_URL = TX_SERVICE_URLS[chainId.toString()]

const response = await fetch(`${TX_SERVICE_URL}/v1/messages/${safeMessageHash}/`, {
headers: { 'Content-Type': 'application/json' },
})

return response.json()
}
2 changes: 1 addition & 1 deletion onboarding-api/src/utils/networks/networkSwitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class NetworkSwitch {
this.network = network
}

verifiyWallet = (req: Request, res: Response) => {
verifyWallet = (req: Request, res: Response) => {
if (this.network === 'substrate') {
return verifySubstrateWallet(req, res)
} else if (this.network === 'evm' || this.network === 'evmOnSubstrate') {
Expand Down

0 comments on commit c895b9e

Please sign in to comment.