Skip to content

Commit

Permalink
Suuport whitelisting for evm on cent chain
Browse files Browse the repository at this point in the history
  • Loading branch information
sophialittlejohn committed Jul 26, 2023
1 parent e349b4f commit 82c79bb
Show file tree
Hide file tree
Showing 23 changed files with 127 additions and 83 deletions.
6 changes: 3 additions & 3 deletions centrifuge-app/src/components/OnboardingAuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ const loginWithSubstrate = async (hexAddress: string, signer: Wallet['signer'],
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ jw3t: token, nonce }),
body: JSON.stringify({ jw3t: token, nonce, network: 'substrate' }),
})
if (authTokenRes.status !== 200) {
throw new Error('Failed to authenticate wallet')
Expand All @@ -181,7 +181,7 @@ const loginWithSubstrate = async (hexAddress: string, signer: Wallet['signer'],
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ jw3t: token, nonce }),
body: JSON.stringify({ jw3t: token, nonce, network: 'substrate' }),
})
if (authTokenRes.status !== 200) {
throw new Error('Failed to authenticate wallet')
Expand Down Expand Up @@ -232,7 +232,7 @@ Issued At: ${new Date().toISOString()}`
signature: signedMessage,
address,
nonce,
...(evmChainId ? { substrateEvmChainId: evmChainId } : {}),
network: evmChainId ? 'evmOnSubstrate' : 'evm',
}),
})
if (tokenRes.status !== 200) {
Expand Down
4 changes: 2 additions & 2 deletions centrifuge-app/src/pages/Onboarding/ApprovalStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ export const ApprovalStatus = ({ signedAgreementUrl }: Props) => {
const trancheId = pool.trancheId
const poolName = pool.name

const onboardingStatus = onboardingUser?.poolSteps?.[poolId]?.[trancheId].status?.status
const onboardingStatus = onboardingUser?.poolSteps?.[poolId]?.[trancheId]?.status?.status

const onFocus = () => {
refetchOnboardingUser()
}

React.useEffect(() => {
if (
onboardingUser.poolSteps?.[poolId]?.[trancheId].status.status === 'pending' ||
onboardingUser.poolSteps?.[poolId]?.[trancheId]?.status?.status === 'pending' ||
(onboardingUser.investorType === 'entity' && onboardingUser?.manualKybStatus)
) {
window.addEventListener('focus', onFocus)
Expand Down
1 change: 1 addition & 0 deletions onboarding-api/env-vars/altair.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ RELAY_WSS_URL=wss://kusama-rpc.polkadot.io
INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
EVM_NETWORK=goerli
ONBOARDING_STORAGE_BUCKET=altair-onboarding-api
EVM_ON_SUBSTRATE_CHAIN_ID=2000
1 change: 1 addition & 0 deletions onboarding-api/env-vars/catalyst.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ RELAY_WSS_URL=wss://rococo-rpc.polkadot.io
INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
EVM_NETWORK=goerli
ONBOARDING_STORAGE_BUCKET=centrifuge-onboarding-api-dev
EVM_ON_SUBSTRATE_CHAIN_ID=2000
1 change: 1 addition & 0 deletions onboarding-api/env-vars/demo.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ RELAY_WSS_URL=wss://fullnode-relay.demo.cntrfg.com
INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
EVM_NETWORK=goerli
ONBOARDING_STORAGE_BUCKET=centrifuge-onboarding-api-dev
EVM_ON_SUBSTRATE_CHAIN_ID=2000
1 change: 1 addition & 0 deletions onboarding-api/env-vars/development.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ RELAY_WSS_URL=wss://fullnode-relay.development.cntrfg.com
INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
EVM_NETWORK=goerli
ONBOARDING_STORAGE_BUCKET=centrifuge-onboarding-api-dev
EVM_ON_SUBSTRATE_CHAIN_ID=2000
1 change: 1 addition & 0 deletions onboarding-api/env-vars/production.env
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ RELAY_WSS_URL=wss://rpc.polkadot.io
INFURA_KEY=bf808e7d3d924fbeb74672d9341d0550
EVM_NETWORK=mainnet
ONBOARDING_STORAGE_BUCKET=centrifuge-onboarding-api
EVM_ON_SUBSTRATE_CHAIN_ID=2000
25 changes: 14 additions & 11 deletions onboarding-api/src/controllers/auth/authenticateWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { isAddress } from '@polkadot/util-crypto'
import { Request, Response } from 'express'
import * as jwt from 'jsonwebtoken'
import { SiweMessage } from 'siwe'
import { InferType, number, object, string } from 'yup'
import { getCentrifuge, getValidSubstrateAddress } from '../../utils/centrifuge'
import { InferType, object, string, StringSchema } from 'yup'
import { SupportedNetworks } from '../../database'
import { getCentrifuge } from '../../utils/centrifuge'
import { reportHttpError } from '../../utils/httpError'
import { networkSwitch } from '../../utils/networkSwitch'
import { validateInput } from '../../utils/validateInput'

const verifyWalletInput = object({
Expand All @@ -29,7 +31,7 @@ const verifyWalletInput = object({
then: (verifyWalletInput) => verifyWalletInput.required(),
}),
nonce: string().required(),
substrateEvmChainId: number().optional(),
network: string().oneOf(['evm', 'substrate', 'evmOnSubstrate']) as StringSchema<SupportedNetworks>,
})

export const authenticateWalletController = async (
Expand All @@ -38,7 +40,9 @@ export const authenticateWalletController = async (
) => {
try {
await validateInput(req.body, verifyWalletInput)
const payload = req.body?.jw3t ? await verifySubstrateWallet(req, res) : await verifyEthWallet(req, res)
const verifyWallet = networkSwitch('verifyWallet', req.body.network)
const payload = await verifyWallet(req, res)

const token = jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '8h',
audience: req.get('origin'),
Expand All @@ -51,12 +55,12 @@ export const authenticateWalletController = async (
}

const AUTHORIZED_ONBOARDING_PROXY_TYPES = ['Any', 'Invest', 'NonTransfer', 'NonProxy']
async function verifySubstrateWallet(req: Request, res: Response): Promise<Request['wallet']> {
export async function verifySubstrateWallet(req: Request, res: Response): Promise<Request['wallet']> {
const { jw3t: token, nonce } = req.body
const { verified, payload } = await await getCentrifuge().auth.verify(token!)

const onBehalfOf = payload?.on_behalf_of
const address = getValidSubstrateAddress(payload.address)
const address = payload.address

const cookieNonce = req.signedCookies[`onboarding-auth-${address.toLowerCase()}`]
if (!cookieNonce || cookieNonce !== nonce) {
Expand All @@ -81,13 +85,13 @@ async function verifySubstrateWallet(req: Request, res: Response): Promise<Reque
}
return {
address,
network: 'substrate',
network: payload.network || 'substrate',
}
}

async function verifyEthWallet(req: Request, res: Response): Promise<Omit<Request['wallet'], 'substrateChainId'>> {
export async function verifyEthWallet(req: Request, res: Response): Promise<Request['wallet']> {
try {
const { message, signature, address, nonce, substrateEvmChainId } = req.body
const { message, signature, address, nonce, network } = req.body

if (!isAddress(address)) {
throw new Error('Invalid address')
Expand All @@ -103,8 +107,7 @@ async function verifyEthWallet(req: Request, res: Response): Promise<Omit<Reques
res.clearCookie(`onboarding-auth-${address.toLowerCase()}`)
return {
address: decodedMessage.data.address,
network: 'evm',
...(substrateEvmChainId ? { substrateEvmChainId } : {}),
network: network,

Check failure on line 110 in onboarding-api/src/controllers/auth/authenticateWallet.ts

View workflow job for this annotation

GitHub Actions / build-onboarding-api

Expected property shorthand
}
} catch (error) {
throw new Error('Invalid message or signature')
Expand Down
10 changes: 3 additions & 7 deletions onboarding-api/src/controllers/emails/signAndSendDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import {
} from '../../database'
import { sendDocumentsMessage } from '../../emails/sendDocumentsMessage'
import { annotateAgreementAndSignAsInvestor } from '../../utils/annotateAgreementAndSignAsInvestor'
import { validateRemark } from '../../utils/centrifuge'
import { fetchUser } from '../../utils/fetchUser'
import { getPoolById } from '../../utils/getPoolById'
import { HttpError, reportHttpError } from '../../utils/httpError'
import { validateEvmRemark } from '../../utils/tinlake'
import { networkSwitch } from '../../utils/networkSwitch'
import { Subset } from '../../utils/types'
import { validateInput } from '../../utils/validateInput'

Expand Down Expand Up @@ -51,11 +50,8 @@ export const signAndSendDocumentsController = async (

const remark = `Signed subscription agreement for pool: ${poolId} tranche: ${trancheId}`

if (wallet.network === 'substrate' || (wallet.network === 'evm' && wallet.substrateEvmChainId)) {
await validateRemark(transactionInfo, remark)
} else {
await validateEvmRemark(req.wallet, transactionInfo, remark)
}
const validateRemark = networkSwitch('validateRemark', wallet.network)
await validateRemark(wallet, transactionInfo, remark)

if (
poolSteps?.[poolId]?.[trancheId]?.signAgreement.completed &&
Expand Down
12 changes: 9 additions & 3 deletions onboarding-api/src/controllers/kyb/manualKybCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ export const manualKybCallbackController = async (
}
const user = userSnapshot.docs.map((doc) => doc.data())[0] as OnboardingUser

const wallet: Request['wallet'] = {
address: user.wallet[0].address,
network: user.wallet[0].network,
// find first possible address, assumes the user has only one wallet
const [network, addresses] =
Object.entries(user.wallets).find(([, addresses]) => addresses && addresses.length > 0) || []
if (!network || !addresses) {
throw new HttpError(404, 'Not found')
}
const wallet = {
address: addresses[0],
network,
} as Request['wallet']

if (user.investorType !== 'entity') {
throw new HttpError(400, 'User is not an entity')
Expand Down
7 changes: 6 additions & 1 deletion onboarding-api/src/controllers/kyb/verifyBusiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ export const verifyBusinessController = async (
address: null,
kycReference: '',
manualKybReference: null,
wallet: [wallet],
wallets: {
evm: [],
substrate: [],
evmOnSubstrate: [],
...{ [wallet.network]: [wallet.address] },
},
name: null,
dateOfBirth: null,
countryOfCitizenship: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Request, Response } from 'express'
import { InferType, mixed, object, string } from 'yup'
import { SupportedNetworks } from '../../database'
import { fetchUser } from '../../utils/fetchUser'
import { reportHttpError } from '../../utils/httpError'
import { SupportedNetworks } from '../../utils/types'
import { validateInput } from '../../utils/validateInput'

const getGlobalOnboardingStatusInput = object({
Expand Down
7 changes: 6 additions & 1 deletion onboarding-api/src/controllers/user/startKyc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ export const startKycController = async (req: Request<any, any, InferType<typeof
const newUser: IndividualUser = {
investorType: 'individual',
address: null,
wallet: [req.wallet],
wallets: {
evm: [],
substrate: [],
evmOnSubstrate: [],
...{ [wallet.network]: [wallet.address] },
},
kycReference,
name: body.name,
dateOfBirth: body.dateOfBirth,
Expand Down
3 changes: 2 additions & 1 deletion onboarding-api/src/controllers/user/updateInvestorStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { sendApproveInvestorMessage } from '../../emails/sendApproveInvestorMess
import { sendApproveIssuerMessage } from '../../emails/sendApproveIssuerMessage'
import { UpdateInvestorStatusPayload } from '../../emails/sendDocumentsMessage'
import { sendRejectInvestorMessage } from '../../emails/sendRejectInvestorMessage'
import { addInvestorToMemberList } from '../../utils/addInvestorToMemberList'
import { fetchUser } from '../../utils/fetchUser'
import { HttpError, reportHttpError } from '../../utils/httpError'
import { networkSwitch } from '../../utils/networkSwitch'
import { signAcceptanceAsIssuer } from '../../utils/signAcceptanceAsIssuer'
import { Subset } from '../../utils/types'
import { validateInput } from '../../utils/validateInput'
Expand Down Expand Up @@ -88,6 +88,7 @@ export const updateInvestorStatusController = async (
`signed-subscription-agreements/${wallet.address}/${poolId}/${trancheId}.pdf`
)

const addInvestorToMemberList = networkSwitch('addInvestorToMemberList', wallet.network)
const { txHash } = await addInvestorToMemberList(wallet, poolId, trancheId)
await Promise.all([
sendApproveInvestorMessage(user.email, poolId, trancheId, countersignedAgreementPDF),
Expand Down
26 changes: 13 additions & 13 deletions onboarding-api/src/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as dotenv from 'dotenv'
import { Request } from 'express'
import { array, bool, date, InferType, lazy, mixed, object, string, StringSchema } from 'yup'
import { HttpError } from '../utils/httpError'
import { Subset, SupportedNetworks } from '../utils/types'
import { Subset } from '../utils/types'

dotenv.config()

Expand All @@ -22,20 +22,18 @@ const uboSchema = object({
countryOfCitizenship: string().required(),
})

const walletSchema = array()
.of(
object({
address: string().required(),
network: string().required() as StringSchema<SupportedNetworks>,
})
)
.required()
const walletSchema = object({
evm: array().of(string()),
substrate: array().of(string()),
evmOnSubstrate: array().of(string()),
}).required()
export type Wallet = InferType<typeof walletSchema>
export type SupportedNetworks = keyof Wallet

export const transactionInfoSchema = object({
txHash: string().required(),
blockNumber: string().required(),
isEvmOnSubstrate: bool().optional().default(false),
isEvmOnSubstrate: bool().optional(),
})
export type TransactionInfo = InferType<typeof transactionInfoSchema>

Expand Down Expand Up @@ -95,7 +93,7 @@ const globalStepsSchema = object({

export const entityUserSchema = object({
investorType: string().default('entity') as StringSchema<Entity>,
wallet: walletSchema,
wallets: walletSchema,
kycReference: string().optional(),
email: string().email().required(),
businessName: string().required(),
Expand All @@ -114,7 +112,7 @@ export const entityUserSchema = object({

export const individualUserSchema = object({
investorType: string().default('individual') as StringSchema<Individual>,
wallet: walletSchema,
wallets: walletSchema,
kycReference: string().optional(),
email: string().default(null).nullable(), // TODO: coming soon
name: string().required(),
Expand Down Expand Up @@ -165,7 +163,9 @@ export const validateAndWriteToFirestore = async <T = undefined | string[]>(
// mergeFields implies that the user has already been created
if (typeof mergeFields !== 'undefined') {
const mergeValidations = (mergeFields as string[]).map((field) => schema.validateAt(field, data))
const userSnapshot = await userCollection.where(`wallet`, 'array-contains', wallet).get()
const userSnapshot = await userCollection
.where(`wallets.${wallet.network}`, 'array-contains', wallet.address)
.get()
if (userSnapshot.empty) {
throw new Error('User not found')
}
Expand Down
13 changes: 7 additions & 6 deletions onboarding-api/src/middleware/verifyAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ export const verifyAuth = async (req: Request, _res: Response, next: NextFunctio
throw new HttpError(401, 'Unauthorized')
}
const token = authorization.split(' ')[1]
const { address, network, aud, substrateEvmChainId } = (await jwt.verify(
token,
process.env.JWT_SECRET
)) as Request['wallet'] & jwt.JwtPayload
const { address, network, aud } = (await jwt.verify(token, process.env.JWT_SECRET)) as Request['wallet'] &
jwt.JwtPayload
if (!address) {
throw new HttpError(401, 'Unauthorized')
}
if (aud !== req.get('origin')) {
throw new HttpError(401, 'Unauthorized')
}
if ((network === 'evm' && !isAddress(address)) || (network === 'substrate' && !getValidSubstrateAddress(address))) {
if (
(network.includes('evm') && !isAddress(address)) ||
(network === 'substrate' && !getValidSubstrateAddress({ address, network }))
) {
throw new HttpError(401, 'Invalid address')
}
req.wallet = { address, network, ...(substrateEvmChainId ? { substrateEvmChainId } : {}) }
req.wallet = { address, network }
next()
}
10 changes: 0 additions & 10 deletions onboarding-api/src/utils/addInvestorToMemberList.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const annotateAgreementAndSignAsInvestor = async ({

const unsignedAgreementUrl = metadata?.onboarding?.tranches?.[trancheId]?.agreement?.uri
? centrifuge.metadata.parseMetadataUrl(metadata?.onboarding?.tranches?.[trancheId]?.agreement?.uri)
: wallet.network === 'substrate' || wallet.substrateEvmChainId
: wallet.network === 'substrate' || wallet.network === 'evmOnSubstrate'
? centrifuge.metadata.parseMetadataUrl(GENERIC_SUBSCRIPTION_AGREEMENT)
: null

Expand Down Expand Up @@ -113,7 +113,7 @@ Agreement hash: ${unsignedAgreementUrl}`,
})

// all tinlake agreements require the executive summary to be appended
if (wallet.network === 'evm' && !wallet.substrateEvmChainId) {
if (wallet.network === 'evm') {
const execSummaryRes = await fetch(metadata.pool.links.executiveSummary.uri)
const execSummary = Buffer.from(await execSummaryRes.arrayBuffer())
const execSummaryPdf = await PDFDocument.load(execSummary)
Expand Down
Loading

0 comments on commit 82c79bb

Please sign in to comment.