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

TW-1614: Temple Tap Airdrop confirmation #1244

Merged
merged 12 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
85 changes: 71 additions & 14 deletions src/app/pages/TempleTapAirdrop/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { FC, memo, PropsWithChildren, useCallback, useMemo, useState } from 'react';

import { isAxiosError } from 'axios';
import { OnSubmit, useForm } from 'react-hook-form';

import { Alert, Anchor, FormField, FormSubmitButton } from 'app/atoms';
import { ReactComponent as TelegramSvg } from 'app/icons/social-tg.svg';
import { ReactComponent as XSocialSvg } from 'app/icons/social-x.svg';
import PageLayout from 'app/layouts/PageLayout';
import { sendTempleTapAirdropUsernameConfirmation } from 'lib/apis/temple-tap';
import { makeSigAuthMessageBytes, SigAuthValues } from 'lib/apis/temple/sig-auth';
import { checkTempleTapAirdropConfirmation, sendTempleTapAirdropUsernameConfirmation } from 'lib/apis/temple-tap';
import { t } from 'lib/i18n';
import { useAccount } from 'lib/temple/front';
import { useTypedSWR } from 'lib/swr';
import { useAccount, useTempleClient, useTezos } from 'lib/temple/front';
import { TempleAccountType } from 'lib/temple/types';
import { useLocalStorage } from 'lib/ui/local-storage';

