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

Feature/atomic-swap-and-asset-optin #3

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
34 changes: 34 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['@typescript-eslint/eslint-plugin', 'react', 'prettier'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'plugin:react/recommended',
'prettier',
],
root: true,
env: {
node: true,
browser: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
},
};
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
36 changes: 36 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
13 changes: 13 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"arrowParens": "always",
"bracketSpacing": true,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"semi": true,
"printWidth": 100,
"useTabs": false,
"endOfLine": "lf"
}
11 changes: 11 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"compile-hero.disable-compile-files-on-did-save-code": true,
"[xml]": {
"editor.defaultFormatter": "ms-vsliveshare.vsliveshare"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
66 changes: 66 additions & 0 deletions information.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Pera Challenge Submission

This is a Next JS application with the following functionality:

- Wallet connection using Pera Wallet.
- Mainnet and Testnet toggle for switching between the algorand mainnet and testnet.
- A list displaying all verified assets on either the testnet or mainnet depending on which network is being used. Infinite scroll is also implemented for retrieving more assets as user scrolls.
- Asset Opt-in transaction for opting into Assets the currently connected wallet has not opted into yet.
- Donation functionality for donating 1 Algo to a provided wallet address.
- Atomic swap feature for swapping one asset for another on the testnet or mainnet.

## How to run the application

In order to run the application, you need to first install dependencies using the following command

`yarn install` or `npm install`

After installing dependencies successfully, you can run the following command to start the application:

`yarn dev` or `npm run dev`

## Code Deep-dive

### Global State Management

For global state management, [Recoil](https://recoiljs.org/) was used. Four (4) global states were created and used in this application, namely:

1. **Address**: This state holds the address of the currently active address connected to the application.

2. **Accounts**: This state holds a string array of all the accounts connected to the application.

3. **Network**: This state holds the value of the currently active network of the application. It has two possible values: 'Testnet' and 'Mainnet'.

4. **Refresh wallet**: This state represents an integer value that triggers a re-fetch of the wallet balance of the active address when its value changes. Its value gets updated after every successful transaction to ensure real-time update of the user's wallet balance.

To see the global states, refer to the `/src/state/wallet.atom.ts` file.

### Hooks

Three important hooks were written for performing important functionality in the application namely:

1. **useAlgoClientConfig**: This hook uses the currently selected network (testnet or mainnet) to create an algod client for creating and submitting transactions.

2. **useClient**: This hook creates an object that contains methods for easily making and managing API calls.

3. **usePeraWallet**: This hook manages the functionality for connecting, disconnecting and reconnecting a user's wallet to the application using PeraWallet Connect. It also provides the address of the currently connected account. Multiple accounts can be connected to the application at once but only one is active.

All three hooks can be found in the `/src/hooks` folder.

### Styling

For styling purposes, a combination of [Sass](https://sass-lang.com/) and [Tailwind CSS](https://tailwindcss.com/) were used.

### Key Components

In this application, a good number of components were created for different purposes, but the key components in order of their hierarchy in the DOM tree include

1. **Home Component**: This component houses the entire home page. All other components are rendered within it. It can be found in the `/src/features/home/index.ts` file.

2. **Navbar Component**: This component represents the top navigation bar of the application. It contains the button for connecting wallets and also displays the currently connected wallet. It also houses the toggle for switching between mainnet and testnet, and the button for donating algos to a hard-coded address. It can be found in the `/src/components/navbar/index.ts` file.

3. **Assets Component**: This component displays all the verified assets gotten from the Public Pera API for retrieving assets. It can be found in the `/src/features/home/assets/index.ts` file. An Intersecion Observer was used in this component to detect when a user scrolls to the bottom of the assets list so that more assets can be retrieved and displayed on the list. This is because the API endpoint that retrieves the assets returns a paginated list.

4. **Asset Card Component**: This component displays a single asset and also contains functionality for opting the user into the asset if the user is not opted in already. It can be found in the `/src/features/home/assets/asset-card.ts` file.

5. **Atomic Swap Component**: This is the most complicated component and renders the form for providing the details required for a simple atomic swap. It also contains all the logic for the atomic swap. It can be found in the `/src/features/home/atomic-swap/index.ts` file.
12 changes: 12 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { isServer }) => {
// Fix for Module not found: Can't resolve fs.
if (!isServer) {
config.resolve.fallback.fs = false;
}
return config;
},
};

export default nextConfig;
43 changes: 43 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "nextjs-typescript-algorand-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@agoralabs-sh/avm-web-provider": "^1.7.0",
"@algorandfoundation/algokit-utils": "^6.0.2",
"@blockshake/defly-connect": "1.1.6",
"@daffiwallet/connect": "1.0.3",
"@perawallet/connect": "1.3.4",
"@txnlab/use-wallet": "2.7.0",
"@walletconnect/modal-sign-html": "2.6.2",
"algosdk": "^2.7.0",
"classnames": "^2.5.1",
"lute-connect": "^1.4.1",
"magic-sdk": "^28.12.0",
"next": "14.2.15",
"pino-pretty": "^11.2.2",
"react": "^18",
"react-dom": "^18",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.3.0",
"react-loading-skeleton": "^3.5.0",
"recoil": "^0.7.7",
"sass": "^1.79.5"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.15",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
8 changes: 8 additions & 0 deletions postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};

export default config;
34 changes: 34 additions & 0 deletions src/actions/assets.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useClient } from '@/hooks/use-client';
import { IAssetResponse } from '@/interface/asset.interface';
import { NetworkAtom } from '@/state';
import { useCallback } from 'react';
import toast from 'react-hot-toast';
import { useRecoilValue } from 'recoil';

export const useAssetActions = () => {
const network = useRecoilValue(NetworkAtom);
const client = useClient();

const getVerifiedAssets = useCallback(
async (next: string | null = null) => {
const baseUrl =
next ||
`https://${network.toLowerCase()}.api.perawallet.app/v1/public/assets?filter=is_verified`;

const response = await client.get<IAssetResponse>(baseUrl, undefined, {
overrideDefaultBaseUrl: true,
});

if (response.data) {
return response.data;
}

toast.error(`Failed to fetch assets: ${response.error}`);
},
[network],
);

return {
getVerifiedAssets,
};
};
Binary file added src/app/favicon.ico
Binary file not shown.
Binary file added src/app/fonts/GeistMonoVF.woff
Binary file not shown.
Binary file added src/app/fonts/GeistVF.woff
Binary file not shown.
27 changes: 27 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--background: #ffffff;
--foreground: #171717;
}

/* @media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
} */

body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
}
56 changes: 56 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import '../styles/global.scss';
import 'react-loading-skeleton/dist/skeleton.css';
import './globals.css';
import { Toaster } from 'react-hot-toast';
import RecoilContextProvider from '@/providers/recoil-provider';

const geistSans = localFont({
src: './fonts/GeistVF.woff',
variable: '--font-geist-sans',
weight: '100 900',
});
const geistMono = localFont({
src: './fonts/GeistMonoVF.woff',
variable: '--font-geist-mono',
weight: '100 900',
});

export const metadata: Metadata = {
title: 'Pera Challenge',
description: 'Pera Challenge',
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<link
href="https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter&family=Noto+Sans&display=swap"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:wght@400;500;600;700&display=swap"
/>
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Toaster />
<RecoilContextProvider>{children}</RecoilContextProvider>
</body>
</html>
);
}
5 changes: 5 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Home } from '@/features/home';

export default function Page() {
return <Home />;
}
Binary file added src/app/pera.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions src/assets/algorand.icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SVGProps } from 'react';

export const AlgorandIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M32 32H27.0095L23.7387 19.9201L16.725 32H11.1275L21.9515 13.2913L20.1981 6.76341L5.59747 32H0L18.5121 0H23.4352L25.5595 7.97476H30.6175L27.1781 13.9642L32 32Z"
fill="#1f1f1f"
></path>
</svg>
);
};
1 change: 1 addition & 0 deletions src/assets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './algorand.icon';
Loading