Skip to content

Commit

Permalink
Implement delegated AG (#184)
Browse files Browse the repository at this point in the history
Why:

We want to implement a delegated AG mechanism so it is citizend to
support the fees of inserting the grants in the blockchain.

This change addresses the need by:

- Calling the smartcontract from the UI with
insertGrantBySignatureMessage that retrieves a message for the user to
sign, granting the required permission for the projects to user later on
- Make User signing the message
- Send result to our server and call insertGrantBySignature, which
closes the flow, by making our server wallet pay the transaction and
inserts the AG from the user to the project in the blockchain
  • Loading branch information
luistorres authored Apr 1, 2024
1 parent 9c3ca01 commit 5823128
Show file tree
Hide file tree
Showing 12 changed files with 500 additions and 65 deletions.
3 changes: 1 addition & 2 deletions packages/web-app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ NEXT_PUBLIC_IDOS_NODE_URL=https://nodes.playground.idos.network
NEXT_PUBLIC_IDOS_DB_ID=x625a832c84f02fbebb229ee3b5e66b6767802b29d87acf72b8dd05d1
NEXT_PUBLIC_IDOS_CONTRACT_ADDRESS=0x1673b9fD14C30332990314d99826F22a628A2601
NEXT_PUBLIC_IDOS_CHAIN_ID=0xaa36a7
NEXT_GRANTS_CONTRACT_ADDRESS=0x032b8275B13D3aEef180d13e29287535aCB667Ef
NEXT_ENCRYPTION_SECRET_KEY=2bu7SyMToRAuFn01/oqU3fx9ZHo9GKugQhQYmDuBXzg=
NEXT_EVM_GRANTEE_PRIVATE_KEY=0x515c2fed89c22eaa9d41cfce6e6e454fa0a39353e711d6a99f34b4ecab4b4859
NEXT_CITIZEND_WALLET_PRIVATE_KEY=0x515c2fed89c22eaa9d41cfce6e6e454fa0a39353e711d6a99f34b4ecab4b4859
3 changes: 1 addition & 2 deletions packages/web-app/additional-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ declare global {
NEXT_PUBLIC_IDOS_DB_ID: string;
NEXT_PUBLIC_IDOS_CONTRACT_ADDRESS: `0x${string}`;
NEXT_PUBLIC_IDOS_CHAIN_ID: string;
NEXT_PUBLIC_GRANTS_CONTRACT_ADDRESS: `0x${string}`;
NEXT_ENCRYPTION_SECRET_KEY: string;
NEXT_EVM_GRANTEE_PRIVATE_KEY: `0x${string}`;
NEXT_CITIZEND_WALLET_PRIVATE_KEY: `0x${string}`;
}
}
}
Expand Down
87 changes: 87 additions & 0 deletions packages/web-app/app/_lib/actions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useIdOS } from '../_providers/idos';
import { getPublicInfo } from '../_server/idos';
import { insertGrantBySignature } from '../_server/grants';
import { useFetchGrantMessage } from './contract-queries';
import { useSignMessage } from 'wagmi';
import { useCallback, useEffect } from 'react';

export const useAcquireAccessGrantMutation = () => {
const { sdk } = useIdOS();
Expand Down Expand Up @@ -29,3 +33,86 @@ export const useAcquireAccessGrantMutation = () => {
},
});
};

export const useInsertGrantBySignatureMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
owner,
grantee,
dataId,
signature,
expiration,
}: {
owner: string;
grantee: string;
dataId: string;
signature: string;
expiration: number;
}) => {
const hash = await insertGrantBySignature({
owner,
grantee,
dataId,
expiration,
signature,
});

return hash;
},
onSuccess: () => {
// queryClient.invalidateQueries({ queryKey: ['credentials'] });
// queryClient.invalidateQueries({ queryKey: ['credential-content'] });
// invalidate queries after 10 seconds
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['grants'] });
}, 10000);
},
});
};

