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: tx manager, ui updates #219

Merged
merged 24 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
7 changes: 6 additions & 1 deletion onboarding/api/src/controllers/agreement.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class AgreementController {
const agreement = await this.agreementRepo.findOrCreate(
user.id,
user.email,
user.entityName || user.fullName,
user.entityName?.length > 0 ? user.entityName : user.fullName,
params.poolId,
profileAgreement.tranche,
profileAgreement.name,
Expand Down Expand Up @@ -105,6 +105,11 @@ export class AgreementController {
return 'OK'
}

if (content.status === 'voided') {
this.agreementRepo.setVoided(agreement.id)
return 'OK'
}

const investor = content.recipients.signers.find((signer: any) => signer.roleName === InvestorRoleName)
const issuer = content.recipients.signers.find((signer: any) => signer.roleName === IssuerRoleName)

Expand Down
13 changes: 13 additions & 0 deletions onboarding/api/src/repos/agreement.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,18 @@ export class AgreementRepo {
return updatedAgreement as Agreement | undefined
}

async setVoided(agreementId: string): Promise<Agreement | undefined> {
const [updatedAgreement] = await this.db.sql`
update agreements
set voided_at = now()
where id = ${agreementId}

returning *
`

return updatedAgreement as Agreement | undefined
}

async getAwaitingCounterSignature(): Promise<Agreement[]> {
const agreements = await this.db.sql`
select *
Expand Down Expand Up @@ -214,4 +226,5 @@ export type Agreement = {
signedAt: Date
counterSignedAt: Date
declinedAt?: Date
voidedAt?: Date
}
21 changes: 18 additions & 3 deletions onboarding/api/src/repos/kyc.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ export class KycRepo {
select *
from kyc
where kyc.created_at is not null
and kyc.status != 'verified'
or (kyc.usa_tax_resident = TRUE and kyc.accredited = FALSE)
and (kyc.status != 'verified'
or (kyc.usa_tax_resident = TRUE and kyc.accredited = FALSE))
and kyc.invalidated_at is null
`
if (!investors) return []

Expand All @@ -54,7 +55,7 @@ export class KycRepo {
)
on conflict (user_id, provider, provider_account_id)
do
update set digest = ${JSON.stringify(digest)}
update set digest = ${JSON.stringify(digest)}, invalidated_at = null

returning *
`
Expand Down Expand Up @@ -82,6 +83,19 @@ export class KycRepo {

return updatedKyc as KycEntity | undefined
}

async invalidate(provider: string, providerAccountId: string): Promise<KycEntity | undefined> {
const [updatedKyc] = await this.db.sql`
update kyc
set invalidated_at = now()
where provider = ${provider}
and provider_account_id = ${providerAccountId}

returning *
`

return updatedKyc as KycEntity | undefined
}
}

export type Blockchain = 'ethereum'
Expand All @@ -96,6 +110,7 @@ export interface KycEntity {
status: KycStatusLabel
accredited: boolean
usaTaxResident: boolean
invalidatedAt?: boolean
}

export interface SecuritizeDigest {
Expand Down
5 changes: 3 additions & 2 deletions onboarding/api/src/repos/user.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ export class UserRepo {
async findByAddress(address: string): Promise<User | undefined> {
const [data] = await this.db.sql`
select users.*
from users
inner join addresses on addresses.address = ${address}
from addresses
inner join users on users.id = addresses.user_id
where addresses.address = ${address}
`

return data as User | undefined
Expand Down
2 changes: 1 addition & 1 deletion onboarding/api/src/services/docusign.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class DocusignService {
const recipientViewRequest = {
authenticationMethod: 'none',
email: user.email,
userName: user.entityName || user.fullName,
userName: user.entityName?.length > 0 ? user.entityName : user.fullName,
roleName: InvestorRoleName,
clientUserId: user.id,
returnUrl: returnUrl,
Expand Down
20 changes: 10 additions & 10 deletions onboarding/api/src/services/memberlist.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ export class MemberlistService {
async update(userId: string, poolId?: string, tranche?: Tranche) {
const user = await this.userRepo.find(userId)
if (config.globalRestrictedCountries.includes(user.countryCode)) {
console.log(`User ${userId} is based in ${user.countryCode}, which is restricted globally.`)
this.logger.log(`User ${userId} is based in ${user.countryCode}, which is restricted globally.`)
return
}
const kyc = await this.kycRepo.find(userId)
if (kyc.status !== 'verified' || (kyc.usaTaxResident && !kyc.accredited)) {
console.log(`User ${userId} is not yet verified or accredited.`)
this.logger.log(`User ${userId} is not yet verified or accredited.`)
return
}

Expand All @@ -38,25 +38,25 @@ export class MemberlistService {
tranches = [tranche]
}

for (let t of tranches) {
tranches.forEach(async (t: Tranche) => {
// If poolId is supplied, whitelist just for that pool. Otherwise, try to whitelist for all pools
const poolIds = poolId === undefined ? await this.poolService.getIds() : [poolId]

for (let poolId of poolIds) {
poolIds.forEach(async (poolId: string) => {
const pool = await this.poolService.get(poolId)
if (!pool || pool?.profile.issuer.restrictedCountryCodes?.includes(user.countryCode)) {
console.log(`User ${userId} is based in ${user.countryCode}, which is restricted for pool ${poolId}.`)
continue
this.logger.log(`User ${userId} is based in ${user.countryCode}, which is restricted for pool ${poolId}.`)
return
}

const agreements = await this.agreementRepo.getByUserPoolTranche(userId, poolId, t)
const done = agreements.every((agreement: Agreement) => agreement.signedAt && agreement.counterSignedAt)
const done = agreements.some((agreement: Agreement) => agreement.signedAt && agreement.counterSignedAt)
if (done && agreements.length > 0) {
await this.poolService.addToMemberlist(userId, poolId, t, agreements[0].id)
} else {
console.log(`User ${userId}'s agreement for ${poolId} has not yet been signed or counter-signed.`)
this.logger.log(`User ${userId}'s agreement for ${poolId} has not yet been signed or counter-signed.`)
}
}
}
})
})
}
}
34 changes: 21 additions & 13 deletions onboarding/api/src/services/pool.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { NonceManager } from '@ethersproject/experimental'
import { Injectable, Logger } from '@nestjs/common'
import { Cron, CronExpression } from '@nestjs/schedule'
import { ethers } from 'ethers'
Expand All @@ -10,6 +9,7 @@ import { UserRepo } from '../repos/user.repo'
import contractAbiMemberAdmin from '../utils/MemberAdmin.abi'
import contractAbiMemberlist from '../utils/Memberlist.abi'
import contractAbiPoolRegistry from '../utils/PoolRegistry.abi'
import { TransactionManager } from '../utils/tx-manager'
const fetch = require('@vercel/fetch-retry')(require('node-fetch'))

