Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add token utility in advanced tab #11

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/app/(index)/ConnectWalletButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ const ConnectWalletButton = () => {
</CopyToClipboard>
<MenuItem>
<Link
href="/account"
href="/advanced"
className="flex w-full items-center gap-x-2 px-4 py-3 text-left text-sm font-semibold text-sand-11 data-[focus]:bg-sand-3 data-[focus]:text-sand-12"
>
<Cog6ToothIcon className="h-5 w-5 flex-shrink-0" />
Account
Advanced
</Link>
</MenuItem>
<MenuItem>
Expand Down
4 changes: 2 additions & 2 deletions src/app/account/layout.tsx → src/app/advanced/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import LayoutWrapper from "@/components/Staking/components/LayoutWrapper";
import { Metadata } from "next";

export const metadata: Metadata = {
title: "Account",
title: "Advanced",
};

const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => (
<LayoutWrapper title="Account" color="white">
<LayoutWrapper title="Advanced" color="white">
{children}
</LayoutWrapper>
);
Expand Down
140 changes: 103 additions & 37 deletions src/app/account/page.tsx → src/app/advanced/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import {
} from "near-api-js/lib/providers/provider";
import ConnectAccount from "@/components/Staking/components/ConnectAccount";
import { useWalletSelector } from "@/contexts/WalletSelectorContext";
import { fetchAccessKeys } from "@/utils/near";
import { fetchAccessKeys, lookupToken, register } from "@/utils/near";
import { formatUnits } from "viem";
import { Card, CardPadding } from "@/components/Card";
import { InformationCircleIcon } from "@heroicons/react/20/solid";
import Button from "@/components/Button";
import {
addTokenToWallet,
playgroundComputeAddress,
} from "@/utils/addTokenToWallet";

const AccountPage = () => {
const AdvancedPage = () => {
const { accountId, selector, isEthereumWallet } = useWalletSelector();
const [dappAccessKeys, setDappAccessKeys] = useState<AccessKeyInfoView[]>([]);
const [onboardingAccessKeys, setOnboardingAccessKeys] = useState<
Expand Down Expand Up @@ -130,43 +135,104 @@ const AccountPage = () => {
</Card>

{isEthereumWallet && (
<Card>
<CardPadding>
<h2 className="font-sans text-2xl font-medium leading-[1.3] text-sand-12">
Onboarding key
</h2>
<p className="mt-4 text-base leading-normal tracking-wider text-sand-12">
This key enables your Ethereum wallet account to transact on NEAR
Protocol.
</p>
{onboardingAccessKeys.length ? (
<ul className="mt-4 space-y-4">
{onboardingAccessKeys.map((key) => (
<li
key={key.public_key}
className="overflow-hidden text-ellipsis rounded-lg bg-sand-3 px-4 py-4 text-base leading-none text-sand-11"
>
{key.public_key}
</li>
))}
</ul>
) : (
<div className="mt-4 flex rounded-lg bg-blue-50 px-4 py-4">
<InformationCircleIcon
className="h-5 w-5 flex-shrink-0 text-blue-400"
aria-hidden="true"
/>
<p className="ml-3 text-sm tracking-wider text-blue-700">
Your account is not yet onboarded, re-connect your wallet to
onboard.
</p>
</div>
)}
</CardPadding>
</Card>
<>
<Card>
<CardPadding>
<h2 className="font-sans text-2xl font-medium leading-[1.3] text-sand-12">
Onboarding key
</h2>
<p className="mt-4 text-base leading-normal tracking-wider text-sand-12">
This key enables your Ethereum wallet account to transact on
NEAR Protocol.
</p>
{onboardingAccessKeys.length ? (
<ul className="mt-4 space-y-4">
{onboardingAccessKeys.map((key) => (
<li
key={key.public_key}
className="overflow-hidden text-ellipsis rounded-lg bg-sand-3 px-4 py-4 text-base leading-none text-sand-11"
>
{key.public_key}
</li>
))}
</ul>
) : (
<div className="mt-4 flex rounded-lg bg-blue-50 px-4 py-4">
<InformationCircleIcon
className="h-5 w-5 flex-shrink-0 text-blue-400"
aria-hidden="true"
/>
<p className="ml-3 text-sm tracking-wider text-blue-700">
Your account is not yet onboarded, re-connect your wallet to
onboard.
</p>
</div>
)}
</CardPadding>
</Card>
<AddCustomToken />
</>
)}
</>
);
};

export default AccountPage;
export default AdvancedPage;

const AddCustomToken = () => {
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const inputDisabled = input === "";
const tryAddToken = async () => {
if (inputDisabled) return;
try {
setLoading(true);
const result = await lookupToken(playgroundComputeAddress(input));
if (result === null) {
await register(input);
return;
}
await addTokenToWallet(input);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
return (
<Card>
<CardPadding>
<h2 className="font-sans text-2xl font-medium leading-[1.3] text-sand-12">
Add a custom token to your wallet
</h2>
<p className="mt-4 text-base leading-normal tracking-wider text-sand-12">
Write a token account id and click Add to track a token in your
wallet.
</p>
<p className="mt-4 text-base leading-normal tracking-wider text-sand-12">
If token has not been registered in adress-map.near contract you will
be promted to register it in the contract otherwise it can't be used
in a EVM wallet.
</p>
<div className="mt-4 flex items-center overflow-hidden text-ellipsis rounded-lg bg-sand-3 px-4 py-3 text-base leading-none text-sand-11">
<input
type="text"
name="search"
value={input}
onChange={(e) => setInput(e.target.value)}
className="flex-1 border-0 bg-sand-3 p-0 pr-4 text-base text-sand-12 placeholder:text-sand-11"
/>
</div>
<Button
className="ml-auto mt-4 !block"
size="sm"
onClick={tryAddToken}
disabled={inputDisabled}
loading={loading}
>
Add
</Button>
</CardPadding>
</Card>
);
};
2 changes: 1 addition & 1 deletion src/components/AddTokenIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function AddTokenIcon({
<Tooltip content="Add token to wallet" tooltipClassname="w-fit text-nowrap">
<PlusCircleIcon
className="h-5 w-5 text-sand-8 group-hover:text-sand-11"
onClick={() => addTokenToWallet(contract, symbol, decimals)}
onClick={() => addTokenToWallet(contract)}
/>
</Tooltip>
) : null;
Expand Down
19 changes: 7 additions & 12 deletions src/utils/addTokenToWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@ import { getConnectorClient } from "@wagmi/core";
import { wagmiConfig } from "@/contexts/WalletSelectorContext";
import { getTokenMetaData } from "@/utils/near";

export async function addTokenToWallet(
contractId: string,
symbol: string,
decimals: number
) {
export async function addTokenToWallet(contractId: string) {
try {
let image = "";
const tokenMetadataRequest = await getTokenMetaData(contractId);
if (tokenMetadataRequest !== null) {
image = tokenMetadataRequest.icon;
if (tokenMetadataRequest === null) {
return;
}

const walletClient = await getConnectorClient(wagmiConfig);
Expand All @@ -24,17 +19,17 @@ export async function addTokenToWallet(
type: "ERC20",
options: {
address: address,
symbol: symbol,
decimals: decimals,
image,
symbol: tokenMetadataRequest.symbol,
decimals: tokenMetadataRequest.decimals,
image: tokenMetadataRequest.icon,
},
});
} catch (e) {
console.error(e);
}
}

function playgroundComputeAddress(input: string) {
export function playgroundComputeAddress(input: string) {
const hash = keccak256(toHex(input));
return "0x" + hash.substring(26, 66);
}
38 changes: 38 additions & 0 deletions src/utils/near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,41 @@ export const getMinStorageBalance = async (
return FT_MINIMUM_STORAGE_BALANCE_LARGE;
}
};
export const lookupToken = async (address: string): Promise<string | null> => {
try {
const nearConnection = await connect(NEAR_CONNECTION_CONFIG);
const acc = new Account(nearConnection.connection, "dontcare");
const data = await acc.viewFunction({
contractId: "address-map.near",
methodName: "lookup",
args: { address },
});

return data;
} catch (e) {
console.error(
`Failed to lookup a token in address-map.near: ${address}`,
e
);
return null;
}
};
export const register = async (account_id: string): Promise<void> => {
try {
const nearConnection = await connect(NEAR_CONNECTION_CONFIG);
const acc = new Account(nearConnection.connection, "dontcare");
await acc.viewFunction({
contractId: "address-map.near",
methodName: "register",
gas: BigInt("3000000000000"),
args: { account_id },
attachedDeposit:
BigInt(account_id.length + 20) * BigInt("10000000000000000000"),
});
} catch (e) {
console.error(
`Failed to register a token in address-map.near: ${account_id}`,
e
);
}
};