export const useSignDelegatedAccessGrant = () => {
const {
mutate,
isPending: isServerPending,
isSuccess,
data: transactionHash,
} = useInsertGrantBySignatureMutation();
const { owner, grantee, dataId, expiration, message } =
useFetchGrantMessage();
const {
data: signature,
signMessage,
isPending: isSignPending,
} = useSignMessage();

useEffect(() => {
if (owner && grantee && dataId && expiration && signature && !isSuccess) {
mutate({
owner,
grantee,
dataId,
signature,
expiration,
});
}
}, [signature, mutate, isSuccess, owner, grantee, dataId, expiration]);

const sign = useCallback(async () => {
try {
if (!message) return;
signMessage({ message });
} catch (error) {
console.log(error);
}
}, [signMessage, message]);

return {
sign,
dataId,
isServerPending,
isSignPending,
isSuccess,
transactionHash,
};
};
63 changes: 63 additions & 0 deletions packages/web-app/app/_lib/contract-queries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useFetchNewDataId } from './queries';
import { useReadContract } from 'wagmi';
import { grantsAbi } from '../_server/grants/abi';
import { useMemo } from 'react';

type TFetchGrantMessage = {
owner: string | undefined;
grantee: string | undefined;
dataId: string | undefined;
expiration: number | undefined;
message: string | undefined;
isSuccess: boolean;
isError: boolean;
isLoading: boolean;
};

/** generate a grant message to be signed by the user and later inserted by our server
*/
export const useFetchGrantMessage = (): TFetchGrantMessage => {
const { data, isSuccess: isSuccessDataId } = useFetchNewDataId();
const {
data: message,
isSuccess: isSuccessMessage,
isError,
isLoading,
} = useReadContract({
abi: grantsAbi,
address: process.env.NEXT_PUBLIC_IDOS_CONTRACT_ADDRESS,
functionName: 'insertGrantBySignatureMessage',
args: [data?.owner, data?.grantee, data?.dataId, data?.expiration],
query: {
enabled: !!(
isSuccessDataId &&
data?.owner &&
data?.grantee &&
data?.dataId &&
data?.expiration
),
},
});

return useMemo(() => {
return {
owner: data?.owner,
grantee: data?.grantee,
dataId: data?.dataId,
expiration: data?.expiration,
message: message as string | undefined, //review later
isSuccess: isSuccessMessage,
isError,
isLoading,
};
}, [
message,
isSuccessMessage,
isError,
isLoading,
data?.owner,
data?.grantee,
data?.dataId,
data?.expiration,
]);
};
113 changes: 101 additions & 12 deletions packages/web-app/app/_lib/queries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,34 @@ import { idOSCredential } from '../_types/idos';
import { useMemo } from 'react';
import { compareAddresses } from './utils';
import { getPublicInfo } from '../_server/idos';
import { getProjectGrants } from '../_server/grants';

export const usePublicInfo = () => {
return useQuery({
queryKey: ['public-info'],
queryFn: async () => {
return await getPublicInfo();
},
});
};

export const useFetchIdOSProfile = () => {
const { sdk } = useIdOS();
const { sdk, hasSigner } = useIdOS();

return useQuery({
queryKey: ['idos-profile'],
queryFn: () => sdk?.auth?.currentUser(),
enabled: !!(sdk && hasSigner),
});
};