import BannerImgSrc from './banner.png';
import { ReactComponent as ConfirmedSvg } from './confirmed.svg';
Expand All @@ -21,37 +23,90 @@ interface FormData {

export const TempleTapAirdropPage = memo(() => {
const account = useAccount();
const accountPkh = account.publicKeyHash;

const tezos = useTezos();
const { silentSign } = useTempleClient();

const canSign = useMemo(
() => [TempleAccountType.HD, TempleAccountType.Imported, TempleAccountType.Ledger].includes(account.type),
[account.type]
);

const [storedRecord, setStoredRecord] = useLocalStorage<LocalStorageRecord | null>(
'TEMPLE_TAP_AIRDROP_PKH_CONFIRMATIONS',
null
);

const [confirmSent, setConfirmSent] = useState(false);
const [confirmed, setConfirmed] = useState(storedRecord?.[accountPkh] ?? false);

const prepSigAuthValues = useCallback(async () => {
const [publicKey, messageBytes] = await Promise.all([
tezos.signer.publicKey(),
makeSigAuthMessageBytes(accountPkh)
]);

const { prefixSig: signature } = await silentSign(accountPkh, messageBytes);
keshan3262 marked this conversation as resolved.
Show resolved Hide resolved

const values: SigAuthValues = { publicKey, messageBytes, signature };
alex-tsx marked this conversation as resolved.
Show resolved Hide resolved

return values;
}, [silentSign, tezos.signer, accountPkh]);

useTypedSWR(
[accountPkh],
async () => {
if (confirmed || !canSign) return;

const sigAuthValues = await prepSigAuthValues();

const confirmedRes = await checkTempleTapAirdropConfirmation(accountPkh, sigAuthValues);

if (!confirmedRes) return false;

setConfirmed(true);

return true;
},
{
suspense: true,
revalidateOnFocus: false,
refreshInterval: 60_000
}
);

const { register, handleSubmit, errors, setError, clearError, formState, reset } = useForm<FormData>();

const submitting = formState.isSubmitting;
const [confirmSent, setConfirmSent] = useState(false);
const [confirmed, setConfirmed] = useState(false);

const onSubmit = useCallback<OnSubmit<FormData>>(
async ({ username }) => {
clearError();

try {
await sendTempleTapAirdropUsernameConfirmation(account.publicKeyHash, username);
const sigAuthValues = await prepSigAuthValues();

const res = await sendTempleTapAirdropUsernameConfirmation(accountPkh, username, sigAuthValues);

switch (res.data.status) {
case 'ACCEPTED':
setConfirmSent(true);
break;
case 'CONFIRMED':
setConfirmed(true);
setStoredRecord(state => ({ ...state, [accountPkh]: true }));
break;
}

setConfirmSent(true);
reset();
} catch (error: any) {
if (isAxiosError(error) && error.response?.data.code === 'ACCOUNT_CONFIRMED') {
setConfirmed(true);
return;
}
console.error(error);

setError('username', 'submit-error', error?.response.data?.message || 'Something went wrong...');
setError('username', 'submit-error', error?.response?.data?.message || 'Something went wrong...');
}
},
[reset, clearError, setError, account.publicKeyHash]
[reset, clearError, setError, setStoredRecord, prepSigAuthValues, accountPkh]
);

return (
Expand Down Expand Up @@ -95,7 +150,7 @@ export const TempleTapAirdropPage = memo(() => {
>
<div className="text-xs leading-5 text-dark-gray">
<span>Your address: </span>
<span>{account.publicKeyHash}</span>
<span>{accountPkh}</span>
</div>

<form onSubmit={handleSubmit(onSubmit)} className="contents">
Expand Down Expand Up @@ -140,6 +195,8 @@ export const TempleTapAirdropPage = memo(() => {
);
});

type LocalStorageRecord = StringRecord<true>;

interface BlockCompProps {
title: string;
description: string;
Expand Down
35 changes: 30 additions & 5 deletions src/lib/apis/temple-tap.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
import { templeWalletApi } from './temple/endpoints/templewallet.api';
import { buildSigAuthHeaders, SigAuthValues } from './temple/sig-auth';

export function sendTempleTapAirdropUsernameConfirmation(accountPkh: string, username: string) {
return templeWalletApi.post('/temple-tap/confirm-airdrop-username', {
accountPkh,
username
});
export function sendTempleTapAirdropUsernameConfirmation(
accountPkh: string,
username: string,
sigAuthValues: SigAuthValues
) {
return templeWalletApi.post<{ status: string }>(
'/temple-tap/confirm-airdrop-username',
{
accountPkh,
username
},
{
headers: buildSigAuthHeaders(sigAuthValues)
}
);
}

export function checkTempleTapAirdropConfirmation(accountPkh: string, sigAuthValues: SigAuthValues) {
return templeWalletApi
.post<boolean>(
'/temple-tap/check-airdrop-confirmation',
{
accountPkh
},
{
headers: buildSigAuthHeaders(sigAuthValues)
}
)
.then(({ data }) => data);
}
59 changes: 59 additions & 0 deletions src/lib/apis/temple/sig-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { templeWalletApi } from './endpoints/templewallet.api';

interface SigningNonce {
value: string;
/** ISO string time */
expiresAt: string;
}

async function fetchTempleSigningNonce(pkh: string) {
const { data } = await templeWalletApi.get<SigningNonce>('signing-nonce', { params: { pkh } });

return data;
}

export interface SigAuthValues {
publicKey: string;
messageBytes: string;
signature: string;
}

export function buildSigAuthHeaders({ publicKey, messageBytes, signature }: SigAuthValues) {
return {
'tw-sig-auth-tez-pk': publicKey,
'tw-sig-auth-tez-msg': messageBytes,
'tw-sig-auth-tez-sig': signature
};
}

export async function makeSigAuthMessageBytes(accountPkh: string) {
const nonce = await fetchTempleSigningNonce(accountPkh);

const message = `Tezos Signed Message: Confirming my identity as ${accountPkh}.\n\nNonce: ${nonce.value}`;

const messageBytes = stringToSigningPayload(message);

return messageBytes;
}

/**
* See: https://tezostaquito.io/docs/signing/#generating-a-signature-with-beacon-sdk
*
* Same payload goes without Beacon.
*/
function stringToSigningPayload(value: string) {
const bytes = stringToHex(value);

const bytesLength = (bytes.length / 2).toString(16);
const addPadding = `00000000${bytesLength}`;
const paddedBytesLength = addPadding.slice(addPadding.length - 8);

return '0501' + paddedBytesLength + bytes;
keshan3262 marked this conversation as resolved.
Show resolved Hide resolved
}

function stringToHex(value: string) {
const buffer = new TextEncoder().encode(value);
const hexArray = Array.from(buffer, byte => byte.toString(16).padStart(2, '0'));

return hexArray.reduce((acc, curr) => acc + curr, '');
}
4 changes: 4 additions & 0 deletions src/lib/temple/back/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,10 @@ const safeAddLocalOperation = async (networkRpc: string, op: any) => {
return undefined;
};

export function silentSign(sourcePkh: string, bytes: string) {
return withUnlocked(({ vault }) => vault.sign(sourcePkh, bytes));
}

export function sign(port: Runtime.Port, id: string, sourcePkh: string, bytes: string, watermark?: string) {
return withUnlocked(
() =>
Expand Down
11 changes: 10 additions & 1 deletion src/lib/temple/back/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,21 @@ const processRequest = async (req: TempleRequest, port: Runtime.Port): Promise<T
opHash
};

case TempleMessageType.SignRequest:
case TempleMessageType.SignRequest: {
const result = await Actions.sign(port, req.id, req.sourcePkh, req.bytes, req.watermark);
return {
type: TempleMessageType.SignResponse,
result
};
}

case TempleMessageType.SilentSignRequest: {
const result = await Actions.silentSign(req.sourcePkh, req.bytes);
return {
type: TempleMessageType.SilentSignResponse,
result
};
}

case TempleMessageType.DAppGetAllSessionsRequest:
const allSessions = await Actions.getAllDAppSessions();
Expand Down
15 changes: 14 additions & 1 deletion src/lib/temple/front/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,18 @@ export const [TempleClientProvider, useTempleClient] = constate(() => {
[]
);

/** (!) Use with caution - for authentication purposes only. */
const silentSign = useCallback(async (sourcePkh: string, bytes: string) => {
const res = await request({
type: TempleMessageType.SilentSignRequest,
sourcePkh,
bytes
});
assertResponse(res.type === TempleMessageType.SilentSignResponse);

return res.result;
}, []);

const getAllDAppSessions = useCallback(async () => {
const res = await request({
type: TempleMessageType.DAppGetAllSessionsRequest
Expand Down Expand Up @@ -393,7 +405,8 @@ export const [TempleClientProvider, useTempleClient] = constate(() => {
createTaquitoWallet,
createTaquitoSigner,
getAllDAppSessions,
removeDAppSession
removeDAppSession,
silentSign
};
});

Expand Down
15 changes: 15 additions & 0 deletions src/lib/temple/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ export enum TempleMessageType {
OperationsResponse = 'TEMPLE_OPERATIONS_RESPONSE',
SignRequest = 'TEMPLE_SIGN_REQUEST',
SignResponse = 'TEMPLE_SIGN_RESPONSE',
SilentSignRequest = 'TEMPLE_SILENT_SIGN_REQUEST',
SilentSignResponse = 'TEMPLE_SILENT_SIGN_RESPONSE',
ConfirmationRequest = 'TEMPLE_CONFIRMATION_REQUEST',
ConfirmationResponse = 'TEMPLE_CONFIRMATION_RESPONSE',
PageRequest = 'TEMPLE_PAGE_REQUEST',
Expand Down Expand Up @@ -317,6 +319,7 @@ export type TempleRequest =
| TempleCreateLedgerAccountRequest
| TempleOperationsRequest
| TempleSignRequest
| TempleSilentSignRequest
| TempleConfirmationRequest
| TempleRemoveAccountRequest
| TemplePageRequest
Expand Down Expand Up @@ -350,6 +353,7 @@ export type TempleResponse =
| TempleCreateLedgerAccountResponse
| TempleOperationsResponse
| TempleSignResponse
| TempleSilentSignResponse
| TempleConfirmationResponse
| TempleRemoveAccountResponse
| TemplePageResponse
Expand Down Expand Up @@ -595,11 +599,22 @@ interface TempleSignRequest extends TempleMessageBase {
watermark?: string;
}

interface TempleSilentSignRequest extends TempleMessageBase {
type: TempleMessageType.SilentSignRequest;
sourcePkh: string;
bytes: string;
}

interface TempleSignResponse extends TempleMessageBase {
type: TempleMessageType.SignResponse;
result: any;
}

interface TempleSilentSignResponse extends TempleMessageBase {
type: TempleMessageType.SilentSignResponse;
result: any;
}

interface TempleConfirmationRequest extends TempleMessageBase {
type: TempleMessageType.ConfirmationRequest;
id: string;
Expand Down
Loading