@Injectable()
Expand All @@ -18,7 +18,10 @@ export class PoolService {
private pools: { [key: string]: Pool } = {}

provider = new FastJsonRpcProvider(config.rpcUrl)
signer = new NonceManager(new ethers.Wallet(config.signerPrivateKey).connect(this.provider))
signer = new TransactionManager(new ethers.Wallet(config.signerPrivateKey), {
initialSpeed: 'fast',
increasedSpeed: 'rapid',
}).connect(this.provider)
registry = new ethers.Contract(config.poolRegistry, contractAbiPoolRegistry, this.provider)

constructor(
Expand Down Expand Up @@ -96,6 +99,7 @@ export class PoolService {
const pool = await this.get(poolId)
if (!pool) throw new Error(`Failed to get pool ${poolId} when adding to memberlist`)

this.logger.log(`Adding user ${userId} to pool ${poolId}`)
const memberAdmin = new ethers.Contract(config.memberAdminContractAddress, contractAbiMemberAdmin, this.signer)
const memberlistAddress = tranche === 'senior' ? pool.addresses.SENIOR_MEMBERLIST : pool.addresses.JUNIOR_MEMBERLIST

Expand All @@ -105,19 +109,23 @@ export class PoolService {

// TODO: this should also filter by blockchain and network
const addresses = await this.addressRepo.getByUser(userId)
for (let address of addresses) {
try {
const tx = await memberAdmin.updateMember(memberlistAddress, address.address, validUntil, { gasLimit: 1000000 })
this.logger.log(
`Submitted tx to add ${address.address} to ${memberlistAddress}: ${tx.hash} (nonce=${tx.nonce})`
)
await this.provider.waitForTransaction(tx.hash)
this.logger.log(`${tx.hash} (nonce=${tx.nonce}) completed`)
const ethAddresses = addresses.map((a) => a.address)

try {
this.logger.log(`Submitting tx to add ${ethAddresses.join(',')} to ${memberlistAddress}`)
const tx = await memberAdmin.updateMembers(memberlistAddress, ethAddresses, validUntil, { gasLimit: 1000000 })

this.logger.log(
`Submitted tx to add ${ethAddresses.join(',')} to ${memberlistAddress}: ${tx.hash} (nonce=${tx.nonce})`
)
await this.provider.waitForTransaction(tx.hash)
this.logger.log(`${tx.hash} (nonce=${tx.nonce}) completed`)

for (let address of addresses) {
await this.checkMemberlist(memberlistAddress, address, pool, tranche, agreementId)
} catch (e) {
console.error(`Failed to add ${address.address} to ${memberlistAddress}: ${e}`)
}
} catch (e) {
console.error(`Failed to add ${ethAddresses.join(',')} to ${memberlistAddress}: ${e}`)
}
}

Expand Down Expand Up @@ -149,7 +157,7 @@ export class PoolService {
tranche,
true,
agreementId,
user.entityName || user.fullName
user.entityName?.length > 0 ? user.entityName : user.fullName
)
} else {
this.logger.log(`${address.address} is not a member of ${pool.metadata.name} - ${tranche}`)
Expand Down
14 changes: 6 additions & 8 deletions onboarding/api/src/services/sync.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class SyncService {

if (!investor) {
console.log(`Failed to retrieve investor status for user ${kyc.userId}`)
await this.kycRepo.invalidate(kyc.provider, kyc.providerAccountId)
return
}

Expand Down Expand Up @@ -68,7 +69,7 @@ export class SyncService {
}

// This is just a backup option, it should already be covered by the Docusign Connect integration
@Cron(CronExpression.EVERY_HOUR)
@Cron(CronExpression.EVERY_DAY_AT_2AM)
async syncAgreementStatus() {
const agreements = await this.agreementRepo.getAwaitingCounterSignature()
if (agreements.length === 0) return
Expand All @@ -86,16 +87,13 @@ export class SyncService {
}
}

@Cron(CronExpression.EVERY_HOUR)
@Cron(CronExpression.EVERY_30_MINUTES)
async syncWhitelistStatus() {
const missedInvestors = await this.addressRepo.getMissingWhitelistedUsers()
console.log(`Whitelisting ${missedInvestors.length} missed investors.`)

for (let investor of missedInvestors) {
await this.memberlistService.update(investor.userId, investor.poolId, investor.tranche)
}
missedInvestors.forEach((investor) => {
this.memberlistService.update(investor.userId, investor.poolId, investor.tranche)
})
}

// @Cron(CronExpression.EVERY_HOUR)
// async syncInvestorBalances() {}
}
Loading