Skip to content

Commit

Permalink
fix: creating a vault after whitelisting a user would make the vault …
Browse files Browse the repository at this point in the history
…not accessible to the user as the whitelisting would have already happened
  • Loading branch information
pasviegas committed Jan 21, 2024
1 parent df5ef91 commit 80e761c
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 184 deletions.
19 changes: 10 additions & 9 deletions packages/api/src/modules/accounts/accounts.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CredbullVault } from '@credbull/contracts';
import { AKYCProvider } from '@credbull/contracts';
import { Test, TestingModule } from '@nestjs/testing';
import { SupabaseClient } from '@supabase/supabase-js';
import { beforeEach, describe, expect, it, vi } from 'vitest';
Expand All @@ -17,15 +17,15 @@ describe('AccountsController', () => {
let controller: AccountsController;
let client: DeepMockProxy<SupabaseClient>;
let admin: DeepMockProxy<SupabaseClient>;
let vault: DeepMockProxy<CredbullVault>;
let kyc: DeepMockProxy<AKYCProvider>;
let ethers: DeepMockProxy<EthersService>;
beforeEach(async () => {
client = mockDeep<SupabaseClient>();
admin = mockDeep<SupabaseClient>();
vault = mockDeep<CredbullVault>();
kyc = mockDeep<AKYCProvider>();
ethers = mockDeep<EthersService>();

(KycService.prototype as any).getVaultInstance = vi.fn().mockReturnValue(vault);
(KycService.prototype as any).getOnChainProvider = vi.fn().mockReturnValue(kyc);

const service = { client: () => client, admin: () => admin };

Expand All @@ -45,11 +45,12 @@ describe('AccountsController', () => {
const select = vi.fn();
const eq = vi.fn();
const single = vi.fn();
select.mockReturnValueOnce({ eq } as any);
eq.mockReturnValueOnce({ single } as any);
const builder = { select, eq, single };
select.mockReturnValueOnce(builder as any);
eq.mockReturnValueOnce(builder as any);
single.mockResolvedValueOnce({ data: null } as any);

client.from.mockReturnValue({ select } as any);
client.from.mockReturnValue(builder as any);

const { status } = await controller.status();

Expand Down Expand Up @@ -85,8 +86,8 @@ describe('AccountsController', () => {
admin.from.mockReturnValue({ select, insert } as any);
ethers.deployer.mockReturnValue({} as any);

vault.isWhitelisted.mockResolvedValueOnce(false);
vault.updateWhitelistStatus.mockResolvedValueOnce({} as any);
kyc.status.mockResolvedValueOnce(false);
kyc.updateStatus.mockResolvedValueOnce({} as any);

const { status } = await controller.whitelist({ user_id, address });

Expand Down
90 changes: 60 additions & 30 deletions packages/api/src/modules/accounts/kyc.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CredbullVault__factory } from '@credbull/contracts';
import { AKYCProvider__factory } from '@credbull/contracts';
import { Injectable } from '@nestjs/common';
import * as _ from 'lodash';

import { EthersService } from '../../clients/ethers/ethers.service';
import { SupabaseService } from '../../clients/supabase/supabase.service';
Expand All @@ -17,66 +18,95 @@ export class KycService {
) {}

async status(): Promise<ServiceResponse<KYCStatus>> {
const events = await this.supabase.client().from('kyc_events').select().eq('event_name', 'accepted').single();
const client = this.supabase.client();

const events = await client.from('kyc_events').select().eq('event_name', 'accepted').single();
if (events.error) return events;

if (!events.data?.address) return { data: KYCStatus.PENDING };

return (await this.checkOnChain(events.data?.address)) //
? { data: KYCStatus.ACTIVE }
: { data: KYCStatus.REJECTED };
const kycProvider = await client.from('vault_distribution_entities').select('*').eq('type', 'kyc_provider');
if (kycProvider.error) return kycProvider;

const distinctProviders = _.uniqBy(kycProvider.data ?? [], 'address');

const check = await this.checkOnChain(distinctProviders, events.data?.address);
if (check.error) return check;

return check.data ? { data: KYCStatus.ACTIVE } : { data: KYCStatus.REJECTED };
}

async whitelist(dto: WhitelistAccountDto): Promise<ServiceResponse<Tables<'kyc_events'>[]>> {
const { address, user_id } = dto;
const admin = this.supabase.admin();

const existing = await admin
.from('kyc_events')
.select()
.eq('address', address)
.eq('user_id', user_id)
.eq('address', dto.address)
.eq('user_id', dto.user_id)
.eq('event_name', 'accepted')
.maybeSingle();

if (existing.error) return existing;
if (existing.data) return { data: [existing.data] };

const wallet = await admin.from('user_wallets').select().eq('address', address).eq('user_id', user_id).single();
const wallet = await admin
.from('user_wallets')
.select()
.eq('address', dto.address)
.eq('user_id', dto.user_id)
.single();
if (wallet.error) return wallet;

const vaults = await admin.from('vaults').select('*').neq('status', 'created').lt('opened_at', 'now()');
if (vaults.error) return vaults;
const query = admin.from('vault_distribution_entities').select('address').eq('type', 'kyc_provider');
if (wallet.data.discriminator) query.eq('tenant', dto.user_id);

const providers = await query;
if (providers.error) return providers;

const errors = [];
if (vaults.data) {
for (const vault of vaults.data) {
const vaultInstance = this.getVaultInstance(vault.address);
const { error, data } = await responseFromRead(vaultInstance.isWhitelisted(address));
if (error) {
errors.push(error);
continue;
}

if (!data) await responseFromWrite(vaultInstance.updateWhitelistStatus([address], [true]));
const distinctProviders = _.uniqBy(providers.data ?? [], 'address');

for (const { address } of distinctProviders) {
const provider = this.getOnChainProvider(address);
const { error, data } = await responseFromRead(provider.status(dto.address));
if (error) {
errors.push(error);
continue;
}

if (!data) await responseFromWrite(provider.updateStatus([dto.address], [true]));
}
if (errors.length) return { error: new AggregateError(errors) };

return admin
.from('kyc_events')
.insert({
address,
user_id: wallet.data.user_id,
event_name: 'accepted',
})
.insert({ ...dto, event_name: 'accepted' })
.select();
}

private async checkOnChain(address: string): Promise<boolean> {
return Boolean(address);
private async checkOnChain(
kycProviders: Tables<'vault_distribution_entities'>[],
address: string,
): Promise<ServiceResponse<boolean>> {
const errors = [];
let status = false;

for (const kyc of kycProviders) {
const provider = this.getOnChainProvider(kyc.address);
const { error, data } = await responseFromRead(provider.status(address));
if (error) {
errors.push(error);
continue;
}

status = status && data;
if (!status) break;
}
return errors.length > 0 ? { error: new AggregateError(errors) } : { data: status };
}

private getVaultInstance(address: string) {
return CredbullVault__factory.connect(address, this.ethers.deployer());
private getOnChainProvider(address: string) {
return AKYCProvider__factory.connect(address, this.ethers.deployer());
}
}
4 changes: 4 additions & 0 deletions packages/api/src/modules/accounts/wallets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class WalletsService {
return { error: new Error('No discriminator provided') };
}

if ((auth.data.user.app_metadata.entity_type as EntityType) !== 'partner' && dto.discriminator) {
return { error: new Error('Discriminator should not be provided') };
}

const verify = await new SiweMessage(message).verify({ signature });
if (verify.error) return { error: verify.error };

Expand Down
28 changes: 24 additions & 4 deletions packages/api/src/types/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,23 @@ export interface Database {
id: number;
order: number;
percentage: number;
tenant: string | null;
};
Insert: {
created_at?: string;
entity_id: number;
id?: number;
order: number;
percentage: number;
tenant?: string | null;
};
Update: {
created_at?: string;
entity_id?: number;
id?: number;
order?: number;
percentage?: number;
tenant?: string | null;
};
Relationships: [
{
Expand All @@ -97,31 +100,48 @@ export interface Database {
referencedRelation: 'vault_distribution_entities';
referencedColumns: ['id'];
},
{
foreignKeyName: 'vault_distribution_configs_tenant_fkey';
columns: ['tenant'];
isOneToOne: false;
referencedRelation: 'users';
referencedColumns: ['id'];
},
];
};
vault_distribution_entities: {
Row: {
address: string;
created_at: string;
id: number;
type: Database['public']['Enums']['vault_distribution_entity_types'];
tenant: string | null;
type: Database['public']['Enums']['vault_entity_types'];
vault_id: number;
};
Insert: {
address: string;
created_at?: string;
id?: number;
type: Database['public']['Enums']['vault_distribution_entity_types'];
tenant?: string | null;
type: Database['public']['Enums']['vault_entity_types'];
vault_id: number;
};
Update: {
address?: string;
created_at?: string;
id?: number;
type?: Database['public']['Enums']['vault_distribution_entity_types'];
tenant?: string | null;
type?: Database['public']['Enums']['vault_entity_types'];
vault_id?: number;
};
Relationships: [
{
foreignKeyName: 'vault_distribution_entities_tenant_fkey';
columns: ['tenant'];
isOneToOne: false;
referencedRelation: 'users';
referencedColumns: ['id'];
},
{
foreignKeyName: 'vault_distribution_entities_vault_id_fkey';
columns: ['vault_id'];
Expand Down Expand Up @@ -187,7 +207,7 @@ export interface Database {
};
Enums: {
kyc_event: 'processing' | 'accepted' | 'rejected';
vault_distribution_entity_types: 'activity_reward' | 'treasury' | 'vault' | 'custodian';
vault_entity_types: 'activity_reward' | 'treasury' | 'vault' | 'custodian' | 'kyc_provider';
vault_status: 'created' | 'ready' | 'matured';
vault_type: 'fixed_yield';
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
create type "public"."vault_entity_types" as enum ('activity_reward', 'treasury', 'vault', 'custodian', 'kyc_provider');

alter table "public"."vault_distribution_entities" alter column "type" set data type vault_entity_types using "type"::text::vault_entity_types;

drop type "public"."vault_distribution_entity_types";


35 changes: 35 additions & 0 deletions packages/api/supabase/migrations/20240121202500_remote_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
drop policy "Enable select for authenticated users only" on "public"."vault_distribution_configs";

drop policy "Enable select for authenticated users only" on "public"."vault_distribution_entities";

alter table "public"."vault_distribution_configs" add column "tenant" uuid;

alter table "public"."vault_distribution_entities" add column "tenant" uuid;

alter table "public"."vault_distribution_configs" add constraint "vault_distribution_configs_tenant_fkey" FOREIGN KEY (tenant) REFERENCES auth.users(id) ON DELETE SET NULL not valid;

alter table "public"."vault_distribution_configs" validate constraint "vault_distribution_configs_tenant_fkey";

alter table "public"."vault_distribution_entities" add constraint "vault_distribution_entities_tenant_fkey" FOREIGN KEY (tenant) REFERENCES auth.users(id) ON DELETE SET NULL not valid;

alter table "public"."vault_distribution_entities" validate constraint "vault_distribution_entities_tenant_fkey";

create policy "Segregate vaults by tenants"
on "public"."vault_distribution_configs"
as permissive
for all
to public
using ((((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) IS NULL) AND (tenant IS NULL)) OR ((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) = 'partner'::text) AND (tenant = auth.uid()))))
with check ((((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) IS NULL) AND (tenant IS NULL)) OR ((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) = 'partner'::text) AND (tenant = auth.uid()))));


create policy "Segregate vaults by tenants"
on "public"."vault_distribution_entities"
as permissive
for all
to public
using ((((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) IS NULL) AND (tenant IS NULL)) OR ((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) = 'partner'::text) AND (tenant = auth.uid()))))
with check ((((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) IS NULL) AND (tenant IS NULL)) OR ((((auth.jwt() -> 'app_metadata'::text) ->> 'entity_type'::text) = 'partner'::text) AND (tenant = auth.uid()))));



39 changes: 13 additions & 26 deletions packages/app/src/app/(protected)/dashboard/debug/debug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,6 @@ const depositSchema = z.object({
amount: z.number().positive(),
});

const whitelistSchema = z.object({
address: z.string().min(42).max(42),
});

const VaultDeposit = ({ erc20Address }: { erc20Address: string }) => {
const { open } = useNotification();
const { isConnected, address } = useAccount();
Expand Down Expand Up @@ -243,11 +239,6 @@ const WhitelistWalletAddress = () => {
const { isConnected, address } = useAccount();
const [isLoading, setLoading] = useState(false);

const form = useForm({
validate: zodResolver(whitelistSchema),
initialValues: { address: '' },
});

const whitelist = async () => {
setLoading(true);
try {
Expand All @@ -272,23 +263,19 @@ const WhitelistWalletAddress = () => {
<Text weight={500}>Whitelist address</Text>
</Group>

<form onSubmit={form.onSubmit(() => whitelist())} style={{ marginTop: 'auto' }}>
<TextInput label="Address" {...form.getInputProps('address')} disabled={!isConnected || isLoading} />

<Group grow>
<Button
type="submit"
variant="light"
color="blue"
mt="md"
radius="md"
disabled={!isConnected || isLoading}
loading={isLoading}
>
Whitelist
</Button>
</Group>
</form>
<Group grow mt="auto">
<Button
onClick={() => whitelist()}
variant="light"
color="blue"
mt="md"
radius="md"
disabled={!isConnected || isLoading}
loading={isLoading}
>
Whitelist
</Button>
</Group>
</Flex>
</Card>
);
Expand Down
Loading

0 comments on commit 80e761c

Please sign in to comment.