diff --git a/compliant-reward-distribution/frontend/src/apiReqeuests.ts b/compliant-reward-distribution/frontend/src/apiReqeuests.ts new file mode 100644 index 00000000..6556921f --- /dev/null +++ b/compliant-reward-distribution/frontend/src/apiReqeuests.ts @@ -0,0 +1,291 @@ +import { AccountAddress, AtomicStatementV2, CredentialStatement, VerifiablePresentation } from '@concordium/web-sdk'; + +interface AccountData { + // The account address that was indexed. + accountAddress: AccountAddress.Type; + // The timestamp of the block the event was included in. + blockTime: string; + // The transaction hash that the event was recorded in. + transactionHash: string; + // A boolean specifying if the account has already claimed its rewards (got + // a reward payout). Every account can only claim rewards once. + claimed: boolean; + // A boolean specifying if this account address has submitted all tasks + // and the regulatory conditions have been proven via a ZK proof. + // A manual check of the completed tasks is required now before releasing + // the reward. + pendingApproval: boolean; +} + +interface TweetData { + // The account address that submitted the tweet. + accountAddress: AccountAddress.Type; + // A tweet id submitted by the above account address (task 1). + tweetId: string | undefined; + // A boolean specifying if the text content of the tweet is eligible for + // the reward. The content of the text was verified by this backend + // before this flag is set (or will be verified manually). + tweetValid: boolean; + // A version that specifies the setting of the tweet verification. This + // enables us to update the tweet verification logic in the future and + // invalidate older versions. + tweetVerificationVersion: number; + // The timestamp when the tweet was submitted. + tweetSubmitTime: string; +} + +interface ZkProofData { + // The account address that submitted the zk proof. + accountAddress: AccountAddress.Type; + // A hash of the concatenated revealed `national_id_number` and + // `nationality` to prevent claiming with different accounts for the + // same identity. + uniquenessHash: string; + // A boolean specifying if the identity associated with the account is + // eligible for the reward (task 2). An associated ZK proof was + // verified by this backend before this flag is set. + zkProofValid: boolean; + // A version that specifies the setting of the ZK proof during the + // verification. This enables us to update the ZK proof-verification + // logic in the future and invalidate older proofs. + zkProofVerificationVersion: number; + // The timestamp when the ZK proof verification was submitted. + zkProofVerificationSubmitTime: string; +} + +interface AccountData { + accountData: AccountData | undefined; + tweetData: TweetData | undefined; + zkProofData: ZkProofData | undefined; +} + +/** + * Generates `POST` and `GET` request options. + * + * @param method - The HTTP method (`'POST'` or `'GET'`). + * @param body - Optional request body (required for `POST`, ignored for `GET`). + * @returns The request options for the specified method. + * @throws An error if the method is invalid or if the body is incorrectly provided for the method. + */ +function createRequestOptions(method: string, body?: string): RequestInit { + switch (method) { + case 'GET': + return { + method: 'GET', + }; + case 'POST': + if (!body) { + throw new Error(`Body is required for method: ${method}`); + } + return { + method: 'POST', + headers: new Headers({ 'content-type': 'application/json' }), + body: body, + }; + default: + throw new Error(`Invalid method: ${method}`); + } +} + +/** + * Sends a `GET`/`POST` request to the backend and optionally parses the response into a given type `T`. + * + * @param T - Optional return value type `T`. + * @param endpoint - The API endpoint. + * @param method - The HTTP method (`'POST'` or `'GET'`). + * @param parseReturnValue - Optional request to parse the return value into provided type `T`. + * @param body - Optional request body (required for `POST`, ignored for `GET`). + * @returns A promise that can be resolved into the parsed return value of type `T`. + * + * @throws An error if the method is invalid, if the body is incorrectly provided for the method, + * if the parsing of the return value fails, or if the backend responses with an error. + */ +async function sendBackendRequest( + endpoint: string, + method: string, + parseReturnValue: boolean, + body?: string, +): Promise { + const api = `api/${endpoint}`; + + const requestOption = createRequestOptions(method, body); + + const response = await fetch(api, requestOption); + + if (!response.ok) { + let parsedError; + + try { + parsedError = await response.json(); + } catch (e) { + throw new Error( + `Unable to send request to the backend '${api}'; StatusCode: ${response.status}; StatusText: ${response.statusText};`, + ); + } + + throw new Error(`Unable to send request to the backend '${api}'; Error: ${JSON.stringify(parsedError)}`); + } + + if (parseReturnValue) { + // Parse the response as type `T` + try { + const returnValue = (await response.json()) as T; + return returnValue; + } catch (e) { + throw new Error(`Failed to parse the response from the backend into expected type.`); + } + } + + return undefined as unknown as T; +} + +/** + * Updates the `claimed` field of an account in the backend database. + * + * @param signer - The address that signed the message. + * @param signature - The signature from the above signer. + * @param recentBlockHeight - The recent block height. The corresponding block hash is included in the signed message. + * @param accountAddress - The account address that should be set its field to `claimed` in the backend database. + * + * @throws An error if the backend responses with an error. + */ +export async function setClaimed(signer: string, signature: string, recentBlockHeight: bigint, accountAddress: string) { + const body = JSON.stringify({ + signingData: { + signer, + message: { + accountAddresses: [accountAddress], + }, + signature, + blockHeight: Number(recentBlockHeight), + }, + }); + + return await sendBackendRequest('setClaimed', 'POST', false, body); +} + +/** + * Fetches the pending approvals from the backend. + * + * @param signer - The address that signed the message. + * @param signature - The signature from the above signer. + * @param recentBlockHeight - The recent block height. The corresponding block hash is included in the signed message. + * @param limit - The maximum number of records to retrieve. + * @param offset - The starting point for record retrieval, useful for pagination. + * + * @throws An error if the backend responses with an error, or parsing of the return value fails. + */ +export async function getPendingApprovals( + signer: string, + signature: string, + recentBlockHeight: bigint, + limit: number, + offset: number, +): Promise { + const body = JSON.stringify({ + signingData: { + signer, + message: { + limit, + offset, + }, + signature, + blockHeight: Number(recentBlockHeight), + }, + }); + + return await sendBackendRequest('getPendingApprovals', 'POST', true, body); +} + +/** + * Fetches account data from the backend. + * + * @param signer - The address that signed the message. + * @param signature - The signature from the above signer. + * @param recentBlockHeight - The recent block height. The corresponding block hash is included in the signed message. + * + * @throws An error if the backend responses with an error, or parsing of the return value fails. + */ +export async function getAccountData( + signer: string, + accountAddress: string, + signature: string, + recentBlockHeight: bigint, +): Promise { + const body = JSON.stringify({ + signingData: { + signer, + message: { + accountAddress, + }, + signature, + blockHeight: Number(recentBlockHeight), + }, + }); + + return await sendBackendRequest('getAccountData', 'POST', true, body); +} + +/** + * Fetches the statement to prove from the backend. + * + * @throws An error if the backend responses with an error, or parsing of the return value fails. + */ +export async function getStatement(): Promise { + const statement = await sendBackendRequest<{ data: AtomicStatementV2[] }>('getZKProofStatements', 'GET', true); + + const credentialStatement: CredentialStatement = { + idQualifier: { + type: 'cred', + // We allow all identity providers on mainnet and on testnet. + // This list is longer than necessary to include all current/future + // identity providers on mainnet and testnet. + issuers: [0, 1, 2, 3, 4, 5, 6, 7], + }, + statement: statement.data, + }; + + return credentialStatement; +} + +/** + * Submit a tweet to the backend. + * + * @param signer - The address that signed the message. + * @param signature - The signature from the above signer. + * @param recentBlockHeight - The recent block height. The corresponding block hash is included in the signed message. + * @param tweet - The tweet URL string. + * + * @throws An error if the backend responses with an error. + */ +export async function submitTweet(signer: string, signature: string, recentBlockHeight: bigint, tweet: string) { + const body = JSON.stringify({ + signingData: { + signer, + message: { + tweet, + }, + signature, + blockHeight: Number(recentBlockHeight), + }, + }); + + return await sendBackendRequest('postTweet', 'POST', false, body); +} + +/** + * Submit a ZK proof to the backend. + * + * @param presentation - The presentation including the ZK proof. + * @param recentBlockHeight - The recent block height. The corresponding block hash is included in challenge of the ZK proof. + * + * @throws An error if the backend responses with an error. + */ +export async function submitZkProof(presentation: VerifiablePresentation, recentBlockHeight: bigint) { + const body = JSON.stringify({ + blockHeight: Number(recentBlockHeight), + presentation: presentation, + }); + + return await sendBackendRequest('postZKProof', 'POST', false, body); +} diff --git a/compliant-reward-distribution/frontend/src/components/Admin/AdminGetAccountData.tsx b/compliant-reward-distribution/frontend/src/components/Admin/AdminGetAccountData.tsx index 8e1d4f74..5349f39d 100644 --- a/compliant-reward-distribution/frontend/src/components/Admin/AdminGetAccountData.tsx +++ b/compliant-reward-distribution/frontend/src/components/Admin/AdminGetAccountData.tsx @@ -5,9 +5,10 @@ import JSONbig from 'json-bigint'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; -import { getAccountData, getARecentBlockHash, requestSignature, validateAccountAddress } from '../../utils'; +import { getRecentBlock, requestSignature, validateAccountAddress } from '../../utils'; import { WalletProvider } from '../../wallet-connection'; import { SCHEMA_GET_ACCOUNT_DATA_MESSAGE } from '../../constants'; +import { getAccountData } from '../../apiReqeuests'; interface Props { signer: string | undefined; @@ -40,8 +41,14 @@ export function AdminGetAccountData(props: Props) { throw Error(`'signer' is undefined. Connect your wallet.`); } - const [recentBlockHash, recentBlockHeight] = await getARecentBlockHash(grpcClient); - const signature = await requestSignature(recentBlockHash, SCHEMA_GET_ACCOUNT_DATA_MESSAGE, address, signer, provider); + const { blockHash: recentBlockHash, blockHeight: recentBlockHeight } = await getRecentBlock(grpcClient); + const signature = await requestSignature( + recentBlockHash, + SCHEMA_GET_ACCOUNT_DATA_MESSAGE, + address, + signer, + provider, + ); const data = await getAccountData(signer, address, signature, recentBlockHeight); setAccountData(JSONbig.stringify(data)); @@ -72,14 +79,9 @@ export function AdminGetAccountData(props: Props) {
- {accountData && ( -
-
{JSON.stringify(JSON.parse(accountData), undefined, 2)}
-
- )} + {accountData &&
{JSON.stringify(JSON.parse(accountData), undefined, 2)}
} {error && {error}} - diff --git a/compliant-reward-distribution/frontend/src/components/Admin/AdminGetPendingApprovals.tsx b/compliant-reward-distribution/frontend/src/components/Admin/AdminGetPendingApprovals.tsx index 1b4b81a7..f6a6878f 100644 --- a/compliant-reward-distribution/frontend/src/components/Admin/AdminGetPendingApprovals.tsx +++ b/compliant-reward-distribution/frontend/src/components/Admin/AdminGetPendingApprovals.tsx @@ -5,9 +5,10 @@ import JSONbig from 'json-bigint'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; -import { getARecentBlockHash, getPendingApprovals, requestSignature } from '../../utils'; +import { getRecentBlock, requestSignature } from '../../utils'; import { WalletProvider } from '../../wallet-connection'; -import { SCHEMA_GET_PENDING_APPROVALS_MESSAGE } from '../../constants'; +import { LIMIT, OFFSET, SCHEMA_GET_PENDING_APPROVALS_MESSAGE } from '../../constants'; +import { getPendingApprovals } from '../../apiReqeuests'; interface Props { provider: WalletProvider | undefined; @@ -32,13 +33,17 @@ export function AdminGetPendingApprovals(props: Props) { throw Error(`'signer' is undefined. Connect your wallet.`); } - const [recentBlockHash, recentBlockHeight] = await getARecentBlockHash(grpcClient); - const limit = 5; - const offset = 0; + const { blockHash: recentBlockHash, blockHeight: recentBlockHeight } = await getRecentBlock(grpcClient); - const signature = await requestSignature(recentBlockHash, SCHEMA_GET_PENDING_APPROVALS_MESSAGE, { limit, offset }, signer, provider); + const signature = await requestSignature( + recentBlockHash, + SCHEMA_GET_PENDING_APPROVALS_MESSAGE, + { limit: LIMIT, offset: OFFSET }, + signer, + provider, + ); - const data = await getPendingApprovals(signer, signature, recentBlockHeight, limit, offset); + const data = await getPendingApprovals(signer, signature, recentBlockHeight, LIMIT, OFFSET); setPendingApprovals(JSONbig.stringify(data)); } catch (error) { setError((error as Error).message); @@ -58,9 +63,7 @@ export function AdminGetPendingApprovals(props: Props) { {error && {error}} {pendingApprovals && ( -
-
{JSON.stringify(JSON.parse(pendingApprovals), undefined, 2)}
-
+
{JSON.stringify(JSON.parse(pendingApprovals), undefined, 2)}
)} diff --git a/compliant-reward-distribution/frontend/src/components/Admin/AdminSetClaimed.tsx b/compliant-reward-distribution/frontend/src/components/Admin/AdminSetClaimed.tsx index b9cd2e3a..5eb959b1 100644 --- a/compliant-reward-distribution/frontend/src/components/Admin/AdminSetClaimed.tsx +++ b/compliant-reward-distribution/frontend/src/components/Admin/AdminSetClaimed.tsx @@ -4,9 +4,10 @@ import { Alert, Button, Form } from 'react-bootstrap'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; -import { getARecentBlockHash, requestSignature, setClaimed, validateAccountAddress } from '../../utils'; +import { getRecentBlock, requestSignature, validateAccountAddress } from '../../utils'; import { WalletProvider } from '../../wallet-connection'; import { SCHEMA_SET_CLAIMED_MESSAGE } from '../../constants'; +import { setClaimed } from '../../apiReqeuests'; interface Props { provider: WalletProvider | undefined; @@ -28,20 +29,29 @@ export function AdminSetClaimed(props: Props) { }); const [error, setError] = useState(undefined); + const [successfulSubmission, setSuccessfulSubmission] = useState(undefined); async function onSubmit() { setError(undefined); + setSuccessfulSubmission(undefined); try { if (!signer) { throw Error(`'signer' is undefined. Connect your wallet.`); } - const [recentBlockHash, recentBlockHeight] = await getARecentBlockHash(grpcClient); + const { blockHash: recentBlockHash, blockHeight: recentBlockHeight } = await getRecentBlock(grpcClient); - const signature = await requestSignature(recentBlockHash, SCHEMA_SET_CLAIMED_MESSAGE, [address], signer, provider); + const signature = await requestSignature( + recentBlockHash, + SCHEMA_SET_CLAIMED_MESSAGE, + [address], + signer, + provider, + ); await setClaimed(signer, signature, recentBlockHeight, address); + setSuccessfulSubmission(true); } catch (error) { setError((error as Error).message); } @@ -70,6 +80,16 @@ export function AdminSetClaimed(props: Props) { {error && {error}} + +
+ diff --git a/compliant-reward-distribution/frontend/src/components/TweetSubmission.tsx b/compliant-reward-distribution/frontend/src/components/TweetSubmission.tsx index cf2718e3..e388521b 100644 --- a/compliant-reward-distribution/frontend/src/components/TweetSubmission.tsx +++ b/compliant-reward-distribution/frontend/src/components/TweetSubmission.tsx @@ -4,9 +4,10 @@ import { Alert, Button, Form } from 'react-bootstrap'; import { ConcordiumGRPCClient } from '@concordium/web-sdk'; -import { getARecentBlockHash, requestSignature, submitTweet } from '../utils'; +import { getRecentBlock, requestSignature } from '../utils'; import { WalletProvider } from '../wallet-connection'; import { SCHEMA_TWEET_MESSAGE } from '../constants'; +import { submitTweet } from '../apiReqeuests'; interface Props { signer: string | undefined; @@ -36,11 +37,11 @@ export function TweetSubmission(props: Props) { }); const [error, setError] = useState(undefined); - const [successfulSubmission, setSuccessfulSubmission] = useState(undefined); + const [successfulSubmission, setSuccessfulSubmission] = useState(undefined); async function onSubmit() { setError(undefined); - setSuccessfulSubmission(undefined) + setSuccessfulSubmission(undefined); try { checkTweetdFromUrl(tweet); @@ -49,13 +50,13 @@ export function TweetSubmission(props: Props) { throw Error(`'signer' is undefined. Connect your wallet.`); } - const [recentBlockHash, recentBlockHeight] = await getARecentBlockHash(grpcClient); + const { blockHash: recentBlockHash, blockHeight: recentBlockHeight } = await getRecentBlock(grpcClient); const signature = await requestSignature(recentBlockHash, SCHEMA_TWEET_MESSAGE, tweet, signer, provider); await submitTweet(signer, signature, recentBlockHeight, tweet); - setSuccessfulSubmission(undefined) + setSuccessfulSubmission(true); } catch (error) { setError((error as Error).message); } @@ -80,7 +81,13 @@ export function TweetSubmission(props: Props) { {error && {error}} - diff --git a/compliant-reward-distribution/frontend/src/components/ZkProofSubmission.tsx b/compliant-reward-distribution/frontend/src/components/ZkProofSubmission.tsx index 2d2905eb..21a6e923 100644 --- a/compliant-reward-distribution/frontend/src/components/ZkProofSubmission.tsx +++ b/compliant-reward-distribution/frontend/src/components/ZkProofSubmission.tsx @@ -12,8 +12,9 @@ import { } from '@concordium/web-sdk'; import { WalletProvider } from '../wallet-connection'; -import { getARecentBlockHash, getStatement, submitZkProof } from '../utils'; +import { getRecentBlock } from '../utils'; import { CONTEXT_STRING } from '../constants'; +import { getStatement, submitZkProof } from '../apiReqeuests'; interface Props { accountAddress: string | undefined; @@ -25,7 +26,7 @@ export function ZkProofSubmission(props: Props) { const { provider, grpcClient, accountAddress } = props; const [error, setError] = useState(undefined); - const [successfulSubmission, setSuccessfulSubmission] = useState(undefined); + const [successfulSubmission, setSuccessfulSubmission] = useState(undefined); const [zkStatement, setZkStatement] = useState(undefined); useEffect(() => { @@ -37,12 +38,12 @@ export function ZkProofSubmission(props: Props) { fetchStatement(); }, []); - interface FormType { } + interface FormType {} const { handleSubmit } = useForm({ mode: 'all' }); async function onSubmit() { setError(undefined); - setSuccessfulSubmission(undefined) + setSuccessfulSubmission(undefined); try { if (!zkStatement) { @@ -55,7 +56,7 @@ export function ZkProofSubmission(props: Props) { ); } - const [recentBlockHash, recentBlockHeight] = await getARecentBlockHash(grpcClient); + const { blockHash: recentBlockHash, blockHeight: recentBlockHeight } = await getRecentBlock(grpcClient); const digest = [recentBlockHash, Buffer.from(CONTEXT_STRING)]; const challenge = sha256(digest.flatMap((item) => Array.from(item))); @@ -81,7 +82,7 @@ export function ZkProofSubmission(props: Props) { await submitZkProof(presentation, recentBlockHeight); - setSuccessfulSubmission('Success'); + setSuccessfulSubmission(true); } catch (error) { setError((error as Error).message); } @@ -98,7 +99,13 @@ export function ZkProofSubmission(props: Props) { {error && {error}} - diff --git a/compliant-reward-distribution/frontend/src/constants.ts b/compliant-reward-distribution/frontend/src/constants.ts index e1a902c8..ad982c01 100644 --- a/compliant-reward-distribution/frontend/src/constants.ts +++ b/compliant-reward-distribution/frontend/src/constants.ts @@ -60,8 +60,17 @@ export const walletConnectOpts: SignClientTypes.Options = { // 2. Step: Get the type parameter schema for the above function with the command: // cargo concordium build --schema-json-out ./ export const SCHEMA_TWEET_MESSAGE = 'FAADAAAADgAAAGNvbnRleHRfc3RyaW5nFgIHAAAAbWVzc2FnZRYCCgAAAGJsb2NrX2hhc2gWAg=='; -export const SCHEMA_GET_ACCOUNT_DATA_MESSAGE = 'FAADAAAADgAAAGNvbnRleHRfc3RyaW5nFgIHAAAAbWVzc2FnZQsKAAAAYmxvY2tfaGFzaBYC'; +export const SCHEMA_GET_ACCOUNT_DATA_MESSAGE = + 'FAADAAAADgAAAGNvbnRleHRfc3RyaW5nFgIHAAAAbWVzc2FnZQsKAAAAYmxvY2tfaGFzaBYC'; export const SCHEMA_GET_PENDING_APPROVALS_MESSAGE = 'FAADAAAADgAAAGNvbnRleHRfc3RyaW5nFgIHAAAAbWVzc2FnZRQAAgAAAAUAAABsaW1pdAQGAAAAb2Zmc2V0BAoAAABibG9ja19oYXNoFgI='; -export const SCHEMA_SET_CLAIMED_MESSAGE = 'FAADAAAADgAAAGNvbnRleHRfc3RyaW5nFgIHAAAAbWVzc2FnZRACCwoAAABibG9ja19oYXNoFgI='; +export const SCHEMA_SET_CLAIMED_MESSAGE = + 'FAADAAAADgAAAGNvbnRleHRfc3RyaW5nFgIHAAAAbWVzc2FnZRACCwoAAABibG9ja19oYXNoFgI='; +// Limit and offset for fetching pending approvals from the backend. +export const LIMIT = 40; +export const OFFSET = 0; + +// The number of blocks after the `best block` (top of chain), where the `recent block` is located. +// The `recent block hash` is included in signatures and ZK proofs to ensure they expire. +export const RECENT_BLOCK_DURATION = 10n; diff --git a/compliant-reward-distribution/frontend/src/utils.ts b/compliant-reward-distribution/frontend/src/utils.ts index 2da4ea4e..caabb296 100644 --- a/compliant-reward-distribution/frontend/src/utils.ts +++ b/compliant-reward-distribution/frontend/src/utils.ts @@ -1,144 +1,24 @@ -import { - AccountAddress, - AtomicStatementV2, - ConcordiumGRPCClient, - CredentialStatement, - VerifiablePresentation, -} from '@concordium/web-sdk'; +import { AccountAddress, ConcordiumGRPCClient } from '@concordium/web-sdk'; import { WalletProvider } from './wallet-connection'; +import { RECENT_BLOCK_DURATION } from './constants'; -interface AccountData { - // The account address that was indexed. - accountAddress: AccountAddress.Type; - // The timestamp of the block the event was included in. - blockTime: string; - // The transaction hash that the event was recorded in. - transactionHash: string; - // A boolean specifying if the account has already claimed its rewards (got - // a reward payout). Every account can only claim rewards once. - claimed: boolean; - // A boolean specifying if this account address has submitted all tasks - // and the regulatory conditions have been proven via a ZK proof. - // A manual check of the completed tasks is required now before releasing - // the reward. - pendingApproval: boolean; -} - -interface TweetData { - // The account address that submitted the tweet. - accountAddress: AccountAddress.Type; - // A tweet id submitted by the above account address (task 1). - tweetId: string | undefined; - // A boolean specifying if the text content of the tweet is eligible for - // the reward. The content of the text was verified by this backend - // before this flag is set (or will be verified manually). - tweetValid: boolean; - // A version that specifies the setting of the tweet verification. This - // enables us to update the tweet verification logic in the future and - // invalidate older versions. - tweetVerificationVersion: number; - // The timestamp when the tweet was submitted. - tweetSubmitTime: string; -} - -interface ZkProofData { - // The account address that submitted the zk proof. - accountAddress: AccountAddress.Type; - // A hash of the concatenated revealed `national_id_number` and - // `nationality` to prevent claiming with different accounts for the - // same identity. - uniquenessHash: string; - // A boolean specifying if the identity associated with the account is - // eligible for the reward (task 2). An associated ZK proof was - // verified by this backend before this flag is set. - zkProofValid: boolean; - // A version that specifies the setting of the ZK proof during the - // verification. This enables us to update the ZK proof-verification - // logic in the future and invalidate older proofs. - zkProofVerificationVersion: number; - // The timestamp when the ZK proof verification was submitted. - zkProofVerificationSubmitTime: string; -} - -interface AccountData { - accountData: AccountData | undefined; - tweetData: TweetData | undefined; - zkProofData: ZkProofData | undefined; -} - -/** - * Fetch pending approvals from the backend - */ -export async function setClaimed(signer: string, signature: string, recentBlockHeight: bigint, accountAddress: string) { - const response = await fetch(`api/setClaimed`, { - method: 'POST', - headers: new Headers({ 'content-type': 'application/json' }), - body: JSON.stringify({ - signingData: { - signer, - message: { - accountAddresses: [accountAddress], - }, - signature, - blockHeight: Number(recentBlockHeight), - }, - }), - }); - - if (!response.ok) { - const error = (await response.json()) as Error; - throw new Error( - `Unable to set claimed in the database; StatusCode:${response.status}; StatusText:${response.statusText}; Error: ${JSON.stringify(error)}`, - ); - } -} - -/** - * Fetch pending approvals from the backend - */ -export async function getPendingApprovals( - signer: string, - signature: string, - recentBlockHeight: bigint, - limit: number, - offset: number, -): Promise { - const response = await fetch(`api/getPendingApprovals`, { - method: 'POST', - headers: new Headers({ 'content-type': 'application/json' }), - body: JSON.stringify({ - signingData: { - signer, - message: { - limit, - offset, - }, - signature, - blockHeight: Number(recentBlockHeight), - }, - }), - }); - - if (!response.ok) { - const error = (await response.json()) as Error; - throw new Error( - `Unable to get pending approvals from the backend; StatusCode:${response.status}; StatusText:${response.statusText}; Error: ${JSON.stringify(error)}`, - ); - } - - const body = (await response.json()) as AccountData[]; - - if (!body) { - throw new Error(`Unable to get pending approvals from the backend`); - } - return body; +interface RecentBlock { + blockHeight: bigint; + blockHash: Uint8Array; } /** - * Fetch account data from the backend + * Gets the recent block hash and height by querying the connected node. + * The recent block is the block that is `RECENT_BLOCK_DURATION` blocks + * behind the best block (top of chain). + * + * @param grpcClient - Connection to a blockchain node. + * @returns The recent block hash and height. + * + * @throws An error if the node responses with one. */ -export async function getARecentBlockHash(grpcClient: ConcordiumGRPCClient | undefined): Promise<[Uint8Array, bigint]> { +export async function getRecentBlock(grpcClient: ConcordiumGRPCClient | undefined): Promise { if (!grpcClient) { throw Error(`'grpcClient' is undefined`); } @@ -149,11 +29,10 @@ export async function getARecentBlockHash(grpcClient: ConcordiumGRPCClient | und throw Error(`Couldn't get 'bestBlockHeight' from chain`); } - const recentBlockHeight = bestBlockHeight.value - 10n; + const recentBlockHeight = bestBlockHeight.value - RECENT_BLOCK_DURATION; const recentBlockHash = ( await grpcClient.client.getBlocksAtHeight({ - // TODO: Type in web-sdk needs to be fixed to do this ergonomically. blocksAtHeight: { oneofKind: 'absolute', absolute: { @@ -166,50 +45,20 @@ export async function getARecentBlockHash(grpcClient: ConcordiumGRPCClient | und if (!recentBlockHash) { throw Error(`Couldn't get 'recentBlockHash' from chain`); } - return [recentBlockHash, recentBlockHeight]; -} - -/** - * Fetch account data from the backend - */ -export async function getAccountData( - signer: string, - accountAddress: string, - signature: string, - recentBlockHeight: bigint, -): Promise { - const response = await fetch(`api/getAccountData`, { - method: 'POST', - headers: new Headers({ 'content-type': 'application/json' }), - body: JSON.stringify({ - signingData: { - signer, - message: { - accountAddress, - }, - signature, - blockHeight: Number(recentBlockHeight), - }, - }), - }); - - if (!response.ok) { - const error = (await response.json()) as Error; - throw new Error( - `Unable to get account data the backend; StatusCode:${response.status}; StatusText:${response.statusText}; Error: ${JSON.stringify(error)}`, - ); - } - - const body = (await response.json()) as AccountData; - - if (!body) { - throw new Error(`Unable to get account data from the backend`); - } - return body; + return { blockHash: recentBlockHash, blockHeight: recentBlockHeight }; } /** - * Request signature from wallet. + * Request a signature from the connected wallet. + * + * @param recentBlockHash - A recent block hash. + * @param schema - The schema to display the message in the wallet. + * @param message - The message to sign in the wallet. + * @param signer - The signer account (is only respected when the browser wallet is connected). + * @param provider - The wallet provider. + * @returns The signature. + * + * @throws An error if the wallet responses with one, it the message can not be serialized/deserialized with the given schema, if the `provider` is undefined, or if a multi-sig account is used as signer.s */ export async function requestSignature( recentBlockHash: Uint8Array, @@ -230,94 +79,7 @@ export async function requestSignature( } /** - * Fetch the statement to prove from the backend - */ -export async function getStatement(): Promise { - const response = await fetch(`api/getZKProofStatements`, { method: 'GET' }); - - if (!response.ok) { - const error = (await response.json()) as Error; - throw new Error(`Unable to get the ZK statement from the backend: ${JSON.stringify(error)}`); - } - - const body = (await response.json()).data as AtomicStatementV2[]; - - if (!body) { - throw new Error(`Unable to get the ZK statement from the backend`); - } - - const credentialStatement: CredentialStatement = { - idQualifier: { - type: 'cred', - // We allow all identity providers on mainnet and on testnet. - // This list is longer than necessary to include all current/future - // identity providers on mainnet and testnet. - // This list should be updated to only include the identity providers that you trust. - issuers: [0, 1, 2, 3, 4, 5, 6, 7], - }, - statement: body, - }; - - return credentialStatement; -} - -/** - * Submit Tweet to the backend - */ -export async function submitTweet(signer: string, signature: string, recentBlockHeight: bigint, tweet: string) { - const response = await fetch(`api/postTweet`, { - method: 'POST', - headers: new Headers({ 'content-type': 'application/json' }), - body: JSON.stringify({ - signingData: { - signer, - message: { - tweet, - }, - signature, - blockHeight: Number(recentBlockHeight), - }, - }), - }); - - if (!response.ok) { - let finalError; - - try { - finalError = await response.json(); - } catch (e) { - throw new Error( - `Unable to submit Tweet to the backend; StatusCode: ${response.status}; StatusText: ${response.statusText};`, - ); - } - - throw new Error(`Unable to submit Tweet to the backend; Error: ${JSON.stringify(finalError)}`); - } -} - -/** - * Submit ZK proof to the backend - */ -export async function submitZkProof(presentation: VerifiablePresentation, recentBlockHeight: bigint) { - const response = await fetch(`api/postZKProof`, { - method: 'POST', - headers: new Headers({ 'content-type': 'application/json' }), - body: JSON.stringify({ - blockHeight: Number(recentBlockHeight), - presentation: presentation, - }), - }); - - if (!response.ok) { - const error = (await response.json()) as Error; - throw new Error( - `Unable to submit ZK proof to the backend; StatusCode:${response.status}; StatusText:${response.statusText}; Error: ${JSON.stringify(error)}`, - ); - } -} - -/** - * This function validates if a string represents a valid accountAddress in base58 encoding. + * Validates if a string represents a valid accountAddress in base58 encoding. * The length, characters, and the checksum are validated. * * @param accountAddress - An account address represented as a base58 encoded string.