Skip to content

Commit

Permalink
tranfer gift flow
Browse files Browse the repository at this point in the history
  • Loading branch information
sokolova-an committed Dec 22, 2023
1 parent 6da1a3a commit 48ab741
Show file tree
Hide file tree
Showing 19 changed files with 162 additions and 51 deletions.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file removed public/videos/create-wallet.webm
Binary file not shown.
Binary file removed public/videos/firework1.webm
Binary file not shown.
6 changes: 3 additions & 3 deletions src/common/providers/contextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { getWallet } from '../wallet';
export interface IContext {
assets: AssetAccount[] | [];
publicKey?: HexString;
selectedAsset?: TrasferAsset | null;
selectedAsset?: Partial<TrasferAsset | null>;
setPublicKey: React.Dispatch<React.SetStateAction<HexString | undefined>>;
setAssets: React.Dispatch<React.SetStateAction<AssetAccount[]>>;
setSelectedAsset: React.Dispatch<React.SetStateAction<TrasferAsset | null>>;
setSelectedAsset: React.Dispatch<React.SetStateAction<Partial<TrasferAsset | null>>>;
}

export const GlobalContext = createContext<IContext>({
Expand All @@ -23,7 +23,7 @@ export const GlobalContext = createContext<IContext>({
export const GlobalStateProvider = ({ children }: { children: React.ReactNode }) => {
const [publicKey, setPublicKey] = useState<HexString>();
const [assets, setAssets] = useState<AssetAccount[]>([]);
const [selectedAsset, setSelectedAsset] = useState<AssetAccount | null>(null);
const [selectedAsset, setSelectedAsset] = useState<Partial<TrasferAsset | null>>(null);

useEffect(() => {
if (!publicKey) {
Expand Down
1 change: 1 addition & 0 deletions src/common/routing/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const Paths = {
TRANSFER_AMOUNT: '/transfer/amount',
TRANSFER_CONFIRMATION: '/transfer/confirmation',
TRANSFER_RESULT: '/transfer/result',
TRANSFER_CREATE_GIFT: '/transfer/create-gift',
} as const;

export type PathValue = (typeof Paths)[keyof typeof Paths];
2 changes: 2 additions & 0 deletions src/common/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type ChainAssetAccount = {
symbol: string;
name: string;
precision: number;
addressPrefix: number;
};

export type AssetAccount = ChainAssetAccount & {
Expand All @@ -37,6 +38,7 @@ export type TrasferAsset = AssetAccount & {
amount?: string;
fee?: number;
transferAll?: boolean;
isGift?: boolean;
};
export type StateResolution<T> = { resolve: (value: T) => void; reject: () => void };

Expand Down
17 changes: 11 additions & 6 deletions src/common/utils/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,9 @@ export const formatAmount = (amount: string, precision: number): string => {
export async function handleSend(
submitExtrinsic: SubmitExtrinsic,
{ destinationAddress, chainId, amount, transferAll, precision }: TrasferAsset,
giftTransfer?: string,
) {
const decodedAddress = decodeAddress(destinationAddress);
const decodedAddress = decodeAddress(destinationAddress || giftTransfer);

return await submitExtrinsic(chainId, (builder) => {
const transferFunction = transferAll
Expand All @@ -180,13 +181,17 @@ export async function handleSend(
});
}

const FAKE_AMMOUNT = '1';

export async function handleFee(estimateFee: EstimateFee, chainId: ChainId, precision: number): Promise<number> {
export async function handleFee(
estimateFee: EstimateFee,
chainId: ChainId,
precision: number,
isGift?: boolean,
): Promise<number> {
return await estimateFee(chainId, (builder: ExtrinsicBuilder) =>
builder.addCall(builder.api.tx.balances.transferKeepAlive(decodeAddress(FAKE_ACCOUNT_ID), FAKE_AMMOUNT)),
builder.addCall(builder.api.tx.balances.transferAll(decodeAddress(FAKE_ACCOUNT_ID), false)),
).then((fee: Balance) => {
const { formattedValue } = formatBalance(fee.toString(), precision);
const finalFee = isGift ? Number(fee) * 2 : fee;
const { formattedValue } = formatBalance(finalFee.toString(), precision);

return Number(formattedValue);
});
Expand Down
29 changes: 18 additions & 11 deletions src/common/wallet/store.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Wallet } from './types';
import { HexString, unwrapHexString } from '@common/types';

const { mnemonicGenerate, mnemonicToMiniSecret, sr25519PairFromSeed } = require('@polkadot/util-crypto');

const { u8aToHex } = require('@polkadot/util');
import { u8aToHex } from '@polkadot/util';
import { encodeAddress, mnemonicGenerate, mnemonicToMiniSecret, sr25519PairFromSeed } from '@polkadot/util-crypto';
import { Keyring } from '@polkadot/api';
import { KeyringPair } from '@polkadot/keyring/types';
import secureLocalStorage from 'react-secure-storage';

const AES = require('crypto-js/aes');
const CryptoJS = require('crypto-js');

import secureLocalStorage from 'react-secure-storage';

import { Keyring } from '@polkadot/api';
import { KeyringPair } from '@polkadot/keyring/types';
import { MNEMONIC_STORE, PUBLIC_KEY_STORE } from '../utils/constants';
import { HexString, unwrapHexString } from '@common/types';
import { GiftWallet, Wallet } from './types';

const keyring = new Keyring({ type: 'sr25519' });

Expand All @@ -35,7 +32,7 @@ export const createWallet = (mnemonic: string): Wallet => {
localStorage.setItem(PUBLIC_KEY_STORE, publicKey);
secureLocalStorage.setItem(MNEMONIC_STORE, mnemonic);

return { publicKey: publicKey };
return { publicKey };
};

export const backupMnemonic = (mnemonic: string, password: string): void => {
Expand Down Expand Up @@ -79,3 +76,13 @@ export const initializeWalletFromCloud = (password: string, encryptedMnemonic: s

return createWallet(mnemonic);
};

export const createGiftWallet = (addressPrefix: number): GiftWallet => {
const mnemonic = mnemonicGenerate();
const seed = mnemonicToMiniSecret(mnemonic);
const keypair = sr25519PairFromSeed(seed);
const publicKey: HexString = u8aToHex(keypair.publicKey);
const address = encodeAddress(publicKey, addressPrefix);

return { address, mnemonic };
};
7 changes: 6 additions & 1 deletion src/common/wallet/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { HexString } from '@common/types';
import { Address, HexString } from '@common/types';

export type Wallet = {
publicKey: HexString;
};

export type GiftWallet = {
address: Address;
mnemonic: string | null;
};
9 changes: 3 additions & 6 deletions src/pages/onboarding/create-wallet/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import ConfettiExplosion from 'react-confetti-explosion';
import { Player } from '@lottiefiles/react-lottie-player';

import { useTelegram } from '@/common/providers/telegramProvider';
import { useGlobalContext } from '@/common/providers/contextProvider';
Expand Down Expand Up @@ -55,18 +56,14 @@ export default function CreateWalletPage() {

return isLoading ? (
<div className="min-h-screen flex flex-col justify-center items-center">
<video autoPlay muted playsInline width={350} preload="auto">
<source src={'/videos/create-wallet.webm'} type="video/webm" />
</video>
<Player src="/gifs/create-wallet.json" loop autoplay className="player" />
<BodyText className="text-icon-on-neutral">Creating your wallet...</BodyText>
</div>
) : (
<div className="min-h-screen flex flex-col justify-center items-center p-5">
<div className="bg-blue-500 rounded-full p-3 w-[114px] h-[114px]">
<ConfettiExplosion particleCount={250} />
<video autoPlay muted playsInline width={90} preload="auto">
<source src={'/videos/firework1.webm'} type="video/webm" />
</video>
{/* <Player src="firework" className="player" /> */}
</div>
<TitleText className="m-3">Your wallet has been created!</TitleText>
<BodyText className="text-text-hint">
Expand Down
8 changes: 7 additions & 1 deletion src/pages/onboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { useTelegram } from '@common/providers/telegramProvider';
import { OnboardingStartPage, RestoreWalletPage } from '@/screens/onboarding';
import { MNEMONIC_STORE } from '@/common/utils/constants';
import { CircularProgress } from '@nextui-org/react';

export default function OnboardingPage() {
const { webApp } = useTelegram();
Expand All @@ -21,7 +22,12 @@ export default function OnboardingPage() {
}, [webApp]);

// TODO: replace with loader
if (isLoading) return <div>LOADING</div>;
if (isLoading)
return (
<div className="min-h-screen flex items-center justify-center">
<CircularProgress size="lg" />
</div>
);

return (
<div className="min-h-screen w-full flex flex-col items-center text-center p-4">
Expand Down
49 changes: 32 additions & 17 deletions src/pages/transfer/amount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { HeadlineText, Icon, Identicon, CaptionText, LargeTitleText, TextBase, L
import { IconNames } from '@/components/Icon/types';
import { useExtrinsicProvider } from '@/common/extrinsicService/ExtrinsicProvider';
import { formatBalance, handleFee } from '@/common/utils/balance';
import { ChainId } from '@/common/types';

export default function AmountPage() {
const router = useRouter();
Expand All @@ -21,31 +22,41 @@ export default function AmountPage() {

const [amount, setAmount] = useState('0');
const [transferAll, setTransferAll] = useState(false);
const [maxAmountToSend, setMaxAmountToSend] = useState<number>();
const [maxAmountToSend, setMaxAmountToSend] = useState<string>();
const [isAmountValid, setIsAmountValid] = useState(true);

useEffect(() => {
router.prefetch(Paths.TRANSFER_CONFIRMATION);
MainButton?.setText('Continue');
MainButton?.setText(selectedAsset?.isGift ? 'Create Gift' : 'Continue');
BackButton?.show();
MainButton?.show();
MainButton?.disable();

const callback = () => {
router.push(Paths.TRANSFER_ADDRESS);
router.push(selectedAsset?.isGift ? Paths.TRANSFER_SELECT_TOKEN : Paths.TRANSFER_ADDRESS);
};
BackButton?.onClick(callback);

if (!selectedAsset) return;

(async () => {
const fee = selectedAsset.fee || (await handleFee(estimateFee, selectedAsset.chainId, selectedAsset.precision));
const fee =
selectedAsset.fee ||
(await handleFee(
estimateFee,
selectedAsset.chainId as ChainId,
selectedAsset.precision as number,
selectedAsset.isGift,
));
const formattedBalance = Number(
formatBalance(selectedAsset.transferableBalance, selectedAsset.precision).formattedValue,
);

const max = Number(Math.max(formattedBalance - fee, 0).toFixed(5));
const max = Math.max(formattedBalance - fee, 0).toFixed(5);

setMaxAmountToSend(max);
setIsAmountValid(+amount <= +max);

setSelectedAsset((prev) => ({ ...prev!, fee }));
})();

Expand All @@ -57,7 +68,7 @@ export default function AmountPage() {
useEffect(() => {
const callback = () => {
setSelectedAsset((prev) => ({ ...prev!, transferAll, amount }));
router.push(Paths.TRANSFER_CONFIRMATION);
router.push(selectedAsset?.isGift ? Paths.TRANSFER_CREATE_GIFT : Paths.TRANSFER_CONFIRMATION);
};

if (!isAmountValid || !Number(amount)) return;
Expand All @@ -78,24 +89,28 @@ export default function AmountPage() {

const handleChange = (value: string) => {
setTransferAll(false);
setIsAmountValid(!!Number(value) && Number(value) <= (maxAmountToSend || 0));
setIsAmountValid(!!Number(value) && +value <= +(maxAmountToSend || 0));
setAmount(value);
};

return (
<>
<div className="grid grid-cols-[40px,1fr,auto] items-center">
<Identicon address={selectedAsset?.destinationAddress} />
<HeadlineText className="flex gap-1">
Send to
<span className="max-w-[120px]">
<MiddleEllipsis>
<TextBase as="span" className="text-body-bold">
{selectedAsset?.destinationAddress}
</TextBase>
</MiddleEllipsis>
</span>
</HeadlineText>
{selectedAsset?.isGift ? (
<HeadlineText>Preparing your gift</HeadlineText>
) : (
<HeadlineText className="flex gap-1">
Send to
<span className="max-w-[120px]">
<MiddleEllipsis>
<TextBase as="span" className="text-body-bold">
{selectedAsset?.destinationAddress}
</TextBase>
</MiddleEllipsis>
</span>
</HeadlineText>
)}
<Button variant="light" size="md" className="p-0" onClick={handleMaxSend}>
<CaptionText className="text-text-link">
Max: {maxAmountToSend || <CircularProgress size="sm" className="inline-block" />} {selectedAsset?.symbol}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/transfer/confirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { IconNames } from '@/components/Icon/types';
import { useExtrinsicProvider } from '@/common/extrinsicService/ExtrinsicProvider';
import { handleSend } from '@/common/utils/balance';
import { TrasferAsset } from '@/common/types';

export default function ConfirmationPage() {
const router = useRouter();
Expand All @@ -35,7 +36,7 @@ export default function ConfirmationPage() {

const mainCallback = async () => {
MainButton?.showProgress(false);
await handleSend(submitExtrinsic, selectedAsset).then(() => {
await handleSend(submitExtrinsic, selectedAsset as TrasferAsset).then(() => {
router.push(Paths.TRANSFER_RESULT);
});
};
Expand Down
59 changes: 59 additions & 0 deletions src/pages/transfer/create-gift.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client';
import { ReactElement, useEffect } from 'react';
import { useRouter } from 'next/router';
import { CircularProgress } from '@nextui-org/react';

import { useTelegram } from '@common/providers/telegramProvider';
import { useGlobalContext } from '@/common/providers/contextProvider';
import { Paths } from '@/common/routing';
import { HeadlineText, Layout } from '@/components';
import { useExtrinsicProvider } from '@/common/extrinsicService/ExtrinsicProvider';
import { handleSend } from '@/common/utils/balance';
import { TrasferAsset } from '@/common/types';
import { createGiftWallet } from '@/common/wallet';

export default function CreateGiftPage() {
const router = useRouter();
const { submitExtrinsic } = useExtrinsicProvider();
const { BackButton, MainButton } = useTelegram();
const { selectedAsset, setSelectedAsset } = useGlobalContext();

useEffect(() => {
BackButton?.hide();
MainButton?.hide();
MainButton?.setText('Gift created');

if (!selectedAsset) return;
const wallet = createGiftWallet(selectedAsset.addressPrefix as number);
(async function () {
await handleSend(submitExtrinsic, selectedAsset as TrasferAsset, wallet.address).then(() => {
//TODO: create tg link
MainButton?.show();
});
})();

const mainCallback = async () => {
router.push(Paths.TRANSFER_RESULT);
};
MainButton?.onClick(mainCallback);

return () => {
setSelectedAsset(null);
MainButton?.hide();
MainButton?.offClick(mainCallback);
};
}, []);

return (
<>
<CircularProgress size="lg" className="m-auto my-10" />
<HeadlineText className="text-text-hint" align="center">
Creating your gift..
</HeadlineText>
</>
);
}

CreateGiftPage.getLayout = function getLayout(page: ReactElement) {
return <Layout>{page}</Layout>;
};
8 changes: 7 additions & 1 deletion src/pages/transfer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import Link from 'next/link';
import { useTelegram } from '@common/providers/telegramProvider';
import { Paths } from '@/common/routing';
import { Icon, Plate, TitleText, BodyText, HelpText } from '@/components';
import { useGlobalContext } from '@/common/providers/contextProvider';

export default function TransferPage() {
const router = useRouter();
const { BackButton } = useTelegram();
const { setSelectedAsset } = useGlobalContext();

useEffect(() => {
const callback = () => {
Expand All @@ -28,7 +30,11 @@ export default function TransferPage() {
<div className="min-h-screen p-4">
<TitleText className="mt-10 mb-6">How to send tokens</TitleText>
<Plate className="mb-2 rounded-lg">
<Link href={Paths.TRANSFER_SELECT_TOKEN} className="w-full grid grid-cols-[auto,1fr,auto] items-center gap-4">
<Link
href={Paths.TRANSFER_SELECT_TOKEN}
className="w-full grid grid-cols-[auto,1fr,auto] items-center gap-4"
onClick={() => setSelectedAsset({ isGift: true })}
>
<Icon name="user" className="w-10 h-10" />
<div className="grid">
<BodyText align="left">Send Gift</BodyText>
Expand Down
Loading

0 comments on commit 48ab741

Please sign in to comment.