-
+
diff --git a/src/components/ui/WalletTxEntity.tsx b/src/components/ui/WalletTxEntity.tsx
new file mode 100644
index 00000000..0e4a6994
--- /dev/null
+++ b/src/components/ui/WalletTxEntity.tsx
@@ -0,0 +1,241 @@
+import {
+ IonAvatar,
+ IonBadge,
+ IonButton,
+ IonChip,
+ IonCol,
+ IonGrid,
+ IonIcon,
+ IonLabel,
+ IonRow,
+ IonText,
+ IonThumbnail,
+} from "@ionic/react";
+import { Transfer, TxInterface } from "@/interfaces/tx.interface";
+import { CHAIN_AVAILABLES } from "@/constants/chains";
+import { useStores } from "pullstate";
+import Store from "@/store";
+import { getWeb3State } from "@/store/selectors";
+import { getSplitedAddress } from "@/utils/getSplitedAddress";
+import { useMemo } from "react";
+import { arrowDown, arrowUp, document, lockOpen, repeat } from "ionicons/icons";
+import { currencyFormat } from "@/utils/currencyFormat";
+
+export function WalletTxEntity(props: { tx: TxInterface }) {
+ const { tx } = props;
+ const { walletAddress } = Store.useState(getWeb3State);
+
+ let icon;
+ let color = 'primary';
+ switch (true) {
+ case tx.attributes.operation_type === 'trade':
+ icon = repeat;
+ break;
+ case tx.attributes.operation_type === 'deposit':
+ case tx.attributes.operation_type === 'receive':
+ color = 'success';
+ icon = arrowDown;
+ break;
+ case tx.attributes.operation_type === 'approve':
+ icon = lockOpen;
+ break
+ case tx.attributes.operation_type === 'send':
+ case tx.attributes.operation_type === 'withdraw':
+ color = 'warning';
+ icon = arrowUp;
+ break;
+ case tx.attributes.operation_type === 'execute':
+ icon = document;
+ break;
+ case tx.attributes.operation_type === 'mint':
+ color = 'success'
+ icon = document;
+ break;
+ default:
+ icon = document;
+ color = 'primary';
+ break;
+ }
+
+ const action1 = tx.attributes.transfers.length > 0
+ ? tx.attributes.transfers[0]
+ : tx.attributes.approvals[0];
+ const action2 = tx.attributes.transfers.length > 0
+ ? tx.attributes.transfers[1]
+ : undefined;
+
+ return (
+ {
+ // setDisplayTxDetail(asset);
+ console.log(tx)
+ }}
+ style={{
+ cursor: "default",
+ borderBottom: "solid 1px rgba(var(--ion-color-primary-rgb), 0.2)",
+ }}
+ >
+
+
+
+
+
+ c.value === tx.relationships.chain.data.id)?.logo
+ }
+ alt={tx.relationships.chain.data.id}
+ style={{ transform: "scale(1.01)" }}
+ onError={(event) => {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${tx.relationships.chain.data.id}&bgColor=%23cccccc&textColor=%23182449`;
+ }}
+ />
+
+
+ {/*
+
+ {tx.attributes.operation_type}
+
+ */}
+
+ {tx.attributes.operation_type}
+
+
+
+
+
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${action1.fungible_info.symbol}&bgColor=%23cccccc&textColor=%23182449`;
+ }}
+ />
+
+
+ {(tx.attributes.operation_type === 'trade' || tx.attributes.operation_type === 'receive' || tx.attributes.operation_type === 'approve'|| tx.attributes.operation_type === 'deposit') && (
+
+ {tx.attributes.operation_type !== 'approve' && (
+ (action1 as Transfer).direction === 'in' ? '+ ' : '- '
+ )}
+ {action1.quantity.float.toFixed(3) + ' '}
+ {action1.fungible_info.symbol}
+ {tx.attributes.operation_type !== 'approve' && (
+
+
{currencyFormat.format((action1 as Transfer).value)}
+
+ )}
+
+ )}
+
+
+ { tx.attributes.operation_type === 'trade' && action2 && (
+
+
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${action2.fungible_info.symbol}&bgColor=%23cccccc&textColor=%23182449`;
+ }}
+ />
+
+
+
+ {(action2 as Transfer).direction === 'in' ? '+ ' : '- '}
+ {action2.quantity.float.toFixed(3) + ' '}
+ {action2.fungible_info.symbol}
+
+
{currencyFormat.format((action2 as Transfer).value)}
+
+
+
+
+ )}
+
+
+
+
+ {tx.attributes.application_metadata?.icon?.url !== undefined && (
+
+
+
+ )}
+ {tx.attributes.application_metadata?.icon?.url === undefined && tx?.attributes?.application_metadata?.contract_address && (
+
+
+ {getSplitedAddress(tx.attributes.application_metadata.contract_address)}
+
+
+ )}
+
+ {tx.attributes.application_metadata?.name}
+
+
+
+
+
+
+
+
+ {new Date(tx.attributes.mined_at).toLocaleDateString()}
+ {new Date(tx.attributes.mined_at).toLocaleTimeString()}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/WalletTxEntitySkeleton.tsx b/src/components/ui/WalletTxEntitySkeleton.tsx
new file mode 100644
index 00000000..473b52d4
--- /dev/null
+++ b/src/components/ui/WalletTxEntitySkeleton.tsx
@@ -0,0 +1,100 @@
+import React from "react";
+import {
+ IonGrid,
+ IonRow,
+ IonCol,
+ IonItem,
+ IonAvatar,
+ IonSkeletonText,
+ IonLabel,
+ IonText,
+} from "@ionic/react";
+
+interface Props {
+ itemCounts?: number;
+}
+
+export const WalletTxEntitySkeleton: React.FC = ({ itemCounts }) => {
+ return (
+
+
+
+ {/* Skeleton item */}
+ {Array.from({ length: itemCounts ? itemCounts : 5 }).map(
+ (_, index) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ )}
+
+
+
+ );
+};
diff --git a/src/constants/chains.ts b/src/constants/chains.ts
index 9f672ff8..132eb3af 100644
--- a/src/constants/chains.ts
+++ b/src/constants/chains.ts
@@ -2,7 +2,7 @@ import { ChainId } from "@aave/contract-helpers";
// Define Network enum that represents supported networks
export enum NETWORK {
- bitcoin = 1000,
+ bitcoin = 128,
mainnet = 1,
polygon = 137,
avalanche = 43114,
@@ -14,8 +14,15 @@ export enum NETWORK {
solana = 1399811149,
base = 8453,
scroll = 534352,
+ /**
+ * HERE ADD TESTNETS
+ */
+ sepolia = 11155111,
+ goerli = 5,
}
+export type chainType = 'evm' | 'cosmos' | 'bitcoin' | 'solana' | 'polkadot';
+
export interface IChain {
id: number;
value: string;
@@ -24,219 +31,221 @@ export interface IChain {
nativeSymbol?: string;
logo?: string;
testnet?: boolean;
- type: 'evm'|'cosmos'|'bitcoin'|'solana'|'polkadot';
-};
+ type: chainType;
+}
-const CHAINS_DISABLED = [
- NETWORK.cosmos,
- // NETWORK.avalanche,
- NETWORK.polkadot,
-]
+const CHAINS_DISABLED = [NETWORK.cosmos, NETWORK.polkadot, NETWORK.avalanche];
-export const CHAIN_AVAILABLES: IChain[] = [
+export const ALL_CHAINS: IChain[] = [
{
id: NETWORK.mainnet,
- value: 'eth',
- name: 'Ethereum',
- nativeSymbol: 'ETH',
- logo: '/assets/cryptocurrency-icons/eth.svg',
- rpcUrl: [
- {primary: false, url: 'https://eth-mainnet-public.unifra.io'},
- {primary: true, url: "https://rpc.ankr.com/eth"}
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "eth",
+ name: "Ethereum",
+ nativeSymbol: "ETH",
+ logo: "/assets/cryptocurrency-icons/eth.svg",
+ rpcUrl:
+ [
+ { primary: false, url: "https://eth-mainnet-public.unifra.io" },
+ { primary: true, url: "https://rpc.ankr.com/eth" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
},
{
id: NETWORK.binancesmartchain,
- value: 'bsc',
- name: 'Binance smart chain',
- nativeSymbol: 'BNB',
- logo: '/assets/cryptocurrency-icons/bnb.svg',
- rpcUrl: [
- {primary: false, url: 'https://rpc.ankr.com/bsc'},
- {primary: true, url: "https://binance.llamarpc.com"}
- ].find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "bsc",
+ name: "Binance smart chain",
+ nativeSymbol: "BNB",
+ logo: "/assets/cryptocurrency-icons/bnb.svg",
+ rpcUrl:
+ [
+ { primary: false, url: "https://rpc.ankr.com/bsc" },
+ { primary: true, url: "https://1rpc.io/bnb" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
},
- // {
- // id: 250,
- // value: 'fantom',
- // name: 'Fantom',
- // nativeSymbol: 'FTM',
- // logo: '/assets/cryptocurrency-icons/eth.svg'
- // },
- // {
- // id: 43114,
- // value: 'avalanche',
- // name: 'Avalanche',
- // nativeSymbol: 'AVAX'
- // },
{
id: NETWORK.polygon,
- value: 'polygon',
- name: 'Polygon',
- nativeSymbol: 'MATIC',
- logo: '/assets/cryptocurrency-icons/matic.svg',
- rpcUrl: [
- {primary: false, url: 'https://polygon-rpc.com'},
- {primary: true, url: "https://rpc.ankr.com/polygon"}
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "polygon",
+ name: "Polygon",
+ nativeSymbol: "MATIC",
+ logo: "/assets/cryptocurrency-icons/matic.svg",
+ rpcUrl:
+ [
+ { primary: false, url: "https://polygon-rpc.com" },
+ { primary: true, url: "https://rpc.ankr.com/polygon" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
},
{
id: NETWORK.arbitrum,
- value: 'arbitrum',
- name: 'Arbitrum',
- nativeSymbol: 'ARB',
- logo: '/assets/icons/arb.svg',
- rpcUrl: [
- {primary: true, url: 'https://arbitrum.llamarpc.com'},
- {primary: false, url: "https://rpc.ankr.com/arbitrum_one"}
- ].find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "arbitrum",
+ name: "Arbitrum",
+ nativeSymbol: "ARB",
+ logo: "/assets/icons/arb.svg",
+ rpcUrl:
+ [
+ { primary: true, url: "https://1rpc.io/arb" },
+ { primary: false, url: "https://rpc.ankr.com/arbitrum_one" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
},
{
id: NETWORK.optimism,
- value: 'optimism',
- name: 'Optimism',
- nativeSymbol: 'OP',
- logo: '/assets/icons/op.svg',
- rpcUrl: [
- {primary: false, url:'https://mainnet.optimism.io'},
- {primary: true, url: "https://rpc.ankr.com/optimism"}
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "optimism",
+ name: "Optimism",
+ nativeSymbol: "OP",
+ logo: "/assets/icons/op.svg",
+ rpcUrl:
+ [
+ { primary: false, url: "https://mainnet.optimism.io" },
+ { primary: true, url: "https://rpc.ankr.com/optimism" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
},
{
id: NETWORK.base,
- value: 'base',
- name: 'Base',
- nativeSymbol: 'ETH',
- logo: '/assets/icons/base.svg',
- rpcUrl: [
- {primary: false, url: 'https://endpoints.omniatech.io/v1/base/mainnet/public'},
- {primary: true, url: "https://base.llamarpc.com"}
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "base",
+ name: "Base",
+ nativeSymbol: "ETH",
+ logo: "/assets/icons/base.svg",
+ rpcUrl:
+ [
+ {
+ primary: false,
+ url: "https://endpoints.omniatech.io/v1/base/mainnet/public",
+ },
+ { primary: true, url: "https://1rpc.io/base" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
},
{
id: NETWORK.scroll,
- value: 'scroll',
- name: 'Scroll',
- nativeSymbol: 'ETH',
- logo: '/assets/icons/scroll.svg',
- rpcUrl: [
- {primary: false, url: 'https://scroll-mainnet.public.blastapi.io'},
- {primary: true, url: "https://1rpc.io/scroll"}
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "scroll",
+ name: "Scroll",
+ nativeSymbol: "ETH",
+ logo: "/assets/icons/scroll.svg",
+ rpcUrl:
+ [
+ { primary: false, url: "https://scroll-mainnet.public.blastapi.io" },
+ { primary: true, url: "https://1rpc.io/scroll" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
},
{
id: NETWORK.cosmos,
- value: 'cosmos',
- name: 'Cosmos',
- nativeSymbol: 'ATOM',
- logo: '/assets/cryptocurrency-icons/atom.svg',
- rpcUrl: [
- {primary: true, url:'https://rpc.cosmos.network:26657'},
- {primary: false, url: "https://cosmos-rpc.publicnode.com:443"},
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'cosmos',
+ value: "cosmos",
+ name: "Cosmos",
+ nativeSymbol: "ATOM",
+ logo: "/assets/cryptocurrency-icons/atom.svg",
+ rpcUrl:
+ [
+ { primary: true, url: "https://rpc.cosmos.network:26657" },
+ { primary: false, url: "https://cosmos-rpc.publicnode.com:443" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "cosmos",
},
{
id: NETWORK.avalanche,
- value: 'avalanche',
- name: 'Avalanche',
- nativeSymbol: 'AVAX',
- logo: '/assets/cryptocurrency-icons/avax.svg',
- rpcUrl: [
- {primary: false, url:'https://avalanche-c-chain.publicnode.com'},
- {primary: true, url: "https://rpc.ankr.com/avalanche"}
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'evm',
+ value: "avalanche",
+ name: "Avalanche",
+ nativeSymbol: "AVAX",
+ logo: "/assets/cryptocurrency-icons/avax.svg",
+ rpcUrl:
+ [
+ { primary: false, url: "https://avalanche-c-chain.publicnode.com" },
+ { primary: true, url: "https://rpc.ankr.com/avalanche" },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "evm",
+ },
+ {
+ id: NETWORK.solana,
+ value: "solana",
+ name: "Solana",
+ nativeSymbol: "SOL",
+ logo: "/assets/cryptocurrency-icons/sol.svg",
+ rpcUrl:
+ [{ primary: true, url: "https://api.devnet.solana.com" }].find(
+ (rpc) => rpc.primary
+ )?.url || "",
+ type: "solana",
},
- // testnets
- // {
- // id: 5,
- // value: 'eth_goerli',
- // name: 'Goerli',
- // testnet: true,
- // rpcUrl: "https://rpc.ankr.com/eth_goerli",
- // },
- // {
- // id: 80001,
- // value: 'polygon_mumbai',
- // name: 'mumbai',
- // testnet: true,
- // rpcUrl: "https://rpc.ankr.com/polygon_mumbai",
- // },
- // {
- // id: 43113,
- // value: 'avalanche_fuji',
- // name: 'Fuji',
- // },
{
id: NETWORK.bitcoin,
- name: 'Bitcoin',
- value: 'bitcoin',
- nativeSymbol: 'BTC',
- rpcUrl: [
- {url: '84-30-190-204.cable.dynamic.v4.ziggo.nl', primary: false},
- {url: 'https://rpc.coinsdo.net/btc', primary: true}
- ].find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'bitcoin',
- logo: '/assets/cryptocurrency-icons/btc.svg',
+ name: "Bitcoin",
+ value: "bitcoin",
+ nativeSymbol: "BTC",
+ rpcUrl:
+ [
+ { url: "84-30-190-204.cable.dynamic.v4.ziggo.nl", primary: false },
+ { url: "https://rpc.coinsdo.net/btc", primary: true },
+ ].find((rpc) => rpc.primary)?.url || "",
+ type: "bitcoin",
+ logo: "/assets/cryptocurrency-icons/btc.svg",
+ },
+
+ /**
+ * HERE ADD TESTNETS
+ */
+ {
+ id: NETWORK.sepolia,
+ value: "sepolia",
+ name: "sepolia",
+ nativeSymbol: "ETH",
+ logo: "/assets/cryptocurrency-icons/eth.svg",
+ rpcUrl: "https://rpc.ankr.com/eth_sepolia",
+ type: "evm",
+ testnet: true,
},
{
- id: NETWORK.solana,
- value: 'solana',
- name: 'Solana',
- nativeSymbol: 'SOL',
- logo: '/assets/cryptocurrency-icons/sol.svg',
- rpcUrl: [
- {primary: true, url: "https://api.devnet.solana.com"}
- ]
- .find(
- (rpc) => rpc.primary
- )?.url||'',
- type: 'solana',
+ id: NETWORK.goerli,
+ value: "eth_goerli",
+ name: "Goerli",
+ testnet: true,
+ logo: "/assets/cryptocurrency-icons/eth.svg",
+ rpcUrl: "https://rpc.ankr.com/eth_goerli",
+ type: "evm",
},
-]
-.filter(c => !CHAINS_DISABLED.includes(c.id)) as IChain[];
+ // {
+ // id: 43113,
+ // value: 'avalanche_fuji',
+ // name: 'Fuji',
+ // },
+];
-const NETWORK_DEFAULT = NETWORK.optimism;
-export const CHAIN_DEFAULT = CHAIN_AVAILABLES.find(c => c.id === NETWORK_DEFAULT) || {
- id: NETWORK_DEFAULT, name: 'default', value: 'default', rpcUrl: '', type: 'evm'
+export const CHAIN_AVAILABLES: IChain[] = ALL_CHAINS
+.filter((c) =>
+ // PROD: only mainnets
+ // LOCAL: only testnets
+ // DEV: all
+ process.env.NEXT_PUBLIC_APP_IS_PROD === "true"
+ ? !c.testnet
+ : process.env.NEXT_PUBLIC_APP_IS_LOCAL === "true"
+ ? c.testnet
+ : true
+)
+.filter((c) => !CHAINS_DISABLED.includes(c.id)) as IChain[];
+
+// PROD: optimism
+// LOCAL: sepolia
+// DEV: optimism
+const NETWORK_DEFAULT =
+ process.env.NEXT_PUBLIC_APP_IS_PROD === "true"
+ ? NETWORK.optimism
+ : process.env.NEXT_PUBLIC_APP_IS_LOCAL === "false"
+ ? NETWORK.optimism
+ : NETWORK.sepolia;
+
+export const CHAIN_DEFAULT = CHAIN_AVAILABLES.find(
+ (c) => c.id === NETWORK_DEFAULT
+) || {
+ id: NETWORK_DEFAULT,
+ name: "default",
+ value: "default",
+ rpcUrl: "",
+ type: "evm",
};
export const minBaseTokenRemainingByNetwork: Record = {
[ChainId.optimism]: "0.0001",
[ChainId.arbitrum_one]: "0.0001",
-};
\ No newline at end of file
+};
diff --git a/src/containers/AuthWithLinkContainer.tsx b/src/containers/AuthWithLinkContainer.tsx
new file mode 100644
index 00000000..da696203
--- /dev/null
+++ b/src/containers/AuthWithLinkContainer.tsx
@@ -0,0 +1,62 @@
+import web3Connector from "@/servcies/firebase-web3-connect";
+import { FirebaseWeb3Connect } from "@hexaonelabs/firebase-web3connect";
+import { IonContent, IonPage, useIonAlert, useIonLoading } from "@ionic/react";
+import { useEffect } from "react";
+
+export default function AuthWithLinkContainer() {
+ const [presentLoader, dismissLoader] = useIonLoading();
+ const [presentAlert] = useIonAlert();
+
+ useEffect(() => {
+ const connectFromEmailLink = FirebaseWeb3Connect.isConnectWithLink();
+ if (!connectFromEmailLink) {
+ presentAlert({
+ backdropDismiss: false,
+ header: "Error",
+ subHeader: "Authentication failed.",
+ message:
+ "Unable to access to this page without email connection link. Restart application & try again.",
+ });
+ return () => {};
+ }
+ // display loader
+ presentLoader({
+ message: "Authenticate with email link...",
+ })
+ // call connetWithLink()
+ .then(async () => await web3Connector.connectWithLink())
+ // hide loader
+ .then(async () => await dismissLoader())
+ // display success message
+ .then(async () =>
+ await presentAlert({
+ backdropDismiss: false,
+ header: "Authentication",
+ subHeader: "Success",
+ message: "Close this page & go back to the app.",
+ })
+ )
+ // handle error
+ .catch(async (error) => {
+ // dismiss loader
+ await dismissLoader();
+ // display alert
+ await presentAlert({
+ backdropDismiss: false,
+ header: "Error",
+ subHeader: "Authentication failed.",
+ message:
+ (error as Error)?.message ||
+ "Unable to authenticate. Restart application & try again.",
+ buttons: ["ok"],
+ });
+ });
+ return () => {};
+ }, []);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/BuyWithFiat.tsx b/src/containers/BuyWithFiat.tsx
index 6a720aea..1d5956dc 100644
--- a/src/containers/BuyWithFiat.tsx
+++ b/src/containers/BuyWithFiat.tsx
@@ -22,7 +22,7 @@ export default function BuyWithFiat(props: {
- Buy
+ Buy
(null);
const chain =
@@ -49,7 +48,7 @@ export const DepositContainer = (props: {
const [presentSelectNetwork, dismissSelectNetwork] = useIonModal(() => (
));
@@ -119,7 +118,7 @@ export const DepositContainer = (props: {
- Deposit
+ Deposit
void;
+}) {
+ const { setIsMagicMigrationModalOpen } = props;
+
+ return (
+ <>
+
+
+ Hexa Lite Update
+
+
+
+
+
+
+
+ We're thrilled to announce the launch of our new Wallet
+ solution! 🎉
+
+
+
+ 100% non-custodial, now you have complete
+ control over your Wallet & can manage all your financial assets securely.
+
+
+ Migration Guide with 2 steps
+
+
+
+
+ 1: Magic Link
+
+
+
+ -
+ Go to{" "}
+
+ https://wallet.magic.link
+
+ .
+
+ - Connect to your Wallet with usual method.
+ - Click you avatar on top left of the wallet card.
+ -
+ Click on Wallet secret phrase.
+
+ - Get you Wallet secret phrase.
+
+
+
+
+
+
+ 2: Hexa Lite
+
+
+
+ -
+ Back to HexaLite App, click Connect button & select Connect Wallet âž¡ Import secret seed{" "}
+ option.
+
+ - Use you secret seed & connect with Google.
+ -
+ Backup your secret seed phrase to ensure that you never
+ loose your wallet access.
+
+
+
+
+
+
+
+ Congrate!
+
+ Now you're ready to enjoy DeFi services with full non-custodial Wallet.
+
+
+ Hexa Lite Team
+
+
+
+
+
+
+
+
+ setIsMagicMigrationModalOpen(false)}
+ >
+ OK
+
+
+
+ >
+ );
+}
diff --git a/src/containers/TransferContainer.tsx b/src/containers/TransferContainer.tsx
index ef42af0c..a92e7aad 100644
--- a/src/containers/TransferContainer.tsx
+++ b/src/containers/TransferContainer.tsx
@@ -7,41 +7,29 @@ import {
IonCol,
IonContent,
IonFab,
- IonFabButton,
+ IonFooter,
IonGrid,
IonHeader,
IonIcon,
IonInput,
- IonItem,
IonLabel,
- IonList,
- IonListHeader,
IonModal,
- IonPopover,
IonRow,
- IonText,
+ IonSpinner,
IonTitle,
IonToolbar,
+ useIonAlert,
} from "@ionic/react";
-import { chevronDown, close, scan } from "ionicons/icons";
-import { SymbolIcon } from "../components/SymbolIcon";
-import {
- Dispatch,
- SetStateAction,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { CHAIN_AVAILABLES, CHAIN_DEFAULT } from "@/constants/chains";
-import { getReadableAmount } from "@/utils/getReadableAmount";
-import { InputInputEventDetail, IonInputCustomEvent } from "@ionic/core";
-import { Html5Qrcode } from "html5-qrcode";
-const isNumberKey = (evt: React.KeyboardEvent
) => {
- var charCode = evt.which ? evt.which : evt.keyCode;
- return !(charCode > 31 && (charCode < 48 || charCode > 57));
-};
+import { useEffect, useRef, useState } from "react";
+
+import { Html5Qrcode } from "html5-qrcode";
+import { CheckIcon } from "@/components/ui/CheckIcon/CheckIcon";
+import { CrossIcon } from "@/components/ui/CrossIcon/CrossIcon";
+import { close, scan } from "ionicons/icons";
+import { InputAssetWithDropDown } from "@/components/ui/InputAssetWithDropDown/InputAssetWithDropDown";
+import { WarningBox } from "@/components/WarningBox";
+import { OverlayEventDetail } from "@ionic/react/dist/types/components/react-component-lib/interfaces";
const scanQrCode = async (
html5QrcodeScanner: Html5Qrcode
@@ -65,7 +53,8 @@ const scanQrCode = async (
// get prefered back camera if available or load the first one
const cameraId =
- cameras.find((c) => c.label.toLowerCase().includes("rear"))?.id || cameras[0].id;
+ cameras.find((c) => c.label.toLowerCase().includes("rear"))?.id ||
+ cameras[0].id;
console.log(">>", cameraId, cameras);
// start scanner
const config = {
@@ -167,224 +156,9 @@ const ScanModal = (props: {
);
};
-const InputAssetWithDropDown = (props: {
- assets: IAsset[];
- inputFromAmount: number;
- setInputFromAmount: Dispatch>;
- setInputFromAsset: Dispatch>;
-}) => {
- const { assets, setInputFromAmount, inputFromAmount, setInputFromAsset } =
- props;
- const [errorMessage, setErrorMessage] = useState();
- const [selectedAsset, setSelectedAsset] = useState(assets[0]);
- const [isLoading, setIsLoading] = useState(false);
- const [popoverOpen, setPopoverOpen] = useState(false);
- // const popover = useRef(null);
-
- useEffect(() => {
- if (selectedAsset) {
- setInputFromAsset(selectedAsset);
- }
- return () => {};
- });
-
- const maxBalance = useMemo(() => {
- // round to the lower tenth
- return Math.floor(selectedAsset?.balance * 10000) / 10000;
- }, [selectedAsset]);
-
- const handleInputChange = async (
- e: IonInputCustomEvent
- ) => {
- let value = Number((e.target as any).value || 0);
- if (maxBalance && value > maxBalance) {
- (e.target as any).value = maxBalance;
- value = maxBalance;
- }
- if (value <= 0) {
- setErrorMessage(() => undefined);
- // UI loader control
- setIsLoading(() => false);
- return;
- }
- setInputFromAmount(() => value);
- setErrorMessage(() => undefined);
- // UI loader control
- setIsLoading(() => false);
- };
-
- return (
- <>
-
-
-
- {
- $event.stopPropagation();
- // set position
- // popover.current!.event = $event;
- // open popover
- setPopoverOpen(() => true);
- }}
- >
-
-
-
-
-
-
-
- {selectedAsset?.symbol}
-
- {
- $event.stopPropagation();
- setInputFromAmount(() => selectedAsset?.balance || 0);
- }}
- >
- Max: {maxBalance}
-
-
-
-
-
-
- {
- if (isNumberKey(e)) {
- setIsLoading(() => true);
- }
- }}
- onIonInput={(e) => handleInputChange(e)}
- />
-
-
-
-
-
- setPopoverOpen(false)}
- className="modalAlert"
- >
-
-
- Available assets
-
-
-
- {assets
- .filter((a) => a.balance > 0)
- .map((asset, index) => (
- {
- setPopoverOpen(() => false);
- setSelectedAsset(asset);
- setInputFromAsset(asset);
- setInputFromAmount(() => 0);
- setErrorMessage(() => undefined);
- // setQuote(() => undefined);
- console.log({ selectedAsset });
- }}
- >
-
-
-
-
- {asset.symbol}
-
-
-
- {
- CHAIN_AVAILABLES.find((c) => c.id === asset?.chain?.id)
- ?.name
- }
-
-
-
-
- {Number(asset?.balance).toFixed(6)}
-
-
-
- {getReadableAmount(
- +asset?.balance,
- Number(asset?.priceUsd),
- "No deposit"
- )}
-
-
-
-
- ))}
-
-
- >
- );
-};
-
export const TransferContainer = (props: { dismiss: () => Promise }) => {
- const {
- walletAddress,
- isMagicWallet,
- assets,
- loadAssets,
- transfer,
- switchNetwork,
- currentNetwork,
- } = Store.useState(getWeb3State);
+ const { assets, loadAssets, transfer, switchNetwork, currentNetwork } =
+ Store.useState(getWeb3State);
const [inputFromAmount, setInputFromAmount] = useState(0);
const [inputToAddress, setInputToAddress] = useState(
undefined
@@ -392,43 +166,93 @@ export const TransferContainer = (props: { dismiss: () => Promise }) => {
const [inputFromAsset, setInputFromAsset] = useState(
undefined
);
- const [isScanModalOpen, setIsScanModalOpen] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
+ const [isScanModalOpen, setIsScanModalOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSuccess, setIsSuccess] = useState(undefined);
+ const [errorMessage, setErrorMessage] = useState(
+ undefined
+ );
+ const [openConfirm, closeConfirm] = useIonAlert()
const isValid =
- inputFromAmount > 0 &&
+ inputFromAmount > 0 &&
+ inputFromAmount <= (inputFromAsset?.balance || 0) &&
inputToAddress &&
inputToAddress.length > 0 &&
inputFromAsset?.contractAddress;
const handleSend = async () => {
- console.log("handleSend: ", {
+ console.log("[INFO] handleSend: ", {
inputFromAmount,
inputToAddress,
inputFromAsset,
});
+ const {detail: {role}}: CustomEvent> = await new Promise(resolve => {
+ openConfirm({
+ header: 'Confirm',
+ message: `
+ You are about to send ${inputFromAmount} ${inputFromAsset?.symbol} to the EVM ${inputFromAsset?.chain?.name} network address ${inputToAddress}.
+ `,
+ buttons: [
+ {
+ text: 'cancel',
+ role: 'cancel',
+ cssClass: 'danger'
+ },
+ {
+ text: 'OK',
+ role: 'ok'
+ }
+ ],
+ onDidDismiss: ($event)=> {
+ resolve($event);
+ }
+ });
+
+ });
+ if (role !== 'ok') {
+ return;
+ }
if (inputFromAmount && inputToAddress && inputFromAsset?.contractAddress) {
- if (
- inputFromAsset?.chain?.id &&
- inputFromAsset?.chain?.id !== currentNetwork
- ) {
- await switchNetwork(inputFromAsset?.chain?.id);
+ try {
+ if (
+ inputFromAsset?.chain?.id &&
+ inputFromAsset?.chain?.id !== currentNetwork
+ ) {
+ await switchNetwork(inputFromAsset?.chain?.id);
+ }
+ await transfer({
+ inputFromAmount,
+ inputToAddress,
+ inputFromAsset: inputFromAsset.contractAddress,
+ });
+ // toggle state
+ setIsSuccess(true);
+ setIsLoading(false);
+ setErrorMessage(undefined);
+ await Promise.all([
+ // ensure waiting to display check animation
+ new Promise((resolve) => setTimeout(resolve, 3000)),
+ // finalize with reload asset list
+ loadAssets(true),
+ ]);
+ // close modal
+ props.dismiss();
+ } catch (error: any) {
+ // toggle state
+ setIsSuccess(false);
+ setIsLoading(false);
+ setErrorMessage(error?.message || "Error while transfer token");
}
- await transfer({
- inputFromAmount,
- inputToAddress,
- inputFromAsset: inputFromAsset.contractAddress,
- });
- // finalize with reload asset list
- await loadAssets(true);
}
};
+
return (
<>
- Send token
+ Send token
Promise }) => {
-
-
-
-
-
- Currently only support native token transfer
-
-
-
-
-
+ {isSuccess === undefined && (
+ <>
+
+
+
+ Token
+
+ a.balance > 0)}
+ inputFromAmount={inputFromAmount}
+ setInputFromAmount={setInputFromAmount}
+ setInputFromAsset={setInputFromAsset}
+ />
+
+
+
+ EVM Destination address
+
+
+
+
+ {
+ setInputToAddress(
+ () => $event.detail.value || undefined
+ );
+ }}
+ />
+
+
+ {
+ setIsScanModalOpen(() => true);
+ }}
+ >
+
+
+
+
+
+ {
+ if (data) {
+ setInputToAddress(() => data);
+ }
+ setIsScanModalOpen(() => false);
+ }}
+ />
+
+
+ >
+ )}
+
+ {isSuccess === true && (
+ <>
+
- Token
-
- a.type === "NATIVE")}
- inputFromAmount={inputFromAmount}
- setInputFromAmount={setInputFromAmount}
- setInputFromAsset={setInputFromAsset}
- />
-
-
-
+
+
+
+ >
+ )}
+
+ {isSuccess === false && errorMessage && (
+ <>
+
- Destination address
-
-
-
-
- {
- setInputToAddress(
- () => $event.detail.value || undefined
- );
- }}
- />
-
-
- {
- setIsScanModalOpen(() => true);
- }}
- >
-
-
-
-
-
- {
- if (data) {
- setInputToAddress(() => data);
- }
- setIsScanModalOpen(() => false);
- }}
- />
-
-
+
+
+
+
+ >
+ )}
+
+ {inputFromAmount > (inputFromAsset?.balance || 0) && (
+ <>
+
+
+
+
+ You don't have enough funds to complete the transaction.
+
+
+
+
+ >
+ )}
+
+
+
+
+ {isSuccess === false && (
+ {
+ props.dismiss()
+ }}
+ >Cancel and retry
+ )}
+
+ {isSuccess === undefined && (
+ <>
Promise }) => {
setIsLoading(false);
}}
>
- Send
+ {isLoading === true ? (
+ <>
+ Waiting
+ confirmation
+ >
+ ) : (
+ "Send"
+ )}
-
-
-
-
+ >
+ )}
+ {isSuccess === true && (
+ <>
+ {
+ props.dismiss();
+ }}
+ >
+ OK
+
+ >
+ )}
+
+
>
);
};
diff --git a/src/containers/desktop/SwapContainer.tsx b/src/containers/desktop/SwapContainer.tsx
index e22f94ad..4b01ed65 100644
--- a/src/containers/desktop/SwapContainer.tsx
+++ b/src/containers/desktop/SwapContainer.tsx
@@ -7,6 +7,7 @@ import {
useIonToast,
} from "@ionic/react";
import {
+ HiddenUI,
RouteExecutionUpdate,
WidgetConfig,
WidgetEvent,
@@ -16,7 +17,7 @@ import type { Route } from "@lifi/sdk";
import { useEffect } from "react";
import { useLoader } from "../../context/LoaderContext";
import { CHAIN_AVAILABLES, CHAIN_DEFAULT, NETWORK } from "../../constants/chains";
-import { ethers } from "ethers";
+import { Wallet, ethers } from "ethers";
import { LiFiWidgetDynamic } from "../../components/LiFiWidgetDynamic";
import { LIFI_CONFIG } from "../../servcies/lifi.service";
// import { SquidWidgetDynamic } from "@/components/SquidWidgetDynamic";
@@ -28,7 +29,7 @@ import { getWeb3State } from "@/store/selectors";
export default function SwapContainer() {
const {
- web3Provider,
+ signer,
currentNetwork,
walletAddress,
connectWallet,
@@ -98,10 +99,9 @@ export default function SwapContainer() {
const chain = CHAIN_AVAILABLES.find((chain) => chain.id === currentNetwork);
switch (true) {
case chain?.type === "evm": {
- const signer =
- web3Provider instanceof ethers.providers.Web3Provider && walletAddress
- ? web3Provider?.getSigner()
- : undefined;
+ const defaultChain = process.env.NEXT_PUBLIC_APP_IS_LOCAL === 'true'
+ ? NETWORK.goerli
+ : currentNetwork || CHAIN_DEFAULT.id;
// load environment config
const widgetConfig: WidgetConfig = {
...LIFI_CONFIG,
@@ -111,12 +111,11 @@ export default function SwapContainer() {
try {
await displayLoader();
await connectWallet();
- if (!(web3Provider instanceof ethers.providers.Web3Provider)) {
+ if (!(signer instanceof ethers.Signer)) {
throw new Error(
- "[ERROR] Only support ethers.providers.Web3Provider"
+ "[ERROR] Only support ethers.Signer"
);
}
- const signer = web3Provider?.getSigner();
console.log("signer", signer);
if (!signer) {
throw new Error("Signer not found");
@@ -127,22 +126,7 @@ export default function SwapContainer() {
} catch (error: any) {
// Log any errors that occur during the connection process
hideLoader();
- await presentToast({
- message: `[ERROR] Connect Failed with reason: ${
- error?.message || error
- }`,
- color: "danger",
- buttons: [
- {
- text: "x",
- role: "cancel",
- handler: () => {
- dismissToast();
- },
- },
- ],
- });
- throw new Error("handleConnect:" + error?.message);
+ return error;
}
},
disconnect: async () => {
@@ -171,12 +155,19 @@ export default function SwapContainer() {
});
}
},
- signer,
+ signer: signer || undefined,
},
+ hiddenUI: [
+ ...(LIFI_CONFIG?.hiddenUI as any[]),
+ HiddenUI.WalletMenu,
+ HiddenUI.ToAddress,
+ // HiddenUI.DrawerButton,
+ // HiddenUI.DrawerCloseButton
+ ],
// set source chain to Polygon
- fromChain: currentNetwork || CHAIN_DEFAULT.id,
+ fromChain: defaultChain,
// set destination chain to Optimism
- toChain: currentNetwork || CHAIN_DEFAULT.id,
+ toChain: defaultChain,
// set source token to ETH (Ethereum)
fromToken: "0x0000000000000000000000000000000000000000",
};
diff --git a/src/containers/desktop/TokenDetailDesktopContainer.tsx b/src/containers/desktop/TokenDetailDesktopContainer.tsx
index d6468edb..bb111a1e 100644
--- a/src/containers/desktop/TokenDetailDesktopContainer.tsx
+++ b/src/containers/desktop/TokenDetailDesktopContainer.tsx
@@ -6,6 +6,8 @@ import {
IonAvatar,
IonBadge,
IonButton,
+ IonCard,
+ IonCardContent,
IonChip,
IonCol,
IonContent,
@@ -33,16 +35,28 @@ import { ethers } from "ethers";
import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { CHAIN_AVAILABLES } from "@/constants/chains";
-import { airplane, chevronDown, close, closeSharp, download, paperPlane, repeat } from "ionicons/icons";
-import { DataItem } from "@/components/ui/LightChart";
-import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
-import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
+import {
+ airplane,
+ chevronDown,
+ close,
+ closeSharp,
+ download,
+ paperPlane,
+ repeat,
+} from "ionicons/icons";
+import { DataItem, SeriesData, SeriesMarkerData } from "@/components/ui/LightChart";
+import { TokenInfo } from '@/servcies/coingecko.service';
import { numberFormat } from "@/utils/numberFormat";
import { currencyFormat } from "@/utils/currencyFormat";
import { TokenDetailDescription } from "@/components/ui/TokenDetailDescription";
import { TokenDetailMarketDetail } from "@/components/ui/TokenDetailMarketData";
import { isStableAsset } from "@/utils/isStableAsset";
import { Currency } from "@/components/ui/Currency";
+import { NetworkTokenDetailCard } from "@/components/ui/NetworkTokenDetailCard/NetworkTokenDetailCard";
+import { getAllocationRatioInPercent } from "@/utils/getAllocationRatioInPercent";
+import { formatTxsAsSeriemarker } from "@/servcies/zerion.service";
+import { CoingeckoAPI } from "@/servcies/coingecko.service";
+import { TxsList } from "@/components/ui/TsxList/TxsList";
const LightChart = lazy(() => import("@/components/ui/LightChart"));
@@ -66,25 +80,29 @@ export const TokenDetailDesktopContainer = (props: {
setState: (state: any) => void;
}) => {
const { data, dismiss } = props;
- const { walletAddress } = Store.useState(getWeb3State);
- const [dataChartHistory, setDataChartHistory] = useState([]);
+ const { walletAddress, txs } = Store.useState(getWeb3State);
+ const [dataChartHistory, setDataChartHistory] = useState(new Map());
+ const [txsChartHistory, setTxsChartHistory] = useState(new Map());
const [tokenInfo, setTokenInfo] = useState(undefined);
const [isInfoOpen, setInfoOpen] = useState(false);
+ const [isAccOpen, setIsAccOpen] = useState(false);
+ const [tokentPrice, setTokentPrice] = useState(tokenInfo?.market_data?.current_price?.usd ||
+ data.priceUsd);
+
+ const filteredTxs = txs.filter((tx) => {
+ return tx.attributes.transfers.some((transfer) => {
+ return transfer.fungible_info.symbol === data.symbol;
+ });
+ });
useEffect(() => {
if (!walletAddress) return;
- getTxsFromAddress(walletAddress);
- getTokenHistoryPrice(props.data.symbol).then((prices) => {
- const data: DataItem[] = prices.map(([time, value]: string[]) => {
- const dataItem = {
- time: new Date(time).toISOString().split("T").shift() || "",
- value: Number(value),
- };
- return dataItem;
- });
- setDataChartHistory(() => data.slice(0, data.length - 1));
+ const TxsSerie = formatTxsAsSeriemarker(filteredTxs);
+ setTxsChartHistory(()=> TxsSerie);
+ CoingeckoAPI.getTokenHistoryPrice(props.data.symbol).then((prices) => {
+ setDataChartHistory(() => prices);
});
- getTokenInfo(props.data.symbol).then((tokenInfo) =>
+ CoingeckoAPI.getTokenInfo(props.data.symbol).then((tokenInfo) =>
setTokenInfo(() => tokenInfo)
);
}, [walletAddress]);
@@ -123,8 +141,11 @@ export const TokenDetailDesktopContainer = (props: {
>
-
-
+
+
{
@@ -157,7 +181,8 @@ export const TokenDetailDesktopContainer = (props: {
- { tokenInfo?.market_data?.price_change_percentage_24h_in_currency?.usd && (
+ {tokenInfo?.market_data
+ ?.price_change_percentage_24h_in_currency?.usd && (
- ({tokenInfo.market_data.price_change_percentage_24h_in_currency.usd.toFixed(
- 2
- )}
- % /24h)
+ (
+ {tokenInfo.market_data.price_change_percentage_24h_in_currency.usd.toFixed(
+ 2
+ )}
+ % /24h)
)}
{isStableAsset(data.symbol) ? (
- {
- setInfoOpen(()=> true);
- }} >
+ onClick={() => {
+ setInfoOpen(() => true);
+ }}
+ >
stable
- ) : ''}
-
-
-
- {
- props.setState({ isTransferModalOpen: true });
- }}>
-
- Send
-
- {
- props.setState({ isDepositModalOpen: true });
- }}>
-
- Deposit
-
-
-
-
-
-
-
-
-
-
-
-
-
- Networks details
-
-
- {data.assets
- .sort((a, b) =>
- a.chain && b.chain
- ? a.chain.id - b.chain.id
- : a.balance + b.balance
- )
- .map((token, index) => (
-
-
- c.id === token.chain?.id
- )?.logo
- }
- alt={token.symbol}
- style={{ transform: "scale(1.01)" }}
- onError={(event) => {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
- {token.chain?.name}
-
-
- { numberFormat.format(token.balance)} {token.symbol}
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
+ ) : (
+ ""
+ )}
+
+
+
+
+
+
+
+ {
+ props.setState({ isTransferModalOpen: true });
+ }}
+ >
+
+ Send
+
+ {
+ props.setState({ isDepositModalOpen: true });
+ }}
+ >
+
+ Deposit
+
+
+
+
+
+
+
+ setIsAccOpen(()=> !isAccOpen)}>
+
+ {isAccOpen ? 'Hide' : 'Display'} Wallet details
+
+
+
+
+
+ {data.assets
+ .sort((a, b) =>
+ a.chain && b.chain
+ ? a.chain.id - b.chain.id
+ : a.balance + b.balance
+ )
+ .map((token, index) => (
+
+
+
+ ))}
+
+
+
+
-
+
-
- >}>
+
+
+
+ >
+ }
+ >
-
+
{props.data.symbol} / USD
- 1 {data.symbol} = {currencyFormat.format(tokenInfo?.market_data?.current_price?.usd||data.priceUsd)}
+ 1 {data.symbol} ={" "}
+ {currencyFormat.format(
+ tokentPrice
+ )}
-
+ {
+ if (action === 'leave') {
+ setTokentPrice(tokenInfo?.market_data?.current_price?.usd ||
+ data.priceUsd);
+ } else {
+ setTokentPrice((payload as any)?.price)
+ }
+ }} />
+ {/* TXs list if existing tx */}
+ {filteredTxs.length > 0
+ ? (
+
+
+
+ Transaction history
+
+
+
+
+ )
+ : null}
+
{tokenInfo && (
-
+
-
+
@@ -328,7 +379,12 @@ export const TokenDetailDesktopContainer = (props: {
Market datas from Coingeeko API
-
Last update: {new Date(tokenInfo?.market_data?.last_updated||new Date ().toLocaleDateString()).toLocaleString()}
+
+ Last update:{" "}
+ {new Date(
+ tokenInfo?.market_data?.last_updated ||
+ new Date().toLocaleDateString()
+ ).toLocaleString()}
@@ -355,7 +411,7 @@ export const TokenDetailDesktopContainer = (props: {
setInfoOpen(false) }
+ onClick={() => setInfoOpen(false)}
>
@@ -363,27 +419,31 @@ export const TokenDetailDesktopContainer = (props: {
-
- What is a stablecoin?
-
+ What is a stablecoin?
- Stablecoins are a type of cryptocurrency whose value is pegged to another asset, such as a fiat currency or gold, to maintain a stable price.
+ Stablecoins are a type of cryptocurrency whose value is
+ pegged to another asset, such as a fiat currency or gold, to
+ maintain a stable price.
- They strive to provide an alternative to the high volatility of popular cryptocurrencies, making them potentially more suitable for common transactions.
+ They strive to provide an alternative to the high volatility
+ of popular cryptocurrencies, making them potentially more
+ suitable for common transactions.
- Stablecoins can be utilized in various blockchain-based financial services and can even be used to pay for goods and services.
+ Stablecoins can be utilized in various blockchain-based
+ financial services and can even be used to pay for goods and
+ services.
diff --git a/src/containers/desktop/WalletDesktopContainer.tsx b/src/containers/desktop/WalletDesktopContainer.tsx
index 02ec8382..ff445eaf 100644
--- a/src/containers/desktop/WalletDesktopContainer.tsx
+++ b/src/containers/desktop/WalletDesktopContainer.tsx
@@ -1,9 +1,10 @@
-import { card, download, eyeOffOutline, eyeOutline, paperPlane } from "ionicons/icons";
+import { card, download, eyeOffOutline, eyeOutline, grid, gridOutline, gridSharp, image, list, logoUsd, paperPlane, ticket, ticketOutline, time } from "ionicons/icons";
import WalletBaseComponent, {
WalletComponentProps,
} from "../../components/base/WalletBaseContainer";
import {
IonButton,
+ IonButtons,
IonCard,
IonCardContent,
IonCol,
@@ -13,9 +14,11 @@ import {
IonModal,
IonRow,
IonSearchbar,
+ IonSegment,
+ IonSegmentButton,
IonText,
} from "@ionic/react";
-import ConnectButton from "@/components/ConnectButton";
+import ConnectButton from "@/components/ui/ConnectButton";
import Store from "@/store";
import { getAppSettings, getWeb3State } from "@/store/selectors";
import { TokenDetailDesktopContainer } from "./TokenDetailDesktopContainer";
@@ -24,6 +27,10 @@ import { WalletAssetEntity } from "@/components/ui/WalletAssetEntity";
import { Currency } from "@/components/ui/Currency";
import { patchAppSettings } from "@/store/actions";
import { ToggleHideCurrencyAmount } from "@/components/ui/ToggleHideCurrencyAmount";
+import { WalletTxEntity } from "@/components/ui/WalletTxEntity";
+import { TxsList } from "@/components/ui/TsxList/TxsList";
+import { NftsList } from "@/components/ui/NftsList/NftsList";
+import { getAllocationRatioInPercent } from "@/utils/getAllocationRatioInPercent";
class WalletDesktopContainer extends WalletBaseComponent {
constructor(props: WalletComponentProps) {
@@ -128,7 +135,7 @@ class WalletDesktopContainer extends WalletBaseComponent {
>
{
super.handleBuyWithFiat(true);
}}
@@ -170,7 +177,7 @@ class WalletDesktopContainer extends WalletBaseComponent {
>
{
super.handleDepositClick()
}}
@@ -179,7 +186,7 @@ class WalletDesktopContainer extends WalletBaseComponent {
-
+
@@ -219,108 +226,170 @@ class WalletDesktopContainer extends WalletBaseComponent {
{/* wrapper to display card with assets items */}
{this.state.assetGroup.length > 0 && (
<>
-
-
+
+
{
this.handleSearchChange(e);
}}
/>
+
+
+ this.setState(state => ({
+ ...state,
+ currentView: 'tokens'
+ }))}>
+
+
+ this.setState(state => ({
+ ...state,
+ currentView: 'nfts'
+ }))}>
+
+
+ this.setState(state => ({
+ ...state,
+ currentView: 'txs'
+ }))}>
+
+
+
+
-
-
-
-
-
-
-
- Asset
-
-
-
-
-
-
-
-
- Price
-
-
-
-
- Balance
-
-
-
-
- Value
-
-
-
-
-
-
-
- {this.state.assetGroup
- .filter((asset) =>
- this.state.filterBy
- ? asset.symbol
- .toLowerCase()
- .includes(this.state.filterBy.toLowerCase())
- : true
- )
- .map((asset, index) => {
- return (
-
- this.handleTokenDetailClick(asset)
- }
- asset={asset}
- key={index}
- />
- );
- })}
+
+ {/* tokens view */}
+ {this.state.currentView === 'tokens' && (
+
+
+ {/* Header List */}
+
+
+
+
+ Asset
+
+
+
+
+
+
+
+
+ Wallet %
+
+
+
+
+ Price USD
+
+
+
+
+ Balance
+
+
+
+
+ Value USD
+
+
+
+
+
+
+
+ {this.state.assetGroup
+ .filter((asset) =>
+ this.state.filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(this.state.filterBy.toLowerCase())
+ : true
+ )
+ .map((asset, index) => {
+ return (
+
+ this.handleTokenDetailClick(asset)
+ }
+ asset={asset}
+ allocationRatioInPercent={getAllocationRatioInPercent(asset.balanceUsd, this.state.totalBalance)}
+ key={index}
+ />
+ );
+ })}
- {(this.state.assetGroup.filter((asset) =>
- this.state.filterBy
- ? asset.symbol
- .toLowerCase()
- .includes(this.state.filterBy.toLowerCase())
- : true
- ).length === 0) && (
- Assets not found in your wallet
- )}
-
+ {(this.state.assetGroup.filter((asset) =>
+ this.state.filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(this.state.filterBy.toLowerCase())
+ : true
+ ).length === 0) && (
+ Assets not found in your wallet
+ )}
+
+ )}
+ {/* txs view */}
+ {this.state.currentView === 'txs' && (
+
+
+
+ )}
+ {/* nfts view */}
+ {this.state.currentView === 'nfts' && (
+
+
+
+ )}
>
)}
diff --git a/src/containers/index.ts b/src/containers/index.ts
new file mode 100644
index 00000000..d637e335
--- /dev/null
+++ b/src/containers/index.ts
@@ -0,0 +1,20 @@
+
+import { lazy } from "react";
+
+export const LeaderboardContainer = lazy(() => import("@/containers/desktop/LeaderboardContainer"));
+export const WalletDesktopContainer = lazy(() => import("@/containers/desktop/WalletDesktopContainer"));
+export const SwapContainer = lazy(() => import("@/containers/desktop/SwapContainer"));
+export const DefiContainer = lazy(() => import("@/containers/desktop/DefiContainer"));
+export const EarnContainer = lazy(() => import("@/containers/desktop/EarnContainer"));
+export const AvailablePlatformsContainer = lazy(() => import("@/containers/desktop/AvailablePlatformsContainer"));
+export const AboutContainer = lazy(() => import("@/containers/desktop/AboutContainer"));
+export const BuyWithFiatContainer = lazy(() => import("@/containers/BuyWithFiat"));
+export const WalletMobileContainer = lazy(
+ () => import("@/containers/mobile/WalletMobileContainer")
+);
+export const WelcomeMobileContainer = lazy(
+ () => import("@/containers/mobile/WelcomeMobileContainer")
+ );
+export const MagicMigrationContainer = lazy(()=> import('@/containers/MagicMigrationContainer'))
+
+export const AuthWithLinkContainer = lazy(() => import('@/containers/AuthWithLinkContainer'));
diff --git a/src/containers/mobile/SwapMobileContainer.tsx b/src/containers/mobile/SwapMobileContainer.tsx
index a542dffe..e6551021 100644
--- a/src/containers/mobile/SwapMobileContainer.tsx
+++ b/src/containers/mobile/SwapMobileContainer.tsx
@@ -44,7 +44,7 @@ export const SwapMobileContainer = (props: {
dismiss: ()=> void;
}) => {
const {
- web3Provider,
+ signer,
currentNetwork,
walletAddress,
connectWallet,
@@ -88,10 +88,6 @@ export const SwapMobileContainer = (props: {
return () => widgetEvents.all.clear();
}, [widgetEvents]);
- const signer =
- web3Provider instanceof ethers.providers.Web3Provider && walletAddress
- ? web3Provider?.getSigner()
- : undefined;
// load environment config
const widgetConfig: WidgetConfig = {
...LIFI_CONFIG,
@@ -102,6 +98,7 @@ export const SwapMobileContainer = (props: {
...(LIFI_CONFIG?.hiddenUI as any[]),
HiddenUI.History,
HiddenUI.WalletMenu,
+ HiddenUI.ToAddress,
// HiddenUI.DrawerButton,
// HiddenUI.DrawerCloseButton
],
@@ -111,12 +108,6 @@ export const SwapMobileContainer = (props: {
try {
await displayLoader();
await connectWallet();
- if (!(web3Provider instanceof ethers.providers.Web3Provider)) {
- throw new Error(
- "[ERROR] Only support ethers.providers.Web3Provider"
- );
- }
- const signer = web3Provider?.getSigner();
console.log("signer", signer);
if (!signer) {
throw new Error("Signer not found");
@@ -127,22 +118,7 @@ export const SwapMobileContainer = (props: {
} catch (error: any) {
// Log any errors that occur during the connection process
hideLoader();
- await presentToast({
- message: `[ERROR] Connect Failed with reason: ${
- error?.message || error
- }`,
- color: "danger",
- buttons: [
- {
- text: "x",
- role: "cancel",
- handler: () => {
- dismissToast();
- },
- },
- ],
- });
- throw new Error("handleConnect:" + error?.message);
+ return error;
}
},
disconnect: async () => {
@@ -171,7 +147,7 @@ export const SwapMobileContainer = (props: {
});
}
},
- signer,
+ signer: signer || undefined,
},
// set source chain to Polygon
fromChain: props?.token?.assets?.[0]?.chain?.id || CHAIN_DEFAULT.id,
diff --git a/src/containers/mobile/TokenDetailMobileContainer.tsx b/src/containers/mobile/TokenDetailMobileContainer.tsx
index 7bc21111..b872baa3 100644
--- a/src/containers/mobile/TokenDetailMobileContainer.tsx
+++ b/src/containers/mobile/TokenDetailMobileContainer.tsx
@@ -34,13 +34,18 @@ import Store from "@/store";
import { getWeb3State } from "@/store/selectors";
import { CHAIN_AVAILABLES } from "@/constants/chains";
import { airplane, chevronDown, close, closeSharp, download, paperPlane } from "ionicons/icons";
-import { DataItem } from "@/components/ui/LightChart";
-import { getTokenHistoryPrice } from "@/utils/getTokenHistoryPrice";
-import { TokenInfo, getTokenInfo } from "@/utils/getTokenInfo";
+import { DataItem, SeriesData, SeriesMarkerData } from "@/components/ui/LightChart";
+import { TokenInfo } from '@/servcies/coingecko.service';
import { TokenDetailMarketDetail } from "@/components/ui/TokenDetailMarketData";
import { TokenDetailDescription } from "@/components/ui/TokenDetailDescription";
import { isStableAsset } from "@/utils/isStableAsset";
import { Currency } from "@/components/ui/Currency";
+import { NetworkTokenDetailCard } from "@/components/ui/NetworkTokenDetailCard/NetworkTokenDetailCard";
+import { getAllocationRatioInPercent } from "@/utils/getAllocationRatioInPercent";
+import { formatTxsAsSeriemarker } from "@/servcies/zerion.service";
+import { CoingeckoAPI } from "@/servcies/coingecko.service";
+import { TxsList } from "@/components/ui/TsxList/TxsList";
+import { currencyFormat } from "@/utils/currencyFormat";
const LightChart = lazy(() => import("@/components/ui/LightChart"));
@@ -65,25 +70,31 @@ export const TokenDetailMobileContainer = (props: {
setIsSwapModalOpen: (state: boolean) => void;
}) => {
const { data, dismiss } = props;
- const { walletAddress } = Store.useState(getWeb3State);
- const [dataChartHistory, setDataChartHistory] = useState([]);
+ const { walletAddress, txs } = Store.useState(getWeb3State);
+ const [dataChartHistory, setDataChartHistory] = useState(new Map());
+ const [txsChartHistory, setTxsChartHistory] = useState(new Map());
const [tokenInfo, setTokenInfo] = useState(undefined);
const [isInfoOpen, setInfoOpen] = useState(false);
+ const [isAccOpen, setIsAccOpen] = useState(false);
+ const [chartInterval, setChartInterval] = useState< "1D"|"1W"|"1M"|"1Y">('1M')
+
+ const filteredTxs = txs.filter((tx) => {
+ return tx.attributes.transfers.some((transfer) => {
+ return transfer.fungible_info.symbol === data.symbol;
+ });
+ });
+
+ const [tokentPrice, setTokentPrice] = useState(tokenInfo?.market_data?.current_price?.usd ||
+ data.priceUsd);
useEffect(() => {
if (!walletAddress) return;
- getTxsFromAddress(walletAddress);
- getTokenHistoryPrice(props.data.symbol).then((prices) => {
- const data: DataItem[] = prices.map(([time, value]: string[]) => {
- const dataItem = {
- time: new Date(time).toISOString().split("T").shift() || "",
- value: Number(value),
- };
- return dataItem;
- });
- setDataChartHistory(() => data.slice(0, data.length - 1));
+ const TxsSerie = formatTxsAsSeriemarker(filteredTxs);
+ setTxsChartHistory(()=> TxsSerie);
+ CoingeckoAPI.getTokenHistoryPrice(props.data.symbol).then((prices) => {
+ setDataChartHistory(() => prices);
});
- getTokenInfo(props.data.symbol).then((tokenInfo) =>
+ CoingeckoAPI.getTokenInfo(props.data.symbol).then((tokenInfo) =>
setTokenInfo(() => tokenInfo)
);
}, [walletAddress]);
@@ -130,7 +141,7 @@ export const TokenDetailMobileContainer = (props: {
-
+
-
+
{data.balance.toFixed(6)} {data.symbol}
@@ -195,82 +206,40 @@ export const TokenDetailMobileContainer = (props: {
) : ''}
-
-
-
-
-
-
-
-
-
-
-
- Networks details
-
-
- {data.assets
- .sort((a, b) =>
- a.chain && b.chain
- ? a.chain.id - b.chain.id
- : a.balance + b.balance
- )
- .map((token, index) => (
-
-
- c.id === token.chain?.id
- )?.logo
- }
- alt={token.symbol}
- style={{ transform: "scale(1.01)" }}
- onError={(event) => {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${token.symbol}&bgColor=%23000000&textColor=%23182449`;
- }}
- />
-
-
- {token.chain?.name}
-
-
- {token.balance.toFixed(6)} {token.symbol}
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ setIsAccOpen(()=> !isAccOpen)}>
+
+ {isAccOpen ? 'Hide' : 'Display'} Wallet details
+
+
+
+
+
+ {data.assets
+ .sort((a, b) =>
+ a.chain && b.chain
+ ? a.chain.id - b.chain.id
+ : a.balance + b.balance
+ )
+ .map((token, index) => (
+
+
+
+ ))}
+
+
+
+
@@ -281,24 +250,85 @@ export const TokenDetailMobileContainer = (props: {
.....>}>
-
-
-
- 1 {data.symbol} = $ {(tokenInfo?.market_data?.current_price?.usd||data.priceUsd).toFixed(2)}
-
-
+
+
+
+
+
+ {props.data.symbol} / USD
+
+ 1 {data.symbol} ={" "}
+ {currencyFormat.format(
+ tokentPrice
+ )}
+
+
+
+
+
+
+
+ {["1D", "1W", "1M", "1Y"].map((interval: any, index: number) => (
+ setChartInterval(()=> interval)}
+ >
+ {interval}
+
+ ))}
+
+
+
+
+
+ {
+ if (action === 'leave') {
+ setTokentPrice(tokenInfo?.market_data?.current_price?.usd ||
+ data.priceUsd);
+ } else {
+ setTokentPrice((payload as any)?.price)
+ }
+ }} />
+ {/* TXs list if existing tx */}
+ {filteredTxs.length > 0
+ ? (
+
+
+
+ Transaction history
+
+
+
+
+ )
+ : null}
+
+ {/* Token detail information */}
- {tokenInfo && (
+ {tokenInfo ? (
<>
@@ -307,7 +337,7 @@ export const TokenDetailMobileContainer = (props: {
>
- )}
+ ): null}
diff --git a/src/containers/mobile/WalletMobileContainer.tsx b/src/containers/mobile/WalletMobileContainer.tsx
index 69a5d454..cd608c4f 100644
--- a/src/containers/mobile/WalletMobileContainer.tsx
+++ b/src/containers/mobile/WalletMobileContainer.tsx
@@ -37,6 +37,8 @@ import {
IonRefresherContent,
RefresherEventDetail,
IonChip,
+ IonSegment,
+ IonSegmentButton,
} from "@ionic/react";
import { card, download, paperPlane, repeat, settings, settingsOutline } from "ionicons/icons";
import { useState } from "react";
@@ -49,9 +51,10 @@ import { currencyFormat } from "@/utils/currencyFormat";
import { isStableAsset } from "@/utils/isStableAsset";
import { Currency } from "@/components/ui/Currency";
import { ToggleHideCurrencyAmount } from "@/components/ui/ToggleHideCurrencyAmount";
+import { TxsList } from "@/components/ui/TsxList/TxsList";
+import { NftsList } from "@/components/ui/NftsList/NftsList";
interface WalletMobileComProps {
- isMagicWallet: boolean;
isSwapModalOpen: SelectedTokenDetail | boolean | undefined;
setIsSwapModalOpen: (
value?: SelectedTokenDetail | boolean | undefined
@@ -94,6 +97,16 @@ class WalletMobileContainer extends WalletBaseComponent<
}
render() {
+ const assetGroup = this.state.assetGroup
+ .filter((asset) =>
+ this.state.filterBy
+ ? asset.symbol
+ .toLowerCase()
+ .includes(this.state.filterBy.toLowerCase())
+ : true
+ )
+ .sort((a, b) => (a.balanceUsd > b.balanceUsd ? -1 : 1));
+
return (
<>
@@ -150,7 +163,7 @@ class WalletMobileContainer extends WalletBaseComponent<
-
+
@@ -191,11 +204,40 @@ class WalletMobileContainer extends WalletBaseComponent<
{
this.handleSearchChange(event);
}}
>
+ {this.state.totalBalance > 0 && (
+
+ this.setState(state => ({
+ ...state,
+ currentView: 'tokens'
+ }))}>
+ Assets
+
+ this.setState(state => ({
+ ...state,
+ currentView: 'nfts'
+ }))}>
+ NFTs
+
+ this.setState(state => ({
+ ...state,
+ currentView: 'txs'
+ }))}>
+ History
+
+
+ )}
)}
@@ -214,7 +256,7 @@ class WalletMobileContainer extends WalletBaseComponent<
{this.state.totalBalance <= 0 && (
- super.handleBuyWithFiat(true)}>
+ super.handleBuyWithFiat(true)}>
@@ -280,119 +322,128 @@ class WalletMobileContainer extends WalletBaseComponent<
className="ion-no-padding"
style={{ maxWidth: "950px", margin: "auto" }}
>
-
-
- {this.state.assetGroup
- .filter((asset) =>
- this.state.filterBy
- ? asset.symbol
- .toLowerCase()
- .includes(this.state.filterBy.toLowerCase())
- : true
- )
- .sort((a, b) => (a.balanceUsd > b.balanceUsd ? -1 : 1))
- .map((asset, index) => (
-
- {
- console.log("handleTokenDetailClick: ", asset);
- this.handleTokenDetailClick(asset);
- }}
- >
-
+
+ {assetGroup
+ .map((asset, index) => (
+
+ {
+ console.log("handleTokenDetailClick: ", asset);
+ this.handleTokenDetailClick(asset);
}}
>
- {
- (
- event.target as any
- ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+
-
-
-
-
- {asset.symbol}
-
-
-
-
- {asset.name}
-
-
-
- {isStableAsset(asset.symbol) ? (
- stable
- ) : ''}
-
-
-
-
+ >
+ {
+ (
+ event.target as any
+ ).src = `https://images.placeholders.dev/?width=42&height=42&text=${asset.symbol}&bgColor=%23000000&textColor=%23182449`;
+ }}
+ />
+
+
+
+
+ {asset.symbol}
+
+
- {asset.balance.toFixed(6)}
+
+ {asset.name}
+
-
-
-
- {
- // close the sliding item after clicking the option
- (event.target as HTMLElement)
- .closest("ion-item-sliding")
- ?.close();
- }}
- >
- {
- this.handleTransferClick(true);
- }}
- >
-
-
- {
- this.setIsSwapModalOpen(asset);
+
+ {isStableAsset(asset.symbol) ? (
+ stable
+ ) : ''}
+
+
+
+
+
+ {asset.balance.toFixed(6)}
+
+
+
+
+ {
+ // close the sliding item after clicking the option
+ (event.target as HTMLElement)
+ .closest("ion-item-sliding")
+ ?.close();
}}
>
-
-
-
-
- ))}
-
-
+ {
+ this.handleTransferClick(true);
+ }}
+ >
+
+
+ {
+ this.setIsSwapModalOpen(asset);
+ }}
+ >
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* txs view */}
+ {this.state.currentView === 'txs' && (
+
+
+
+ )}
+
+ {/* nfts view */}
+ {this.state.currentView === 'nfts' && (
+
+
+
+ )}
)}
@@ -476,7 +527,7 @@ const withStore = (
) => {
// use named function to prevent re-rendering failure
return function WalletMobileContainerWithStore() {
- const { walletAddress, assets, isMagicWallet, loadAssets } =
+ const { walletAddress, assets, loadAssets } =
Store.useState(getWeb3State);
const [isSettingOpen, setIsSettingOpen] = useState(false);
const [isAlertOpen, setIsAlertOpen] = useState(false);
@@ -486,7 +537,6 @@ const withStore = (
return (
- {!walletAddress && (
+ {walletAddress === undefined
+ ? (
+
+ )
+ : (
)}
diff --git a/src/context/LoaderContext.tsx b/src/context/LoaderContext.tsx
index b4815ff4..94608052 100644
--- a/src/context/LoaderContext.tsx
+++ b/src/context/LoaderContext.tsx
@@ -8,12 +8,12 @@ import { useIonLoading } from "@ionic/react";
// Define the type for the user context.
type LoaderContextType = {
isVisible: boolean;
- display: () => Promise;
+ display: (msg?: string) => Promise;
hide: () => Promise;
};
const LoaderContext = createContext({
isVisible: false,
- display: () => Promise.resolve(),
+ display: (msg?: string) => Promise.resolve(),
hide: () => Promise.resolve(),
})
diff --git a/src/interfaces/nft.interface.ts b/src/interfaces/nft.interface.ts
new file mode 100644
index 00000000..68edd633
--- /dev/null
+++ b/src/interfaces/nft.interface.ts
@@ -0,0 +1,6 @@
+import { IChain } from "@/constants/chains";
+import { IAnkrNTFResponse } from "@/servcies/ankr.service";
+
+export type INFT = (IAnkrNTFResponse & {
+ chain: IChain;
+});
\ No newline at end of file
diff --git a/src/interfaces/tx.interface.ts b/src/interfaces/tx.interface.ts
new file mode 100644
index 00000000..51ef9f5e
--- /dev/null
+++ b/src/interfaces/tx.interface.ts
@@ -0,0 +1,137 @@
+export enum TxType {
+ ['0x0'] = 'Natif',
+}
+
+export interface TxInterface1 {
+ timestamp: Date;
+ nonce: number;
+ blockNumber: number;
+ from: string;
+ to: string;
+ gas: number;
+ gasPrice: number;
+ input: string;
+ transactionIndex: string;
+ blockHash: string;
+ value: string;
+ type: string;
+ cumulativeGasUsed: number;
+ gasUsed: number;
+ hash: string;
+ status: number;
+ blockchain: string;
+}
+
+export interface TxInterface {
+ type: string;
+ id: string;
+ attributes: TxAttributes;
+ relationships: Relationships;
+}
+
+export interface TxAttributes {
+ operation_type: string;
+ hash: string;
+ mined_at_block: number;
+ mined_at: string;
+ sent_from: string;
+ sent_to: string;
+ status: string;
+ nonce: number;
+ fee: Fee;
+ transfers: Transfer[];
+ approvals: Approval[];
+ application_metadata: TxApplicationMetadata;
+ flags: AttributesFlags;
+}
+
+export interface TxApplicationMetadata {
+ name: string;
+ icon: Icon;
+ contract_address: string;
+ method: Method;
+}
+
+export interface Icon {
+ url: string;
+}
+
+export interface Method {
+ id: string;
+ name: string;
+}
+
+export interface Approval {
+ fungible_info: FungibleInfo;
+ quantity: Quantity;
+ sender: string;
+}
+
+export interface FungibleInfo {
+ name: string;
+ symbol: string;
+ icon: Icon | null;
+ flags: FungibleInfoFlags;
+ implementations: Implementation[];
+}
+
+export interface FungibleInfoFlags {
+ verified: boolean;
+}
+
+export interface Implementation {
+ chain_id: string;
+ address: string;
+ decimals: number;
+}
+
+export interface Quantity {
+ int: string;
+ decimals: number;
+ float: number;
+ numeric: string;
+}
+
+export interface Fee {
+ fungible_info: FungibleInfo;
+ quantity: Quantity;
+ price: number;
+ value: number;
+}
+
+export interface AttributesFlags {
+ is_trash: boolean;
+}
+
+export interface Transfer {
+ fungible_info: FungibleInfo;
+ direction: string;
+ quantity: Quantity;
+ value: number;
+ price: number;
+ sender: string;
+ recipient: string;
+}
+
+export interface Relationships {
+ chain: Chain;
+ dapp: Dapp;
+}
+
+export interface Chain {
+ links: Links;
+ data: Data;
+}
+
+export interface Data {
+ type: string;
+ id: string;
+}
+
+export interface Links {
+ related: string;
+}
+
+export interface Dapp {
+ data: Data;
+}
diff --git a/src/interfaces/web3.interface.ts b/src/interfaces/web3.interface.ts
index 17a3c9ff..f48d262b 100644
--- a/src/interfaces/web3.interface.ts
+++ b/src/interfaces/web3.interface.ts
@@ -1,5 +1,5 @@
import { StargateClient } from "@cosmjs/stargate";
-import { ethers } from "ethers";
-import { Connection as SolanaClient } from '@solana/web3.js';
+import { Signer as EVMSigner } from "ethers";
+import { Connection as SolanaClient, Signer as SolSigner } from '@solana/web3.js';
-export type Web3ProviderType = ethers.providers.Web3Provider | StargateClient | SolanaClient; // | Avalanche;
+export type Web3SignerType = EVMSigner // | SolSigner;
\ No newline at end of file
diff --git a/src/lib/assets/svg/download-outline.svg b/src/lib/assets/svg/download-outline.svg
new file mode 100644
index 00000000..f9af237e
--- /dev/null
+++ b/src/lib/assets/svg/download-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/assets/svg/extension-puzzle-outline.svg b/src/lib/assets/svg/extension-puzzle-outline.svg
new file mode 100644
index 00000000..f37b76a1
--- /dev/null
+++ b/src/lib/assets/svg/extension-puzzle-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/assets/svg/extension-puzzle.svg b/src/lib/assets/svg/extension-puzzle.svg
new file mode 100644
index 00000000..5c3ad1ce
--- /dev/null
+++ b/src/lib/assets/svg/extension-puzzle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/assets/svg/key-outline.svg b/src/lib/assets/svg/key-outline.svg
new file mode 100644
index 00000000..65aa8853
--- /dev/null
+++ b/src/lib/assets/svg/key-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/assets/svg/wallet-outline.svg b/src/lib/assets/svg/wallet-outline.svg
new file mode 100644
index 00000000..0089d6d7
--- /dev/null
+++ b/src/lib/assets/svg/wallet-outline.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/assets/svg/wallet.svg b/src/lib/assets/svg/wallet.svg
new file mode 100644
index 00000000..7f1c9b0e
--- /dev/null
+++ b/src/lib/assets/svg/wallet.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/lib/constant.ts b/src/lib/constant.ts
new file mode 100644
index 00000000..dc791a98
--- /dev/null
+++ b/src/lib/constant.ts
@@ -0,0 +1,31 @@
+// enum of available signin methods
+export enum SigninMethod {
+ Google = 'connect-google',
+ Email = 'connect-email',
+ EmailLink = 'connect-email-link',
+ Wallet = 'connect-wallet'
+}
+
+export const DEFAULT_SIGNIN_METHODS: SigninMethod[] = [
+ SigninMethod.Google,
+ SigninMethod.Email,
+ SigninMethod.EmailLink,
+ SigninMethod.Wallet
+];
+
+export enum KEYS {
+ AUTH_SIGNATURE_KEY = 'hexa-signature',
+ AUTH_SIGNATURE_VALUE = 'hexa-signature-value',
+ STORAGE_PRIVATEKEY_KEY = 'hexa-private-key',
+ STORAGE_SEED_KEY = 'hexa-seed-key',
+ STORAGE_SECRET_KEY = 'hexa-secret',
+ STORAGE_BACKUP_KEY = 'hexa-backup',
+ STORAGE_SKIP_BACKUP_KEY = 'hexa-skip',
+ STORAGE_EMAIL_FOR_SIGNIN_KEY = 'hexa-connect-email-for-sign-in',
+ URL_QUERYPARAM_FINISH_SIGNUP = 'finishSignUp',
+ STORAGE_AUTH_METHOD_KEY = 'hexa-auth-method'
+}
+
+export const MAX_SKIP_BACKUP_TIME = 15 * 60 * 1000; // 15 minutes
+
+export * from '@/constants/chains';
\ No newline at end of file
diff --git a/src/lib/index.ts b/src/lib/index.ts
new file mode 100644
index 00000000..fbdc8584
--- /dev/null
+++ b/src/lib/index.ts
@@ -0,0 +1,6 @@
+import { SigninMethod } from './constant';
+import { KEYS, CHAIN_AVAILABLES, NETWORK } from './constant';
+import { parseApiKey } from './utils';
+
+export * from './sdk';
+export { KEYS, SigninMethod, CHAIN_AVAILABLES, NETWORK, parseApiKey };
diff --git a/src/lib/interfaces/auth-provider.interface.ts b/src/lib/interfaces/auth-provider.interface.ts
new file mode 100644
index 00000000..55fe26f1
--- /dev/null
+++ b/src/lib/interfaces/auth-provider.interface.ts
@@ -0,0 +1,23 @@
+import { Auth, User } from 'firebase/auth';
+
+type Unsubscribe = () => void;
+
+// type User = { uid: string; isAnonymous: boolean };
+type UserCredential = { user: User };
+
+export interface IAuthProvider {
+ signinWithGoogle: () => Promise;
+ sendLinkToEmail: (email: string, ops?: { url?: string }) => Promise;
+ signInWithLink: () => Promise;
+ signInAsAnonymous: () => Promise;
+ signInWithEmailPwd: (
+ email: string,
+ password: string,
+ privateKey?: string
+ ) => Promise;
+ signOut: () => Promise;
+ getOnAuthStateChanged: (cb: (user: User | null) => void) => Unsubscribe;
+ getCurrentUserAuth: () => Promise;
+ updateUserAndTriggerStateChange: () => Promise;
+ initialize: (auth: Auth, ops?: string) => void;
+}
diff --git a/src/lib/interfaces/dialog-element.interface.ts b/src/lib/interfaces/dialog-element.interface.ts
new file mode 100644
index 00000000..8100624a
--- /dev/null
+++ b/src/lib/interfaces/dialog-element.interface.ts
@@ -0,0 +1,86 @@
+import { DialogUIOptions } from './sdk.interface';
+
+export type WalletConnectType =
+ | 'browser-extension'
+ | 'import-privatekey'
+ | 'import-seed';
+
+export type FirebaseWeb3ConnectDialogElement = HTMLElement & {
+ ops: DialogUIOptions | undefined;
+
+ /**
+ * Method that reset the dialog element
+ */
+ reset(): void;
+
+ /**
+ * Method that display the dialog element to the user
+ */
+ showModal(): void;
+
+ /**
+ * Method that hide the dialog element from the user
+ */
+ hideModal(): void;
+
+ /**
+ * Method that remove a spinner and display a check icon to the user
+ */
+ toggleSpinnerAsCheck(message?: string): Promise;
+
+ /**
+ * Method that remove spinner and display a cross icon
+ * with an optional message to the user
+ * @param message
+ */
+ toggleSpinnerAsCross(message?: string): Promise;
+
+ /**
+ * Methods that display a prompt to the user
+ * and return the user's response as a string.
+ */
+ promptPassword(): Promise;
+
+ /**
+ * Methods that display a prompt to the user
+ * and return the user's response as Object `{password: string; email: string;}`
+ */
+ promptEmailPassword(ops?: {
+ hideEmail?: boolean;
+ hidePassword?: boolean;
+ }): Promise<{ password?: string; email?: string }>;
+
+ /**
+ * Methods that display a prompt to the user to backup their wallet
+ * and return the user's response as Object.
+ */
+ promptBackup(): Promise<{
+ withEncryption?: boolean | undefined;
+ skip?: boolean | undefined;
+ }>;
+
+ /**
+ * Method that display prompt to user before signout
+ * to request backup of wallet seed phrase
+ */
+ promptSignoutWithBackup(): Promise<{
+ withEncryption?: boolean | undefined;
+ skip?: boolean | undefined;
+ clearStorage?: boolean;
+ cancel?: boolean;
+ }>;
+
+ /**
+ * Methods that display a prompt to the user to select the wallet type
+ * that they want to connect with.
+ * This method returns the user's response as a string.
+ */
+ promptWalletType(): Promise;
+
+ /**
+ * Methods that display a prompt to the user to select the authentication method
+ * that they want to connect with.
+ * @info This method is under development and will be available in future releases.
+ */
+ promptAuthMethods(): Promise;
+};
diff --git a/src/lib/interfaces/sdk.interface.ts b/src/lib/interfaces/sdk.interface.ts
new file mode 100644
index 00000000..3e1122ef
--- /dev/null
+++ b/src/lib/interfaces/sdk.interface.ts
@@ -0,0 +1,29 @@
+import { SigninMethod } from '../constant';
+import { IStorageProvider } from './storage-provider.interface';
+
+export type SDKApiKey = string;
+
+export type DialogUIOptions = {
+ integrator?: string;
+ logoUrl?: string;
+ template?: {
+ primaryColor?: string;
+ secondaryColor?: string;
+ backgroundColor?: string;
+ };
+ isLightMode?: boolean;
+ enabledSigninMethods?: SigninMethod[];
+ ops?: {
+ authProvider?: {
+ authEmailUrl?: string;
+ };
+ };
+};
+
+export type SDKOptions = {
+ dialogUI?: Omit;
+ chainId?: number;
+ rpcUrl?: string;
+ enabledSigninMethods?: SigninMethod[];
+ storageService?: IStorageProvider;
+};
diff --git a/src/lib/interfaces/storage-provider.interface.ts b/src/lib/interfaces/storage-provider.interface.ts
new file mode 100644
index 00000000..d3ffead3
--- /dev/null
+++ b/src/lib/interfaces/storage-provider.interface.ts
@@ -0,0 +1,8 @@
+export interface IStorageProvider {
+ initialize(apiKey?: string): Promise;
+ getItem(key: string): Promise;
+ setItem(key: string, value: string): Promise;
+ removeItem(key: string): Promise;
+ clear(): Promise;
+ getUniqueID(): string;
+}
diff --git a/src/lib/interfaces/storage-service.interface.ts b/src/lib/interfaces/storage-service.interface.ts
new file mode 100644
index 00000000..ed370940
--- /dev/null
+++ b/src/lib/interfaces/storage-service.interface.ts
@@ -0,0 +1,7 @@
+import { IStorageProvider } from './storage-provider.interface';
+
+export interface IStorageService extends Omit {
+ isExistingPrivateKeyStored(): Promise;
+ executeBackup(requestBackup: boolean, secret?: string): Promise;
+ initialize(storageProvider: IStorageProvider, apiKey?: string): Promise;
+}
diff --git a/src/lib/interfaces/walllet-provider.interface.ts b/src/lib/interfaces/walllet-provider.interface.ts
new file mode 100644
index 00000000..e7ae6843
--- /dev/null
+++ b/src/lib/interfaces/walllet-provider.interface.ts
@@ -0,0 +1,9 @@
+import { Web3Wallet } from '../networks/web3-wallet';
+
+export interface IWalletProvider {
+ connectWithExternalWallet: (ops?: {
+ chainId?: number;
+ }) => Promise;
+ generateWalletFromMnemonic: (ops: T) => Promise;
+ generateDID: (address: string) => string;
+}
diff --git a/src/lib/networks/bitcoin.ts b/src/lib/networks/bitcoin.ts
new file mode 100644
index 00000000..072bc359
--- /dev/null
+++ b/src/lib/networks/bitcoin.ts
@@ -0,0 +1,142 @@
+import { generateMnemonic, mnemonicToSeedSync } from 'bip39';
+import { BIP32Factory } from 'bip32';
+import * as bitcoin from 'bitcoinjs-lib';
+import ecc from '@bitcoinerlab/secp256k1';
+import { Web3Wallet } from './web3-wallet';
+import { TransactionResponse } from '@ethersproject/abstract-provider';
+import { IWalletProvider } from '../interfaces/walllet-provider.interface';
+import { Logger } from '../utils';
+
+const generateDID = (address: string) => {
+ return `did:ethr:${address}`;
+};
+
+class BTCWallet extends Web3Wallet {
+ public chainId: number;
+ private _mnemonic!: string;
+
+ constructor(
+ mnemonic: string,
+ network: bitcoin.Network = bitcoin.networks.bitcoin,
+ derivationPath: string = "m/44'/0'/0'/0/0"
+ ) {
+ super();
+ if (!mnemonic) {
+ throw new Error('Mnemonic is required to generate wallet');
+ }
+ const bip32 = BIP32Factory(ecc);
+ const seed = mnemonicToSeedSync(mnemonic);
+ const path = derivationPath;
+ // generate key pair
+ const { privateKey, publicKey } = bip32.fromSeed(seed).derivePath(path);
+ if (!privateKey || !publicKey) {
+ throw new Error('Failed to generate key pair');
+ }
+ // generate address
+ const { address } = bitcoin.payments.p2pkh({
+ pubkey: publicKey,
+ network
+ });
+ // check if address is generated
+ if (!address) {
+ throw new Error('Failed to generate wallet');
+ }
+ // set wallet properties
+ this.address = address;
+ this.publicKey = publicKey.toString('hex');
+ this._privateKey = privateKey.toString('hex');
+ this.chainId = network.wif;
+ this._mnemonic = mnemonic;
+ }
+
+ sendTransaction(tx: unknown): Promise {
+ Logger.log('sendTransaction', tx);
+ throw new Error('Method not implemented.');
+ }
+
+ signTransaction(tx: unknown): Promise {
+ Logger.log('signTransaction', tx);
+ throw new Error('Method not implemented.');
+ }
+
+ async signMessage(message: string): Promise {
+ Logger.log('signMessage', message);
+ // Sign the message with the private key
+ const bufferMsg = Buffer.from(message, 'utf-8');
+ const hash = bitcoin.crypto.sha256(bufferMsg);
+ // generate KeyPair from private key
+ const keyPair = this._generateKeyPair();
+ const signature = keyPair.sign(hash);
+ Logger.log(`Signature: ${signature.toString('base64')}\n`);
+ return signature.toString('base64');
+ }
+
+ verifySignature(message: string, signature: string): boolean {
+ if (!this.address) {
+ throw new Error('Address is required to verify signature');
+ }
+ const bufferMsg = Buffer.from(message, 'utf-8');
+ const hash = bitcoin.crypto.sha256(bufferMsg);
+ // generate KeyPair from private key
+ const keyPair = this._generateKeyPair();
+ const isValid = keyPair.verify(hash, Buffer.from(signature, 'base64'));
+ return isValid;
+ }
+
+ async switchNetwork(chainId: number): Promise {
+ Logger.log('switchNetwork', chainId);
+ throw new Error('Method not implemented.');
+ }
+
+ async getSigner(): Promise {
+ Logger.log('getSigner');
+ throw new Error('Method not implemented.');
+ }
+
+ private _generateKeyPair() {
+ if (!this._mnemonic) {
+ throw new Error('Mnemonic is required to sign message');
+ }
+ const seed = mnemonicToSeedSync(this._mnemonic);
+ const path = "m/44'/0'/0'/0/0";
+ // generate key pair
+ const bip32 = BIP32Factory(ecc);
+ const keyPair = bip32.fromSeed(seed).derivePath(path);
+ if (!keyPair) {
+ throw new Error('Failed to generate key pair');
+ }
+ return keyPair;
+ }
+}
+
+const generateWalletFromMnemonic = async (ops: {
+ mnemonic?: string;
+ derivationPath?: string;
+ network?: bitcoin.Network;
+}): Promise => {
+ const { mnemonic = generateMnemonic(), derivationPath, network } = ops;
+ if (derivationPath) {
+ const purpose = derivationPath?.split('/')[1];
+ if (purpose !== "44'") {
+ throw new Error('Invalid derivation path ');
+ }
+ }
+ const wallet = new BTCWallet(mnemonic, network, derivationPath);
+ return wallet;
+};
+
+const btcWallet: Readonly<
+ IWalletProvider<{
+ mnemonic?: string;
+ derivationPath?: string;
+ network?: bitcoin.Network;
+ }>
+> = Object.freeze({
+ connectWithExternalWallet: async () => {
+ throw new Error('Method not implemented.');
+ },
+ generateWalletFromMnemonic,
+ generateDID
+});
+
+export default btcWallet;
diff --git a/src/lib/networks/evm.ts b/src/lib/networks/evm.ts
new file mode 100755
index 00000000..5a985dad
--- /dev/null
+++ b/src/lib/networks/evm.ts
@@ -0,0 +1,289 @@
+import { Wallet, utils, providers, Contract, constants, Signer } from 'ethers';
+import { CHAIN_AVAILABLES, CHAIN_DEFAULT } from '../constant';
+import { generateMnemonic, validateMnemonic } from 'bip39';
+import { IWalletProvider } from '../interfaces/walllet-provider.interface';
+import { Web3Wallet } from './web3-wallet';
+import { Logger } from '../utils';
+// import cryptoRandomString from 'crypto-random-string';
+// const generatePrivateKey = () => {
+// // Générer une clé privée aléatoire de 32 octets
+// return cryptoRandomString({ length: 64, type: 'hex' });
+// };
+
+// Generate a DID from an Ethereum address
+const generateDID = (address: string) => {
+ return `did:ethr:${address}`;
+};
+
+class EVMWallet extends Web3Wallet {
+ public did!: string;
+ public chainId: number;
+
+ constructor(mnemonic: string, provider: providers.JsonRpcProvider) {
+ super();
+ if (!mnemonic) {
+ throw new Error('Mnemonic is required to generate wallet');
+ }
+ const _w = Wallet.fromMnemonic(mnemonic);
+ const _wallet = new Wallet(_w.privateKey, provider);
+ const wallet = _wallet.connect(provider);
+ this.address = wallet.address;
+ this.publicKey = wallet.publicKey;
+ this._privateKey = wallet.privateKey;
+ this.did = generateDID(this.address);
+ this.provider = provider;
+ this.chainId = provider.network.chainId;
+ }
+
+ async switchNetwork(chainId: number): Promise {
+ if (this.chainId === chainId) {
+ return;
+ }
+ const chain = CHAIN_AVAILABLES.find(c => c.id === chainId);
+ if (!chain) {
+ throw new Error('Chain not available');
+ }
+ if (!this.provider) {
+ throw new Error('Provider not available');
+ }
+ const provider = new providers.JsonRpcProvider(chain.rpcUrl, chain.id);
+ this.provider = provider;
+ this.chainId = chainId;
+ }
+
+ async sendTransaction(tx: {
+ to: string;
+ value: string;
+ contractAddress: string;
+ }): Promise {
+ if (!this._privateKey) {
+ throw new Error('Private key is required to send token');
+ }
+ const {
+ to: destination,
+ value: decimalAmount,
+ contractAddress = constants.AddressZero
+ } = tx;
+ try {
+ const wallet = new Wallet(this._privateKey, this.provider);
+ // Check if the receiver address is the same as the token contract address
+ if (destination.toLowerCase() === contractAddress.toLowerCase()) {
+ // Sending tokens to the token contract address
+ throw new Error(
+ 'Sending tokens to ERC20 contract address is not allowed.'
+ );
+ }
+ const amount = utils.parseUnits(decimalAmount.toString()); // Convert 1 ether to wei
+
+ let tx;
+ // Check if the token address is the same as the native ETH address
+ if (
+ contractAddress.toLowerCase() === constants.AddressZero.toLowerCase()
+ ) {
+ Logger.log('[INFO] Sending native token');
+ tx = await wallet.sendTransaction({
+ to: destination,
+ value: amount
+ });
+ } else {
+ Logger.log('[INFO] Sending erc20 token');
+ // ABI (Application Binary Interface) of the ERC20 token contract
+ const tokenABI = [
+ // Standard ERC20 functions
+ 'function balanceOf(address) view returns (uint)',
+ 'function transfer(address to, uint amount) returns (boolean)'
+ ];
+ const wallet = new Wallet(this._privateKey, this.provider);
+ // Load the ERC20 token contract
+ const tokenContract = new Contract(contractAddress, tokenABI, wallet);
+ // Convert amount to wei if necessary
+ // (depends on the token's decimal precision)
+ // Call the transfer function of the ERC20 token contract
+ tx = await tokenContract.transfer(destination, amount);
+ }
+ Logger.log('[INFO] Transaction Hash:', tx.hash);
+ const receipt = await tx.wait();
+ Logger.log('[INFO] Transaction confirmed');
+ return receipt;
+ } catch (error) {
+ Logger.error('[ERROR] _sendToken:', error);
+ throw error;
+ }
+ }
+
+ async signMessage(message: string): Promise {
+ if (!this._privateKey) {
+ throw new Error('Private key is required to sign message');
+ }
+ const wallet = new Wallet(this._privateKey, this.provider);
+ return wallet.signMessage(message);
+ }
+
+ async signTransaction(
+ message: providers.TransactionRequest
+ ): Promise {
+ if (!this._privateKey) {
+ throw new Error('Private key is required to sign transaction');
+ }
+ const wallet = new Wallet(this._privateKey, this.provider);
+ return wallet.signTransaction(message);
+ }
+
+ async getSigner(): Promise {
+ if (!this._privateKey) {
+ throw new Error('Private key is required to get signer');
+ }
+ if (!this.provider) {
+ throw new Error('Provider is required to get signer');
+ }
+ const eoaWallet = new Wallet(this._privateKey, this.provider);
+ const signer = eoaWallet.connect(this.provider);
+ return signer as Signer;
+ }
+
+ verifySignature(message: string, signature: string): boolean {
+ if (!this.publicKey) {
+ throw new Error('Public key is required to verify signature');
+ }
+ return utils.verifyMessage(message, signature) === this.address;
+ }
+}
+
+class ExternalEVMWallet extends Web3Wallet {
+ public chainId: number;
+ public externalProvider!: providers.Web3Provider;
+ private _signer!: Signer;
+
+ constructor(
+ public readonly provider: providers.Web3Provider,
+ fromInitializer: boolean = false
+ ) {
+ super();
+ if (!fromInitializer) {
+ throw new Error('Use create method to initialize ExternalEVMWallet');
+ }
+ this.chainId = provider?.network?.chainId;
+ this.externalProvider = provider;
+ this._signer = provider.getSigner();
+ }
+
+ static async create(chainId: number) {
+ // get current account
+ const externalProvider = new providers.Web3Provider(
+ (window as unknown as WindowWithEthereumProvider).ethereum
+ );
+ await externalProvider.send('eth_requestAccounts', []);
+ // build wallet
+ const wallet = new ExternalEVMWallet(externalProvider, true);
+ // switch to default chain
+ if (wallet.externalProvider?.network?.chainId !== chainId || !wallet.chainId) {
+ await wallet.switchNetwork(chainId);
+ }
+ wallet.chainId = chainId;
+ wallet.address = await wallet._signer.getAddress();
+ return wallet;
+ }
+
+ async switchNetwork(chainId: number): Promise {
+ const chain = CHAIN_AVAILABLES.find(c => c.id === chainId);
+ if (!chain) {
+ throw new Error('Chain not available');
+ }
+ if (chain.type !== 'evm') {
+ throw new Error('Only EVM chain is supported with external wallet.');
+ }
+ const chainIdAsHex = utils.hexValue(chainId);
+ await this.externalProvider.send('wallet_switchEthereumChain', [
+ { chainId: chainIdAsHex }
+ ]);
+ this.chainId = chainId;
+ }
+ async sendTransaction(tx: utils.Deferrable) {
+ return this._signer.sendTransaction(tx);
+ }
+
+ async signMessage(message: string) {
+ return this._signer.signMessage(message);
+ }
+
+ async signTransaction(tx: utils.Deferrable) {
+ return this._signer.signTransaction(tx);
+ }
+
+ async getSigner(): Promise {
+ return this._signer as Signer;
+ }
+
+ verifySignature(message: string, signature: string) {
+ return utils.verifyMessage(message, signature) === this.address;
+ }
+}
+
+const generateWalletFromMnemonic = async (
+ ops: {
+ mnemonic?: string;
+ chainId?: number;
+ } = {}
+) => {
+ const { mnemonic = generateMnemonic(), chainId } = ops;
+ // validate mnemonic
+ if (!validateMnemonic(mnemonic)) {
+ throw new Error('Invalid mnemonic');
+ }
+ const chain = CHAIN_AVAILABLES.find(c => c.id === chainId) || CHAIN_DEFAULT;
+ const provider = new providers.JsonRpcProvider(chain.rpcUrl, chain.id);
+ const web3Wallet = new EVMWallet(mnemonic, provider);
+ return web3Wallet;
+};
+
+// const generateWalletFromPrivateKey = async (
+// privateKey: string,
+// chainId?: number
+// ): Promise => {
+// if (!utils.isHexString(privateKey)) {
+// throw new Error('Invalid private key');
+// }
+
+// const chain = CHAIN_AVAILABLES.find(c => c.id === chainId) || CHAIN_DEFAULT;
+// const provider = new providers.JsonRpcProvider(chain.rpcUrl, chain.id);
+// const wallet = new EVMWallet(privateKey, provider);
+// return wallet;
+// // const ethrDid = generateDID(wallet.address);
+// // return {
+// // privateKey: wallet.privateKey,
+// // publicKey: wallet.publicKey,
+// // address: wallet.address,
+// // provider
+// // };
+// };
+
+interface WindowWithEthereumProvider extends Window {
+ ethereum: providers.ExternalProvider;
+}
+
+const connectWithExternalWallet = async (ops?: {
+ chainId?: number;
+}): Promise => {
+ // check if metamask/browser extension is installed
+ if (!(window as unknown as WindowWithEthereumProvider).ethereum) {
+ throw new Error(`
+ No web3 wallet extension found.
+ Install browser extensions like Metamask or Rabby wallet to connect with the app using your existing or hardware wallet.
+ `);
+ }
+ // return object fromated as Web3Wallet
+ const wallet = await ExternalEVMWallet.create(
+ ops?.chainId || CHAIN_DEFAULT.id
+ );
+ return wallet;
+};
+
+const evmWallet: Readonly<
+ IWalletProvider<{ mnemonic?: string; chainId?: number }>
+> = Object.freeze({
+ connectWithExternalWallet,
+ generateWalletFromMnemonic,
+ generateDID
+});
+
+export default evmWallet;
diff --git a/src/lib/networks/solana.ts b/src/lib/networks/solana.ts
new file mode 100644
index 00000000..0260ff24
--- /dev/null
+++ b/src/lib/networks/solana.ts
@@ -0,0 +1,205 @@
+import { generateMnemonic, mnemonicToSeedSync } from 'bip39';
+import {
+ Connection,
+ Keypair,
+ LAMPORTS_PER_SOL,
+ PublicKey,
+ SystemProgram,
+ Transaction,
+ TransactionResponse,
+ sendAndConfirmTransaction
+} from '@solana/web3.js';
+import {
+ getOrCreateAssociatedTokenAccount,
+ transfer as transferToken,
+ getMint
+} from '@solana/spl-token';
+import { derivePath } from 'ed25519-hd-key';
+import * as bs58 from 'bs58';
+import { Web3Wallet } from './web3-wallet';
+import { IWalletProvider } from '../interfaces/walllet-provider.interface';
+import { NETWORK } from '../constant';
+import { Logger } from '../utils';
+
+declare type bs58 = any;
+
+const generateDID = (address: string) => {
+ return `did:ethr:${address}`;
+};
+
+class SolanaWallet extends Web3Wallet {
+ private _rpcUrl: string = '';
+ public chainId: number = NETWORK.solana;
+
+ constructor(mnemonic: string, derivationPath: string = "m/44'/501'/0'/0'") {
+ super();
+ if (!mnemonic) {
+ throw new Error('Mnemonic is required to generate wallet');
+ }
+ const seed = mnemonicToSeedSync(mnemonic);
+ const path = derivationPath;
+ const derivedSeed = derivePath(path, seed as unknown as string).key;
+ // generate key pair
+ const { publicKey, secretKey } = Keypair.fromSeed(derivedSeed);
+ const privateKey = bs58.encode(secretKey);
+ if (!privateKey || !publicKey) {
+ throw new Error('Failed to generate key pair');
+ }
+ // generate address
+ const address = publicKey.toBase58();
+ // check if address is generated
+ if (!address) {
+ throw new Error('Failed to generate wallet');
+ }
+ // set wallet properties
+ this.address = address;
+ this.publicKey = publicKey.toString();
+ this._privateKey = privateKey;
+ }
+
+ async sendTransaction(tx: {
+ recipientAddress: string;
+ amount: number;
+ tokenAddress?: string;
+ }): Promise {
+ Logger.log('sendTransaction', tx);
+ if (!this._privateKey) {
+ throw new Error('Private key is required to send transaction');
+ }
+ const connection = this._getConnection(this._rpcUrl);
+
+ const recipient = new PublicKey(tx.recipientAddress);
+ let secretKey;
+ let signature;
+
+ if (this._privateKey.split(',').length > 1) {
+ secretKey = new Uint8Array(
+ this._privateKey.split(',') as Iterable
+ );
+ } else {
+ secretKey = bs58.decode(this._privateKey);
+ }
+
+ const from = Keypair.fromSecretKey(secretKey, {
+ skipValidation: true
+ });
+
+ if (tx.tokenAddress) {
+ // Get token mint
+ const mint = await getMint(connection, new PublicKey(tx.tokenAddress));
+
+ // Get the token account of the from address, and if it does not exist, create it
+ const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
+ connection,
+ from,
+ mint.address,
+ from.publicKey
+ );
+
+ // Get the token account of the recipient address, and if it does not exist, create it
+ const recipientTokenAccount = await getOrCreateAssociatedTokenAccount(
+ connection,
+ from,
+ mint.address,
+ recipient
+ );
+
+ signature = await transferToken(
+ connection,
+ from,
+ fromTokenAccount.address,
+ recipientTokenAccount.address,
+ from.publicKey,
+ LAMPORTS_PER_SOL * tx.amount
+ );
+ } else {
+ const transaction = new Transaction().add(
+ SystemProgram.transfer({
+ fromPubkey: from.publicKey,
+ toPubkey: recipient,
+ lamports: LAMPORTS_PER_SOL * tx.amount
+ })
+ );
+
+ // Sign transaction, broadcast, and confirm
+ signature = await sendAndConfirmTransaction(connection, transaction, [
+ from
+ ]);
+ }
+
+ const txRecipe = await connection.getTransaction(signature);
+ if (!txRecipe) {
+ throw new Error('Transaction not found');
+ }
+ return {
+ ...txRecipe
+ };
+ }
+
+ signTransaction(tx: unknown): Promise {
+ Logger.log('signTransaction', tx);
+ throw new Error('Method not implemented.');
+ }
+
+ async signMessage(message: string): Promise {
+ Logger.log('signMessage', message);
+ throw new Error('Method not implemented.');
+ }
+
+ verifySignature(message: string, signature: string): boolean {
+ Logger.log('[INFO]: verifySignature:', { message, signature });
+ if (!this.address) {
+ throw new Error('Address is required to verify signature');
+ }
+ throw new Error('Method not implemented.');
+ }
+
+ async switchNetwork(chainId: number): Promise {
+ Logger.log('switchNetwork', chainId);
+ throw new Error('Method not implemented.');
+ }
+
+ async getSigner(): Promise {
+ return this._getConnection(this._rpcUrl) as Connection;
+ }
+
+ private _getConnection = (rpcUrl?: string): Connection => {
+ const connection = this._provider(rpcUrl);
+
+ return connection;
+ };
+
+ private _provider(rpcUrl?: string) {
+ return new Connection(rpcUrl as string);
+ }
+}
+
+const generateWalletFromMnemonic = async (ops: {
+ mnemonic?: string;
+ derivationPath?: string;
+}): Promise => {
+ const { mnemonic = generateMnemonic(), derivationPath } = ops;
+ if (derivationPath) {
+ const purpose = derivationPath?.split('/')[1];
+ if (purpose !== "44'") {
+ throw new Error('Invalid derivation path ');
+ }
+ }
+ const wallet = new SolanaWallet(mnemonic, derivationPath);
+ return wallet;
+};
+
+const solanaWallet: Readonly<
+ IWalletProvider<{
+ mnemonic?: string;
+ derivationPath?: string;
+ }>
+> = Object.freeze({
+ connectWithExternalWallet: async () => {
+ throw new Error('Method not implemented.');
+ },
+ generateWalletFromMnemonic,
+ generateDID
+});
+
+export default solanaWallet;
diff --git a/src/lib/networks/web3-wallet.ts b/src/lib/networks/web3-wallet.ts
new file mode 100644
index 00000000..d76a4fb9
--- /dev/null
+++ b/src/lib/networks/web3-wallet.ts
@@ -0,0 +1,16 @@
+import { providers } from 'ethers';
+
+export abstract class Web3Wallet {
+ public address!: string;
+ public publicKey: string | undefined;
+ public provider: providers.JsonRpcProvider | undefined;
+ protected _privateKey: string | undefined;
+ public abstract chainId: number;
+
+ public abstract getSigner(): Promise;
+ abstract sendTransaction(tx: unknown): Promise;
+ abstract signTransaction(tx: unknown): Promise;
+ abstract signMessage(message: string): Promise;
+ abstract verifySignature(message: string, signature: string): boolean;
+ abstract switchNetwork(chainId: number): Promise;
+}
diff --git a/src/lib/providers/auth/firebase.ts b/src/lib/providers/auth/firebase.ts
new file mode 100755
index 00000000..85f0fbe9
--- /dev/null
+++ b/src/lib/providers/auth/firebase.ts
@@ -0,0 +1,206 @@
+// Import the functions you need from the SDKs you need
+// import { FirebaseOptions, initializeApp } from 'firebase/app';
+// TODO: Add SDKs for Firebase products that you want to use
+// https://firebase.google.com/docs/web/setup#available-libraries
+import {
+ GoogleAuthProvider,
+ signInWithPopup,
+ sendSignInLinkToEmail,
+ isSignInWithEmailLink,
+ signInWithEmailLink,
+ signInAnonymously,
+ signOut as signOutFormFirebase,
+ Auth,
+ // onAuthStateChanged as onAuthStateChangedFirebase,
+ User,
+ // browserPopupRedirectResolver,
+ signInWithEmailAndPassword,
+ createUserWithEmailAndPassword,
+ sendEmailVerification,
+ getAdditionalUserInfo,
+ onIdTokenChanged
+ // updateCurrentUser,
+ // beforeAuthStateChanged,
+ // getAdditionalUserInfo
+} from 'firebase/auth';
+import { IAuthProvider } from '../../interfaces/auth-provider.interface';
+import { KEYS } from '../../constant';
+import { Logger } from '../../utils';
+
+let auth!: Auth;
+
+const signinWithGoogle = async () => {
+ // Initialize Firebase Google Auth
+ const provider = new GoogleAuthProvider();
+ const credential = await signInWithPopup(
+ auth,
+ provider
+ // browserPopupRedirectResolver
+ );
+ if (!credential) {
+ throw new Error('Credential not found');
+ }
+ // TODO: implement this
+ const { isNewUser } = getAdditionalUserInfo(credential) || {};
+ if (!isNewUser) {
+ // await signOut();
+ // throw new Error(`auth/google-account-already-in-use`);
+ }
+ return credential.user;
+};
+
+const sendLinkToEmail = async (
+ email: string,
+ ops?: {
+ url?: string;
+ }
+) => {
+ const url =
+ ops?.url ||
+ `${window.location.origin}/?${KEYS.URL_QUERYPARAM_FINISH_SIGNUP}=true`;
+ const actionCodeSettings = {
+ // URL you want to redirect back to. The domain (www.example.com) for this
+ // URL must be in the authorized domains list in the Firebase Logger.
+ url,
+ // This must be true.
+ handleCodeInApp: true
+ // dynamicLinkDomain: 'example.page.link'
+ };
+ // Initialize Firebase email Auth
+
+ // await setPersistence(auth, browserLocalPersistence);
+ await sendSignInLinkToEmail(auth, email, actionCodeSettings);
+ // The link was successfully sent. Inform the user.
+ // Save the email locally so you don't need to ask the user for it again
+ // if they open the link on the same device.
+ window.localStorage.setItem(KEYS.STORAGE_EMAIL_FOR_SIGNIN_KEY, email);
+};
+
+const signInWithLink = async () => {
+ if (!isSignInWithEmailLink(auth, window.location.href)) {
+ return undefined;
+ }
+ Logger.log(
+ '[INFO] FirebaseWeb3Connect - signInWithLink: ',
+ window.location.href
+ );
+ // Additional state parameters can also be passed via URL.
+ // This can be used to continue the user's intended action before triggering
+ // the sign-in operation.
+ // Get the email if available. This should be available if the user completes
+ // the flow on the same device where they started it.
+ let email = window.localStorage.getItem(KEYS.STORAGE_EMAIL_FOR_SIGNIN_KEY);
+ if (!email) {
+ // User opened the link on a different device. To prevent session fixation
+ // attacks, ask the user to provide the associated email again. For example:
+ email = window.prompt('Please provide your email for confirmation');
+ }
+ if (!email) {
+ throw new Error('No email provided');
+ }
+
+ // The client SDK will parse the code from the link for you.
+ const credential = await signInWithEmailLink(
+ auth,
+ email,
+ window.location.href
+ ); //.catch(err => err)
+ // You can check if the user is new or existing:
+ // result.additionalUserInfo.isNewUser
+ // Clear email from storage.
+ window.localStorage.removeItem(KEYS.STORAGE_EMAIL_FOR_SIGNIN_KEY);
+ return credential;
+};
+
+const signInAsAnonymous = async () => {
+ return await signInAnonymously(auth);
+};
+
+const signInWithEmailPwd = async (email: string, password: string) => {
+ let user!: User;
+ try {
+ // Create user with email and password
+ const credential = await createUserWithEmailAndPassword(
+ auth,
+ email,
+ password
+ );
+ user = credential.user;
+ if (!user.emailVerified) {
+ await sendEmailVerification(user);
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ console.log('[ERROR] auth with email pwd: ', error, {
+ code: error?.code,
+ message: error?.message
+ });
+ if (
+ error?.code === 'auth/email-already-in-use' ||
+ error?.code === 'auth/network-request-failed'
+ ) {
+ const credential = await signInWithEmailAndPassword(
+ auth,
+ email,
+ password
+ );
+ user = credential.user;
+ return user;
+ }
+
+ // TODO: implement this to prevent user from creating new account if email already in use
+ // if (error?.code === 'auth/email-already-in-use' && privateKey) {
+ // const credential = await signInWithEmailAndPassword(
+ // auth,
+ // email,
+ // password
+ // );
+ // user = credential.user;
+ // return user;
+ // }
+ throw error;
+ }
+ if (!user) {
+ throw new Error('User not found');
+ }
+ return user;
+};
+
+const signOut = async () => {
+ await signOutFormFirebase(auth);
+};
+
+const initialize = (_auth: Auth) => {
+ auth = _auth;
+ // Object.freeze(auth);
+};
+
+const getOnAuthStateChanged = (cb: (user: User | null) => void) => {
+ return onIdTokenChanged(auth, user => cb(user));
+ // return onAuthStateChangedFirebase(auth, user => cb(user));
+};
+
+const getCurrentUserAuth = async () => {
+ return auth.currentUser;
+};
+
+const updateUserAndTriggerStateChange = async () => {
+ const user = auth.currentUser;
+ // update if user is connected
+ await user?.getIdToken(true);
+};
+
+const FirebaseAuthProvider: IAuthProvider = {
+ signinWithGoogle,
+ sendLinkToEmail,
+ signInWithLink,
+ signInAsAnonymous,
+ signInWithEmailPwd,
+ signOut,
+ getOnAuthStateChanged,
+ getCurrentUserAuth,
+ updateUserAndTriggerStateChange,
+ initialize
+};
+
+export default FirebaseAuthProvider;
diff --git a/src/lib/providers/crypto/crypto.ts b/src/lib/providers/crypto/crypto.ts
new file mode 100644
index 00000000..d07b3dd5
--- /dev/null
+++ b/src/lib/providers/crypto/crypto.ts
@@ -0,0 +1,316 @@
+import { Logger } from '../../utils';
+
+export default class Crypto {
+ // iterations: It must be a number and should be set as high as possible.
+ // So, the more is the number of iterations, the more secure the derived key will be,
+ // but in that case it takes greater amount of time to complete.
+ // number of interation - the value of 2145 is randomly chosen
+ private static iteration = 1000;
+
+ // algorithm - AES 256 GCM Mode
+ private static encryptionAlgorithm = 'AES-GCM';
+
+ // random initialization vector length
+ private static ivLength = 12;
+
+ // random salt length
+ private static saltLength = 16;
+
+ // digest: It is a digest algorithms of string type.
+ private static digest = 'SHA-256';
+
+ // text encoder
+ private static enc = new TextEncoder();
+
+ // text decoder
+ private static dec = new TextDecoder();
+
+ /**
+ *
+ * @param u8
+ * @returns
+ */
+ private static base64Encode(u8: Uint8Array): string {
+ return btoa(String.fromCharCode.apply(undefined, Array.from(u8)));
+ }
+
+ /**
+ *
+ * @param str
+ * @returns
+ */
+ private static base64Decode(str: string): Uint8Array {
+ return Uint8Array.from(atob(str), c => c.charCodeAt(0));
+ }
+
+ /**
+ *
+ * @param secretKey
+ * @returns
+ */
+ private static getPasswordKey(secretKey: string): Promise {
+ return window.crypto.subtle.importKey(
+ 'raw',
+ Crypto.enc.encode(secretKey),
+ 'PBKDF2',
+ false,
+ ['deriveKey']
+ );
+ }
+
+ /**
+ *
+ * @param passwordKey
+ * @param salt
+ * @param iteration
+ * @param digest
+ * @param encryptionAlgorithm
+ * @param keyUsage
+ * @returns
+ */
+ private static deriveKey(
+ passwordKey: CryptoKey,
+ salt: Uint8Array,
+ iteration: number,
+ digest: string,
+ encryptionAlgorithm: string,
+ keyUsage: ['encrypt'] | ['decrypt']
+ ): Promise {
+ return window.crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt,
+ iterations: iteration,
+ hash: digest
+ },
+ passwordKey,
+ {
+ name: encryptionAlgorithm,
+ length: 256
+ },
+ false,
+ keyUsage
+ );
+ }
+
+ /**
+ *
+ * @param secretKey
+ * @param data
+ * @returns
+ */
+ public static async encrypt(
+ secretKey: string,
+ data: string
+ ): Promise {
+ try {
+ // generate random salt
+ const salt = window.crypto.getRandomValues(
+ new Uint8Array(Crypto.saltLength)
+ );
+
+ // How to transport IV ?
+ // Generally the IV is prefixed to the ciphertext or calculated using some kind of nonce on both sides.
+ const iv = window.crypto.getRandomValues(new Uint8Array(Crypto.ivLength));
+
+ // create master key from secretKey
+ // The method gives an asynchronous Password-Based Key Derivation
+ // Create a password based key (PBKDF2) that will be used to derive the AES-GCM key used for encryption
+ const passwordKey = await Crypto.getPasswordKey(secretKey);
+
+ // to derive a secret key from a master key for encryption
+ // Create an AES-GCM key using the PBKDF2 key and a randomized salt value.
+ const aesKey = await Crypto.deriveKey(
+ passwordKey,
+ salt,
+ Crypto.iteration,
+ Crypto.digest,
+ Crypto.encryptionAlgorithm,
+ ['encrypt']
+ );
+
+ // create a Cipher object, with the stated algorithm, key and initialization vector (iv).
+ // @algorithm - AES 256 GCM Mode
+ // @key
+ // @iv
+ // @options
+ // Encrypt the input data using the AES-GCM key and a randomized initialization vector (iv).
+ const encryptedContent = await window.crypto.subtle.encrypt(
+ {
+ name: Crypto.encryptionAlgorithm,
+ iv
+ },
+ aesKey,
+ Crypto.enc.encode(data)
+ );
+
+ // convert encrypted string to buffer
+ const encryptedContentArr: Uint8Array = new Uint8Array(encryptedContent);
+
+ // create buffer array with length [salt + iv + encryptedContentArr]
+ const buff: Uint8Array = new Uint8Array(
+ salt.byteLength + iv.byteLength + encryptedContentArr.byteLength
+ );
+
+ // set salt at first postion
+ buff.set(salt, 0);
+
+ // set iv at second postion
+ buff.set(iv, salt.byteLength);
+ // set encrypted at third postion
+ buff.set(encryptedContentArr, salt.byteLength + iv.byteLength);
+ // encode the buffer array
+ const base64Buff: string = Crypto.base64Encode(buff);
+
+ // return encrypted string
+ return base64Buff;
+ } catch (error) {
+ // if any expection occurs
+ Logger.error(`Error - ${error}`);
+ return '';
+ }
+ }
+
+ /**
+ *
+ * @param secretKey
+ * @param ciphertext
+ * @returns
+ */
+ public static async decrypt(secretKey: string, ciphertext: string) {
+ try {
+ // Creates a new Buffer containing the given JavaScript string {str}
+ const encryptedDataBuff = Crypto.base64Decode(ciphertext);
+
+ // extract salt from encrypted data
+ const salt = encryptedDataBuff.slice(0, Crypto.saltLength);
+
+ // extract iv from encrypted data
+ const iv = encryptedDataBuff.slice(
+ Crypto.saltLength,
+ Crypto.saltLength + Crypto.ivLength
+ );
+
+ // extract encrypted text from encrypted data
+ const data = encryptedDataBuff.slice(Crypto.saltLength + Crypto.ivLength);
+
+ // create master key from secretKey
+ // The method gives an asynchronous Password-Based Key Derivation
+ // Create a password based key (PBKDF2) that will be used to derive the AES-GCM key used for decryption.
+ const passwordKey = await Crypto.getPasswordKey(secretKey);
+
+ // to derive a secret key from a master key for decryption
+ // Create an AES-GCM key using the PBKDF2 key and the salt from the ArrayBuffer.
+ const aesKey = await Crypto.deriveKey(
+ passwordKey,
+ salt,
+ Crypto.iteration,
+ Crypto.digest,
+ Crypto.encryptionAlgorithm,
+ ['decrypt']
+ );
+
+ // Return the buffer containing the value of cipher object.
+ // Decrypt the input data using the AES-GCM key and the iv from the ArrayBuffer.
+ const decryptedContent = await window.crypto.subtle.decrypt(
+ {
+ name: Crypto.encryptionAlgorithm,
+ iv
+ },
+ aesKey,
+ data
+ );
+
+ // Returns the result of running encoding's decoder.
+ return Crypto.dec.decode(decryptedContent);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (error: any) {
+ // // if any expection occurs
+ // Logger.error(`Error - ${error}`);
+ // return "";
+ throw new Error(
+ error?.message ||
+ 'Error while decrypting data.
Invalid secret or content.'
+ );
+ }
+ }
+
+ public static async signMessageFromPassword(
+ password: string,
+ message: string
+ ): Promise {
+ // generate key thath allow signature
+ const key = await window.crypto.subtle.importKey(
+ 'raw',
+ Crypto.enc.encode(password),
+ { name: 'HMAC', hash: { name: 'SHA-256' } },
+ false,
+ ['sign']
+ );
+ const signature = await window.crypto.subtle.sign(
+ {
+ name: 'HMAC',
+ hash: { name: 'SHA-256' }
+ },
+ key,
+ Crypto.enc.encode(message)
+ );
+ return Crypto.base64Encode(new Uint8Array(signature));
+ }
+
+ public static async verifySignatureFromPassword(
+ password: string,
+ message: string,
+ signature: string
+ ): Promise {
+ const key = await window.crypto.subtle.importKey(
+ 'raw',
+ Crypto.enc.encode(password),
+ { name: 'HMAC', hash: { name: 'SHA-256' } },
+ false,
+ ['verify']
+ );
+ const result = await window.crypto.subtle.verify(
+ {
+ name: 'HMAC',
+ hash: { name: 'SHA-256' }
+ },
+ key,
+ new Uint8Array(Crypto.base64Decode(signature)),
+ Crypto.enc.encode(message)
+ );
+ return result;
+ }
+
+ /**
+ * @deprecated
+ */
+ public static async generatePrivateKeyFromPassword(
+ password: string
+ ): Promise {
+ const passwordKey = await window.crypto.subtle.importKey(
+ 'raw',
+ Crypto.enc.encode(password),
+ 'PBKDF2',
+ false,
+ ['deriveBits']
+ );
+
+ const salt = window.crypto.getRandomValues(
+ new Uint8Array(Crypto.saltLength)
+ );
+
+ const key = await window.crypto.subtle.deriveBits(
+ {
+ name: 'PBKDF2',
+ salt,
+ iterations: Crypto.iteration,
+ hash: Crypto.digest
+ },
+ passwordKey,
+ 256
+ );
+
+ return Crypto.base64Encode(new Uint8Array(key));
+ }
+}
diff --git a/src/lib/providers/crypto/password.ts b/src/lib/providers/crypto/password.ts
new file mode 100644
index 00000000..22034d08
--- /dev/null
+++ b/src/lib/providers/crypto/password.ts
@@ -0,0 +1,26 @@
+import Crypto from './crypto';
+import { KEYS } from '../../constant';
+import { storageService } from '../../services/storage.service';
+
+export const passwordValidationOrSignature = (value: string) => ({
+ execute: async () => {
+ const privateKey = await storageService.isExistingPrivateKeyStored();
+ const signature = await storageService.getItem(KEYS.AUTH_SIGNATURE_KEY);
+ if (privateKey && signature) {
+ const isSignatureValid = await Crypto.verifySignatureFromPassword(
+ value,
+ KEYS.AUTH_SIGNATURE_VALUE,
+ signature
+ );
+ if (!isSignatureValid) {
+ throw new Error('Invalid password');
+ }
+ } else {
+ const signature = await Crypto.signMessageFromPassword(
+ value,
+ KEYS.AUTH_SIGNATURE_VALUE
+ );
+ await storageService.setItem(KEYS.AUTH_SIGNATURE_KEY, signature);
+ }
+ }
+});
diff --git a/src/lib/providers/storage/firebase.ts b/src/lib/providers/storage/firebase.ts
new file mode 100644
index 00000000..ce2eec7f
--- /dev/null
+++ b/src/lib/providers/storage/firebase.ts
@@ -0,0 +1,43 @@
+import { FirebaseApp } from 'firebase/app';
+import { Database, getDatabase, ref, set as setData, get as getDoc } from 'firebase/database';
+
+const _params: {
+ db: Database | undefined;
+ ops: {
+ collectionName: string;
+ };
+} = {
+ db: undefined,
+ ops: { collectionName: '_logs-users' }
+};
+
+export const initialize = (
+ app: FirebaseApp,
+ ops?: {
+ usersCollectionName?: string;
+ }
+) => {
+ _params.db = getDatabase(app);
+ if (ops) {
+ _params.ops = Object.freeze({
+ ..._params.ops,
+ ...ops
+ } as const);
+ }
+ Object.freeze(_params);
+ Object.seal(_params);
+};
+
+export const set = async (refUrl: string, data: unknown) => {
+ if (!_params.db) throw new Error('Database not initialized');
+ const dbRef = ref(_params.db, `${_params.ops.collectionName}/${refUrl}`);
+ return await setData(dbRef, data);
+};
+
+export const get = async (refUrl: string) => {
+ if (!_params.db) throw new Error('Database not initialized');
+ const dbRef = ref(_params.db, `${_params.ops.collectionName}/${refUrl}`);
+ // check doc exists
+ const snapshot = await getDoc(dbRef);
+ return snapshot.exists() ? snapshot.val() : null;
+}
\ No newline at end of file
diff --git a/src/lib/providers/storage/fleek.ts b/src/lib/providers/storage/fleek.ts
new file mode 100644
index 00000000..881e2361
--- /dev/null
+++ b/src/lib/providers/storage/fleek.ts
@@ -0,0 +1,29 @@
+import { FleekSdk, ApplicationAccessTokenService } from '@fleekxyz/sdk';
+
+let fleekSdk!:FleekSdk;
+
+const initialize = Object.freeze((clientId: string) => {
+ const applicationService = new ApplicationAccessTokenService({
+ clientId,
+ });
+ fleekSdk = new FleekSdk({ accessTokenService: applicationService });
+});
+
+const uploadToIPFS = Object.freeze(async (filename: string, content: Buffer) => {
+ if (!fleekSdk) {
+ throw new Error('Fleek SDK not initialized');
+ }
+ const result = await fleekSdk.ipfs().add({
+ path: filename,
+ content: content,
+ })
+
+ return result
+});
+
+const FleekStorageProvider = {
+ initialize,
+ uploadToIPFS,
+}
+
+export default FleekStorageProvider;
\ No newline at end of file
diff --git a/src/lib/providers/storage/local.ts b/src/lib/providers/storage/local.ts
new file mode 100644
index 00000000..49dc3581
--- /dev/null
+++ b/src/lib/providers/storage/local.ts
@@ -0,0 +1,139 @@
+import { IStorageProvider } from '../../interfaces/storage-provider.interface';
+import Crypto from '../crypto/crypto';
+
+const generateBucketNameUsingWebGlSignature = () => {
+ const res = [];
+ const canvas = document.createElement('canvas');
+ const gl = canvas.getContext('webgl2');
+ res.push(gl?.getParameter(gl.RENDERER));
+ res.push(gl?.getParameter(gl.VENDOR));
+ const dbgRenderInfo = gl?.getExtension('WEBGL_debug_renderer_info');
+ res.push(dbgRenderInfo?.UNMASKED_RENDERER_WEBGL);
+ res.push(dbgRenderInfo?.UNMASKED_VENDOR_WEBGL);
+ const encoded = new TextEncoder().encode(res.join(''));
+ return btoa(String.fromCharCode(...Array.from(encoded)));
+};
+
+const generateUIDUsingCanvasID = (): string => {
+ const canvas = document.createElement('canvas');
+ canvas.height = 100;
+ canvas.width = 800;
+ const ctx = canvas.getContext('2d');
+ if (ctx !== null) {
+ ctx.font = '30px Arial';
+ ctx?.fillText('Hello World', 20, 90);
+ }
+ return canvas.toDataURL().split(',').pop() as string;
+};
+
+export const Environment = Object.freeze({
+ applyEncryption: () => (process.env.NEXT_PUBLIC_APP_IS_PROD === 'true' ? true : false),
+ bucketName: generateBucketNameUsingWebGlSignature()
+});
+
+const isStringified = Object.freeze((input: string = '') => {
+ try {
+ return JSON.parse(input);
+ } catch {
+ return input;
+ }
+});
+
+class LocalStorage implements IStorageProvider {
+ private _uid!: string;
+ private _inMemoryDB?: Map;
+
+ public async initialize() {
+ const uid = generateUIDUsingCanvasID().slice(0, 16);
+ this._uid = uid;
+ await this._getDatabase();
+ }
+
+ private async _getDatabase() {
+ if (!this._inMemoryDB) {
+ const jsonString =
+ window.localStorage.getItem(Environment.bucketName) || undefined;
+ const data =
+ Environment.applyEncryption() && this._uid && jsonString
+ ? await Crypto.decrypt(this._uid, jsonString)
+ : jsonString;
+ const arrayOfData = isStringified(data);
+ this._inMemoryDB = new Map(arrayOfData);
+ }
+ return this._inMemoryDB as Map;
+ }
+
+ private async _saveDatabase() {
+ if (!this._inMemoryDB) {
+ throw new Error('Database not initialized');
+ }
+ const jsonString = JSON.stringify(Array.from(this._inMemoryDB.entries()));
+ const data =
+ Environment.applyEncryption() && this._uid && jsonString
+ ? await Crypto.encrypt(this._uid, jsonString)
+ : jsonString;
+ window.localStorage.setItem(Environment.bucketName, data);
+ }
+
+ public getUniqueID(): string {
+ return this._uid;
+ }
+
+ /**
+ *
+ * @param key
+ * @returns
+ */
+ public async getItem(key: string): Promise {
+ const result = await this._getDatabase().then(db => db.get(key));
+ return result || null;
+ }
+
+ /**
+ *
+ * @param key
+ * @param value
+ */
+ public async setItem(key: string, value: string): Promise {
+ if (!this._inMemoryDB) {
+ throw new Error('Database not initialized');
+ }
+ this._inMemoryDB.set(key, value);
+ await this._saveDatabase();
+ }
+
+ /**
+ *
+ * @param key
+ */
+ public async removeItem(key: string) {
+ if (!this._inMemoryDB) {
+ throw new Error('Database not initialized');
+ }
+ this._inMemoryDB.delete(key);
+ await this._saveDatabase();
+ if (this._inMemoryDB.size === 0) {
+ window.localStorage.removeItem(Environment.bucketName);
+ }
+ }
+
+ /**
+ *
+ * @param keys
+ */
+ public async removeItems(keys: string[]) {
+ for (const key of keys) await this.removeItem(key);
+ }
+
+ /**
+ *
+ */
+ public async clear() {
+ this._inMemoryDB = new Map();
+ window.localStorage.clear();
+ }
+}
+
+const storageProvider: IStorageProvider = new LocalStorage();
+
+export default storageProvider;
diff --git a/src/lib/providers/storage/localforage.ts b/src/lib/providers/storage/localforage.ts
new file mode 100644
index 00000000..b68d5059
--- /dev/null
+++ b/src/lib/providers/storage/localforage.ts
@@ -0,0 +1,126 @@
+import { IStorageProvider } from '../../interfaces/storage-provider.interface';
+import { Storage } from '@ionic/storage';
+import Crypto from '../crypto/crypto';
+
+const generateBucketNameUsingWebGlSignature = () => {
+ const res = [];
+ const canvas = document.createElement('canvas');
+ const gl = canvas.getContext('webgl2');
+ res.push(gl?.getParameter(gl.RENDERER));
+ res.push(gl?.getParameter(gl.VENDOR));
+ const dbgRenderInfo = gl?.getExtension('WEBGL_debug_renderer_info');
+ res.push(dbgRenderInfo?.UNMASKED_RENDERER_WEBGL);
+ res.push(dbgRenderInfo?.UNMASKED_VENDOR_WEBGL);
+ const encoded = new TextEncoder().encode(res.join(''));
+ return btoa(String.fromCharCode(...Array.from(encoded)));
+};
+
+const generateUIDUsingCanvasID = (): string => {
+ const canvas = document.createElement('canvas');
+ canvas.height = 100;
+ canvas.width = 800;
+ const ctx = canvas.getContext('2d');
+ if (ctx !== null) {
+ ctx.font = '30px Arial';
+ ctx?.fillText('Hello World', 20, 90);
+ }
+ return canvas.toDataURL().split(',').pop() as string;
+};
+
+const Environment = Object.freeze({
+ applyEncryption: () => (process.env.NEXT_PUBLIC_APP_IS_PROD === 'true' ? true : false),
+ bucketName: generateBucketNameUsingWebGlSignature()
+});
+
+const isStringified = Object.freeze((input: string = '') => {
+ try {
+ return JSON.parse(input);
+ } catch {
+ return input;
+ }
+});
+
+class LocalForageStorage implements IStorageProvider {
+ private _uid!: string;
+ private _inMemoryDB?: Map;
+ private _provider?: Storage;
+
+ private async _getDatabase() {
+ if (!this._inMemoryDB) {
+ const values =
+ await this._provider?.get(Environment.bucketName) || undefined;
+ const data =
+ Environment.applyEncryption() && this._uid && values
+ ? JSON.parse(await Crypto.decrypt(this._uid, values))
+ : values;
+ const arrayOfData = isStringified(data);
+ console.log(arrayOfData);
+ this._inMemoryDB = new Map(arrayOfData);
+ }
+ return this._inMemoryDB as Map;
+ }
+
+ private async _saveDatabase() {
+ if (!this._inMemoryDB) {
+ throw new Error('Database not initialized');
+ }
+ const values = Array.from(this._inMemoryDB.entries());
+ const data =
+ Environment.applyEncryption() && this._uid && values
+ ? await Crypto.encrypt(this._uid, JSON.stringify(values))
+ : values;
+ await this._provider?.set(Environment.bucketName, data);
+ }
+
+ async initialize(apiKey?: string | undefined): Promise {
+ this._uid = generateUIDUsingCanvasID().slice(0, 16);
+ const dbKey = Environment.bucketName;
+ this._provider = new Storage({
+ dbKey
+ });
+ await this._provider.create();
+ await this._getDatabase();
+ }
+
+ getUniqueID(): string {
+ return this._uid;
+ }
+
+ async getItem(key: string): Promise {
+ const result = await this._getDatabase().then((db) => db.get(key));
+ return result || null;
+ }
+
+ async setItem(key: string, value: string): Promise {
+ if (!this._inMemoryDB) {
+ throw new Error('Database not initialized');
+ }
+ this._inMemoryDB.set(key, value);
+ await this._saveDatabase();
+ }
+
+ async removeItem(key: string): Promise {
+ if (!this._inMemoryDB) {
+ throw new Error('Database not initialized');
+ }
+ this._inMemoryDB.delete(key);
+ await this._saveDatabase();
+ if (this._inMemoryDB.size === 0) {
+ await this._provider?.remove(Environment.bucketName);
+ }
+ }
+
+ async removesItems(keys: string[]): Promise {
+ for (const key of keys) await this.removeItem(key);
+ }
+
+ async clear(): Promise {
+ this._inMemoryDB = new Map();
+ await this._provider?.clear();
+ }
+
+}
+
+const storageProvider: IStorageProvider = new LocalForageStorage();
+
+export default storageProvider;
\ No newline at end of file
diff --git a/src/lib/sdk.ts b/src/lib/sdk.ts
new file mode 100644
index 00000000..2902ecfa
--- /dev/null
+++ b/src/lib/sdk.ts
@@ -0,0 +1,746 @@
+import authProvider from './providers/auth/firebase';
+import storageProvider from './providers/storage/localforage';
+import './ui/dialog-element/dialogElement';
+import {
+ addAndWaitUIEventsResult,
+ setupSigninDialogElement
+} from './ui/dialog-element';
+import {
+ CHAIN_AVAILABLES,
+ CHAIN_DEFAULT,
+ DEFAULT_SIGNIN_METHODS,
+ KEYS,
+ MAX_SKIP_BACKUP_TIME,
+ NETWORK,
+ SigninMethod
+} from './constant';
+// import { parseApiKey } from './utils';
+import { generateMnemonic, initWallet } from './services/wallet.service.ts';
+
+import { Auth } from 'firebase/auth';
+import { SDKOptions } from './interfaces/sdk.interface';
+import { storageService } from './services/storage.service';
+import { Web3Wallet } from './networks/web3-wallet';
+import Crypto from './providers/crypto/crypto';
+import { Logger } from './utils';
+import {
+ initialize as initializeRealtimeDB,
+ set, get
+} from './providers/storage/firebase';
+import { authWithExternalWallet } from './services/auth.servcie';
+import { FirebaseWeb3ConnectDialogElement } from './interfaces/dialog-element.interface';
+import { passwordValidationOrSignature } from './providers/crypto/password';
+
+export class FirebaseWeb3Connect {
+ private readonly _apiKey!: string;
+ private _ops?: SDKOptions;
+ private _encryptedSecret!: string | undefined;
+ private _uid!: string | undefined;
+ private _cloudBackupEnabled!: boolean | undefined;
+ private _wallet!: Web3Wallet | undefined;
+ private _wallets: Web3Wallet[] = [];
+ private _requestSignout: boolean = false;
+
+ get provider() {
+ return this._wallet?.provider;
+ }
+
+ get userInfo() {
+ return this._wallet
+ ? {
+ address: this._wallet.address,
+ publicKey: this._wallet.publicKey,
+ chainId: this._wallet.chainId,
+ uid: this._uid,
+ cloudBackupEnabled: this._cloudBackupEnabled
+ }
+ : null;
+ }
+
+ get wallet() {
+ return this._wallet;
+ }
+
+ constructor(auth: Auth, apiKey: string, ops?: SDKOptions) {
+ this._apiKey = apiKey; // parseApiKey(apiKey.slice(2));
+ this._ops = {
+ enabledSigninMethods: DEFAULT_SIGNIN_METHODS,
+ ...ops
+ };
+ // initialize service dependencies
+ authProvider.initialize(auth);
+ // set storage.uid
+ storageService.initialize(this._ops?.storageService || storageProvider);
+ // init realtimeDatabase users collection
+ initializeRealtimeDB(auth.app);
+ // check if window is available and HTMLDialogElement is supported
+ if (!window || !window.HTMLDialogElement) {
+ throw new Error(
+ '[ERROR] FirebaseWeb3Connect: HTMLDialogElement not supported'
+ );
+ }
+ Logger.log(`[INFO] FirebaseWeb3Connect initialized and ready!`, {
+ config: this._ops,
+ isProd: process.env.NEXT_PUBLIC_APP_IS_PROD,
+ apiKey: this._apiKey,
+ auth
+ });
+ }
+
+ static isConnectWithLink() {
+ // check special paramettre in url
+ const isSignInWithLink = window.location.search.includes(
+ KEYS.URL_QUERYPARAM_FINISH_SIGNUP
+ );
+ if (!isSignInWithLink) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public async connectWithLink() {
+ if (!FirebaseWeb3Connect.isConnectWithLink()) {
+ return;
+ }
+ try {
+ authProvider.signInWithLink();
+ } catch (error: unknown) {
+ Logger.error(
+ '[ERROR] FirebaseWeb3Connect - connectWithLink: ',
+ (error as Error).message
+ );
+ throw error;
+ }
+ }
+
+ public async connectWithUI(isLightMode?: boolean) {
+ if (this._requestSignout && this._uid) {
+ this._requestSignout = false;
+ await authProvider.updateUserAndTriggerStateChange();
+ return this.userInfo;
+ }
+ this._requestSignout = false;
+ // check if have an existing auth method setup
+ const authMethod = (await storageService.getItem(
+ KEYS.STORAGE_AUTH_METHOD_KEY
+ )) as SigninMethod | null;
+ // build UI
+ const dialogElement = await setupSigninDialogElement(document.body, {
+ isLightMode,
+ enabledSigninMethods:
+ authMethod && authMethod !== SigninMethod.Wallet
+ ? [authMethod, SigninMethod.Wallet]
+ : this._ops?.enabledSigninMethods,
+ integrator: this._ops?.dialogUI?.integrator,
+ logoUrl: this._ops?.dialogUI?.logoUrl,
+ ops: this._ops?.dialogUI?.ops
+ });
+ // open modal
+ dialogElement.showModal();
+ try {
+ // wait for connect event
+ const {
+ isAnonymous = false,
+ uid,
+ authMethod
+ } = (await addAndWaitUIEventsResult(dialogElement)) || {};
+ // store default auth method
+ if (authMethod && authMethod !== SigninMethod.Wallet) {
+ await storageService.setItem(KEYS.STORAGE_AUTH_METHOD_KEY, authMethod);
+ }
+ // handle close event && anonymous user from External wallet
+ if (!uid && !isAnonymous) {
+ dialogElement.hideModal();
+ await new Promise(resolve => setTimeout(resolve, 225));
+ dialogElement.remove();
+ return this.userInfo;
+ }
+ // init external wallet here,
+ // all other wallet will be initialized after user connection
+ // using the `onAuthStateChanged` hook
+ if (!uid && isAnonymous) {
+ // first connect external wallet
+ await this._initWallets({ uid: '', isAnonymous });
+ // then connect with auth provider as Anonymous
+ const { uid: anonymousUid } = await authWithExternalWallet();
+ this._uid = anonymousUid;
+ // manage UI & close modal
+ await dialogElement.toggleSpinnerAsCheck();
+ dialogElement.hideModal();
+ // wait 225ms to let the dialog close wth animation
+ await new Promise(resolve => setTimeout(resolve, 225));
+ // remove dialog element
+ dialogElement?.remove();
+ return this.userInfo;
+ }
+ // handle user connection
+ this._uid = uid || this._uid;
+ if (!this._uid) {
+ throw new Error('User not connected');
+ }
+ if (isAnonymous) {
+ throw new Error('External wallet have to be handled to be setup.');
+ }
+ // await user auth state changed that tigger wallet initialization
+ await new Promise(resolve => {
+ const unsubscribe = authProvider.getOnAuthStateChanged(async user => {
+ if (user && this.userInfo?.address) {
+ console.log({ user, userInfo: this.userInfo });
+ resolve(user);
+ unsubscribe();
+ }
+ });
+ });
+ } catch (error: unknown) {
+ const message =
+ (error as Error)?.message || 'An error occured while connecting';
+ await dialogElement.toggleSpinnerAsCross(message);
+ throw error;
+ }
+ await dialogElement.toggleSpinnerAsCheck();
+ // close modal with animation and resolve the promise with user info
+ dialogElement.hideModal();
+ // wait 225ms to let the dialog close wth animation
+ await new Promise(resolve => setTimeout(resolve, 225));
+ // remove dialog element
+ dialogElement?.remove();
+ console.log(`[INFO] Closing dialog`, { userInfo: this.userInfo });
+ return this.userInfo;
+ }
+
+ public async signout(withUI?: boolean, isLightMode?: boolean) {
+ this._requestSignout = true;
+ // display dialog to backup seed if withUI is true
+ const isExternalWallet = !this.wallet?.publicKey;
+ if (withUI && !isExternalWallet) {
+ const dialogElement = await setupSigninDialogElement(document.body, {
+ isLightMode,
+ enabledSigninMethods: [SigninMethod.Wallet],
+ integrator: this._ops?.dialogUI?.integrator,
+ logoUrl: this._ops?.dialogUI?.logoUrl,
+ ops: this._ops?.dialogUI?.ops
+ });
+ // remove all default login buttons
+ const btnsElement = dialogElement.shadowRoot?.querySelector(
+ 'dialog .buttonsList'
+ ) as HTMLElement;
+ btnsElement.remove();
+ // display modal
+ dialogElement.showModal();
+ const {
+ withEncryption,
+ skip: reSkip,
+ clearStorage,
+ cancel
+ } = await dialogElement.promptSignoutWithBackup();
+ if (cancel) {
+ dialogElement.hideModal();
+ // wait 250ms to let the dialog close wth animation
+ await new Promise(resolve => setTimeout(resolve, 250));
+ // remove dialog element from DOM
+ dialogElement.remove();
+ return;
+ }
+ if (!reSkip && this._encryptedSecret) {
+ await storageService.executeBackup(
+ Boolean(withEncryption),
+ await Crypto.decrypt(
+ storageService.getUniqueID(),
+ this._encryptedSecret
+ )
+ );
+ }
+ if (reSkip === true) {
+ await storageService.setItem(KEYS.STORAGE_SKIP_BACKUP_KEY, `${Date.now()}`);
+ }
+
+ if (clearStorage) {
+ await storageService.clear();
+ await authProvider.signOut();
+ }
+ dialogElement.hideModal();
+ // wait 250ms to let the dialog close wth animation
+ await new Promise(resolve => setTimeout(resolve, 150));
+ // remove dialog element from DOM
+ dialogElement.remove();
+ }
+
+ this._wallet = undefined;
+ this._wallets = [];
+ this._encryptedSecret = undefined;
+
+ if (isExternalWallet && withUI) {
+ await authProvider.signOut();
+ return;
+ }
+
+ const unsubscribe = authProvider.getOnAuthStateChanged(user => {
+ if (user && this._requestSignout === true) {
+ unsubscribe();
+ const t = setTimeout(async () => {
+ await this.connectWithUI();
+ clearTimeout(t);
+ }, 500);
+ }
+ });
+ await authProvider.updateUserAndTriggerStateChange();
+ // await storageService.removeItem(KEYS.STORAGE_SECRET_KEY);
+ // await authProvider.signOut();
+ }
+
+ public async backupWallet(withUI?: boolean, isLightMode?: boolean) {
+ if (withUI) {
+ const dialogElement = await setupSigninDialogElement(document.body, {
+ isLightMode,
+ enabledSigninMethods: [SigninMethod.Wallet],
+ integrator: this._ops?.dialogUI?.integrator,
+ logoUrl: this._ops?.dialogUI?.logoUrl,
+ ops: this._ops?.dialogUI?.ops
+ });
+ // remove all default login buttons
+ const btnsElement = dialogElement.shadowRoot?.querySelector(
+ 'dialog .buttonsList'
+ ) as HTMLElement;
+ btnsElement.remove();
+ dialogElement.showModal();
+ const { withEncryption, skip: reSkip } =
+ await dialogElement.promptBackup();
+ if (!reSkip && this._encryptedSecret) {
+ await storageService.executeBackup(
+ Boolean(withEncryption),
+ await Crypto.decrypt(
+ storageService.getUniqueID(),
+ this._encryptedSecret
+ )
+ );
+ }
+ if (reSkip === true) {
+ await storageService.setItem(KEYS.STORAGE_SKIP_BACKUP_KEY, `${Date.now()}`);
+ }
+ dialogElement.hideModal();
+ await new Promise(resolve => setTimeout(resolve, 125));
+ dialogElement.remove();
+ } else {
+ throw new Error('Backup wallet without UI is not implemented yet');
+ }
+ }
+
+ /**
+ * Method that manage the entire wallet management process base on user state.
+ * Wallet values are set with the corresponding method base on the user authentication provider.
+ * If no user is connected, all wallet values are set to null with a default provider and the method will return null.
+ *
+ * @param cb Call back function that return the formated user information to the caller.
+ * @returns
+ */
+ public onConnectStateChanged(cb: (user: { address: string } | null) => void) {
+ return authProvider.getOnAuthStateChanged(async user => {
+ console.log('[INFO] bof onConnectStateChanged()', user);
+ if (user?.uid && !user?.emailVerified && !user?.isAnonymous) {
+ await this._displayVerifyEMailModal();
+ return;
+ }
+ this._uid = user?.uid;
+
+ switch(true) {
+ // manage external wallet
+ case !this.userInfo && user && user?.isAnonymous: {
+ // initialize all wallets
+ await this._initWallets(user as any);
+ break;
+ }
+ // manage local wallet
+ case !this.userInfo && user && !user.isAnonymous && this._requestSignout === false: {
+ try {
+ // initialize all wallets
+ await this._initWallets(user as any);
+ // check local storage to existing tag to trigger backup download of private key
+ const requestBackup = localStorage.getItem(KEYS.STORAGE_BACKUP_KEY);
+ if (this.userInfo && requestBackup && this._encryptedSecret) {
+ await storageService.executeBackup(
+ Boolean(requestBackup),
+ await Crypto.decrypt(
+ storageService.getUniqueID(),
+ this._encryptedSecret
+ )
+ );
+ }
+ // ask to download if user skip download prompt from more than 15 minutes
+ const skip = await storageService.getItem(
+ KEYS.STORAGE_SKIP_BACKUP_KEY
+ );
+ const skipTime = skip ? parseInt(skip) : Date.now();
+ // check if is more than 15 minutes
+ // TODO: check if is working correctly
+ const isOut = Date.now() - skipTime > MAX_SKIP_BACKUP_TIME;
+ const dialogElement = document.querySelector(
+ 'firebase-web3connect-dialog'
+ ) as FirebaseWeb3ConnectDialogElement;
+ if (
+ this.userInfo &&
+ isOut &&
+ this._encryptedSecret &&
+ dialogElement
+ ) {
+ const { withEncryption, skip: reSkip } =
+ await dialogElement.promptBackup();
+ if (!reSkip) {
+ await storageService.executeBackup(
+ Boolean(withEncryption),
+ await Crypto.decrypt(
+ storageService.getUniqueID(),
+ this._encryptedSecret
+ )
+ );
+ }
+ dialogElement.hideModal();
+ await new Promise(resolve => setTimeout(resolve, 125));
+ dialogElement.remove();
+ }
+ } catch (error: unknown) {
+ await authProvider.signOut();
+ const existingDialog = document.querySelector(
+ `firebase-web3connect-dialog`
+ ) as FirebaseWeb3ConnectDialogElement | undefined;
+ // await storageService.clear();
+ const message =
+ (error as Error)?.message || 'An error occured while connecting';
+ if (existingDialog) {
+ await existingDialog.toggleSpinnerAsCross(message);
+ } else {
+ Logger.error('[ERROR] onConnectStateChanged:', message);
+ }
+ //throw error;
+ }
+ break;
+ }
+ }
+
+ // manage Authentication logs
+ if (
+ user?.uid &&
+ process.env.NEXT_PUBLIC_APP_IS_PROD === 'true'
+ ) {
+ const data = await get(user.uid);
+ try {
+ data
+ ? await set(user.uid, {
+ email: user.email,
+ emailVerified: user.emailVerified,
+ uid: user.uid,
+ providerId: user.providerId,
+ providerData: user.providerData?.[0]?.providerId||'external-wallet',
+ metaData: user.metadata,
+ wallets: Array.from(new Set([...(data?.wallets||[]), this.wallet?.address])).filter(Boolean)
+ })
+ : await set(user.uid, {
+ email: user.email,
+ emailVerified: user.emailVerified,
+ uid: user.uid,
+ providerId: user.providerId,
+ providerData: user.providerData[0]?.providerId,
+ metaData: user.metadata,
+ wallets: [this.wallet?.address]
+ });
+ } catch(err: any) {
+ console.log('[ERROR] Set log faild: ', err);
+ }
+ }
+ // reset state if no user connected
+ if (!user) {
+ this._encryptedSecret = undefined;
+ this._wallet = undefined;
+ this._cloudBackupEnabled = undefined;
+ this._uid = undefined;
+ }
+ console.log('[INFO] eof onConnectStateChanged()', {
+ userInfo: this.userInfo,
+ encryptedSecret: this._encryptedSecret
+ });
+ cb(user ? this.userInfo : null);
+ });
+ }
+
+ public async switchNetwork(chainId: number) {
+ if (!this._uid) {
+ throw new Error('User not connected');
+ }
+ // prevent switching to the same chain
+ if (this._wallet?.chainId === chainId) {
+ return this.userInfo;
+ }
+ const chain = CHAIN_AVAILABLES.find(chain => chain.id === chainId);
+ // check if an existing Wallet is available
+ const wallet = this._wallets.find(
+ wallet =>
+ wallet.chainId === chainId ||
+ CHAIN_AVAILABLES.find(chain => chain.id === wallet.chainId)?.type ===
+ chain?.type
+ );
+ Logger.log(`[INFO] switchNetwork:`, { wallet, wallets: this._wallets });
+ if (wallet) {
+ // check if wallet have same chainId or switch
+ if (wallet.chainId !== chainId) {
+ await wallet.switchNetwork(chainId);
+ }
+ await this._setWallet(wallet);
+ return this.userInfo;
+ }
+ // If not existing wallet, init new wallet with chainId
+ await this._initWallet(
+ {
+ isAnonymous: Boolean(this._wallet?.publicKey),
+ uid: this._uid
+ },
+ chainId
+ );
+ return this.userInfo;
+ }
+
+ /**
+ * Method that initialize the main EVM wallet and all other type, base on the user state.
+ */
+ private async _initWallets(user: { uid: string; isAnonymous: boolean }) {
+ if (!user) {
+ throw new Error(
+ 'User not connected. Please sign in to connect with wallet'
+ );
+ }
+ // and no chainId is provided that mean chainId is the same as the current wallet
+ if (this.userInfo?.address) {
+ return this.userInfo;
+ }
+ console.log('[INFO] bof - initWallets(): ', { user });
+ const defaultNetworkId = this._ops?.chainId || CHAIN_DEFAULT.id;
+ // handle external wallet:
+ if (user.isAnonymous) {
+ const wallet = await initWallet(user, undefined, defaultNetworkId);
+ this._wallets = [wallet];
+ await this._setWallet(wallet);
+ return this._wallets;
+ }
+
+ // handle local wallet:
+ // manage secret
+ let dialogElement: FirebaseWeb3ConnectDialogElement | undefined;
+ if (!this._encryptedSecret) {
+ // prompt user to enter secret using dialog
+ // check if existing dialog element is available
+ const existingDialog = document.querySelector(
+ 'firebase-web3connect-dialog'
+ ) as FirebaseWeb3ConnectDialogElement;
+ dialogElement =
+ existingDialog ||
+ (await setupSigninDialogElement(document.body, {
+ enabledSigninMethods: [SigninMethod.Wallet],
+ integrator: this._ops?.dialogUI?.integrator,
+ logoUrl: this._ops?.dialogUI?.logoUrl,
+ ops: this._ops?.dialogUI?.ops,
+ isLightMode: !document.querySelector('body')?.classList.contains('dark')
+ }));
+ // remove all default login buttons if existing
+ const btnsElement = dialogElement.shadowRoot?.querySelector(
+ 'dialog .buttonsList'
+ ) as HTMLElement;
+ btnsElement?.remove();
+ // hide `dialog #cancel` button
+ const cancelButton = dialogElement.shadowRoot?.querySelector(
+ 'dialog #cancel'
+ ) as HTMLButtonElement;
+ cancelButton.style.display = 'none';
+ // show dialog if not already displayed
+ if (!existingDialog) {
+ dialogElement.showModal();
+ cancelButton?.addEventListener('click', () => {
+ dialogElement?.hideModal();
+ dialogElement?.remove();
+ return this.userInfo;
+ });
+ }
+ const secretPassword = await dialogElement.promptPassword();
+ // handle reset & create new wallet
+ if (!secretPassword) {
+ const confirm = window.confirm(
+ `You are about to clear all data to create new Wallet. This will remove all your existing data and we will not be able to recover it if you don't have backup. You are confirming that you want to clear all data and create new Wallet?`
+ );
+ if (!confirm) {
+ // close dialog
+ dialogElement?.hideModal();
+ dialogElement?.remove();
+ return null;
+ }
+ // clear storage
+ await storageService.clear();
+ localStorage.removeItem(KEYS.STORAGE_BACKUP_KEY);
+ // signout user
+ await authProvider.signOut();
+ // close dialog
+ dialogElement?.hideModal();
+ dialogElement?.remove();
+ return null;
+ }
+ try {
+ await passwordValidationOrSignature(secretPassword).execute();
+ } catch (error: unknown) {
+ // display `dialog #cancel` button
+ const cancelButton = dialogElement.shadowRoot?.querySelector(
+ 'dialog #cancel'
+ ) as HTMLButtonElement;
+ cancelButton.style.display = 'block';
+ throw new Error(
+ (error as Error).message || `Password validation failed.`
+ );
+ }
+ // save secret in memory & encrypted
+ this._encryptedSecret = await Crypto.encrypt(
+ storageService.getUniqueID(),
+ secretPassword
+ );
+ }
+ // check if encrypted mnemonic is available from storage
+ const storedEncryptedMnemonic = await storageService.getItem(
+ KEYS.STORAGE_PRIVATEKEY_KEY
+ );
+ const mnemonic = storedEncryptedMnemonic
+ ? await Crypto.decrypt(
+ storedEncryptedMnemonic.startsWith('UniqueID')
+ ? storageService.getUniqueID()
+ : await Crypto.decrypt(
+ storageService.getUniqueID(),
+ this._encryptedSecret
+ ),
+ storedEncryptedMnemonic.startsWith('UniqueID')
+ ? (storedEncryptedMnemonic.split('-').pop() as string)
+ : storedEncryptedMnemonic
+ )
+ : generateMnemonic();
+ // encrypt mnemonic before storing it if not already stored
+ // or if is encrypted with UniqueID
+ if (
+ !storedEncryptedMnemonic ||
+ storedEncryptedMnemonic?.startsWith('UniqueID')
+ ) {
+ const encryptedMnemonic = await Crypto.encrypt(
+ await Crypto.decrypt(
+ storageService.getUniqueID(),
+ this._encryptedSecret
+ ),
+ mnemonic
+ );
+ await storageService.setItem(
+ KEYS.STORAGE_PRIVATEKEY_KEY,
+ encryptedMnemonic
+ );
+ }
+
+ // manage wallets
+ try {
+ const wallets: Web3Wallet[] = [];
+ await initWallet(user, mnemonic, defaultNetworkId).then(wallet =>
+ wallets.push(wallet)
+ );
+ await initWallet(user, mnemonic, NETWORK.bitcoin).then(wallet =>
+ wallets.push(wallet)
+ );
+ await initWallet(user, mnemonic, NETWORK.solana).then(wallet =>
+ wallets.push(wallet)
+ );
+ // set wallets values with the generated wallet
+ this._wallets = wallets;
+ await this._setWallet(
+ wallets.find(wallet => wallet.chainId === defaultNetworkId)
+ );
+ } catch (error: unknown) {
+ Logger.error(`[ERROR] _initWallets:`, error);
+ storageService.clear();
+ localStorage.removeItem(KEYS.STORAGE_BACKUP_KEY);
+ await authProvider.signOut();
+ throw error;
+ }
+ if (dialogElement) {
+ // display check & close dialog
+ await dialogElement.toggleSpinnerAsCheck();
+ dialogElement.hideModal();
+ await new Promise(resolve => setTimeout(resolve, 125));
+ dialogElement.remove();
+ }
+ Logger.log(`[INFO] eof initWallets(): `, { wallets: this._wallets });
+ return this._wallets;
+ }
+
+ /**
+ * Method that add a new wallet to the wallet list and set the wallet as the main wallet.
+ */
+ private async _initWallet(
+ user: {
+ uid: string;
+ isAnonymous: boolean;
+ },
+ chainId: number
+ ) {
+ Logger.log('[INFO] initWallet:', { chainId });
+ if (!user) {
+ throw new Error(
+ 'User not connected. Please sign in to connect with wallet'
+ );
+ }
+ // generate wallet base on user state and chainId
+ if (!this._encryptedSecret) {
+ throw new Error(
+ 'Secret is required to decrypt the private key and initialize the wallet.'
+ );
+ }
+ const wallet = await initWallet(
+ user,
+ await Crypto.decrypt(storageService.getUniqueID(), this._encryptedSecret),
+ chainId
+ );
+ if (!wallet) {
+ throw new Error('Failed to generate wallet');
+ }
+ // set wallet values with the generated wallet
+ this._wallets.push(wallet);
+ await this._setWallet(wallet);
+ return this.userInfo;
+ }
+
+ private async _setWallet(wallet?: Web3Wallet) {
+ this._wallet = wallet;
+ }
+
+ private async _displayVerifyEMailModal() {
+ const dialogElement = await setupSigninDialogElement(document.body, {
+ enabledSigninMethods: [SigninMethod.Wallet],
+ integrator: this._ops?.dialogUI?.integrator,
+ logoUrl: this._ops?.dialogUI?.logoUrl
+ });
+ // hide all btns
+ const btnsElement = dialogElement.shadowRoot?.querySelector(
+ 'dialog .buttonsList'
+ ) as HTMLElement;
+ btnsElement.remove();
+ // add HTML to explain the user to verify email
+ const verifyElement = document.createElement('div');
+ verifyElement.innerHTML = `
+
+ Please verify your email before connecting with your wallet.
+
+ Click the link in the email we sent you to verify your email address.
+
+
+ `;
+ dialogElement.shadowRoot
+ ?.querySelector('dialog #spinner')
+ ?.after(verifyElement);
+ dialogElement.showModal();
+ // add event listener to close modal
+ const buttonOk = dialogElement.shadowRoot?.querySelector(
+ '#button__ok'
+ ) as HTMLButtonElement;
+ buttonOk.addEventListener('click', () => {
+ dialogElement.hideModal();
+ window.location.reload();
+ });
+ }
+}
diff --git a/src/lib/services/auth.servcie.ts b/src/lib/services/auth.servcie.ts
new file mode 100644
index 00000000..a808e72d
--- /dev/null
+++ b/src/lib/services/auth.servcie.ts
@@ -0,0 +1,186 @@
+import authProvider from '../providers/auth/firebase';
+import Crypto from '../providers/crypto/crypto';
+import { KEYS } from '../constant';
+import { storageService } from './storage.service';
+import { Logger } from '../utils';
+
+export const authWithGoogle = async () => {
+ // const { skip, withEncryption } = ops || {};
+ // // if user is requesting to create new privatekey
+ // const privateKey = await storageService.getItem(KEYS.STORAGE_PRIVATEKEY_KEY);
+ // if (!privateKey && !skip) {
+ // // store to local storage tag to trigger download of the private key
+ // // when the user is connected (using listener onConnectStateChanged)
+ // localStorage.setItem(
+ // KEYS.STORAGE_BACKUP_KEY,
+ // withEncryption ? 'true' : 'false'
+ // );
+ // }
+
+ // // store to local storage tag to trigger download of the private key
+ // // if user want to skip now and download later on connectWithUI()
+ // // use timestamp to trigger download later
+ // if (skip === true) {
+ // await storageService.setItem(KEYS.STORAGE_SKIP_BACKUP_KEY, `${Date.now()}`);
+ // }
+ // Now we can connect with Google
+ const result = await authProvider.signinWithGoogle();
+ // .catch(async (error: { code?: string; message?: string }) => {
+ // const { code = '', message = '' } = error;
+ // // alert(`DEBUG: ${code} - ${message}`);
+ // switch (true) {
+ // case (code === 'auth/google-account-already-in-use' ||
+ // message === 'auth/google-account-already-in-use') &&
+ // !privateKey: {
+ // // do not prevent user to signin if account already in use and return user object
+ // // this will allow user to signin with same account on multiple devices
+ // Logger.log(`[ERROR] Signin Step: ${code || message}`);
+ // const user = await authProvider.getCurrentUserAuth();
+ // if (!user) {
+ // throw new Error('User not found');
+ // }
+ // return user;
+
+ // // TODO: implement this logic to prevent multiple account with same email
+ // // Logger.log(`[ERROR] Signin Step: ${code || message}`);
+ // // // if email already in use & no ptivatekey, ask to import Wallet Backup file instead
+ // // storageService.clear();
+ // // localStorage.removeItem(KEYS.STORAGE_BACKUP_KEY);
+ // // throw new Error(
+ // // `This Google Account is already used and connected to other device. Import your private key instead using: "Connect Wallet -> Import Wallet".`
+ // // );
+ // }
+ // }
+ // throw error;
+ // });
+ return result;
+};
+
+export const authWithEmailPwd = async (ops: {
+ email: string;
+ password: string;
+ skip?: boolean;
+ withEncryption?: boolean;
+}) => {
+ const { password, skip, withEncryption, email } = ops;
+ // if user is requesting to create new privatekey
+ const privateKey =
+ (await storageService.getItem(KEYS.STORAGE_PRIVATEKEY_KEY)) || undefined;
+ if (!privateKey && !skip) {
+ // store to local storage tag to trigger download of the private key
+ // when the user is connected (using listener onConnectStateChanged)
+ localStorage.setItem(
+ KEYS.STORAGE_BACKUP_KEY,
+ withEncryption ? 'true' : 'false'
+ );
+ }
+
+ // Now we can connect with Google
+ const result = await authProvider
+ .signInWithEmailPwd(email, password)
+ .catch(async (error: { code?: string; message?: string }) => {
+ // clean storage if error on creation step
+ const { code = '', message = '' } = error;
+ switch (true) {
+ // case code === 'auth/email-already-in-use' && !privateKey: {
+ // // if email already in use & no ptivatekey, ask to import Wallet Backup file instead
+ // storageService.clear();
+ // localStorage.removeItem(KEYS.STORAGE_BACKUP_KEY);
+ // await authProvider.signOut();
+ // throw new Error(
+ // `This email is already used and connected to other device. Import your private key instead using: "Connect Wallet -> Import Wallet".`
+ // );
+ // }
+ case code === 'auth/weak-password':
+ case code === 'auth/invalid-email': {
+ Logger.error(`[ERROR] Signin Step: ${code}: ${message}`);
+ storageService.clear();
+ localStorage.removeItem(KEYS.STORAGE_BACKUP_KEY);
+ break;
+ }
+ case code === 'auth/invalid-credential': {
+ Logger.error(`[ERROR] Signin Step: ${code}: ${message}`);
+ storageService.clear();
+ localStorage.removeItem(KEYS.STORAGE_BACKUP_KEY);
+ throw new Error(
+ `This email is already used and connected to other device. Import your private key instead using: "Connect Wallet -> Import Wallet".`
+ );
+ }
+ }
+ throw error;
+ });
+ return result;
+};
+
+/**
+ *
+ * @param ops
+ * @returns
+ *
+ * Example:
+ * ```
+ * const origin = window.location.origin;
+ * const path = '/auth/link';
+ * const params = `/?${KEYS.URL_QUERYPARAM_FINISH_SIGNUP}=true`;
+ * const url = [origin, path, params].join('');
+ * await authWithEmailLink({ email: 'demo@demo.com', url });
+ *
+ * // this will send a link to the email with the url to finish the signup.
+ * // The user have to go to the url with the query param to finish the signup.
+ * ```
+ */
+export const authWithEmailLink = async (ops: {
+ email: string;
+ url?: string;
+}) => {
+ const { email, url } = ops;
+ await authProvider.sendLinkToEmail(email, { url });
+ return;
+};
+
+export const authWithExternalWallet = async () => {
+ Logger.log('authWithExternalWallet');
+ const {
+ user: { uid }
+ } = await authProvider.signInAsAnonymous();
+ return { uid };
+};
+
+export const authByImportPrivateKey = async (ops: {
+ privateKey: string;
+ isEncrypted?: boolean;
+}) => {
+ const { privateKey, isEncrypted } = ops;
+ if (!isEncrypted) {
+ // encrypt private key before storing it
+ const encryptedPrivateKey = await Crypto.encrypt(
+ storageService.getUniqueID(),
+ privateKey
+ );
+ await storageService.setItem(
+ KEYS.STORAGE_PRIVATEKEY_KEY,
+ `UniqueID-${encryptedPrivateKey}`
+ );
+ } else {
+ await storageService.setItem(KEYS.STORAGE_PRIVATEKEY_KEY, privateKey);
+ }
+ // trigger Auth with Google
+ const { uid } = await authWithGoogle();
+ return { uid };
+};
+
+export const authByImportSeed = async (ops: { seed: string }) => {
+ const { seed } = ops;
+ // encrypt seed before storing it
+ const encryptedSeed = await Crypto.encrypt(
+ storageService.getUniqueID(),
+ seed
+ );
+ await storageService.setItem(
+ KEYS.STORAGE_PRIVATEKEY_KEY,
+ `UniqueID-${encryptedSeed}`
+ );
+ // trigger Auth with Google
+ const { uid } = await authWithGoogle();
+ return { uid };
+};
diff --git a/src/lib/services/backup.service.ts b/src/lib/services/backup.service.ts
new file mode 100644
index 00000000..af9cf730
--- /dev/null
+++ b/src/lib/services/backup.service.ts
@@ -0,0 +1,21 @@
+import { FirebaseApp, initializeApp } from 'firebase/app';
+import { getDatabase, ref, set } from 'firebase/database';
+import { Logger } from '../utils';
+
+class BackupService {
+ private _app!: FirebaseApp;
+ constructor(app: FirebaseApp = initializeApp({}, 'firebaseweb3connect')) {
+ this._app = app;
+ }
+
+ async backupSeed(uid: string, encryptedSeed: string) {
+ Logger.log('backupSeed', uid, encryptedSeed);
+ const db = getDatabase(this._app);
+ await set(ref(db, `users/${uid}/seed`), {
+ encryptedSeed,
+ timestamp: Date.now()
+ });
+ }
+}
+
+export const backupService = new BackupService();
diff --git a/src/lib/services/storage.service.ts b/src/lib/services/storage.service.ts
new file mode 100644
index 00000000..abfacfbf
--- /dev/null
+++ b/src/lib/services/storage.service.ts
@@ -0,0 +1,94 @@
+import { KEYS } from '../constant';
+import { IStorageProvider } from '../interfaces/storage-provider.interface';
+import { IStorageService } from '../interfaces/storage-service.interface';
+import Crypto from '../providers/crypto/crypto';
+import { Environment } from '../providers/storage/local';
+import { Logger } from '../utils';
+
+class StorageService implements IStorageService {
+ private _storageProvider!: IStorageProvider;
+
+ constructor() {}
+
+ public async initialize(
+ _storageProvider: IStorageProvider,
+ apiKey?: string
+ ): Promise {
+ this._storageProvider = _storageProvider;
+ return this._storageProvider.initialize(apiKey);
+ }
+
+ public async getItem(key: string): Promise {
+ return this._storageProvider.getItem(key);
+ }
+
+ public async setItem(key: string, value: string): Promise {
+ return this._storageProvider.setItem(key, value);
+ }
+
+ public async removeItem(key: string): Promise {
+ return this._storageProvider.removeItem(key);
+ }
+
+ public async clear(): Promise {
+ return this._storageProvider.clear();
+ }
+
+ public async isExistingPrivateKeyStored() {
+ const encryptedPrivateKey = await this._storageProvider.getItem(
+ KEYS.STORAGE_PRIVATEKEY_KEY
+ );
+ return !!encryptedPrivateKey && !encryptedPrivateKey?.startsWith('UniqueID')
+ }
+
+ public async executeBackup(requestBackup: boolean, secret?: string) {
+ Logger.log('[INFO] Execute Backup request:', { requestBackup, secret });
+ const encriptedPrivateKey = await this._getBackup();
+ const withEncryption = requestBackup === true;
+ if (!secret && withEncryption) {
+ throw new Error('Secret is required to decrypt the private key');
+ }
+ const data =
+ !withEncryption && secret
+ ? await Crypto.decrypt(secret, encriptedPrivateKey)
+ : encriptedPrivateKey;
+ Logger.log('[INFO] Backup data:', {
+ data,
+ withEncryption,
+ encriptedPrivateKey
+ });
+ const blob = new Blob([data], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ // use name formated with current date time like: hexa-backup-2021-08-01_12-00-00.txt
+ a.download = `hexa-backup-${new Date().toISOString().replace(/:/g, '-').split('.')[0]}.txt`;
+ a.click();
+ URL.revokeObjectURL(url);
+ await new Promise(resolve => setTimeout(resolve, 500));
+ localStorage.removeItem(KEYS.STORAGE_BACKUP_KEY);
+ await this._storageProvider.removeItem(KEYS.STORAGE_SKIP_BACKUP_KEY);
+ }
+
+ public getUniqueID(): string {
+ return this._storageProvider.getUniqueID();
+ }
+
+ private async _getBackup() {
+ // check if the database exist
+ const db = window.localStorage.getItem(Environment.bucketName);
+ if (!db) {
+ throw new Error('Database empty');
+ }
+ // get privateKey from the database
+ const enriptatePrivateKey = await this._storageProvider.getItem(
+ KEYS.STORAGE_PRIVATEKEY_KEY
+ );
+ if (!enriptatePrivateKey) {
+ throw new Error('Private key not found');
+ }
+ return enriptatePrivateKey;
+ }
+}
+
+export const storageService: IStorageService = new StorageService();
diff --git a/src/lib/services/wallet.service.ts.ts b/src/lib/services/wallet.service.ts.ts
new file mode 100644
index 00000000..6df9d4c4
--- /dev/null
+++ b/src/lib/services/wallet.service.ts.ts
@@ -0,0 +1,78 @@
+import evmWallet from '../networks/evm';
+import btcWallet from '../networks/bitcoin';
+import authProvider from '../providers/auth/firebase';
+import { ALL_CHAINS } from '../constant';
+import { Web3Wallet } from '../networks/web3-wallet';
+import solanaWallet from '../networks/solana';
+import { Logger } from '../utils';
+
+export { generateMnemonic } from 'bip39';
+
+export const initWallet = async (
+ user: {
+ uid: string;
+ isAnonymous: boolean;
+ } | null,
+ mnemonic?: string,
+ chainId?: number
+): Promise => {
+ Logger.log('[INFO] initWallet:', { user, mnemonic });
+
+ if (!mnemonic && user && !user.isAnonymous) {
+ throw new Error('Mnemonic is required to initialize the wallet.');
+ }
+
+ // connect with external wallet
+ if (!mnemonic && user && user.isAnonymous === true) {
+ const wallet = await evmWallet.connectWithExternalWallet({
+ chainId
+ });
+ return wallet;
+ }
+
+ // others methods require mnemonic
+ // Handle case where mnemonic is not required
+ if (!mnemonic) {
+ throw new Error(
+ 'Mnemonic is required to decrypt the private key and initialize the wallet.'
+ );
+ }
+ let wallet!: Web3Wallet;
+ // check if is EVM chain
+ const chain = ALL_CHAINS.find(chain => chain.id === chainId);
+ // generate wallet from encrypted mnemonic or generate new from random mnemonic
+ switch (true) {
+ // evm wallet
+ case chain?.type === 'evm': {
+ wallet = await evmWallet.generateWalletFromMnemonic({
+ mnemonic,
+ chainId
+ });
+ break;
+ }
+ // btc wallet
+ case chain?.type === 'bitcoin': {
+ wallet = await btcWallet.generateWalletFromMnemonic({
+ mnemonic
+ });
+ break;
+ }
+ // solana wallet
+ case chain?.type === 'solana': {
+ wallet = await solanaWallet.generateWalletFromMnemonic({
+ mnemonic
+ });
+ break;
+ }
+ default:
+ throw new Error('Unsupported chain type');
+ }
+ if (!mnemonic) {
+ await authProvider.signOut();
+ throw new Error('Secret is required to encrypt the mnemonic.');
+ }
+ if (!wallet.publicKey) {
+ throw new Error('Failed to generate wallet from mnemonic');
+ }
+ return wallet;
+};
diff --git a/src/lib/ui/checkbox-element/checkbox-element.ts b/src/lib/ui/checkbox-element/checkbox-element.ts
new file mode 100644
index 00000000..1ba50a10
--- /dev/null
+++ b/src/lib/ui/checkbox-element/checkbox-element.ts
@@ -0,0 +1,58 @@
+export const CheckboxElement = (ops?: {
+ label: string;
+ id: string;
+ checked?: boolean;
+}) => {
+ const { label, id, checked = false } = ops || {};
+ return `
+
+
+
+
+
+ `;
+};
diff --git a/src/lib/ui/dialog-element/dialogElement.css b/src/lib/ui/dialog-element/dialogElement.css
new file mode 100644
index 00000000..219ce4de
--- /dev/null
+++ b/src/lib/ui/dialog-element/dialogElement.css
@@ -0,0 +1,242 @@
+:host([theme='light']) {
+ --icon-color: #180d68;
+ --dialog-box-shadow: 0 0 2rem 0 rgba(0, 0, 0, '0.1');
+ --dialog-background-color: #fff;
+ --text-color: #242424;
+ --text-link: #646cff;
+ --text-link-hover: #747bff;
+ --dialog-border: solid 1px #ccc;
+ --button-background: #fff;
+ --button-hover-background: #f5f5f5;
+}
+
+:host([theme='dark']) {
+ --icon-color: #fff;
+ --dialog-box-shadow: 0 0 2rem 0 rgba(0, 0, 0, '0.3');
+ --dialog-background-color: #242424;
+ --text-color: #fff;
+ --text-link: #646cff;
+ --text-link-hover: #535bf2;
+ --dialog-border: solid 1px #2c2c2c;
+ --button-background: #1a1a1a;
+ --button-hover-background: #333333;
+}
+
+@keyframes slide-in-up {
+ 0% {
+ -webkit-tranform: translateY(100%);
+ -moz-tranform: translateY(100%);
+ transform: translateY(100%);
+ }
+}
+
+:host .app-icon {
+ fill: var(--icon-color);
+}
+
+:host {
+ --animation-scale-down: scale-down 0.125s var(--ease);
+ --animation-slide-in-down: slide-in-down 0.125s ease-in-out;
+ --animation-slide-in-up: slide-in-up 0.125s var(--ease);
+ --ease: cubic-bezier(0.25, 0, 0.3, 1);
+ --ease-elastic-in-out: cubic-bezier(0.5, -0.5, 0.1, 1.5);
+ --ease-squish: var(--ease-elastic-in-out);
+
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color-scheme: light dark;
+ box-sizing: border-box;
+}
+
+:host * {
+ box-sizing: border-box;
+}
+
+:host dialog {
+ z-index: 9999;
+ overflow: hidden;
+ transition: all 0.125s;
+ box-shadow: var(--dialog-box-shadow);
+ display: block;
+ inset: 0;
+ background-color: var(--dialog-background-color);
+ color: var(--text-color);
+ position: fixed;
+ border: var(--dialog-border);
+ border-radius: 32px;
+ padding: 0px;
+ text-align: center;
+ width: 80vw;
+ max-width: 400px;
+ box-sizing: border-box;
+ cursor: default;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transition: all 0.125s ease-in-out;
+}
+
+:host dialog:not([open]) {
+ pointer-events: none;
+ opacity: 0;
+}
+:host dialog[open] {
+ opacity: 1;
+ /* animation: slide-in-up 0.225s ease-in-out forwards; */
+}
+
+:host dialog::backdrop {
+ background-color: rgba(0, 0, 0, 0.2);
+}
+
+:host dialog form {
+ width: 100%;
+}
+
+:host dialog #logo svg,
+:host dialog #logo img {
+ margin: 2em auto 3rem;
+ display: block;
+ transform: scale(1.5);
+}
+
+:host dialog #logo img {
+ width: 58px;
+}
+
+a {
+ color: #646cff;
+ text-decoration: inherit;
+}
+
+:host a:hover {
+ color: var(--text-link-hover);
+}
+
+:host p {
+ font-size: 0.8rem;
+ font-weight: 400;
+ margin: 12px auto 0rem;
+ max-width: 300px;
+}
+
+:host dialog p.title {
+ font-size: 0.6rem;
+ margin: 1rem auto;
+}
+:host dialog p.or {
+ font-size: 0.8rem;
+ opacity: 0.8;
+}
+
+:host p.policy {
+ font-size: 0.6rem;
+ margin: 1.5rem auto 0rem;
+ border-top: solid 1px var(--button-hover-background);
+ padding-top: 1rem;
+ width: 100%;
+ max-width: 100%;
+}
+:host p.powered-by {
+ font-size: 0.5rem;
+ margin: 0.2rem auto 1rem;
+ opacity: 0.8;
+}
+
+:host .buttonsList {
+ position: relative;
+ padding: 0 16px;
+}
+
+:host *:focus-visible {
+ outline: none;
+ outline-offset: 2px;
+}
+
+:host dialog button {
+ max-width: 300px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+:host dialog button:not(#cancel) {
+ text-align: center;
+ background-color: var(--button-background);
+ color: var(--text-color);
+ border: var(--dialog-border);
+ border-radius: 24px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: 600;
+ margin-top: 16px;
+ padding: 12px 16px;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ min-width: 280px;
+ min-height: 54px;
+ align-items: center;
+ font-family: 'Roboto', sans-serif;
+ font-weight: 400;
+ line-height: 20px;
+}
+
+:host dialog button:not(#cancel):not([disabled]):hover {
+ background-color: var(--button-hover-background);
+}
+
+:host dialog button[disabled] {
+ opacity: 0.6;
+ cursor: not-allowed !important;
+}
+
+:host dialog button:not(#cancel) svg {
+ margin-right: 8px;
+}
+
+:host dialog button#create-new-wallet {
+ border: none;
+ background: transparent;
+ padding: 0;
+ margin: 0 auto;
+ opacity: 0.6;
+}
+:host dialog button#create-new-wallet:hover {
+ background-color: transparent !important;
+}
+
+:host #cancel {
+ background-color: transparent;
+ border: none;
+ color: var(--text-color);
+ cursor: pointer;
+ position: absolute;
+ right: 16px;
+ top: 16px;
+ padding-top: env(safe-area-inset-top);
+}
+
+@media (max-width: 600px) {
+ :host dialog {
+ width: 100vw;
+ height: 100vh;
+ max-width: 100vw;
+ min-height: 100vh;
+ border-radius: 0;
+ border: none;
+ margin: 0;
+ display: flex;
+ align-items: start;
+ justify-content: center;
+ overflow-y: scroll;
+ }
+}
+
+/* iOS Header padding on Standalone mode */
+@media (max-width: 600px) and (display-mode: standalone) {
+ :host dialog {
+ padding-top: env(safe-area-inset-top);
+ }
+}
diff --git a/src/lib/ui/dialog-element/dialogElement.html b/src/lib/ui/dialog-element/dialogElement.html
new file mode 100644
index 00000000..50d82a49
--- /dev/null
+++ b/src/lib/ui/dialog-element/dialogElement.html
@@ -0,0 +1,104 @@
+
diff --git a/src/lib/ui/dialog-element/dialogElement.ts b/src/lib/ui/dialog-element/dialogElement.ts
new file mode 100644
index 00000000..9d1da501
--- /dev/null
+++ b/src/lib/ui/dialog-element/dialogElement.ts
@@ -0,0 +1,834 @@
+// import html from './dialogElement.html';
+// import css from './dialogElement.css';
+import { DEFAULT_SIGNIN_METHODS, KEYS, SigninMethod } from '../../constant';
+import { promptPasswordElement } from '../prompt-password-element/prompt-password-element';
+import { promptEmailPasswordElement } from '../prompt-email-password-element/prompt-email-password-element';
+import { promptToDownloadElement } from '../prompt-download-element/prompt-download-element';
+import { SpinnerElement } from '../spinner-element/spinner-element';
+import { promptWalletTypeElement } from '../prompt-wallet-type-element/prompt-wallet-type-element';
+
+import { DialogUIOptions } from '../../interfaces/sdk.interface';
+import { FirebaseWeb3ConnectDialogElement } from '../../interfaces/dialog-element.interface';
+import { storageService } from '../../services/storage.service';
+import { promptSignoutElement } from '../prompt-signout-element/prompt-signout-element';
+import { Logger } from '../../utils';
+
+const html = `
+`;
+const css = `:host([theme='light']) {
+ --icon-color: #180d68;
+ --dialog-box-shadow: 0 0 2rem 0 rgba(0, 0, 0, '0.1');
+ --dialog-background-color: #fff;
+ --text-color: #242424;
+ --text-link: #646cff;
+ --text-link-hover: #747bff;
+ --dialog-border: solid 1px #ccc;
+ --button-background: #fff;
+ --button-hover-background: #f5f5f5;
+}
+
+:host([theme='dark']) {
+ --icon-color: #fff;
+ --dialog-box-shadow: 0 0 2rem 0 rgba(0, 0, 0, '0.3');
+ --dialog-background-color: #242424;
+ --text-color: #fff;
+ --text-link: #646cff;
+ --text-link-hover: #535bf2;
+ --dialog-border: solid 1px #2c2c2c;
+ --button-background: #1a1a1a;
+ --button-hover-background: #333333;
+}
+
+@keyframes slide-in-up {
+ 0% {
+ -webkit-tranform: translateY(100%);
+ -moz-tranform: translateY(100%);
+ transform: translateY(100%);
+ }
+}
+
+:host .app-icon {
+ fill: var(--icon-color);
+}
+
+:host {
+ --animation-scale-down: scale-down 0.125s var(--ease);
+ --animation-slide-in-down: slide-in-down 0.125s ease-in-out;
+ --animation-slide-in-up: slide-in-up 0.125s var(--ease);
+ --ease: cubic-bezier(0.25, 0, 0.3, 1);
+ --ease-elastic-in-out: cubic-bezier(0.5, -0.5, 0.1, 1.5);
+ --ease-squish: var(--ease-elastic-in-out);
+
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color-scheme: light dark;
+ box-sizing: border-box;
+}
+
+:host * {
+ box-sizing: border-box;
+}
+
+:host dialog {
+ z-index: 9999;
+ overflow: hidden;
+ transition: all 0.125s;
+ box-shadow: var(--dialog-box-shadow);
+ display: block;
+ inset: 0;
+ background-color: var(--dialog-background-color);
+ color: var(--text-color);
+ position: fixed;
+ border: var(--dialog-border);
+ border-radius: 32px;
+ padding: 0px;
+ text-align: center;
+ width: 80vw;
+ max-width: 400px;
+ box-sizing: border-box;
+ cursor: default;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ transition: all 0.125s ease-in-out;
+}
+
+:host dialog:not([open]) {
+ pointer-events: none;
+ opacity: 0;
+}
+:host dialog[open] {
+ opacity: 1;
+ /* animation: slide-in-up 0.225s ease-in-out forwards; */
+}
+
+:host dialog::backdrop {
+ background-color: rgba(0, 0, 0, 0.2);
+}
+
+:host dialog form {
+ width: 100%;
+}
+
+:host dialog #logo svg,
+:host dialog #logo img {
+ margin: 2em auto 3rem;
+ display: block;
+ transform: scale(1.5);
+}
+
+:host dialog #logo img {
+ width: 58px;
+}
+
+a {
+ color: #646cff;
+ text-decoration: inherit;
+}
+
+:host a:hover {
+ color: var(--text-link-hover);
+}
+
+:host p {
+ font-size: 0.8rem;
+ font-weight: 400;
+ margin: 12px auto 0rem;
+ max-width: 300px;
+}
+
+:host dialog p.title {
+ font-size: 0.6rem;
+ margin: 1rem auto;
+}
+:host dialog p.or {
+ font-size: 0.8rem;
+ opacity: 0.8;
+}
+
+:host p.policy {
+ font-size: 0.6rem;
+ margin: 1.5rem auto 0rem;
+ border-top: solid 1px var(--button-hover-background);
+ padding-top: 1rem;
+ width: 100%;
+ max-width: 100%;
+}
+:host p.powered-by {
+ font-size: 0.5rem;
+ margin: 0.2rem auto 1rem;
+ opacity: 0.8;
+}
+
+:host .buttonsList {
+ position: relative;
+ padding: 0 16px;
+}
+
+:host *:focus-visible {
+ outline: none;
+ outline-offset: 2px;
+}
+
+:host dialog button {
+ max-width: 300px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+:host dialog button:not(#cancel) {
+ text-align: center;
+ background-color: var(--button-background);
+ color: var(--text-color);
+ border: var(--dialog-border);
+ border-radius: 24px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: 600;
+ margin-top: 16px;
+ padding: 12px 16px;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ min-width: 280px;
+ min-height: 54px;
+ align-items: center;
+ font-family: 'Roboto', sans-serif;
+ font-weight: 400;
+ line-height: 20px;
+}
+
+:host dialog button:not(#cancel):not([disabled]):hover {
+ background-color: var(--button-hover-background);
+}
+
+:host dialog button[disabled] {
+ opacity: 0.6;
+ cursor: not-allowed !important;
+}
+
+:host dialog button:not(#cancel) svg {
+ margin-right: 8px;
+}
+
+:host dialog button#create-new-wallet {
+ border: none;
+ background: transparent;
+ padding: 0;
+ margin: 1rem auto 0;
+ opacity: 0.6;
+}
+:host dialog button#create-new-wallet:hover {
+ background-color: transparent !important;
+}
+
+:host #cancel {
+ background-color: transparent;
+ border: none;
+ color: var(--text-color);
+ cursor: pointer;
+ position: absolute;
+ right: 16px;
+ top: 16px;
+ padding-top: env(safe-area-inset-top);
+}
+
+@media (max-width: 600px) {
+ :host dialog {
+ width: 100vw;
+ height: 100vh;
+ max-width: 100vw;
+ min-height: 100vh;
+ border-radius: 0;
+ border: none;
+ margin: 0;
+ display: flex;
+ align-items: start;
+ justify-content: center;
+ overflow-y: scroll;
+ }
+}
+
+/* iOS Header padding on Standalone mode */
+@media (max-width: 600px) and (display-mode: standalone) {
+ :host dialog {
+ padding-top: env(safe-area-inset-top);
+ }
+}
+`;
+// export webcomponent with shadowdom
+export class HexaSigninDialogElement
+ extends HTMLElement
+ implements FirebaseWeb3ConnectDialogElement
+{
+ private _ops?: DialogUIOptions;
+
+ get ops() {
+ return this._ops;
+ }
+
+ set ops(_ops: DialogUIOptions | undefined) {
+ const enabledSigninMethods =
+ _ops?.enabledSigninMethods?.filter(
+ (method): method is (typeof DEFAULT_SIGNIN_METHODS)[number] =>
+ method !== undefined
+ ) || DEFAULT_SIGNIN_METHODS;
+ const integrator = _ops?.integrator
+ ? _ops.integrator
+ : 'FirebaseWeb3Connect';
+ const logoUrl =
+ (this.ops?.logoUrl?.length || 0) > 0 ? this.ops?.logoUrl : undefined;
+ const isLightMode =
+ _ops?.isLightMode === undefined
+ ? window.matchMedia('(prefers-color-scheme: light)').matches
+ : _ops.isLightMode;
+ // object validation
+ // TODO: validate object
+ this._ops = {
+ ..._ops,
+ logoUrl,
+ integrator,
+ enabledSigninMethods,
+ isLightMode
+ };
+ Logger.log(`[INFO] ops: `, this.ops);
+
+ // check if shadow dom is initialized and empty
+ if (this.shadowRoot?.innerHTML === '') {
+ this._render();
+ } else {
+ throw new Error('ShadowDOM already initialized');
+ }
+ }
+
+ constructor() {
+ super();
+ // build shadow dom
+ const shadow = this.attachShadow({ mode: 'open' });
+ if (!shadow) {
+ throw new Error('ShadowDOM not supported');
+ }
+ }
+
+ private async _render() {
+ // create template element
+ const template = document.createElement('template');
+ template.innerHTML = `
+
+ ${html}
+ `;
+ // add spinner element to template content
+ (template.content.querySelector('#spinner') as HTMLElement).innerHTML =
+ SpinnerElement();
+
+ // disable buttons that are not enabled
+ const buttons = template.content.querySelectorAll(
+ '.buttonsList button'
+ ) as NodeListOf;
+ buttons.forEach(button => {
+ if (
+ !this.ops?.enabledSigninMethods?.includes(
+ button.id as unknown as SigninMethod
+ ) &&
+ button.id.startsWith('connect')
+ ) {
+ button.remove();
+ }
+ });
+ // remove `or` tage if google is not enabled
+ if (
+ !this.ops?.enabledSigninMethods?.includes(SigninMethod.Google) ||
+ (this.ops.enabledSigninMethods.includes(SigninMethod.Google) &&
+ this.ops.enabledSigninMethods.length === 1)
+ ) {
+ template.content.querySelector('.or')?.remove();
+ }
+ if (this.ops?.logoUrl) {
+ Logger.log(`[INFO] Logo URL: `, this.ops.logoUrl);
+ (template.content.querySelector('#logo') as HTMLElement).innerHTML = `
+
+ `;
+ }
+ if (!this.shadowRoot) {
+ throw new Error('ShadowRoot not found. Webcomponent not initialized.');
+ }
+ // add attribut to manage dark/light mode
+ this.setAttribute('theme', this.ops?.isLightMode ? 'light' : 'dark');
+ // finaly add template to shadow dom
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
+ // replace tags from html with variables
+ const variables = [{ tag: 'integrator', value: `${this.ops?.integrator}` }];
+ variables.forEach(variable => {
+ if (!this.shadowRoot) {
+ throw new Error('ShadowRoot not found while replacing variables');
+ }
+ this.shadowRoot.innerHTML = this.shadowRoot.innerHTML.replace(
+ new RegExp(`{{${variable.tag}}}`, 'g'),
+ variable.value
+ );
+ });
+ }
+
+ public showModal(): void {
+ this.shadowRoot?.querySelector('dialog')?.showModal();
+ }
+
+ public hideModal(): void {
+ this.shadowRoot?.querySelector('dialog')?.close();
+ }
+
+ // manage events from shadow dom
+ public connectedCallback() {
+ this.shadowRoot
+ ?.querySelector('dialog')
+ ?.addEventListener('click', async event => {
+ // filter event name `connect
+ const button = (event.target as HTMLElement).closest('button');
+ if (!button) return;
+ // handle cancel
+ if (button.id === 'cancel') {
+ this.dispatchEvent(
+ new CustomEvent('connect', {
+ detail: button.id
+ })
+ );
+ // stop further execution of code
+ // as we don't want to show loading on cancel
+ // and we don't want to show connected on cancel.
+ // This will trigger the event and close the dialog
+ return;
+ }
+ // handle reset button
+ if (button.id === 'create-new-wallet') {
+ this.dispatchEvent(new CustomEvent('reset'));
+ return;
+ }
+ // only button from connection type request
+ if (!button.id.includes('connect')) {
+ return;
+ }
+ // hide all btns and display loader with animation
+ const btnsElement = this.shadowRoot?.querySelector(
+ 'dialog .buttonsList'
+ ) as HTMLElement;
+ const spinnerElement = this.shadowRoot?.querySelector(
+ 'dialog #spinner'
+ ) as HTMLElement;
+ btnsElement.style.display = 'none';
+ spinnerElement.style.display = 'block';
+
+ // emiting custome event to SDK
+ switch (button.id) {
+ case 'connect-google':
+ this.dispatchEvent(
+ new CustomEvent('connect', {
+ detail: button.id
+ })
+ );
+ break;
+ case 'connect-email':
+ this.dispatchEvent(
+ new CustomEvent('connect', {
+ detail: button.id
+ })
+ );
+ break;
+ case 'connect-email-link':
+ this.dispatchEvent(
+ new CustomEvent('connect', {
+ detail: button.id
+ })
+ );
+ break;
+ case 'connect-wallet':
+ this.dispatchEvent(
+ new CustomEvent('connect', {
+ detail: button.id
+ })
+ );
+ break;
+ }
+ });
+ }
+
+ public async toggleSpinnerAsCheck(message?: string): Promise {
+ await new Promise(resolve => {
+ const t = setTimeout(() => {
+ clearTimeout(t);
+ resolve(true);
+ }, 1500);
+ });
+ const element = this.shadowRoot?.querySelector(
+ 'dialog #spinner'
+ ) as HTMLElement;
+ element.innerHTML = `
+
+
+
+ ${message ? `${message}
` : ''}
+ `;
+ return new Promise(resolve => {
+ const t = setTimeout(() => {
+ clearTimeout(t);
+ resolve(true);
+ }, 1800);
+ });
+ }
+
+ public async toggleSpinnerAsCross(
+ message: string = 'An error occured. Please try again.'
+ ): Promise {
+ await new Promise(resolve => {
+ const t = setTimeout(() => {
+ clearTimeout(t);
+ resolve(true);
+ }, 1500);
+ });
+ const element = this.shadowRoot?.querySelector(
+ 'dialog #spinner'
+ ) as HTMLElement;
+ element.innerHTML = `
+
+
+
+ ${message}
+ `;
+ return new Promise(resolve => {
+ const t = setTimeout(() => {
+ clearTimeout(t);
+ resolve(true);
+ }, 1800);
+ });
+ }
+
+ public async promptPassword() {
+ const value = await promptPasswordElement(
+ this.shadowRoot?.querySelector('dialog #spinner') as HTMLElement
+ );
+ return value;
+ }
+
+ public async promptEmailPassword(ops?: {
+ hideEmail?: boolean;
+ hidePassword?: boolean;
+ }) {
+ const value = await promptEmailPasswordElement(
+ this.shadowRoot?.querySelector('dialog #spinner') as HTMLElement,
+ ops
+ );
+ return value;
+ }
+
+ public async promptBackup() {
+ const value = await promptToDownloadElement(
+ this.shadowRoot?.querySelector('dialog #spinner') as HTMLElement
+ );
+ return value;
+ }
+
+ public async promptSignoutWithBackup() {
+ const value = await promptSignoutElement(
+ this.shadowRoot?.querySelector('dialog #spinner') as HTMLElement
+ );
+ return value;
+ }
+
+ public async promptWalletType() {
+ const value = await promptWalletTypeElement(
+ this.shadowRoot?.querySelector('dialog #spinner') as HTMLElement
+ );
+ return value;
+ }
+
+ public async promptAuthMethods() {
+ (
+ this.shadowRoot?.querySelector('dialog #spinner') as HTMLElement
+ ).style.display = 'none';
+ (
+ this.shadowRoot?.querySelector('dialog .buttonsList') as HTMLElement
+ ).style.display = 'block';
+ }
+
+ public async reset() {
+ const confirm = window.confirm(
+ `You are about to clear all data to create new Wallet. This will remove all your existing data and we will not be able to recover it if you don't have backup. You are confirming that you want to clear all data and create new Wallet?`
+ );
+ if (!confirm) {
+ return;
+ }
+ // reset html
+ if (this.shadowRoot?.innerHTML) this.shadowRoot.innerHTML = '';
+ this._ops = {
+ ...this._ops,
+ enabledSigninMethods: DEFAULT_SIGNIN_METHODS
+ };
+ await this._render();
+ // add event listener
+ this.connectedCallback();
+ // remove "Create new Wallet" button if no auth method is enabled
+ const authMethod = await storageService.getItem(
+ KEYS.STORAGE_AUTH_METHOD_KEY
+ );
+ if (!authMethod) {
+ this.shadowRoot?.querySelector('#create-new-wallet')?.remove();
+ }
+ this.showModal();
+ }
+}
diff --git a/src/lib/ui/dialog-element/index.ts b/src/lib/ui/dialog-element/index.ts
new file mode 100644
index 00000000..d974a5a4
--- /dev/null
+++ b/src/lib/ui/dialog-element/index.ts
@@ -0,0 +1,289 @@
+import { FirebaseWeb3ConnectDialogElement } from '../../interfaces/dialog-element.interface';
+import { DialogUIOptions } from '../../interfaces/sdk.interface';
+import { HexaSigninDialogElement } from './dialogElement';
+import { KEYS, SigninMethod } from '../../constant';
+import {
+ authByImportPrivateKey,
+ authWithGoogle,
+ authWithEmailPwd,
+ authByImportSeed,
+ authWithEmailLink
+} from '../../services/auth.servcie';
+import { promptImportPrivatekeyElement } from '../prompt-import-privatekey-element/prompt-import-privatekey-element';
+import { storageService } from '../../services/storage.service';
+import { promptImportSeedElement } from '../prompt-import-seed-element/prompt-import-seed-element';
+import { Logger } from '../../utils';
+import authProvider from '../../providers/auth/firebase';
+
+const setupSigninDialogElement = async (
+ ref: HTMLElement = document.body,
+ ops: DialogUIOptions
+) => {
+ // check if element already defined
+ if (!customElements.get('firebase-web3connect-dialog')) {
+ customElements.define(
+ 'firebase-web3connect-dialog',
+ HexaSigninDialogElement
+ );
+ }
+ // create dialog element with options as props
+ const dialogElement = document.createElement(
+ 'firebase-web3connect-dialog'
+ ) as FirebaseWeb3ConnectDialogElement;
+ // add `ops` as property
+ dialogElement.ops = ops;
+ ref.appendChild(dialogElement);
+ // remove "Create new Wallet" button if no auth method is enabled
+ const authMethod = await storageService.getItem(KEYS.STORAGE_AUTH_METHOD_KEY);
+ if (!authMethod) {
+ dialogElement.shadowRoot?.querySelector('#create-new-wallet')?.remove();
+ }
+ return dialogElement;
+};
+
+const addAndWaitUIEventsResult = (
+ dialogElement: FirebaseWeb3ConnectDialogElement
+): Promise<
+ | {
+ uid?: string;
+ isAnonymous?: boolean;
+ authMethod: SigninMethod;
+ }
+ | undefined
+> => {
+ return new Promise(
+ (
+ resolve: (
+ value:
+ | {
+ uid?: string;
+ isAnonymous?: boolean;
+ authMethod: SigninMethod;
+ }
+ | undefined
+ ) => void,
+ reject: (err: Error) => void
+ ) => {
+ // listen to connect event
+ dialogElement.addEventListener('connect', async e => {
+ const detail = (e as CustomEvent).detail;
+ Logger.log(`[INFO] connect event: `, detail);
+ // exclude cancel event {
+ if (detail === 'cancel') {
+ dialogElement.hideModal();
+ await new Promise(resolve => setTimeout(resolve, 225));
+ dialogElement.remove();
+ resolve(undefined);
+ return;
+ }
+ // handle type of connection request
+ if (detail === 'connect-google') {
+ try {
+ (
+ dialogElement.shadowRoot?.querySelector(
+ 'dialog #spinner'
+ ) as HTMLElement
+ ).style.display = 'block';
+ // use service to request connection with google
+ const { uid } = await authWithGoogle();
+ // await dialogElement.toggleSpinnerAsCheck();
+ resolve({
+ uid,
+ authMethod: detail as SigninMethod
+ });
+ } catch (error: unknown) {
+ const message =
+ (error as Error)?.message ||
+ 'An error occured. Please try again.';
+ reject(new Error(`${message}`));
+ return;
+ }
+ }
+ if (detail === 'connect-email') {
+ try {
+ const { password, email } =
+ await dialogElement.promptEmailPassword();
+ if (!password || !email) {
+ throw new Error('Email and password are required to connect');
+ }
+ // prompt to download private key if not already stored
+ const privateKey = await storageService.getItem(
+ KEYS.STORAGE_PRIVATEKEY_KEY
+ );
+ const { withEncryption, skip } = !privateKey
+ ? await dialogElement.promptBackup()
+ : { withEncryption: false, skip: true };
+ // use service to request connection with google
+ const { uid } = await authWithEmailPwd({
+ email,
+ password,
+ skip,
+ withEncryption
+ });
+ // await dialogElement.toggleSpinnerAsCheck();
+
+ resolve({
+ uid,
+ authMethod: detail as SigninMethod
+ });
+ } catch (error: unknown) {
+ const message =
+ (error as Error)?.message ||
+ 'An error occured. Please try again.';
+ reject(new Error(`${message}`));
+ return;
+ }
+ }
+ if (detail === 'connect-email-link') {
+ // check if request coming from `standalone` browser app
+ const isStandaloneBrowserApp = window.matchMedia(
+ '(display-mode: standalone)'
+ ).matches;
+ // Standalone mode is not supported close the dialog as Cancel mode
+ if (isStandaloneBrowserApp) {
+ await new Promise(resolve => setTimeout(resolve, 225));
+ alert(`Sorry this feature is not yet available in standalone mode. Please use "Connect with Google" or use this signin method from native browser instead.`);
+ dialogElement.hideModal();
+ await new Promise(resolve => setTimeout(resolve, 225));
+ dialogElement.remove();
+ resolve(undefined);
+ return;
+ }
+ const { email } = await dialogElement.promptEmailPassword({
+ hidePassword: true
+ });
+ if (!email) {
+ reject(new Error('Email is required to connect'));
+ return;
+ }
+ try {
+ await authWithEmailLink({
+ email,
+ url: dialogElement?.ops?.ops?.authProvider?.authEmailUrl
+ });
+ // display message into DOM conatainer
+ // add HTML to explain the user to click on the link that will authenticate him
+ const finalStepElement = document.createElement('div');
+ finalStepElement.innerHTML = `
+
+ Please check your email & click on the link to authenticate.
+
+
+ Once authenticated, you will be prompted to provide a password
+ to lock your Wallet account from unauthorized access
+
+ `;
+ dialogElement.shadowRoot
+ ?.querySelector('dialog #spinner')
+ ?.after(finalStepElement);
+ // listen to auth state change to manage dialog content
+ const unsubscribe = authProvider.getOnAuthStateChanged(
+ async user => {
+ if (user) {
+ finalStepElement.remove();
+ unsubscribe();
+ resolve({
+ uid: user.uid,
+ isAnonymous: user.isAnonymous,
+ authMethod: detail as SigninMethod
+ });
+ }
+ }
+ );
+ } catch (error: unknown) {
+ dialogElement.hideModal();
+ const message =
+ (error as Error)?.message ||
+ 'An error occured. Please try again.';
+ reject(
+ new Error(`Error while connecting with ${detail}: ${message}`)
+ );
+ }
+ return;
+ }
+ if (detail === 'connect-wallet') {
+ try {
+ const walletType = await dialogElement.promptWalletType();
+ Logger.log(`[INFO] Wallet type: `, walletType);
+ switch (walletType) {
+ case 'browser-extension': {
+ // const { uid } = await authWithExternalWallet();
+ // await dialogElement.toggleSpinnerAsCheck();
+ resolve({
+ uid: undefined,
+ isAnonymous: true,
+ authMethod: detail as SigninMethod
+ });
+ break;
+ }
+ case 'import-seed': {
+ // import seed
+ const { seed } = await promptImportSeedElement(
+ dialogElement?.shadowRoot?.querySelector(
+ '#spinner'
+ ) as HTMLElement
+ );
+ Logger.log(`[INFO] Import seed: `, {
+ seed
+ });
+ if (!seed) {
+ throw new Error('Seed is required to connect');
+ }
+ const { uid } = await authByImportSeed({
+ seed
+ });
+ resolve({
+ uid,
+ authMethod: detail as SigninMethod
+ });
+ break;
+ }
+ case 'import-privatekey': {
+ // import private key and request password
+ const { privateKey, isEncrypted } =
+ await promptImportPrivatekeyElement(
+ dialogElement?.shadowRoot?.querySelector(
+ '#spinner'
+ ) as HTMLElement
+ );
+ Logger.log(`[INFO] Import private key: `, {
+ privateKey,
+ isEncrypted
+ });
+ if (!privateKey) {
+ throw new Error('Private key is required to connect');
+ }
+ const { uid } = await authByImportPrivateKey({
+ privateKey,
+ isEncrypted
+ });
+ resolve({
+ uid,
+ authMethod: detail as SigninMethod
+ });
+ break;
+ }
+ default:
+ throw new Error('Invalid wallet type');
+ }
+ } catch (error: unknown) {
+ const message =
+ (error as Error)?.message ||
+ 'An error occured. Please try again.';
+ reject(new Error(`Error while connecting: ${message}`));
+ }
+ }
+ });
+ dialogElement.addEventListener('reset', async () => {
+ await storageService.clear();
+ dialogElement.reset();
+ });
+ }
+ );
+};
+
+export {
+ HexaSigninDialogElement,
+ setupSigninDialogElement,
+ addAndWaitUIEventsResult
+};
diff --git a/src/lib/ui/prompt-download-element/prompt-download-element.css b/src/lib/ui/prompt-download-element/prompt-download-element.css
new file mode 100644
index 00000000..17c2e978
--- /dev/null
+++ b/src/lib/ui/prompt-download-element/prompt-download-element.css
@@ -0,0 +1,3 @@
+.button__skip {
+ cursor: pointer;
+}
diff --git a/src/lib/ui/prompt-download-element/prompt-download-element.ts b/src/lib/ui/prompt-download-element/prompt-download-element.ts
new file mode 100644
index 00000000..e1ce91bc
--- /dev/null
+++ b/src/lib/ui/prompt-download-element/prompt-download-element.ts
@@ -0,0 +1,81 @@
+import { CheckboxElement } from '../checkbox-element/checkbox-element';
+// import css from './prompt-download-element.css';
+
+const css = `.button__skip {
+ cursor: pointer;
+}
+`;
+
+export const promptToDownloadElement = async (
+ ref: HTMLElement
+): Promise<{
+ withEncryption?: boolean;
+ skip?: boolean;
+}> => {
+ const container = document.createElement('div');
+ container.classList.add('prompt-container');
+ ref.after(container);
+ ref.style.display = 'none';
+
+ return new Promise(resolve => {
+ container.innerHTML = `
+
+
+
+ Backup your wallet PrivateKey
+
+
+
+ Download your backup file. We don't keep any copies of your private key, so make sure to keep it safe!
+ It's the only way to recover your wallet.
+
+
+ ${CheckboxElement({
+ label: 'Encrypt backup file',
+ id: 'toggle__encription',
+ checked: true
+ })}
+
+
+
+ skip
+ `;
+
+ const toggleEncription = container.querySelector(
+ '#toggle__encription'
+ ) as HTMLInputElement;
+ const buttonSkip = container.querySelector(
+ '.button__skip'
+ ) as HTMLButtonElement;
+ buttonSkip.addEventListener('click', e => {
+ e.preventDefault();
+ resolve({
+ skip: true
+ });
+ container.remove();
+ ref.style.display = 'block';
+ });
+
+ const buttonDownload = container.querySelector(
+ '#button__download'
+ ) as HTMLButtonElement;
+ buttonDownload.addEventListener('click', async () => {
+ resolve({
+ withEncryption: toggleEncription.checked
+ });
+ container.remove();
+ ref.style.display = 'block';
+ });
+
+ // manage dialog close btn
+ const mainCloseBtn = ref.closest('dialog')?.querySelector('#cancel');
+ if (mainCloseBtn) {
+ mainCloseBtn.addEventListener('click', e => {
+ e.preventDefault();
+ resolve({ skip: true });
+ container.remove();
+ ref.style.display = 'block';
+ });
+ }
+ });
+};
diff --git a/src/lib/ui/prompt-email-password-element/prompt-email-password-element.ts b/src/lib/ui/prompt-email-password-element/prompt-email-password-element.ts
new file mode 100644
index 00000000..a16dc10f
--- /dev/null
+++ b/src/lib/ui/prompt-email-password-element/prompt-email-password-element.ts
@@ -0,0 +1,157 @@
+import { storageService } from '../../services/storage.service';
+
+const isValideInputs = (
+ inputs: {
+ inputPassword: HTMLInputElement;
+ inputEmail?: HTMLInputElement;
+ },
+ ops?: {
+ hideEmail?: boolean;
+ hidePassword?: boolean;
+ }
+) => {
+ const { inputPassword, inputEmail } = inputs;
+ const { hideEmail, hidePassword } = ops || {};
+ const minPasswordLength = 6;
+ const maxPasswordLength = 32;
+ const isValidEmail = hideEmail ? true : inputEmail?.checkValidity();
+ const isValidPassword = hidePassword
+ ? true
+ : !hidePassword &&
+ inputPassword?.value.length >= minPasswordLength &&
+ inputPassword?.value.length <= maxPasswordLength;
+ return isValidEmail && isValidPassword;
+};
+
+export const promptEmailPasswordElement = async (
+ ref: HTMLElement,
+ ops?: {
+ hideEmail?: boolean;
+ hidePassword?: boolean;
+ }
+): Promise<{
+ password?: string;
+ email?: string;
+}> => {
+ const { hideEmail, hidePassword } = ops || {};
+ console.log({ hideEmail, hidePassword });
+ const minPasswordLength = 4;
+ const maxPasswordLength = 32;
+ const isCreating = !(await storageService.isExistingPrivateKeyStored());
+
+ return new Promise(resolve => {
+ const container = document.createElement('div');
+ container.classList.add('prompt-container');
+ const html = `
+
+
+ ${isCreating ? '
Create a new Wallet
' : '
Connect to your Wallet
'}
+
+ ${
+ isCreating
+ ? `Enter your ${hideEmail ? '' : 'email '}${hidePassword ? '' : '& password'} to create a new wallet. ${hidePassword ? '' : `The password you enter will only be used to authenticate you.`}`
+ : `Enter your ${hideEmail ? '' : 'email '}${hidePassword ? '' : '& password'}`
+ }
+
+
+ ${
+ hideEmail
+ ? ''
+ : ``
+ }
+ ${
+ hidePassword
+ ? ''
+ : ``
+ }
+
+
+ `;
+ container.innerHTML = html;
+ ref.after(container);
+ ref.style.display = 'none';
+
+ const inputPassword = container.querySelector(
+ '.prompt__input.password'
+ ) as HTMLInputElement;
+ const inputEmail = container.querySelector(
+ '.prompt__input.email'
+ ) as HTMLInputElement;
+ const button = container.querySelector(
+ '.prompt__button'
+ ) as HTMLButtonElement;
+
+ // manage validation of input to enable button
+ inputPassword?.addEventListener('input', () => {
+ const isValid = isValideInputs({ inputPassword, inputEmail }, ops);
+ button.disabled = !isValid;
+ });
+ inputEmail?.addEventListener('input', () => {
+ const isValid = isValideInputs({ inputPassword, inputEmail }, ops);
+ button.disabled = !isValid;
+ });
+
+ button.addEventListener('click', () => {
+ resolve({
+ password: inputPassword?.value,
+ email: inputEmail?.value
+ });
+ container.remove();
+ // prevent flash ui. ref will be hiden to display backup step
+ // if is creating wallet. This is why we dont switch to display block
+ // if (!isCreating) {
+ ref.style.display = 'block';
+ // }
+ });
+ });
+};
diff --git a/src/lib/ui/prompt-import-privatekey-element/prompt-import-privatekey-element.ts b/src/lib/ui/prompt-import-privatekey-element/prompt-import-privatekey-element.ts
new file mode 100644
index 00000000..c724f3bd
--- /dev/null
+++ b/src/lib/ui/prompt-import-privatekey-element/prompt-import-privatekey-element.ts
@@ -0,0 +1,71 @@
+import { CheckboxElement } from '../checkbox-element/checkbox-element';
+
+export const promptImportPrivatekeyElement = async (
+ ref: HTMLElement
+): Promise<{
+ privateKey: string;
+ isEncrypted: boolean;
+}> => {
+ const container = document.createElement('div');
+ container.classList.add('prompt-container');
+ ref.after(container);
+ ref.style.display = 'none';
+ container.innerHTML = `
+
+
+ Import Privatekey backup file
+ & Authenticate with Google to continue
+
+
+ ${CheckboxElement({
+ label: 'Encrypted backup file',
+ id: 'toggle__encription'
+ })}
+
+
+
+ `;
+
+ const buttonImportPrivatekey = container.querySelector(
+ '#button__import_privatekey'
+ ) as HTMLButtonElement;
+ const inputImportFile = container.querySelector(
+ '#input__import_file'
+ ) as HTMLInputElement;
+ const toggleEncription = container.querySelector(
+ '#toggle__encription'
+ ) as HTMLInputElement;
+
+ const resutl = await new Promise<{
+ privateKey: string;
+ isEncrypted: boolean;
+ }>((resolve, reject) => {
+ buttonImportPrivatekey.addEventListener('click', e => {
+ e.preventDefault();
+ inputImportFile.click();
+ });
+
+ inputImportFile.addEventListener('change', async () => {
+ const file = inputImportFile.files?.[0];
+ if (!file) return;
+ try {
+ const content = await file.text();
+ const isEncrypted = toggleEncription.checked;
+ resolve({
+ privateKey: content,
+ isEncrypted
+ });
+ container.remove();
+ ref.style.display = 'block';
+ } catch (error) {
+ reject(error);
+ container.remove();
+ ref.style.display = 'block';
+ }
+ });
+ });
+
+ return resutl;
+};
diff --git a/src/lib/ui/prompt-import-seed-element/prompt-import-seed-element.css b/src/lib/ui/prompt-import-seed-element/prompt-import-seed-element.css
new file mode 100644
index 00000000..948b7de0
--- /dev/null
+++ b/src/lib/ui/prompt-import-seed-element/prompt-import-seed-element.css
@@ -0,0 +1,31 @@
+.prompt__message {
+ margin-bottom: 1.5rem;
+}
+.prompt__message h4 {
+ margin: 0 auto -0.7rem;
+ font-size: 1.3em;
+}
+#input__seed,
+#input__password {
+ display: block;
+ min-height: 50px;
+ width: 100%;
+ max-width: 300px;
+ margin: 0rem auto 0.5rem;
+ text-align: center;
+ color: var(--text-color);
+ background: var(--button-background);
+ border: var(--dialog-border);
+ border-radius: 12px;
+ padding: 12px 16px;
+ font-size: 1em;
+ font-family: inherit;
+}
+
+#input__seed {
+ min-height: 110px;
+}
+#input__seed::placeholder {
+ text-align: center;
+ transform: translateY(50%);
+}
diff --git a/src/lib/ui/prompt-import-seed-element/prompt-import-seed-element.ts b/src/lib/ui/prompt-import-seed-element/prompt-import-seed-element.ts
new file mode 100644
index 00000000..7cb6b54d
--- /dev/null
+++ b/src/lib/ui/prompt-import-seed-element/prompt-import-seed-element.ts
@@ -0,0 +1,79 @@
+// import css from './prompt-import-seed-element.css';
+
+const css = `.prompt__message {
+ margin-bottom: 1.5rem;
+}
+.prompt__message h4 {
+ margin: 0 auto -0.7rem;
+ font-size: 1.3em;
+}
+#input__seed,
+#input__password {
+ display: block;
+ min-height: 50px;
+ width: 100%;
+ max-width: 300px;
+ margin: 0rem auto 0.5rem;
+ text-align: center;
+ color: var(--text-color);
+ background: var(--button-background);
+ border: var(--dialog-border);
+ border-radius: 12px;
+ padding: 12px 16px;
+ font-size: 1em;
+ font-family: inherit;
+}
+
+#input__seed {
+ min-height: 110px;
+}
+#input__seed::placeholder {
+ text-align: center;
+ transform: translateY(50%);
+}
+`;
+
+export const promptImportSeedElement = async (
+ ref: HTMLElement
+): Promise<{
+ seed: string;
+}> => {
+ const container = document.createElement('div');
+ container.classList.add('prompt-container');
+ ref.after(container);
+ ref.style.display = 'none';
+ container.innerHTML = `
+
+
+
Import Secret Seed
+
Access your Wallet with secret seed & authenticate with Google.
+
+
+ The password you enter will encrypts your seed & gives access to your funds. Please store your password in a safe place. We don’t keep your information & can’t restore it.
+
+
+
+ `;
+
+ const inputSeed = container.querySelector('#input__seed') as HTMLInputElement;
+ const buttonImportSeed = container.querySelector(
+ '#button__import_seed'
+ ) as HTMLButtonElement;
+
+ return new Promise(resolve => {
+ buttonImportSeed.addEventListener('click', () => {
+ resolve({
+ seed: inputSeed.value,
+ });
+ container.remove();
+ ref.style.display = 'block';
+ });
+ });
+};
diff --git a/src/lib/ui/prompt-password-element/prompt-password-element.ts b/src/lib/ui/prompt-password-element/prompt-password-element.ts
new file mode 100644
index 00000000..a2ea7ac9
--- /dev/null
+++ b/src/lib/ui/prompt-password-element/prompt-password-element.ts
@@ -0,0 +1,165 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { storageService } from '../../services/storage.service';
+
+export const promptPasswordElement = async (
+ ref: HTMLElement
+): Promise => {
+ const minPasswordLength = 6;
+ const maxPasswordLength = 32;
+
+ const isCreating = !(await storageService.isExistingPrivateKeyStored());
+ console.log('isCreating', isCreating);
+ const isValideInputs = (value: string, confirmeValue?: string) => {
+ if (!confirmeValue) {
+ return (
+ value.length > minPasswordLength - 1 &&
+ value.length < maxPasswordLength - 1
+ );
+ }
+ return (
+ value.length >= minPasswordLength - 1 &&
+ confirmeValue?.length > minPasswordLength - 1 &&
+ value === confirmeValue &&
+ value.length < maxPasswordLength - 1
+ );
+ };
+
+ const focusNextInput = (input: HTMLInputElement) => {
+ // focus next input
+ const next = input.nextElementSibling as HTMLInputElement | null;
+ if (next) {
+ next.focus();
+ } else {
+ input.blur();
+ }
+ };
+
+ return new Promise(resolve => {
+ const container = document.createElement('div');
+ container.classList.add('prompt-container');
+ const html = `
+
+
+ ${isCreating ? '
Create a new Wallet
' : '
Connect to your Wallet
'}
+
${
+ isCreating ? 'Protect your Wallet with a password' : 'Welcome back!'
+ }
+
+ ${
+ isCreating
+ ? `The password you enter encrypts your private key & gives access to your funds. Please store your password in a safe place. We don’t keep your information & can’t restore it.`
+ : `Unlock with your password.`
+ }
+
+
+
+
+
+
+
+
+
+
+
+ ${
+ !isCreating
+ ? ``
+ : ''
+ }
+
+ `;
+ container.innerHTML = html;
+ ref.after(container);
+ ref.style.display = 'none';
+
+ const inputs = Array.from(
+ container.querySelectorAll('.prompt__input.single')
+ ) as HTMLInputElement[];
+ const button = container.querySelector(
+ '.prompt__button'
+ ) as HTMLButtonElement;
+ button.addEventListener('click', () => {
+ resolve(inputs.map(i => i.value).join(''));
+ container.remove();
+ // prevent flash ui. ref will be hiden to display backup step
+ // if is creating wallet. This is why we dont switch to display block
+ // if (!isCreating) {
+ // ref.style.display = 'block';
+ // }
+ ref.style.display = 'block';
+ });
+
+ // manage validation of input to enable button
+ for (let index = 0; index < inputs.length; index++) {
+ const input = inputs[index];
+ input.addEventListener('focus', e => {
+ // clear input value on focus and prevent autofill & preventDefault
+ e.preventDefault();
+ input.value = '';
+ const isValid = isValideInputs(inputs.map(i => i.value).join(''));
+ button.disabled = !isValid;
+ });
+ input.addEventListener('input', () => {
+ if (input.value !== '' && input.value.length === 1) {
+ focusNextInput(input);
+ }
+ const isValid = isValideInputs(inputs.map(i => i.value).join(''));
+ button.disabled = !isValid;
+ });
+ }
+
+ // manage reset & create new wallet
+ const createBtn = container.querySelector(
+ '#create-new-wallet'
+ ) as HTMLButtonElement;
+ createBtn?.addEventListener('click', () => {
+ resolve(null);
+ container.remove();
+ ref.style.display = 'block';
+ });
+ });
+};
diff --git a/src/lib/ui/prompt-signout-element/prompt-signout-element.css b/src/lib/ui/prompt-signout-element/prompt-signout-element.css
new file mode 100644
index 00000000..21c0ff57
--- /dev/null
+++ b/src/lib/ui/prompt-signout-element/prompt-signout-element.css
@@ -0,0 +1,10 @@
+.button__skip {
+ cursor: pointer;
+}
+p.information {
+ margin: 12px auto 16px;
+ border: solid 1px rgb(255, 217, 0);
+ background: rgb(255 215 0 / 25%);
+ padding: 0.5rem 0.25rem;
+ border-radius: 3px;
+}
diff --git a/src/lib/ui/prompt-signout-element/prompt-signout-element.ts b/src/lib/ui/prompt-signout-element/prompt-signout-element.ts
new file mode 100644
index 00000000..aa8ec5f6
--- /dev/null
+++ b/src/lib/ui/prompt-signout-element/prompt-signout-element.ts
@@ -0,0 +1,103 @@
+import { CheckboxElement } from '../checkbox-element/checkbox-element';
+// import css from './prompt-signout-element.css';
+
+const css = `.button__skip {
+ cursor: pointer;
+}
+p.information {
+ margin: 12px auto 16px;
+ border: solid 1px rgb(255, 217, 0);
+ background: rgb(255 215 0 / 25%);
+ padding: 0.5rem 0.25rem;
+ border-radius: 3px;
+}
+`;
+
+export const promptSignoutElement = async (
+ ref: HTMLElement,
+ ops?: {
+ html?: string;
+ }
+): Promise<{
+ withEncryption?: boolean;
+ skip?: boolean;
+ clearStorage?: boolean;
+ cancel?: boolean;
+}> => {
+ const { html } = ops || {};
+ const container = document.createElement('div');
+ container.classList.add('prompt-container');
+ ref.after(container);
+ ref.style.display = 'none';
+
+ return new Promise(resolve => {
+ container.innerHTML = `
+
+ ${
+ html ||
+ `
+
+ Signout
+
+
+ You are about to signout from your Wallet and your Private Key still remains encrypted on this device unless you remove it.
+
+ ${CheckboxElement({
+ label: 'Download encrypted backup file',
+ id: 'toggle__download',
+ checked: true
+ })}
+ ${CheckboxElement({
+ label: 'Remove data from device',
+ id: 'toggle__clear_data'
+ })}
+
+
+
+ cancel
+ `
+ }
+
+ `;
+
+ const toggleDownload = container.querySelector(
+ '#toggle__download'
+ ) as HTMLInputElement;
+ const toggleClear = container.querySelector(
+ '#toggle__clear_data'
+ ) as HTMLInputElement;
+ const buttonCancel = container.querySelector(
+ '.button__cancel'
+ ) as HTMLButtonElement;
+ buttonCancel.addEventListener('click', e => {
+ e.preventDefault();
+ resolve({ cancel: true });
+ container.remove();
+ ref.style.display = 'block';
+ });
+
+ const buttonDownload = container.querySelector(
+ '#button__signout'
+ ) as HTMLButtonElement;
+ buttonDownload.addEventListener('click', async () => {
+ resolve({
+ withEncryption: toggleDownload.checked,
+ skip: !toggleDownload.checked ? true : false,
+ clearStorage: toggleClear.checked
+ });
+ container.remove();
+ ref.style.display = 'block';
+ });
+
+ // manage dialog close btn
+ const mainCloseBtn = ref.closest('dialog')?.querySelector('#cancel');
+ if (mainCloseBtn) {
+ mainCloseBtn.addEventListener('click', e => {
+ e.preventDefault();
+ resolve({ cancel: true });
+ container.remove();
+ ref.style.display = 'block';
+ });
+ }
+ });
+};
diff --git a/src/lib/ui/prompt-wallet-type-element/prompt-wallet-type-element.css b/src/lib/ui/prompt-wallet-type-element/prompt-wallet-type-element.css
new file mode 100644
index 00000000..a95f0e19
--- /dev/null
+++ b/src/lib/ui/prompt-wallet-type-element/prompt-wallet-type-element.css
@@ -0,0 +1,14 @@
+
+.prompt__wallet_type button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+}
+
+.prompt__wallet_type button svg {
+ width: 28px;
+ height: 28px;
+ color: var(--text-color);
+}
diff --git a/src/lib/ui/prompt-wallet-type-element/prompt-wallet-type-element.ts b/src/lib/ui/prompt-wallet-type-element/prompt-wallet-type-element.ts
new file mode 100644
index 00000000..3761aace
--- /dev/null
+++ b/src/lib/ui/prompt-wallet-type-element/prompt-wallet-type-element.ts
@@ -0,0 +1,85 @@
+// import extensionIcon from '../../assets/svg/extension-puzzle-outline.svg';
+// import downloadIcon from '../../assets/svg/download-outline.svg';
+// import keyOutlineIcon from '../../assets/svg/key-outline.svg';
+// import css from './prompt-wallet-type-element.css';
+
+const extensionIcon = ``;
+const downloadIcon = ``;
+const keyOutlineIcon = ``;
+
+const css = `
+.prompt__wallet_type button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+}
+
+.prompt__wallet_type button svg {
+ width: 28px;
+ height: 28px;
+ color: var(--text-color);
+}
+`;
+
+export const promptWalletTypeElement = async (
+ ref: HTMLElement
+): Promise<'browser-extension' | 'import-privatekey' | 'import-seed'> => {
+ const container = document.createElement('div');
+ container.classList.add('prompt-container');
+ ref.after(container);
+ ref.style.display = 'none';
+ container.innerHTML = `
+
+
+
+
+
+
+ `;
+
+ const buttonExternalWallet = container.querySelector(
+ '#button__external_wallet'
+ ) as HTMLButtonElement;
+ const buttonImportWallet = container.querySelector(
+ '#button__import_wallet'
+ ) as HTMLButtonElement;
+ const buttonImportSeed = container.querySelector(
+ '#button__import_seed'
+ ) as HTMLButtonElement;
+
+ return new Promise(resolve => {
+ buttonExternalWallet.addEventListener('click', () => {
+ resolve('browser-extension');
+ container.remove();
+ ref.style.display = 'block';
+ });
+
+ buttonImportWallet.addEventListener('click', () => {
+ // request `import-seed` or `import-privatekey`
+ // based on user selection
+ // this will be handled in the next step
+ resolve('import-privatekey');
+ container.remove();
+ // ref.style.display = 'block';
+ });
+
+ buttonImportSeed.addEventListener('click', () => {
+ resolve('import-seed');
+ container.remove();
+ ref.style.display = 'block';
+ });
+ });
+};
diff --git a/src/lib/ui/spinner-element/spinner-element.ts b/src/lib/ui/spinner-element/spinner-element.ts
new file mode 100644
index 00000000..02cabbe4
--- /dev/null
+++ b/src/lib/ui/spinner-element/spinner-element.ts
@@ -0,0 +1,103 @@
+export const SpinnerElement = () => {
+ return `
+
+
+ `;
+};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 00000000..f97b48db
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,50 @@
+export const parseApiKey = (hex: string) => {
+ // converte hex string to utf-8 string
+ if (!hex || hex.length <= 0) {
+ throw new Error('Unexisting API key');
+ }
+ const json = Buffer.from(hex, 'hex').toString('utf-8');
+ const apiKey = JSON.parse(json);
+ return apiKey as any;
+};
+
+/**
+ * Logger function
+ */
+export const Logger = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ log: (...args: any[]) => {
+ if (process.env.NEXT_PUBLIC_APP_IS_PROD === 'true') {
+ return;
+ }
+ console.log(...args);
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ error: (...args: any[]) => {
+ if (process.env.NEXT_PUBLIC_APP_IS_PROD === 'true') {
+ return;
+ }
+ console.error(...args);
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ warn: (...args: any[]) => {
+ if (process.env.NEXT_PUBLIC_APP_IS_PROD === 'true') {
+ return;
+ }
+ console.warn(...args);
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ info: (...args: any[]) => {
+ if (process.env.NEXT_PUBLIC_APP_IS_PROD === 'true') {
+ return;
+ }
+ console.info(...args);
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ debug: (...args: any[]) => {
+ if (process.env.NEXT_PUBLIC_APP_IS_PROD === 'true') {
+ return;
+ }
+ console.debug(...args);
+ }
+};
diff --git a/src/lib/vite-env.d.ts b/src/lib/vite-env.d.ts
new file mode 100755
index 00000000..55f00d62
--- /dev/null
+++ b/src/lib/vite-env.d.ts
@@ -0,0 +1,11 @@
+// ///
+
+// // https://medium.com/@sampsonjoliver/importing-html-files-from-typescript-bd1c50909992
+// declare module '*.html' {
+// const value: string;
+// export default value
+// }
+// declare module '*.css' {
+// const value: string;
+// export default value
+// }
\ No newline at end of file
diff --git a/src/network/Avalanche.ts b/src/network/Avalanche.ts
deleted file mode 100644
index d77c7dbd..00000000
--- a/src/network/Avalanche.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Magic } from 'magic-sdk';
-import { AvalancheExtension } from '@magic-ext/avalanche';
-import { Avalanche, BinTools, Buffer, BN } from '@avalabs/avalanchejs';
-
-import { MagicWalletUtils } from "./MagicWallet";
-import { CHAIN_DEFAULT, NETWORK } from "../constants/chains";
-import { RPC_NODE_OPTIONS } from "../servcies/magic";
-
-// // The EVM (Ethereum Virtual Machine) class extends the abstract MagicWallet class
-// export class EVM extends MagicWallet {
-// // Web3 instance to interact with the Ethereum blockchain
-// web3Provider: Avalanche | null = null;
-
-// // The network for this class instance
-// public network: NETWORK;
-
-// constructor(network: NETWORK) {
-// super();
-// // Setting the network type
-// this.network = network;
-// // Calling the initialize method from MagicWallet
-// this.initialize();
-// }
-
-// // Asynchronous method to initialize the Web3 instance
-// async initializeWeb3(): Promise {
-// const RPC_NODE = RPC_NODE_OPTIONS.find((n) => n.chainId === this.network);
-// // const provider = await this.magic?.wallet.getProvider(); // Get the provider from the magic
-// this.web3Provider = new Avalanche(RPC_NODE?.rpcUrl, 443, 'https', 4, 'X'); // Initializing the Web3 instance
-// }
-
-// }
diff --git a/src/network/Bitcoin.ts b/src/network/Bitcoin.ts
deleted file mode 100644
index 376ad220..00000000
--- a/src/network/Bitcoin.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { BitcoinExtension } from "@magic-ext/bitcoin";
-import { MagicWalletUtils } from "./MagicWallet";
-import { NETWORK } from "@/constants/chains";
-import { RPC_NODE_OPTIONS, getMagic } from "@/servcies/magic";
-
-
-export class BitcoinWalletUtils extends MagicWalletUtils {
-
- public web3Provider: null = null;
- public isMagicWallet = true;
-
- constructor(network: NETWORK) {
- super();
- this.network = network;
- }
-
- async _initializeWeb3() {
- console.log(`[INFO] Bitcoin: initializeWeb3...`);
- const magic = await getMagic({chainId: this.network});
- const RPC_NODE = RPC_NODE_OPTIONS.find((n) => n.chainId === this.network);
- if (!RPC_NODE) {
- throw new Error("RPC Node config fail. Incorect params, ");
- }
- // get account address and wallet type
- try {
- const info = await magic.user.getInfo() || undefined;
- this.walletAddress = info?.publicAddress|| undefined;
- } catch (error) {
- console.log('error', error);
- }
- }
-
- async loadBalances() {
- throw new Error("loadBalances() - Method not implemented.");
- }
-
- async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
- throw new Error("sendToken() - Method not implemented.");
- }
-}
\ No newline at end of file
diff --git a/src/network/Cosmos.ts b/src/network/Cosmos.ts
deleted file mode 100644
index ada8de3e..00000000
--- a/src/network/Cosmos.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { SDKBase } from "@magic-sdk/provider"
-import { CosmosExtension } from '@magic-ext/cosmos';
-import { StargateClient } from '@cosmjs/stargate';
-
-import { MagicWalletUtils } from "./MagicWallet";
-import { CHAIN_DEFAULT, NETWORK } from "../constants/chains";
-import { RPC_NODE_OPTIONS, getMagic } from "../servcies/magic";
-import { IAsset } from "../interfaces/asset.interface";
-
-// // The EVM (Ethereum Virtual Machine) class extends the abstract MagicWallet class
-// export class Cosmos extends MagicWallet {
-// // Web3 instance to interact with the Ethereum blockchain
-// web3Provider: StargateClient|null = null;
-
-// // The network for this class instance
-// public network: NETWORK;
-
-// public assets: IAsset[] = [];
-
-// constructor(network: NETWORK) {
-// super();
-// // Setting the network type
-// this.network = network;
-// // Calling the initialize method from MagicWallet
-// this.initialize();
-// }
-
-// // Asynchronous method to initialize the Web3 instance
-// async initializeWeb3(): Promise {
-// const RPC_NODE = RPC_NODE_OPTIONS.find((n) => n.chainId === this.network);
-// if (!RPC_NODE) {
-// throw new Error("RPC Node config fail. Incorect params, ");
-// }
-// this.web3Provider = await StargateClient.connect(RPC_NODE?.rpcUrl);
-// }
-
-// public override async connect() {
-// // prompt to get email
-// const email = prompt('Enter your email');
-// if (!email) {
-// throw new Error("User not logged in");
-// }
-// try {
-// await this.loginWithOTP(email);
-// const infos = await this.getInfo();
-// console.log("connect infos", infos);
-// this.walletAddress = infos?.publicAddress;
-// // this.assets = await this._fetchUserAssets();
-// } catch (error) {
-// console.log("connect error", error);
-// }
-// }
-
-// protected async _fetchUserAssets(): Promise {
-// return [];
-// // const { publicAddress } = (await this.getInfo()) || {};
-// // if (!publicAddress) return [];
-// // const assets = await fetchUserAssets(publicAddress);
-// // if (!assets) return [];
-// // return assets;
-// }
-
-
-// public async sendTx(ops: {
-// toAddress: string;
-// amount: number;
-// }): Promise {
-// if (!this.walletAddress) {
-// throw new Error("User not logged in");
-// }
-// const {toAddress, amount } = ops;
-// const message = [
-// {
-// typeUrl: '/cosmos.bank.v1beta1.MsgSend',
-// value: {
-// fromAddress: this.walletAddress,
-// toAddress,
-// amount: [
-// {
-// amount: String(amount),
-// denom: 'atom',
-// },
-// ],
-// },
-// },
-// ];
-// const fee = {
-// amount: [{ denom: 'uatom', amount: '500' }],
-// gas: '200000',
-// };
-// if (!this._magicExtention) {
-// throw new Error("Magic extention not found");
-// }
-// const signTransactionResult = await this._magicExtention.sign(message, fee);
-// console.log('signTransactionResult', signTransactionResult);
-// return signTransactionResult;
-// }
-// }
-
-/**
- * Cosmos Wallet Utils
- * Support for Cosmos Wallet
- * StargateClient docs: https://cosmos.github.io/cosmjs/latest/stargate/classes/StargateClient.html
- */
-export class CosmosWalletUtils extends MagicWalletUtils {
-
- public web3Provider: StargateClient | null = null;
- public isMagicWallet = true;
- constructor(network: NETWORK) {
- super();
- this.network = network;
- }
-
- async _initializeWeb3() {
- const magic = await getMagic({chainId: this.network});
- const RPC_NODE = RPC_NODE_OPTIONS.find((n) => n.chainId === this.network);
- if (!RPC_NODE) {
- throw new Error("RPC Node config fail. Incorect params, ");
- }
- const web3Provider = await StargateClient.connect(RPC_NODE?.rpcUrl);
- // this.web3Provider = web3Provider;
- // get account address and wallet type
- try {
- const info = await magic.user.getInfo() || undefined;
- this.walletAddress = info?.publicAddress|| undefined;
- } catch (error) {
- console.log('error', error);
- }
- }
-
- async loadBalances() {
- throw new Error("loadBalances() - Method not implemented.");
- }
-
- async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
- throw new Error("sendToken() - Method not implemented.");
- }
-}
\ No newline at end of file
diff --git a/src/network/EVM.ts b/src/network/EVM.ts
deleted file mode 100644
index dabb8b1b..00000000
--- a/src/network/EVM.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { ethers } from "ethers";
-import { NETWORK } from "../constants/chains";
-import { getTokensBalances } from "../servcies/ankr.service";
-import { MagicWalletUtils } from "./MagicWallet";
-import { getMagic } from "@/servcies/magic";
-import { getTokensPrice } from "@/servcies/lifi.service";
-
-/**
- * Function tha takes wallet address and fetches all assets for that wallet
- * using Ankr API. It also fetches token price from LiFi API if Ankr response contains
- * token with balance > 0 && balanceUsd === 0 && priceUsd === 0
- * This ensures that all tokens have price in USD and the total balance is calculated correctly
- * for each token that user has in the wallet.
- */
-const fetchUserAssets = async (walletAddress: string, force?: boolean) => {
- console.log(`[INFO] fetchUserAssets()`, walletAddress);
- if (!walletAddress) return null;
- const assets = await getTokensBalances([], walletAddress, force);
- // remove elements with 0 balance and add to new arrany using extracting
- const assetWithBalanceUsd = [],
- assetsWithoutBalanceUsd = [];
- for (let i = 0; i < assets.length; i++) {
- const asset = assets[i];
- (asset.balanceUsd === 0 && asset.balance > 0)
- ? assetsWithoutBalanceUsd.push(asset)
- : assetWithBalanceUsd.push(asset);
- }
- // get token price for tokens without balanceUsd
- const tokenWithbalanceUsd = await getTokensPrice(assetsWithoutBalanceUsd);
- return [
- ...assetWithBalanceUsd,
- ...tokenWithbalanceUsd
- ];
-};
-
-export class EVMWalletUtils extends MagicWalletUtils {
- public web3Provider: ethers.providers.Web3Provider | null = null;
- public isMagicWallet: boolean = true;
-
- constructor(network: NETWORK) {
- super();
- this.network = network;
- }
-
- async _initializeWeb3() {
- const magic = await getMagic({ chainId: this.network });
- const provider = await magic.wallet.getProvider();
- const web3Provider = new ethers.providers.Web3Provider(provider, "any");
- this.web3Provider = web3Provider;
- // detect if is metamask and set correct network
- if (
- web3Provider?.connection?.url === "metamask" ||
- web3Provider.provider.isMetaMask
- ) {
- this.isMagicWallet = false;
- await this._setMetamaskNetwork();
- } else {
- this.isMagicWallet = true;
- }
- // get account address and wallet type
- try {
- const signer = web3Provider?.getSigner();
- this.walletAddress = (await signer?.getAddress()) || undefined;
- } catch (error) {
- console.error(
- "[ERROR] User is not connected. Unable to get wallet address.",
- error
- );
- // return;
- }
- }
-
- async loadBalances(force?: boolean) {
- if (!this.walletAddress) return;
- const assets = await fetchUserAssets(this.walletAddress, force);
- if (!assets) return;
- this.assets = assets;
- }
-
- async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
- if(!this.web3Provider) {
- throw new Error("Web3Provider is not initialized");
- }
- try {
- console.log({
- destination, decimalAmount, contactAddress
- })
- const signer = this.web3Provider.getSigner();
- const from = await signer.getAddress();
- const amount = ethers.utils.parseUnits(decimalAmount.toString(), 18); // Convert 1 ether to wei
- const contract = new ethers.Contract(contactAddress, ["function transfer(address, uint256)"], signer);
-
- const data = contract.interface.encodeFunctionData("transfer", [destination, amount] );
-
- const tx = await signer.sendTransaction({
- to: destination,
- value: amount,
- // data
- });
- const receipt = await tx.wait();
- // // Load token contract
- // const tokenContract = new ethers.Contract(contactAddress, ['function transfer(address, uint256)'], signer);
-
- // // Send tokens to recipient
- // const transaction = await tokenContract.transfer(destination, amount);
- // const receipt = await transaction.wait();
- // console.log(receipt);
-
-
-
- //Define the data parameter
- // const data = contract.interface.encodeFunctionData("transfer", [destination, amount] )
- // const tx = await signer.sendTransaction({
- // to: contactAddress,
- // from,
- // value: ethers.utils.parseUnits("0.000", "ether"),
- // data: data
- // });
- // // const tx = await contract.transfer(destination, amount);
- // // Wait for transaction to be mined
- // const receipt = await tx.wait();
- return receipt;
- } catch (err: any) {
- console.error(err);
- throw new Error("Error during sending token");
- }
- }
-
- private async _setMetamaskNetwork() {
- if (!this.web3Provider) {
- throw new Error("Web3Provider is not initialized");
- }
- // check current network is same as selected network
- const network = await this.web3Provider.getNetwork();
- if (network.chainId === this.network) {
- return;
- }
- // switch network with ether
- try {
- await this.web3Provider.send("wallet_switchEthereumChain", [
- { chainId: ethers.utils.hexValue(this.network) },
- ]);
- } catch (error) {
- throw new Error(
- `Error during network setting. Please switch to ${this.network} network and try again.`
- );
- }
- }
-
- async estimateGas() {
- // const limit = await provider.estimateGas({
- // from: signer.address,
- // to: tokenContract,
- // value: ethers.utils.parseUnits("0.000", "ether"),
- // data: data
-
- // });
- }
-}
diff --git a/src/network/MagicWallet.ts b/src/network/MagicWallet.ts
deleted file mode 100644
index c8384dd1..00000000
--- a/src/network/MagicWallet.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { CHAIN_DEFAULT, NETWORK } from "../constants/chains";
-import { connect, disconnect } from "../servcies/magic";
-import { IAsset } from "../interfaces/asset.interface";
-import { Web3ProviderType } from "@/interfaces/web3.interface";
-
-
-// Abstract class to handle magic network-specific operations
-export abstract class MagicWalletUtils {
-
- public get walletAddress():string|undefined {
- return this._walletAddress;
- };
- public set walletAddress(walletAddress: string | undefined) {
- this._walletAddress = walletAddress;
- }
- public get network(): NETWORK {
- return this._network;
- };
- public set network(network: NETWORK) {
- this._network = network;
- }
- public get assets(): IAsset[] {
- return this._assets;
- };
- public set assets(assets: IAsset[]) {
- this._assets = assets;
- }
- private _walletAddress: string | undefined;
- private _network!: NETWORK;
- private _assets: IAsset[] = [];
-
- public abstract web3Provider: Web3ProviderType | null;
- public abstract isMagicWallet: boolean;
- public abstract loadBalances(force?: boolean): Promise;
- public abstract sendToken(destination: string, decimalAmount: number, contactAddress: string): Promise;
- protected abstract _initializeWeb3(): Promise;
-
- /**
- * Static method to create MagicWallet instance based on network type
- * @param network Chain ID as number
- * @returns
- */
- public static async create(network: NETWORK = CHAIN_DEFAULT.id): Promise {
- let walletUtil: MagicWalletUtils;
- switch (network) {
- // case NETWORK.avalanche: {
- // const { Avalanche } = require("./Avalanche");
- // return new Avalanche(network);
- // break;
- // }
- case NETWORK.bitcoin: {
- const { BitcoinWalletUtils } = require("./Bitcoin");
- walletUtil = new BitcoinWalletUtils(network);
- break;
- }
- case NETWORK.cosmos: {
- const { CosmosWalletUtils } = require("./Cosmos");
- walletUtil = new CosmosWalletUtils(network);
- break;
- }
- case NETWORK.solana: {
- const { SolanaWalletUtils } = require("./Solana");
- walletUtil = new SolanaWalletUtils(network);
- break;
- }
- default: {
- const { EVMWalletUtils } = require("./EVM");
- walletUtil = new EVMWalletUtils(network);
- break;
- }
- }
- await walletUtil._initializeWeb3();
- return walletUtil;
- }
-
- async connect(ops?: {
- email: string;
- }) {
- const address = await connect(ops);
- if (!address) {
- throw new Error("Connect wallet fail");
- }
- await this._initializeWeb3();
- this.walletAddress = address;
- return this.walletAddress;
- }
-
- async disconnect() {
- this.walletAddress = undefined;
- this.assets = [];
- return disconnect();
- }
-
-}
\ No newline at end of file
diff --git a/src/network/Solana.ts b/src/network/Solana.ts
deleted file mode 100644
index 300f9f54..00000000
--- a/src/network/Solana.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-import { Connection, Transaction} from '@solana/web3.js';
-import { MagicWalletUtils } from "./MagicWallet";
-import { RPC_NODE_OPTIONS, getMagic } from "../servcies/magic";
-import { IAsset } from "../interfaces/asset.interface";
-import { NETWORK } from '@/constants/chains';
-
-/**
- * Solana Wallet Utils
- * Support for Solana Wallet
- */
-export class SolanaWalletUtils extends MagicWalletUtils {
-
- public web3Provider: Connection | null = null;
- public isMagicWallet: boolean = true;
- constructor(network: NETWORK) {
- super();
- this.network = network;
- }
-
- async _initializeWeb3() {
- console.log(`[INFO] Solana: initializeWeb3...`);
- const magic = await getMagic({chainId: this.network});
- const RPC_NODE = RPC_NODE_OPTIONS.find((n) => n.chainId === this.network);
- if (!RPC_NODE) {
- throw new Error("RPC Node config fail. Incorect params, ");
- }
- const web3Provider = new Connection(RPC_NODE?.rpcUrl);
- this.web3Provider = web3Provider;
- // get account address and wallet type
- try {
- const info = await magic.user.getInfo() || undefined;
- console.log('[INFO] Solana: user info', info);
-
- this.walletAddress = info?.publicAddress|| undefined;
- } catch (error) {
- console.log('error', error);
- }
- }
-
- async loadBalances() {
- throw new Error("loadBalances() - Method not implemented.");
- }
-
- async sendToken(destination: string, decimalAmount: number, contactAddress: string) {
- throw new Error("sendToken() - Method not implemented.");
- }
-}
\ No newline at end of file
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 915204f0..e8c98c22 100755
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -41,7 +41,7 @@ function MyApp({ Component, pageProps }: AppProps) {
-
+
@@ -130,7 +130,23 @@ function MyApp({ Component, pageProps }: AppProps) {
-
+ {/* */}
+ {process.env.NEXT_PUBLIC_APP_IS_PROD !== 'true' && (<>
+
+ {process.env.NEXT_PUBLIC_APP_IS_LOCAL === 'true' && (<>[ENV] LOCAL: Using fake data.>)}
+ {process.env.NEXT_PUBLIC_APP_IS_LOCAL === 'false' &&
+ process.env.NEXT_PUBLIC_APP_IS_PROD === 'false' && (<>[ENV] DEV: Using you own API Keys>)}
+
+ >)}
>
);
}
diff --git a/src/pool/Aave.pool.ts b/src/pool/Aave.pool.ts
index 999840c2..18fd7ae9 100644
--- a/src/pool/Aave.pool.ts
+++ b/src/pool/Aave.pool.ts
@@ -8,7 +8,7 @@ import {
supplyWithPermit,
withdraw,
} from "@/servcies/aave.service";
-import { Web3ProviderType } from "@/interfaces/web3.interface";
+import { Web3SignerType } from "@/interfaces/web3.interface";
export interface IAavePool extends IMarketPool {
readonly unborrowedLiquidity: string;
@@ -87,20 +87,20 @@ export class AavePool extends MarketPool implements IAavePool {
public async deposit(
amount: number,
- provider: Web3ProviderType
+ signer: Web3SignerType
): Promise {
// handle invalid amount
if (isNaN(amount) || amount <= 0) {
throw new Error("Invalid amount. Value must be greater than 0.");
}
- if (!(provider instanceof ethers.providers.Web3Provider)) {
- throw new Error("No EVM web3Provider");
+ if (!signer) {
+ throw new Error("No EVM signer");
}
const markets = getMarkets(this.chainId);
// call method
const { chainId, aTokenAddress, underlyingAsset } = this;
const params = {
- provider: provider as ethers.providers.Web3Provider,
+ signer,
reserve: { chainId, aTokenAddress, underlyingAsset },
amount: amount.toString(),
onBehalfOf: undefined,
@@ -118,7 +118,7 @@ export class AavePool extends MarketPool implements IAavePool {
public async withdraw(
amount: number,
- provider: Web3ProviderType
+ signer: Web3SignerType
): Promise {
// handle invalid amount
if (isNaN(amount) || amount <= 0) {
@@ -126,14 +126,14 @@ export class AavePool extends MarketPool implements IAavePool {
"[INFO] AavePool.withdraw() Invalid amount. Value must be greater than 0."
);
}
- if (!(provider instanceof ethers.providers.Web3Provider)) {
- throw new Error("[INFO] AavePool.withdraw() No EVM web3Provider");
+ if (!signer) {
+ throw new Error("[INFO] AavePool.withdraw() No EVM signer");
}
const markets = getMarkets(this.chainId);
const { underlyingAsset, aTokenAddress } = this;
// call method
const params = {
- provider: provider as ethers.providers.Web3Provider,
+ signer,
reserve: { underlyingAsset, aTokenAddress },
amount: amount.toString(),
onBehalfOf: undefined,
@@ -151,7 +151,7 @@ export class AavePool extends MarketPool implements IAavePool {
public async borrow(
amount: number,
- provider: Web3ProviderType
+ signer: Web3SignerType
): Promise {
// handle invalid amount
if (isNaN(amount) || amount <= 0) {
@@ -159,15 +159,15 @@ export class AavePool extends MarketPool implements IAavePool {
"[INFO] AavePool.borrow() Invalid amount. Value must be greater than 0."
);
}
- if (!(provider instanceof ethers.providers.Web3Provider)) {
- throw new Error("[INFO] AavePool.borrow() No EVM web3Provider");
+ if (!signer) {
+ throw new Error("[INFO] AavePool.borrow() No EVM signer");
}
const markets = getMarkets(this.chainId);
// call method
const { underlyingAsset } = this;
// call method
const params = {
- provider,
+ signer,
reserve: { underlyingAsset },
amount: amount.toString(),
onBehalfOf: undefined,
@@ -185,21 +185,21 @@ export class AavePool extends MarketPool implements IAavePool {
public async repay(
amount: number,
- provider: Web3ProviderType
+ signer: Web3SignerType
): Promise {
// handle invalid amount
if (isNaN(amount) || amount <= 0) {
throw new Error("Invalid amount. Value must be greater than 0.");
}
- if (!(provider instanceof ethers.providers.Web3Provider)) {
- throw new Error("[INFO] AavePool.borrow() No EVM web3Provider");
+ if (!signer) {
+ throw new Error("[INFO] AavePool.borrow() No EVM signer");
}
const markets = getMarkets(this.chainId);
// call method
const { underlyingAsset } = this;
// call method
const params = {
- provider: provider as ethers.providers.Web3Provider,
+ signer,
reserve: { underlyingAsset },
amount: amount.toString(),
onBehalfOf: undefined,
diff --git a/src/pool/Market.pool.ts b/src/pool/Market.pool.ts
index 085f84e6..1a851d20 100644
--- a/src/pool/Market.pool.ts
+++ b/src/pool/Market.pool.ts
@@ -1,6 +1,7 @@
import { IMarketPool } from "@/interfaces/reserve.interface";
import { IAavePool } from "./Aave.pool";
-import { Web3ProviderType } from "@/interfaces/web3.interface";
+import { Web3SignerType } from "@/interfaces/web3.interface";
+import { ethers } from "ethers";
export abstract class MarketPool implements IMarketPool {
readonly id: string;
@@ -40,10 +41,10 @@ export abstract class MarketPool implements IMarketPool {
public borrowBalance: number;
- public abstract deposit(amount: number, provider: Web3ProviderType): Promise;
- public abstract withdraw(amount: number, provider: Web3ProviderType): Promise;
- public abstract borrow(amount: number, provider: Web3ProviderType): Promise;
- public abstract repay(amount: number, provider: Web3ProviderType): Promise;
+ public abstract deposit(amount: number, signer: ethers.Signer): Promise;
+ public abstract withdraw(amount: number, signer: ethers.Signer): Promise;
+ public abstract borrow(amount: number, signer: ethers.Signer): Promise;
+ public abstract repay(amount: number, signer: ethers.Signer): Promise;
constructor(pool: IMarketPool) {
this.id = pool.id;
diff --git a/src/servcies/aave.service.ts b/src/servcies/aave.service.ts
index dda6ee4e..0adff11d 100644
--- a/src/servcies/aave.service.ts
+++ b/src/servcies/aave.service.ts
@@ -7,7 +7,7 @@ import {
UiPoolDataProvider,
WalletBalanceProvider,
} from "@aave/contract-helpers";
-import { ethers } from "ethers";
+import { Signer, Wallet, ethers, providers } from "ethers";
import * as MARKETS from "@bgd-labs/aave-address-book";
import {
FormatReserveUSDResponse,
@@ -19,15 +19,15 @@ import { ChainId } from "@aave/contract-helpers";
import { CHAIN_AVAILABLES } from "../constants/chains";
import { IUserSummary } from "../interfaces/reserve.interface";
import { IAavePool } from "@/pool/Aave.pool";
+import web3Connector from "./firebase-web3-connect";
const submitTransaction = async (ops: {
- provider: ethers.providers.Web3Provider; // Signing transactions requires a wallet provider
+ signer: Signer; // Signing transactions requires a wallet provider
tx: EthereumTransactionTypeExtended;
}) => {
- const { provider, tx } = ops;
+ const { signer, tx } = ops;
const extendedTxData = await tx.tx();
const { from, ...txData } = extendedTxData;
- const signer = provider.getSigner(from);
const txResponse = await signer.sendTransaction({
...txData,
value: txData.value || undefined,
@@ -42,16 +42,16 @@ const submitTransaction = async (ops: {
* @returns
*/
const submitMultiplesTransaction = async (ops: {
- provider: ethers.providers.Web3Provider; // Signing transactions requires a wallet provider
+ signer: Signer; // Signing transactions requires a wallet provider
txs: EthereumTransactionTypeExtended[];
}) => {
- const { provider, txs } = ops;
+ const { signer, txs } = ops;
let txResponses: ethers.providers.TransactionResponse[] = [];
for (let i = 0; i < txs.length; i++) {
const tx = txs[i];
console.log("submit tx: ", i, tx);
const txResponse = await submitTransaction({
- provider,
+ signer,
tx,
});
txResponses.push(txResponse);
@@ -69,20 +69,22 @@ export const fetchTVL = async (reserves: {totalLiquidityUSD: string;}[]): Promis
};
export const supply = async (ops: {
- provider: ethers.providers.Web3Provider;
+ signer: Signer; // Signing transactions requires a wallet provider
reserve: Pick;
amount: string;
onBehalfOf?: string;
poolAddress: string;
gatewayAddress: string;
}) => {
- const { provider, reserve: {underlyingAsset}, amount, onBehalfOf, poolAddress, gatewayAddress } =
+ const { signer, reserve: {underlyingAsset}, amount, onBehalfOf, poolAddress, gatewayAddress } =
ops;
- const pool = new Pool(provider, {
+ if(!signer.provider) {
+ throw new Error('Provider not available');
+ }
+ const pool = new Pool(signer.provider, {
POOL: poolAddress,
WETH_GATEWAY: gatewayAddress,
});
- const signer = provider.getSigner();
const user = await signer?.getAddress();
let txs: EthereumTransactionTypeExtended[];
try {
@@ -98,7 +100,7 @@ export const supply = async (ops: {
}
console.log("txs: ", txs);
const txResponses: ethers.providers.TransactionResponse[] = await submitMultiplesTransaction({
- provider,
+ signer,
txs,
});
console.log("result: ", txResponses);
@@ -108,21 +110,24 @@ export const supply = async (ops: {
};
export const supplyWithPermit = async (ops: {
- provider: ethers.providers.Web3Provider;
+ signer: Signer;
reserve: Pick;
amount: string;
onBehalfOf?: string;
poolAddress: string;
gatewayAddress: string;
}) => {
- const { provider, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
+ const { signer, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
ops;
- const pool = new Pool(provider, {
+ if(!signer.provider) {
+ throw new Error('Provider not available');
+ }
+ const pool = new Pool(signer.provider, {
POOL: poolAddress,
WETH_GATEWAY: gatewayAddress,
});
// handle incorrect network
- const network = await provider.getNetwork();
+ const network = await signer.provider.getNetwork();
if (network.chainId !== reserve.chainId) {
throw new Error(
`Incorrect network, please switch to ${CHAIN_AVAILABLES.find(
@@ -130,13 +135,11 @@ export const supplyWithPermit = async (ops: {
)?.name}`
);
}
- const signer = provider.getSigner();
const user = await signer?.getAddress();
const tokenAdress = reserve.underlyingAsset;
// create timestamp of 10 minutes from now
const deadline = `${new Date().setMinutes(new Date().getMinutes() + 10)}`;
-
- const isTestnet = provider.network?.chainId === 5 || provider.network?.chainId === 80001 || false;
+ const isTestnet = network?.chainId === 5 || network?.chainId === 80001 || false;
const havePermitConfig =
permitByChainAndToken[network.chainId]?.[tokenAdress] || false;
if (!havePermitConfig || isTestnet) {
@@ -150,9 +153,9 @@ export const supplyWithPermit = async (ops: {
amount,
deadline,
});
- console.log("dataToSign: ", dataToSign);
-
- const signature = await provider.send("eth_signTypedData_v4", [
+ console.log("dataToSign: ", dataToSign, web3Connector.currentWallet());
+
+ const signature = await web3Connector.currentWallet()?.provider?.send("eth_signTypedData_v4", [
user,
dataToSign,
]);
@@ -169,7 +172,7 @@ export const supplyWithPermit = async (ops: {
console.log("txs: ", txs);
const txResponses: ethers.providers.TransactionResponse[] = await submitMultiplesTransaction({
- provider,
+ signer,
txs,
});
console.log("result: ", txResponses);
@@ -178,22 +181,23 @@ export const supplyWithPermit = async (ops: {
};
export const withdraw = async (ops: {
- provider: ethers.providers.Web3Provider;
+ signer: Signer;
reserve: Pick;
amount: string;
onBehalfOf?: string;
poolAddress: string;
gatewayAddress: string;
}) => {
- const { provider, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
+ const { signer, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
ops;
-
- const pool = new Pool(provider, {
+ if(!signer.provider) {
+ throw new Error('Provider not available');
+ }
+ const pool = new Pool(signer.provider, {
POOL: poolAddress,
WETH_GATEWAY: gatewayAddress,
});
- const signer = provider.getSigner();
const user = await signer?.getAddress();
/*
@@ -212,7 +216,7 @@ export const withdraw = async (ops: {
});
const txResponses: ethers.providers.TransactionResponse[] = await submitMultiplesTransaction({
- provider,
+ signer,
txs,
});
console.log("result: ", txResponses);
@@ -220,24 +224,25 @@ export const withdraw = async (ops: {
};
export const borrow = async (ops: {
- provider: ethers.providers.Web3Provider;
+ signer: Signer;
reserve:Pick;
amount: string;
onBehalfOf?: string;
poolAddress: string;
gatewayAddress: string;
}) => {
- const { provider, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
+ const { signer, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
ops;
-
- const pool = new Pool(provider, {
+ if(!signer.provider) {
+ throw new Error('Provider not available');
+ }
+ const pool = new Pool(signer.provider, {
POOL: poolAddress,
WETH_GATEWAY: gatewayAddress,
});
console.log("pool: ", pool);
- const signer = provider.getSigner();
const currentAccount = await signer?.getAddress();
const txs = await pool.borrow({
@@ -250,7 +255,7 @@ export const borrow = async (ops: {
console.log("txs: ", txs);
const txResponses: ethers.providers.TransactionResponse[] = await submitMultiplesTransaction({
- provider,
+ signer,
txs,
});
console.log("result: ", txResponses);
@@ -260,24 +265,25 @@ export const borrow = async (ops: {
};
export const repay = async (ops: {
- provider: ethers.providers.Web3Provider;
+ signer: Signer;
reserve: Pick;
amount: string;
onBehalfOf?: string;
poolAddress: string;
gatewayAddress: string;
}) => {
- const { provider, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
+ const { signer, reserve, amount, onBehalfOf, poolAddress, gatewayAddress } =
ops;
-
- const pool = new Pool(provider, {
+ if(!signer.provider) {
+ throw new Error('Provider not available');
+ }
+ const pool = new Pool(signer.provider, {
POOL: poolAddress,
WETH_GATEWAY: gatewayAddress,
});
console.log("pool: ", pool);
- const signer = provider.getSigner();
const currentAccount = await signer?.getAddress();
const txs = await pool.repay({
@@ -290,7 +296,7 @@ export const repay = async (ops: {
console.log("txs: ", txs);
const txResponses: ethers.providers.TransactionResponse[] = await submitMultiplesTransaction({
- provider,
+ signer,
txs,
});
console.log("result: ", txResponses);
@@ -305,14 +311,8 @@ export const getMarkets = (chainId: number) => {
return MARKETS.AaveV3Ethereum;
case chainId === MARKETS.AaveV3Polygon.CHAIN_ID:
return MARKETS.AaveV3Polygon;
- case chainId === MARKETS.AaveV3Mumbai.CHAIN_ID:
- return MARKETS.AaveV3Mumbai;
- case chainId === MARKETS.AaveV3Fuji.CHAIN_ID:
- return MARKETS.AaveV3Fuji;
case chainId === MARKETS.AaveV3Arbitrum.CHAIN_ID:
return MARKETS.AaveV3Arbitrum;
- case chainId === MARKETS.AaveV3ArbitrumGoerli.CHAIN_ID:
- return MARKETS.AaveV3ArbitrumGoerli;
case chainId === MARKETS.AaveV3Optimism.CHAIN_ID:
return MARKETS.AaveV3Optimism;
case chainId === MARKETS.AaveV3Avalanche.CHAIN_ID:
@@ -325,13 +325,23 @@ export const getMarkets = (chainId: number) => {
return MARKETS.AaveV3Base;
case chainId === MARKETS.AaveV3Scroll.CHAIN_ID:
return MARKETS.AaveV3Scroll;
+ /**
+ * HERE TESTNETS
+ */
+ case chainId === MARKETS.AaveV3ArbitrumGoerli.CHAIN_ID:
+ return MARKETS.AaveV3ArbitrumGoerli;
+ case chainId === MARKETS.AaveV3Sepolia.CHAIN_ID:
+ return MARKETS.AaveV3Sepolia;
+ case chainId === MARKETS.AaveV3Mumbai.CHAIN_ID:
+ return MARKETS.AaveV3Mumbai;
+ case chainId === MARKETS.AaveV3Fuji.CHAIN_ID:
+ return MARKETS.AaveV3Fuji;
default:
throw new Error(`ChainId ${chainId} not supported`);
}
};
export const getPools = async (ops: {
- // provider: ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider;
market: MARKETTYPE;
currentTimestamp: number;
}) => {
@@ -479,7 +489,7 @@ export const getContractData = async (ops: {
};
const getWalletBalance = async (ops: {
- provider: ethers.providers.Web3Provider | ethers.providers.JsonRpcProvider;
+ provider: ethers.providers.JsonRpcProvider;
market: MARKETTYPE;
user: string | null;
currentTimestamp: number;
@@ -604,16 +614,20 @@ export const getUserSummaryAndIncentives = async (ops: {
export type MARKETTYPE =
| typeof MARKETS.AaveV3Ethereum
| typeof MARKETS.AaveV3Polygon
- | typeof MARKETS.AaveV3Mumbai
| typeof MARKETS.AaveV3Avalanche
- | typeof MARKETS.AaveV3Fuji
| typeof MARKETS.AaveV3Arbitrum
| typeof MARKETS.AaveV3ArbitrumGoerli
| typeof MARKETS.AaveV3Optimism
| typeof MARKETS.AaveV3BNB
| typeof MARKETS.AaveV3PolygonZkEvm
| typeof MARKETS.AaveV3Base
- | typeof MARKETS.AaveV3Scroll;
+ | typeof MARKETS.AaveV3Scroll
+ /**
+ * HERE TESTNETS
+ */
+ | typeof MARKETS.AaveV3Mumbai
+ | typeof MARKETS.AaveV3Fuji
+ | typeof MARKETS.AaveV3Sepolia;
export const permitByChainAndToken: {
[chainId: number]: Record;
diff --git a/src/servcies/ankr.service.ts b/src/servcies/ankr.service.ts
index 955ef635..0962f14f 100644
--- a/src/servcies/ankr.service.ts
+++ b/src/servcies/ankr.service.ts
@@ -1,5 +1,6 @@
import { IAsset } from "@/interfaces/asset.interface";
import { IChain, CHAIN_AVAILABLES } from "../constants/chains";
+import { TxInterface } from "@/interfaces/tx.interface";
interface IAnkrTokenReponse {
blockchain: string;
@@ -16,7 +17,57 @@ interface IAnkrTokenReponse {
contractAddress?: string;
}
+interface AnkrTransactionResponseInterface {
+ v: string;
+ r: string;
+ s: string;
+ nonce: string;
+ blockNumber: string;
+ from: string;
+ to: string;
+ gas: string;
+ gasPrice: string;
+ input: string;
+ transactionIndex: string;
+ blockHash: string;
+ value: string;
+ type: string;
+ cumulativeGasUsed: string;
+ gasUsed: string;
+ hash: string;
+ status: string;
+ blockchain: string;
+ timestamp: string;
+}
+// Generated by https://quicktype.io
+
+export interface IAnkrNTFResponse {
+ blockchain: string;
+ name: string;
+ tokenId: string;
+ tokenUrl: string;
+ imageUrl: string;
+ collectionName: string;
+ symbol: string;
+ contractType: string;
+ contractAddress: string;
+}
+
const fake_data = [
+ {
+ blockchain: "sepolia",
+ tokenName: "Link",
+ tokenSymbol: "LINK",
+ tokenDecimals: 18,
+ tokenType: "ERC20",
+ holderAddress: "0x475ef9fb4f8d43b63ac9b22fa41fd4db8a103550",
+ contractAddress: "0xf8fb3713d459d7c1018bd0a49d19b4c44290ebe5",
+ balance: "100",
+ balanceRawInteger: "100000000000000000000",
+ balanceUsd: "2000",
+ tokenPrice: "20",
+ thumbnail: "",
+ },
{
blockchain: "optimism",
tokenName: "Ether",
@@ -45,6 +96,82 @@ const fake_data = [
thumbnail: "",
},
];
+const fake_nft_data = [
+ {
+ "blockchain": "sepolia",
+ "name": "",
+ "tokenId": "12",
+ "tokenUrl": "https://treatdao.com/api/nft/12",
+ "imageUrl": "",
+ "collectionName": "Treat NFT Minter",
+ "symbol": "TreatNFTMinter",
+ "contractType": "ERC1155",
+ "contractAddress": "0x36f8f51f65fe200311f709b797baf4e193dd0b0d",
+ "quantity": "1"
+ },
+ {
+ "blockchain": "sepolia",
+ "name": "India",
+ "tokenId": "531",
+ "tokenUrl": "https://airxnft.herokuapp.com/api/token/531",
+ "imageUrl": "https://ipfs.io/ipfs/QmXxSCFNTNLLrJfBJyi9uT5PFTbx1HDbd6qZxrrSchi4aG",
+ "collectionName": "Aircoins Metaverse",
+ "symbol": "AIRx",
+ "contractType": "ERC721",
+ "contractAddress": "0x025983cd3530f78b71b4874eb5272c189b357e61",
+ "traits": [
+ {
+ "trait_type": "Property TYPE",
+ "value": "Command Center"
+ }
+ ]
+ },
+ {
+ "blockchain": "sepolia",
+ "name": "vitalik.cloud",
+ "tokenId": "73906452355594127029039375271145516945927406532858726769026903911185640775143",
+ "tokenUrl": "https://md.namefi.io/vitalik.cloud",
+ "imageUrl": "https://md.namefi.io/svg/vitalik.cloud/image.svg",
+ "collectionName": "NamefiNFT",
+ "symbol": "NFNFT",
+ "contractType": "ERC721",
+ "contractAddress": "0x0000000000cf80e7cf8fa4480907f692177f8e06",
+ "traits": [
+ {
+ "trait_type": "Is Locked",
+ "value": "🔓 Unlocked"
+ },
+ {
+ "trait_type": "Is Frozen",
+ "value": "false"
+ },
+ {
+ "trait_type": "Top Level Domain (TLD)",
+ "value": "cloud"
+ },
+ {
+ "trait_type": "TLD Length",
+ "value": "5"
+ },
+ {
+ "trait_type": "Second Level Domain (TLD)",
+ "value": "vitalik"
+ },
+ {
+ "trait_type": "SLD Length",
+ "value": "7"
+ },
+ {
+ "trait_type": "Is IDN",
+ "value": "false"
+ },
+ {
+ "trait_type": "Expiration Date",
+ "value": "2024-10-11"
+ }
+ ]
+ }
+];
const formatingTokensBalances = (
assets: IAnkrTokenReponse[],
@@ -72,6 +199,21 @@ const formatingTokensBalances = (
});
};
+const formatingNFTsBalances = (
+ assets: IAnkrNTFResponse[],
+ chainsList: IChain[]
+): (IAnkrNTFResponse & {chain: IChain})[] => {
+ const result = assets.map((asset) => {
+ return {
+ ...asset,
+ chain: chainsList.find((c) => c.value === asset.blockchain)
+ };
+ })
+ //.filter((asset) => asset.chain !== undefined);
+
+ return result as (IAnkrNTFResponse & {chain: IChain})[];
+}
+
const getCachedData = async (key: string, force?: boolean) => {
const data = localStorage.getItem(key);
if (!data) {
@@ -113,9 +255,9 @@ export const getTokensBalances = async (
: CHAIN_AVAILABLES.filter((availableChain) =>
chainIds.find((c) => c === availableChain.id)
);
- // return fake_data for DEV mode
- if (process.env.NEXT_PUBLIC_APP_IS_PROD === "false") {
- console.log("[INFO] DEV mode return fake data");
+ // return fake_data for LOCAL mode
+ if (process.env.NEXT_PUBLIC_APP_IS_LOCAL === 'true') {
+ console.log("[INFO] LOCAL mode return fake data");
const balances = formatingTokensBalances(fake_data, chainsList);
return balances;
}
@@ -154,3 +296,123 @@ export const getTokensBalances = async (
await setCachedData(KEY, balances);
return balances;
};
+
+/**
+ * Doc url: https://api-docs.ankr.com/reference/post_ankr-getnftsbyowner
+ * @param chainIds array of chain ids
+ * @param address wallet address to get balances
+ * @param force to get data from API
+ * @returns object with balances property that contains an array of TokenInterface
+ */
+export const getNFTsBalances = async (
+ chainIds: number[],
+ address: string,
+ force?: boolean // force to get data from API
+): Promise<(IAnkrNTFResponse & {
+ chain: IChain;
+})[]> => {
+ const chainsList =
+ chainIds.length <= 0
+ ? CHAIN_AVAILABLES
+ : CHAIN_AVAILABLES.filter((availableChain) =>
+ chainIds.find((c) => c === availableChain.id)
+ );
+ // return fake_data for LOCAL mode
+ if (process.env.NEXT_PUBLIC_APP_IS_LOCAL === 'true') {
+ console.log("[INFO] LOCAL mode return fake data");
+ const nfts: any[] = fake_nft_data;
+ return formatingNFTsBalances(nfts, chainsList);
+ }
+ const KEY = `hexa-ankr-nft-service-${address}`;
+ const cachedData = await getCachedData(KEY, force);
+ console.log("cachedData:", cachedData);
+ if (cachedData) {
+ return cachedData;
+ }
+ const APP_ANKR_APIKEY = process.env.NEXT_PUBLIC_APP_ANKR_APIKEY;
+ const blockchain = chainsList
+ .filter(({ type }) => type === "evm")
+ .map(({ value }) => value);
+ const url = `https://rpc.ankr.com/multichain/${APP_ANKR_APIKEY}/?ankr_getNFTsByOwner=`;
+ const options: RequestInit = {
+ method: "POST",
+ headers: {
+ accept: "application/json",
+ "content-type": "application/json",
+ },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ method: "ankr_getNFTsByOwner",
+ params: {
+ blockchain,
+ walletAddress: address,
+ },
+ id: 1,
+ }),
+ };
+ const res = await fetch(url, options);
+ const assets = (await res.json())?.result?.assets||[];
+ const balances = formatingNFTsBalances(assets, chainsList);
+ console.log("[INFO] {ankrFactory} getNFTsBalances(): ", balances);
+ await setCachedData(KEY, balances);
+ return balances;
+}
+
+// export const getTransactionsHistory = async (
+// chainIds: number[],
+// address: string
+// ) => {
+// const KEY = `hexa-ankr-service-txs-${address}`;
+// const cachedData = await getCachedData(KEY);
+// console.log("cachedData:", cachedData);
+// if (cachedData) {
+// return cachedData;
+// }
+// const url = `https://rpc.ankr.com/multichain/${process.env.NEXT_PUBLIC_APP_ANKR_APIKEY}/?ankr_getTransactionsByAddress=`;
+// const blockchain = CHAIN_AVAILABLES
+// .filter(({testnet}) => !testnet)
+// .filter(({ type }) => type === "evm")
+// .map(({ value }) => value);
+// // fromTimestamp = Beginning of a time period starting 30 days ago. UNIX timestamp.
+// const fromTimestamp = Math.floor(Date.now() / 10000) - 30 * 24 * 60 * 60;
+// const toTimestamp = Math.floor(Date.now() / 1000);
+// const options: RequestInit = {
+// method: "POST",
+// headers: {
+// accept: "application/json",
+// "content-type": "application/json",
+// },
+// body: JSON.stringify({
+// jsonrpc: "2.0",
+// method: "ankr_getTransactionsByAddress",
+// params: {
+// blockchain,
+// address: [address],
+// fromTimestamp,
+// toTimestamp,
+// descOrder: true,
+// },
+// id: 1,
+// }),
+// };
+// const res = await fetch(url, options);
+// const transactions: AnkrTransactionResponseInterface[] =
+// (await res.json())?.result?.transactions || [];
+// // convert transaction.timestamp to Date
+// const txs: TxInterface[] = transactions.map((tx) => {
+// return {
+// ...tx,
+// blockNumber: parseInt(tx.blockNumber),
+// cumulativeGasUsed: parseInt(tx.cumulativeGasUsed),
+// gas: parseInt(tx.gas),
+// gasPrice: parseInt(tx.gasPrice),
+// gasUsed: parseInt(tx.gasUsed),
+// nonce: parseInt(tx.nonce),
+// status: parseInt(tx.status),
+// timestamp: new Date(parseInt(tx.timestamp) * 1000)
+// };
+// });
+// await setCachedData(KEY, txs);
+// console.log("[INFO] {ankrFactory} getTransactionsHistory(): ", txs);
+// return txs;
+// };
diff --git a/src/servcies/coingecko.service.ts b/src/servcies/coingecko.service.ts
new file mode 100644
index 00000000..49dcebef
--- /dev/null
+++ b/src/servcies/coingecko.service.ts
@@ -0,0 +1,176 @@
+import { SeriesData } from "@/components/ui/LightChart";
+
+export type TokenInfo = {
+ description: {en: string};
+ categories: string[];
+ image: {
+ thumb: string;
+ small: string;
+ large: string;
+ };
+ market_data: {
+ ath: {usd: number};
+ ath_change_percentage: {usd: number};
+ ath_date: { usd: string };
+ atl: {usd: number};
+ atl_change_percentage: {usd: number};
+ atl_date: { usd: string };
+ circulating_supply: number;
+ current_price: { usd: number };
+ fully_diluted_valuation: { usd: number };
+ high_24h: { usd: number };
+ last_updated: string;
+ low_24h: { usd: number };
+ market_cap: { usd: number };
+ market_cap_change_24h: number;
+ market_cap_change_24h_in_currency: { usd: number };
+ market_cap_change_percentage_24h: number;
+ market_cap_change_percentage_24h_in_currency: { usd: number };
+ market_cap_fdv_ratio: number;
+ market_cap_rank: number;
+ max_supply: number;
+ price_change_24h: number;
+ price_change_24h_in_currency: { usd: number };
+ price_change_percentage_1h_in_currency: { usd: number };
+ price_change_percentage_1y_in_currency: { usd: number };
+ price_change_percentage_7d_in_currency: { usd: number };
+ price_change_percentage_14d_in_currency: { usd: number };
+ price_change_percentage_24h_in_currency: { usd: number };
+ price_change_percentage_30d_in_currency: { usd: number };
+ price_change_percentage_60d_in_currency: { usd: number };
+ price_change_percentage_200d_in_currency: { usd: number };
+ total_supply: number;
+ total_value_locked: number|null;
+ total_volume: { usd: number };
+ };
+ sentiment_votes_down_percentage: number;
+ sentiment_votes_up_percentage: number;
+
+};
+
+export class CoingeckoAPI {
+
+ static options?:RequestInit = process.env.NEXT_PUBLIC_APP_IS_PROD === 'true'
+ ? {
+ headers: new Headers({
+ 'x-cg-demo-api-key': process.env.NEXT_PUBLIC_APP_COINGECKO_APIKEY
+ })
+ }
+ : undefined;
+
+ /**
+ * Method to get Coingecko token id from symbol
+ * @param symbol
+ * @returns
+ */
+ static async getTokenId(symbol: string) {
+ // convert symbol to coingeeko id
+ const responseList = localStorage.getItem('hexa-lite-coingeeko/coinList');
+ let data;
+ if (responseList) {
+ data = JSON.parse(responseList);
+ } else {
+ const fetchResponse = await fetch(`https://api.coingecko.com/api/v3/coins/list`, this.options);
+ data = await fetchResponse.json();
+ localStorage.setItem('hexa-lite-coingeeko/coinList', JSON.stringify(data));
+ }
+ const coin: {id?: string} = data.find(
+ (c: any) =>
+ c.symbol.toLowerCase() === symbol.toLowerCase()
+ && !c.name.toLowerCase().includes('bridged')
+ );
+ return coin?.id;
+ }
+
+ /**
+ * Method to get coin market chart data
+ * @param id
+ * @param interval
+ */
+ static async getTokenHistoryPrice(
+ symbol: string,
+ intervals: ('1D' | '1W' | '1M' | '1Y')[] = ['1D','1W','1M', '1Y']
+ ): Promise{
+ // convert symbol to coingeeko id
+ const coinId = await CoingeckoAPI.getTokenId(symbol);
+ if (!coinId) return new Map() as SeriesData;
+
+ const seriesData: SeriesData = new Map();
+ for (let index = 0; index < intervals.length; index++) {
+ const interval = intervals[index];
+
+ const responseToken = localStorage.getItem(`hexa-lite-coingeeko/coin/${coinId}/market_chart?interval=${interval}`);
+ const jsonData = JSON.parse(responseToken||'{}');
+ const isDeadlineReach = (Date.now() - jsonData.timestamp) > (60 * 1000 * 30);
+ if (responseToken && !isDeadlineReach && jsonData.data) {
+ seriesData.set(interval, jsonData.data);
+ } else {
+ const days = interval === '1D' ? 1 : interval === '1W' ? 7 : interval === '1M' ? 30 : 365;
+ const dataInterval = interval === '1D' ? '' : interval === '1W' ? '' : interval === '1M' ? '' : '&interval=daily';
+ const url = `https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=${days}${dataInterval}`;
+ const result = await fetch(url, this.options)
+ .then((res) => res.json())
+ .catch((error) => ({prices: []}));
+ const prices = (result?.prices as number[][]||[]);
+ const data = prices
+ .map(([time, value]: number[]) => {
+ const dataItem = {
+ time: time / 1000|| "",
+ value: Number(value),
+ };
+ return dataItem;
+ })
+ // remove latest element
+ .slice(0, -1)
+ // remove duplicates
+ .filter((item, index, self) => index === self.findIndex((t) => t.time === item.time));
+ seriesData.set(interval, data);
+ localStorage.setItem(`hexa-lite-coingeeko/coin/${coinId}/market_chart?interval=${interval}`, JSON.stringify({
+ data,
+ timestamp: Date.now()
+ }));
+ }
+ }
+ return seriesData;
+ }
+ /**
+ * Method to get token info: description, market data, community data
+ * @param symbol
+ * @returns
+ */
+ static async getTokenInfo(symbol: string) {
+ const tokenId = await CoingeckoAPI.getTokenId(symbol);
+ if (!tokenId) return undefined;
+ // check localstorage if data is stored from less than 1 day
+ const response = localStorage.getItem(`hexa-lite-coingeeko/coin/${tokenId}/info`);
+ const jsonData = JSON.parse(response||'{}');
+ const isDeadlineReach = (Date.now() - jsonData.timestamp) > (60 * 1000 * 60 * 24);
+ let tokenInfo;
+ if (response && !isDeadlineReach && jsonData.data) {
+ tokenInfo = jsonData.data;
+ } else {
+ // fetch data from coingecko
+ tokenInfo = await fetch(`https://api.coingecko.com/api/v3/coins/${tokenId}?market_data=true&community_data=true`, this.options)
+ .then((res) => res.json());
+ localStorage.setItem(`hexa-lite-coingeeko/coin/${tokenId}/info`, JSON.stringify({
+ data: tokenInfo,
+ timestamp: Date.now()
+ }));
+ }
+ return tokenInfo as TokenInfo;
+ }
+
+ /**
+ * Method to get simple price of a token
+ * @param id
+ * @param vs_currencies
+ * @returns
+ */
+ static async getSimplePrice(id: string, vs_currencies: string) {
+ const response = await fetch(
+ `https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=${vs_currencies}`, this.options
+ );
+ const json = await response.json();
+ return json[id]?.[vs_currencies];
+ }
+}
\ No newline at end of file
diff --git a/src/servcies/firebase-web3-connect.ts b/src/servcies/firebase-web3-connect.ts
new file mode 100644
index 00000000..e5e3390b
--- /dev/null
+++ b/src/servcies/firebase-web3-connect.ts
@@ -0,0 +1,209 @@
+import { FirebaseWeb3Connect } from '@/lib';
+import { auth } from '@/firebase-config';
+import { CHAIN_AVAILABLES, CHAIN_DEFAULT } from '@/constants/chains';
+import { TxInterface } from '@/interfaces/tx.interface';
+import { getTransactionsHistory } from './zerion.service';
+import { IAsset } from '@/interfaces/asset.interface';
+import { getNFTsBalances, getTokensBalances } from './ankr.service';
+import { getTokensPrice } from './lifi.service';
+import { Signer, utils } from 'ethers';
+import { INFT } from '@/interfaces/nft.interface';
+
+/**
+ * Function tha takes wallet address and fetches all assets for that wallet
+ * using Ankr API. It also fetches token price from LiFi API if Ankr response contains
+ * token with balance > 0 && balanceUsd === 0 && priceUsd === 0
+ * This ensures that all tokens have price in USD and the total balance is calculated correctly
+ * for each token that user has in the wallet.
+ */
+const fetchEVMAssets = async (walletAddress: string, force?: boolean) => {
+ console.log(`[INFO] fetchUserAssets()`, walletAddress);
+ if (!walletAddress) return null;
+ const assets = await getTokensBalances([], walletAddress, force);
+ // remove elements with 0 balance and add to new arrany using extracting
+ const assetWithBalanceUsd = [],
+ assetsWithoutBalanceUsd = [];
+ for (let i = 0; i < assets.length; i++) {
+ const asset = assets[i];
+ (asset.balanceUsd === 0 && asset.balance > 0)
+ ? assetsWithoutBalanceUsd.push(asset)
+ : assetWithBalanceUsd.push(asset);
+ }
+ // get token price for tokens without balanceUsd
+ const tokenWithbalanceUsd = await getTokensPrice(assetsWithoutBalanceUsd);
+ return [
+ ...assetWithBalanceUsd,
+ ...tokenWithbalanceUsd
+ ];
+};
+
+const origin = window.location.origin;
+const path = '/auth/link';
+const params = `/?finishSignUp=true`;
+const EMAIL_LINK_URL = [origin, path, params].join('');
+
+/**
+ * Web3Connector class that wraps FirebaseWeb3Connect class
+ * that provides methods to connect, disconnect, switch accross networks and
+ * get signer for the connected wallet.
+ * It also provides methods to load balances and transactions for the connected wallet
+ * and also provides methods to listen to connect state changes.
+ */
+class Web3Connector {
+
+ private readonly _connector = new FirebaseWeb3Connect(auth, 'APIKEY', {
+ chainId: CHAIN_DEFAULT.id,
+ dialogUI: {
+ integrator: 'Hexa Lite',
+ ops: {
+ authProvider: {
+ authEmailUrl: EMAIL_LINK_URL
+ }
+ }
+ }
+ });
+
+ async connect(){
+ const isLightmode = !document.querySelector('body')?.classList.contains('dark');
+ const { address } = await this._connector.connectWithUI(isLightmode) || {};
+ if (!address) {
+ throw new Error('Connect wallet fail');
+ }
+ return address;
+ }
+
+ async connectWithLink() {
+ await this._connector.connectWithLink();
+ }
+
+ async disconnect(){
+ const isLightmode = !document.querySelector('body')?.classList.contains('dark');
+ await this._connector.signout(true, isLightmode);
+ return true;
+ }
+ wallets(){
+ return [this._connector.wallet];
+ }
+ async switchNetwork(chainId: number){
+ await this._connector.switchNetwork(chainId);
+ }
+ currentWallet(){
+ return this._connector.wallet;
+ }
+
+ async getSigner(): Promise {
+ try {
+ const signer = await this._connector.wallet?.getSigner();
+ return signer;
+ } catch (error) {
+ return undefined;
+ }
+ }
+
+ async getNetworkFeesAsUSD(): Promise {
+ // get ethers network fees using ethers.js
+ const signer = await this.getSigner();
+ if (!signer) {
+ throw new Error('Signer not available');
+ }
+ const network = await signer.provider?.getNetwork();
+ if (!network) {
+ throw new Error('Network not available');
+ }
+ // get network fees in USD
+ const networkFees = await signer.provider?.getFeeData();
+ if (!networkFees || !networkFees.gasPrice || !networkFees.maxFeePerGas) {
+ throw new Error('Network fees not available');
+ }
+ // get network fees in ETH
+ const totalFee = utils.formatUnits(
+ networkFees.gasPrice.add(networkFees.maxFeePerGas), 'gwei'
+ );
+ // const response = await fetch('https://api.coingecko.com/api/v3/coins/ethereum');
+ // const result = await response.json();
+ // const ethPriceUSD = result?.market_data?.current_price?.usd as number||0;
+ // const total = ethPriceUSD * Number(totalFee);
+ return Number(totalFee).toFixed(0) + ' Gwei';
+ }
+
+ onConnectStateChanged(callback: (user: {
+ address: string;
+ } | null) => void){
+ this._connector.onConnectStateChanged(callback);
+ }
+
+ async loadBalances(force?: boolean){
+ const assets: IAsset[] = [];
+ for (const wallet of this.wallets()) {
+ if (!wallet) {
+ return assets;
+ }
+ const chain = CHAIN_AVAILABLES.find((chain) => chain.id === wallet.chainId);
+ switch (true) {
+ // evm wallet type
+ case chain?.type === 'evm': {
+ const evmAssets = await fetchEVMAssets(wallet.address, force)||[];
+ assets.push(...evmAssets);
+ break;
+ }
+ default:
+ break
+ }
+ }
+ return assets;
+ };
+
+ async loadTxs(force?: boolean) {
+ const txs: TxInterface[] = [];
+ for (const wallet of this.wallets()) {
+ if (!wallet) {
+ return txs;
+ }
+ const chain = CHAIN_AVAILABLES.find((chain) => chain.id === wallet.chainId);
+ switch (true) {
+ // evm wallet type
+ case chain?.type === 'evm': {
+ const result = await getTransactionsHistory(wallet.address);
+ txs.push(...result);
+ break;
+ }
+ default:
+ break
+ }
+ }
+ return txs;
+ }
+
+ async loadNFTs(force?: boolean) {
+ const nfts: INFT[] = [];
+ for (const wallet of this.wallets()) {
+ if (!wallet) {
+ return nfts;
+ }
+ const chain = CHAIN_AVAILABLES.find((chain) => chain.id === wallet.chainId);
+ switch (true) {
+ // evm wallet type
+ case chain?.type === 'evm': {
+ const evmAssets = await getNFTsBalances([], wallet.address, force)||[];
+ nfts.push(...evmAssets);
+ break;
+ }
+ default:
+ break
+ }
+ }
+ return nfts;
+
+ }
+
+ async backupWallet() {
+ const isLightmode = !document.querySelector('body')?.classList.contains('dark');
+ return this._connector.backupWallet(true, isLightmode);
+
+ }
+
+}
+const web3Connector = new Web3Connector();
+
+// export default instance
+export default web3Connector;
diff --git a/src/servcies/lifi.service.ts b/src/servcies/lifi.service.ts
index 651f294d..420d4eb8 100644
--- a/src/servcies/lifi.service.ts
+++ b/src/servcies/lifi.service.ts
@@ -764,9 +764,8 @@ export const fakeQuote = {
*/
export const sendTransaction = async (
quote: LiFiQuoteResponse,
- provider: ethers.providers.Web3Provider
+ signer: ethers.providers.JsonRpcSigner
) => {
- const signer = provider.getSigner();
const tx = await signer.sendTransaction(quote.transactionRequest);
const receipt = await tx.wait();
return receipt;
@@ -778,7 +777,7 @@ export const sendTransaction = async (
*/
export const checkAndSetAllowance = async (
- provider: ethers.providers.Web3Provider,
+ signer: ethers.providers.JsonRpcSigner,
tokenAddress: string,
approvalAddress: string,
amount: string
@@ -787,7 +786,6 @@ export const checkAndSetAllowance = async (
if (tokenAddress === ethers.constants.AddressZero) {
return;
}
- const signer = provider.getSigner();
const erc20 = new Contract(tokenAddress, ERC20_ABI, signer);
const address = await signer.getAddress();
const allowance = await erc20.allowance(address, approvalAddress);
@@ -811,7 +809,7 @@ export const swapWithLiFi = async (
fromAmount: string;
fromAddress: string;
},
- provider: ethers.providers.Web3Provider
+ signer: ethers.providers.JsonRpcSigner
) => {
const quote = await getQuote(
ops.fromChain,
@@ -822,12 +820,12 @@ export const swapWithLiFi = async (
ops.fromAddress
);
await checkAndSetAllowance(
- provider,
+ signer,
quote.action.fromToken.address,
quote.estimate.approvalAddress,
quote.action.fromAmount
);
- const receipt = await sendTransaction(quote, provider);
+ const receipt = await sendTransaction(quote, signer);
return receipt;
};
@@ -875,11 +873,12 @@ export const getTokensPrice = async (tokens: IAsset[]) => {
tokensResponse = responseData.tokens;
}
} catch (error) {
- throw error;
+ // throw error;
+ tokensResponse = {};
}
const tokenWithPrice: IAsset[] = [];
for (const token of tokens) {
- const index = tokensResponse[token.chain?.id as number].findIndex(
+ const index = tokensResponse?.[token.chain?.id as number].findIndex(
(t) => t.symbol === token.symbol
);
if (index > -1) {
@@ -926,16 +925,16 @@ export const LIFI_CONFIG = Object.freeze({
secondary: "rgba(var(--ion-text-color-rgb), 0.6)",
},
background: {
- paper: "rgb(var(--item-background-shader-rgb))", // green
+ paper: "rgb(var(--lifi-paper-background-rgb))", // green
// default: '#182449',
},
primary: {
main: "#0090FF",
- contrastText: "rgb(var(--ion-text-color.rgb))",
+ contrastText: "rgb(var(--ion-text-color-rgb))",
},
secondary: {
main: "#4CCCE6",
- contrastText: "rgb(var(--ion-text-color.rgb))",
+ contrastText: "rgb(var(--ion-text-color-rgb))",
},
},
},
diff --git a/src/servcies/magic-web3-connect.ts b/src/servcies/magic-web3-connect.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/src/servcies/zerion.service.ts b/src/servcies/zerion.service.ts
new file mode 100644
index 00000000..098324dc
--- /dev/null
+++ b/src/servcies/zerion.service.ts
@@ -0,0 +1,113 @@
+import { SeriesData, SeriesMarkerData } from "@/components/ui/LightChart";
+import { TxInterface } from "@/interfaces/tx.interface";
+import { SeriesMarker, Time } from "lightweight-charts";
+
+export const formatTxsAsSeriemarker = (txs: TxInterface[]): SeriesMarkerData => {
+ // only `in` and `out` transfers form `symbol` token
+ const filteredTxs = txs.filter((tx) =>
+ tx.attributes.transfers[0].direction === "in" || tx.attributes.transfers[0].direction === "out"
+ );
+ const serieData:SeriesMarkerData = new Map();
+ serieData.set("1D", []);
+ serieData.set("1W", []);
+ serieData.set("1M", []);
+ serieData.set("1Y", []);
+ const now = new Date();
+ const oneDay = 24 * 60 * 60 * 1000;
+ const oneWeek = 7 * oneDay;
+ const oneMonth = 30 * oneDay;
+ const oneYear = 365 * oneDay;
+ const today = now.getTime();
+ const oneDayAgo = today - oneDay;
+ const oneWeekAgo = today - oneWeek;
+ const oneMonthAgo = today - oneMonth;
+ const oneYearAgo = today - oneYear;
+
+ // loop over txs and create SeriesMarkerData
+ filteredTxs
+ .forEach((tx) => {
+ // `tx.attributes.mined_at` as `2024-06-20`
+ const txDate = new Date(tx.attributes.mined_at).getTime();
+ const time = new Date(tx.attributes.mined_at).toISOString().split('T').shift()||``;
+ // SeriesMarker