export const useFetchCredentials = () => {
const { sdk } = useIdOS();
const { sdk, hasSigner } = useIdOS();

return useQuery({
queryKey: ['credentials'],
queryFn: async ({ queryKey: [tableName] }) => {
if (!sdk) return [];
if (!sdk || !hasSigner) return null;

const credentials = await sdk.data.list<idOSCredential>(tableName);
return credentials.map((credential) => ({
Expand All @@ -31,17 +42,19 @@ export const useFetchCredentials = () => {
}));
},
select: (credentials) =>
credentials &&
credentials.filter((credential) => !credential.original_id),
enabled: !!(sdk && hasSigner),
});
};

export const useFetchCredentialContent = (credentialId: string | undefined) => {
const { sdk } = useIdOS();
const { sdk, hasSigner } = useIdOS();

return useQuery({
queryKey: ['credential-content', credentialId],
queryFn: async ({ queryKey: [, credentialId] }) => {
if (!sdk || !credentialId) return '';
if (!sdk || !credentialId || !hasSigner) return '';

const credential = await sdk.data.get<
idOSCredential & { content: string }
Expand All @@ -60,36 +73,49 @@ export const useFetchCredentialContent = (credentialId: string | undefined) => {

return JSON.parse(credential?.content);
},
enabled: !!credentialId,
enabled: !!(sdk && credentialId && hasSigner),
});
};

export const useFetchWallets = () => {
const { sdk } = useIdOS();
const { sdk, hasSigner } = useIdOS();

return useQuery({
queryKey: ['wallets'],
queryFn: () => sdk?.data.list('wallets'),
enabled: !!(sdk && hasSigner),
});
};

export const useFetchGrants = () => {
const { sdk, address } = useIdOS();
const { sdk, address, hasSigner } = useIdOS();
const { data: publicInfo, isSuccess: isServerQuerySuccess } = usePublicInfo();

return useQuery({
queryKey: ['grants', address],
queryFn: async () => {
const { grantee } = await getPublicInfo();
return sdk?.grants.list({ owner: address, grantee });
return sdk?.grants.list({ owner: address, grantee: publicInfo?.grantee });
},
enabled: !!(
sdk &&
hasSigner &&
address &&
isServerQuerySuccess &&
publicInfo
),
});
};

export const useFetchKycCredential = () => {
const { data: credentials, isLoading, error } = useFetchCredentials();
const {
data: credentials,
isLoading,
error,
isSuccess,
} = useFetchCredentials();

const credential = useMemo(() => {
if (!credentials) return;
if (!credentials) return null;

return credentials.find((c) => c.credential_type === 'kyc');
}, [credentials]);
Expand All @@ -100,6 +126,7 @@ export const useFetchKycCredential = () => {
approved: credential?.credential_status === 'approved',
isLoading,
error,
isSuccess,
};
};

Expand Down Expand Up @@ -129,11 +156,13 @@ export const useFetchKycData = () => {
approved,
isLoading: kycLoading,
error: kycError,
isSuccess: kycSuccess,
} = useFetchKycCredential();
const {
data: credentialContent,
isLoading: contentLoading,
error: contentError,
isSuccess: contentSuccess,
} = useFetchCredentialContent(id);

const credential: TCredentialContent = useMemo(() => {
Expand All @@ -153,6 +182,7 @@ export const useFetchKycData = () => {
),
isLoading: kycLoading || contentLoading,
error: kycError || contentError,
isSuccess: kycSuccess && contentSuccess,
};
}, [
credentialContent,
Expand All @@ -163,7 +193,66 @@ export const useFetchKycData = () => {
contentLoading,
kycError,
contentError,
kycSuccess,
contentSuccess,
]);

return credential;
};

export const useFetchProjectGrants = () => {
return useQuery({
queryKey: ['grants-contract'],
queryFn: async () => {
const { grantee } = await getPublicInfo();

return getProjectGrants(grantee);
},
});
};

/**
* Get a new dataId from idOS to be used in the grant process
*/
export const useFetchNewDataId = () => {
const { sdk, address: owner, hasSigner } = useIdOS();
const { data: publicInfo, isSuccess: isServerQuerySuccess } = usePublicInfo();
const { id } = useFetchKycData();

return useQuery({
queryKey: [
'insert-grant-by-signature-message',
owner,
id,
publicInfo?.grantee,
publicInfo?.lockTimeSpanSeconds,
publicInfo?.encryptionPublicKey,
],
queryFn: async () => {
if (!owner || !sdk || !id || !publicInfo) return null;

const { grantee, lockTimeSpanSeconds, encryptionPublicKey } = publicInfo;

try {
const expiration = Math.floor(Date.now() / 1000) + lockTimeSpanSeconds;

// generate a dataId on idOS side
const { id: dataId } = await sdk.data.share(
'credentials',
id,
encryptionPublicKey,
);

return {
owner,
grantee,
dataId,
expiration,
};
} catch (error) {
return null;
}
},
enabled: !!(hasSigner && owner && isServerQuerySuccess && id && publicInfo),
});
};
Loading

0 comments on commit 5823128

Please sign in to comment.