From 4443d664f786fef275d54dc833dbd7ddadd4d052 Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 20 Jun 2023 11:08:30 -0400 Subject: [PATCH] Faucet: Use foreignAsset key for faucet & Onboarding: minimum age 18 (#1463) * Use nodemon and cent-js * Fetch currency key and make api currency agnostic * Update faucet UI * Use native balance symbol * Fix naming * Fix build * Fix build * Enforce 18 as minimum age --- .../Dialogs/FaucetConfirmationDialog.tsx | 10 ++-- centrifuge-app/src/components/Faucet.tsx | 47 +++++++++++++-- .../src/components/PageWithSideBar.tsx | 28 +-------- .../Onboarding/KnowYourCustomer/index.tsx | 2 +- .../Onboarding/UltimateBeneficialOwners.tsx | 2 +- .../src/pages/Pool/Overview/index.tsx | 6 +- faucet-api/package.json | 7 ++- faucet-api/src/index.ts | 58 +++++++++++++------ .../src/controllers/kyb/confirmOwners.ts | 5 +- onboarding-api/src/database/index.ts | 5 +- yarn.lock | 4 +- 11 files changed, 111 insertions(+), 63 deletions(-) diff --git a/centrifuge-app/src/components/Dialogs/FaucetConfirmationDialog.tsx b/centrifuge-app/src/components/Dialogs/FaucetConfirmationDialog.tsx index dbdd193782..1dd8f9750e 100644 --- a/centrifuge-app/src/components/Dialogs/FaucetConfirmationDialog.tsx +++ b/centrifuge-app/src/components/Dialogs/FaucetConfirmationDialog.tsx @@ -1,4 +1,4 @@ -import { Box, Dialog, IconInfo, Stack, Text } from '@centrifuge/fabric' +import { Box, Dialog, Stack, Text } from '@centrifuge/fabric' import * as React from 'react' import { copyToClipboard } from '../../utils/copyToClipboard' @@ -9,17 +9,19 @@ export const FaucetConfirmationDialog: React.FC<{ onClose: () => void }> = ({ hash, open, onClose, error }) => { return ( - } title="Faucet claim"> + - {hash ? 'Success. Please allow a couple of minutes for the tokens to reach your wallet' : error} + {hash + ? 'The transfer has been submitted. Please allow a couple of minutes for the transaction to complete.' + : error} {hash && !error && ( - Transaction hash: + Hash: { +const MIN_DEVEL_BALANCE = 10 +const MIN_POOL_CURRENCY_BALANCE = 100 + +export const Faucet = () => { const { selectedAccount } = useWallet().substrate const [hash, setHash] = React.useState('') const [error, setError] = React.useState('') const [isLoading, setIsLoading] = React.useState(false) + const currencies = useCurrencies() + const { pid: poolId } = useParams<{ pid: string }>() + const pool = usePool(poolId) + const balances = useBalances(useAddress('substrate')) + + const { connectedType } = useWallet() + const isTinlakePool = poolId?.startsWith('0x') + + const hasLowNativeBalance = + balances && new CurrencyBalance(balances.native.balance, 18).toDecimal().lte(MIN_DEVEL_BALANCE) + const poolCurrency = findCurrency(currencies ?? [], pool?.currency?.key) + const poolCurrencyBalance = + balances?.currencies.find((curr) => curr.currency.symbol === poolCurrency?.symbol)?.balance ?? new BN(0) + const hasLowPoolCurrencyBalance = + (poolCurrency && + new CurrencyBalance(poolCurrencyBalance, poolCurrency.decimals).toDecimal().lte(MIN_POOL_CURRENCY_BALANCE)) || + !poolCurrency + + const shouldRenderFaucet = + poolCurrency && + connectedType === 'substrate' && + !isTinlakePool && + import.meta.env.REACT_APP_FAUCET_URL && + hasLowNativeBalance && + hasLowPoolCurrencyBalance const handleClaim = async () => { setIsLoading(true) try { - const response = await fetch(`${import.meta.env.REACT_APP_FAUCET_URL}?address=${selectedAccount?.address}`) + const response = await fetch( + `${import.meta.env.REACT_APP_FAUCET_URL}?address=${selectedAccount?.address}&poolId=${pool?.id}` + ) if (response.status !== 200) { throw response.text() } @@ -25,7 +60,7 @@ export const Faucet: React.VFC = () => { } } - return ( + return shouldRenderFaucet ? ( <> { Faucet - 1k {import.meta.env.REACT_APP_COLLATOR_WSS_URL.includes('demo') ? 'DEMO' : 'DEVEL'} and 10k aUSD + 1k {balances.native.currency.symbol} and 10k {pool.currency.symbol} - ) + ) : null } diff --git a/centrifuge-app/src/components/PageWithSideBar.tsx b/centrifuge-app/src/components/PageWithSideBar.tsx index 8507d9baef..fc54c3fbb0 100644 --- a/centrifuge-app/src/components/PageWithSideBar.tsx +++ b/centrifuge-app/src/components/PageWithSideBar.tsx @@ -1,13 +1,9 @@ -import { CurrencyBalance } from '@centrifuge/centrifuge-js' -import { useBalances, useWallet, WalletMenu } from '@centrifuge/centrifuge-react' +import { WalletMenu } from '@centrifuge/centrifuge-react' import { Box, Grid, Shelf, Stack } from '@centrifuge/fabric' import * as React from 'react' -import { useParams } from 'react-router-dom' import { useTheme } from 'styled-components' import { config } from '../config' -import { useAddress } from '../utils/useAddress' import { useIsAboveBreakpoint } from '../utils/useIsAboveBreakpoint' -import { Faucet } from './Faucet' import { Footer } from './Footer' import { LoadBoundary } from './LoadBoundary' import { LogoLink } from './LogoLink' @@ -19,9 +15,6 @@ type Props = { children?: React.ReactNode } -const MIN_DEVEL_BALANCE = 10 -const MIN_AUSD_BALANCE = 100 - const TOOLBAR_HEIGHT = 75 const HEADER_HEIGHT = 56 const MENU_WIDTH = 80 @@ -30,24 +23,7 @@ export const PAGE_GUTTER = ['gutterMobile', 'gutterTablet', 'gutterDesktop'] export const PageWithSideBar: React.FC = ({ children, sidebar = true }) => { const isMedium = useIsAboveBreakpoint('M') - const { connectedType } = useWallet() - const { pid: poolId } = useParams<{ pid: string }>() - const isTinlakePool = poolId?.startsWith('0x') - const theme = useTheme() - const balances = useBalances(useAddress('substrate')) - const hasLowDevelBalance = - balances && new CurrencyBalance(balances.native.balance, 18).toDecimal().lte(MIN_DEVEL_BALANCE) - const aUSD = balances && balances.currencies.find((curr) => curr.currency.key === 'AUSD') - const hasLowAusdBalance = - (aUSD && new CurrencyBalance(aUSD.balance, aUSD.currency.decimals).toDecimal().lte(MIN_AUSD_BALANCE)) || !aUSD - - const shouldRenderFaucet = - connectedType === 'substrate' && - !isTinlakePool && - import.meta.env.REACT_APP_FAUCET_URL && - hasLowDevelBalance && - hasLowAusdBalance return ( = ({ children, sidebar = true }) = ]} /> - {shouldRenderFaucet && } - {sidebar} )} diff --git a/centrifuge-app/src/pages/Onboarding/KnowYourCustomer/index.tsx b/centrifuge-app/src/pages/Onboarding/KnowYourCustomer/index.tsx index e73c54a4ea..7008bc8452 100644 --- a/centrifuge-app/src/pages/Onboarding/KnowYourCustomer/index.tsx +++ b/centrifuge-app/src/pages/Onboarding/KnowYourCustomer/index.tsx @@ -14,7 +14,7 @@ const getValidationSchema = (investorType: 'individual' | 'entity') => dateOfBirth: date() .required('Please enter a date of birth') .min(new Date(1900, 0, 1), 'Date of birth must be after 1900') - .max(new Date(), 'Date of birth must be in the past'), + .max(new Date(new Date().getFullYear() - 18, new Date().getMonth()), 'You must be 18 or older'), countryOfCitizenship: string().required('Please select a country of citizenship'), countryOfResidency: string().required('Please select a country of residency'), isAccurate: boolean().oneOf([true], 'You must confirm that the information is accurate'), diff --git a/centrifuge-app/src/pages/Onboarding/UltimateBeneficialOwners.tsx b/centrifuge-app/src/pages/Onboarding/UltimateBeneficialOwners.tsx index 42e952c20b..ffb8e83e81 100644 --- a/centrifuge-app/src/pages/Onboarding/UltimateBeneficialOwners.tsx +++ b/centrifuge-app/src/pages/Onboarding/UltimateBeneficialOwners.tsx @@ -31,7 +31,7 @@ const validationSchema = object({ dateOfBirth: date() .required('Please enter a date of birth') .min(new Date(1900, 0, 1), 'Date of birth must be after 1900') - .max(new Date(), 'Date of birth must be in the past'), + .max(new Date(new Date().getFullYear() - 18, new Date().getMonth()), 'UBO must be 18 or older'), countryOfCitizenship: string().required('Please select a country of citizenship'), countryOfResidency: string().required('Please select a country of residency'), }).required() diff --git a/centrifuge-app/src/pages/Pool/Overview/index.tsx b/centrifuge-app/src/pages/Pool/Overview/index.tsx index 5fb9b66bd3..5bea99d655 100644 --- a/centrifuge-app/src/pages/Pool/Overview/index.tsx +++ b/centrifuge-app/src/pages/Pool/Overview/index.tsx @@ -2,6 +2,7 @@ import { useWallet } from '@centrifuge/centrifuge-react' import { Button, Shelf, Stack, Text, TextWithPlaceholder } from '@centrifuge/fabric' import * as React from 'react' import { useLocation, useParams } from 'react-router' +import { Faucet } from '../../../components/Faucet' import { ActionsRef, InvestRedeem } from '../../../components/InvestRedeem/InvestRedeem' import { IssuerSection } from '../../../components/IssuerSection' import { LabelValueStack } from '../../../components/LabelValueStack' @@ -38,7 +39,10 @@ export function PoolDetailOverviewTab() { return ( + <> + + + } > diff --git a/faucet-api/package.json b/faucet-api/package.json index 6232293b9f..e7fdbc7d2d 100644 --- a/faucet-api/package.json +++ b/faucet-api/package.json @@ -5,9 +5,10 @@ "main": "dist/index.js", "scripts": { "lint": "eslint src/**/*.ts", - "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --minify", + "build:centjs": "cd ../centrifuge-js && yarn build && cd ../faucet-api", + "build": "yarn build:centjs && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --minify", "start:types": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --watch", - "start:functions": "npx functions-framework --target=faucet --signature-type=http --port=8081", + "start:functions": "nodemon --watch ./dist/ --exec npx functions-framework --target=faucet --signature-type=http --port=8081", "develop": "run-p -l start:functions start:types", "deploy:demo": "gcloud functions deploy faucet --region=us-central1 --source=dist --project=peak-vista-185616 --trigger-http --runtime=nodejs16 --service-account=functions-firestore@peak-vista-185616.iam.gserviceaccount.com --allow-unauthenticated --entry-point=faucetDemo", "deploy:dev": "gcloud functions deploy faucet --region=us-central1 --source=dist --project=peak-vista-185616 --trigger-http --runtime=nodejs16 --service-account=functions-firestore@peak-vista-185616.iam.gserviceaccount.com --allow-unauthenticated --entry-point=faucetDev --env-vars-file .env.yaml " @@ -16,6 +17,7 @@ "author": "", "license": "ISC", "dependencies": { + "@centrifuge/centrifuge-js": "workspace:*", "@google-cloud/firestore": "^6.5.0", "@google-cloud/functions-framework": "^3.1.2", "bn.js": "^5.2.1" @@ -26,6 +28,7 @@ "@types/node": "^18.7.16", "esbuild": "^0.15.8", "eslint": "^8.23.1", + "nodemon": "^2.0.22", "npm-run-all": "^4.1.5", "typescript": "^4.8.3" } diff --git a/faucet-api/src/index.ts b/faucet-api/src/index.ts index 6ee3299ba6..50a9b0c45a 100644 --- a/faucet-api/src/index.ts +++ b/faucet-api/src/index.ts @@ -1,3 +1,4 @@ +import Centrifuge, { CurrencyBalance, findCurrency } from '@centrifuge/centrifuge-js' import { Firestore } from '@google-cloud/firestore' import { ApiPromise, WsProvider } from '@polkadot/api' import Keyring from '@polkadot/keyring' @@ -5,17 +6,13 @@ import { cryptoWaitReady } from '@polkadot/util-crypto' import BN from 'bn.js' import * as dotenv from 'dotenv' import { Request, Response } from 'express' +import { firstValueFrom } from 'rxjs' dotenv.config() const URL = process.env.COLLATOR_WSS_URL ?? 'wss://fullnode.algol.cntrfg.com/public-ws' -const ONE_AUSD = new BN(10).pow(new BN(12)) -const ONE_DEVEL = new BN(10).pow(new BN(18)) -const ONE_THOUSAND_DEVEL = ONE_DEVEL.muln(1000) -const TEN_DEVEL = ONE_DEVEL.muln(10) -const TEN_THOUSAND_AUSD = ONE_AUSD.muln(10000) -const ONE_HUNDRED_AUSD = ONE_AUSD.muln(100) +const AUSD_KEY = { foreignAsset: 2 } const MAX_API_REQUESTS_PER_WALLET = 100 const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000 @@ -32,6 +29,10 @@ const centrifugeDomains = [ /^(https:\/\/.*k-f\.dev)/, ] +const centrifuge = new Centrifuge({ + centrifugeWsUrl: URL, +}) + function hexToBN(value: string | number) { if (typeof value === 'number') return new BN(value) return new BN(value.toString().substring(2), 'hex') @@ -57,22 +58,37 @@ async function faucet(req: Request, res: Response) { res.set('Access-Control-Max-Age', '3600') return res.status(204).send('') } - const { address } = req.query + const { address, poolId } = req.query - if (!address) { + if (!address || !poolId) { return res.status(400).send('Invalid address param') } const api = await ApiPromise.create({ provider: wsProvider }) + const currencies = await firstValueFrom(centrifuge.pools.getCurrencies()) + const pools = await firstValueFrom(centrifuge.pools.getPools()) + const pool = pools.find((p) => p.id === poolId) + if (!pool) { + throw new Error('Pool not found') + } + const currency = findCurrency(currencies, pool.currency.key) + if (!currency) { + throw new Error('Currency not found') + } + // check DEVEL and aUSD balances - const [nativeBalanceResponse, ausdBalanceResponse] = await Promise.all([ + const [nativeBalanceResponse, currencyBalanceResponse] = await Promise.all([ api.query.system.account(address), - api.query.ormlTokens.accounts(address, 'AUSD'), + api.query.ormlTokens.accounts(address, currency?.key ?? AUSD_KEY), ]) const nativeBalance = hexToBN((nativeBalanceResponse?.toJSON() as any)?.data?.free || 0) - const ausdBalance = hexToBN((ausdBalanceResponse?.toJSON() as any)?.free || 0) - if (ausdBalance.gte(ONE_HUNDRED_AUSD) || nativeBalance.gte(TEN_DEVEL)) { + const currencyBalance = hexToBN((currencyBalanceResponse?.toJSON() as any)?.free || 0) + + if ( + currencyBalance.gte(CurrencyBalance.fromFloat(100, currency.decimals)) || + nativeBalance.gte(CurrencyBalance.fromFloat(10, 18)) + ) { api.disconnect() return res.status(400).send('Wallet already has sufficient aUSD/DEVEL balances') } @@ -101,20 +117,26 @@ async function faucet(req: Request, res: Response) { .set({ address, timestamp: Date.now(), + currency: currency?.key ?? AUSD_KEY, count: (doc.data()?.count ?? 0) + 1, }) const txBatch = api.tx.utility.batchAll([ - api.tx.tokens.transfer(address, { Native: true }, ONE_THOUSAND_DEVEL.toString()), - api.tx.tokens.transfer(address, { AUSD: true }, TEN_THOUSAND_AUSD.toString()), + api.tx.tokens.transfer(address, { Native: true }, CurrencyBalance.fromFloat(1000, 18).toString()), + api.tx.tokens.transfer( + address, + currency?.key ?? AUSD_KEY, + CurrencyBalance.fromFloat(10000, currency.decimals).toString() + ), ]) await cryptoWaitReady() const keyring = new Keyring({ type: 'sr25519' }) console.log('signing and sending tx') - const hash = URL.includes('demo') - ? await txBatch.signAndSend(keyring.addFromUri(process.env.SEED_HEX as string)) - : await txBatch.signAndSend(keyring.addFromUri('//Alice')) - console.log('signed and sent tx') + const hash = URL.includes('development') + ? await txBatch.signAndSend(keyring.addFromUri('//Alice')) + : await txBatch.signAndSend(keyring.addFromUri(process.env.SEED_HEX as string)) + + console.log('signed and sent tx with hash', hash) api.disconnect() return res.status(200).json({ hash }) } catch (e) { diff --git a/onboarding-api/src/controllers/kyb/confirmOwners.ts b/onboarding-api/src/controllers/kyb/confirmOwners.ts index f3d63c730e..bb515b12c2 100644 --- a/onboarding-api/src/controllers/kyb/confirmOwners.ts +++ b/onboarding-api/src/controllers/kyb/confirmOwners.ts @@ -10,7 +10,10 @@ const confirmOwnersInput = object({ ultimateBeneficialOwners: array( object({ name: string().required(), - dateOfBirth: date().required().min(new Date(1900, 0, 1)).max(new Date()), + dateOfBirth: date() + .required() + .min(new Date(1900, 0, 1)) + .max(new Date(new Date().getFullYear() - 18, new Date().getMonth()), 'UBO must be 18 or older'), countryOfResidency: string().required(), countryOfCitizenship: string().required(), }).required() diff --git a/onboarding-api/src/database/index.ts b/onboarding-api/src/database/index.ts index 5db2ead95c..0a647df841 100644 --- a/onboarding-api/src/database/index.ts +++ b/onboarding-api/src/database/index.ts @@ -14,7 +14,10 @@ export type InvestorType = Individual | Entity const uboSchema = object({ name: string().required(), - dateOfBirth: date().required().min(new Date(1900, 0, 1)).max(new Date()), + dateOfBirth: date() + .required() + .min(new Date(1900, 0, 1)) + .max(new Date(new Date().getFullYear() - 18, new Date().getMonth()), 'UBO must be 18 or older'), countryOfResidency: string().required(), countryOfCitizenship: string().required(), }) diff --git a/yarn.lock b/yarn.lock index fb2e6746b9..95f9b50e10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15593,6 +15593,7 @@ __metadata: version: 0.0.0-use.local resolution: "faucet-api@workspace:faucet-api" dependencies: + "@centrifuge/centrifuge-js": "workspace:*" "@google-cloud/firestore": ^6.5.0 "@google-cloud/functions-framework": ^3.1.2 "@types/bn.js": ^5 @@ -15601,6 +15602,7 @@ __metadata: bn.js: ^5.2.1 esbuild: ^0.15.8 eslint: ^8.23.1 + nodemon: ^2.0.22 npm-run-all: ^4.1.5 typescript: ^4.8.3 languageName: unknown @@ -20743,7 +20745,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"nodemon@npm:^2.0.15, nodemon@npm:^2.0.20": +"nodemon@npm:^2.0.15, nodemon@npm:^2.0.20, nodemon@npm:^2.0.22": version: 2.0.22 resolution: "nodemon@npm:2.0.22" dependencies: