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

Pulling refs/heads/staging into test-staging #2378

Merged
merged 28 commits into from
Mar 28, 2024
Merged
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,6 @@ NEXT_PUBLIC_WHITELISTED_TRUSTED_CREATORS=["xxxxxx"]
IPFS_NODE_BASIC_AUTH_USERNAME=xxxxx
IPFS_NODE_BASIC_AUTH_PASSWORD=xxxxx

NEXT_PUBLIC_SHOW_AIRDROP=true


39 changes: 38 additions & 1 deletion components/top-bar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@ const TopBar = () => {
</div>
<MarketSearch />
<div className="center relative ml-auto gap-3">
<GetTokensButton />
{process.env.NEXT_PUBLIC_SHOW_AIRDROP !== "true" ? (
<GetTokensButton />
) : (
<AirdropButton />
)}
<AccountButton />
<Alerts />
</div>
Expand Down Expand Up @@ -265,6 +269,39 @@ const GetTokensButton = () => {
);
};

const AirdropButton = () => {
return (
<Transition
as={Fragment}
show={true}
enter="transition-all duration-250"
enterFrom="opacity-0 scale-90"
enterTo="opacity-100 scale-100"
leave="transition-all duration-250"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-90"
>
<Link
className="group relative hidden h-11 overflow-hidden rounded-md p-0.5 sm:block"
href="/claim"
>
<div
className="absolute left-0 top-0 z-10 h-full w-full group-hover:-left-6 group-hover:-top-6 group-hover:h-[150%] group-hover:w-[150%] group-hover:animate-spin"
style={{
background:
"linear-gradient(180deg, #FF00E6 0%, #F36464 50%, #04C3FF 100%)",
}}
/>
<div className="relative z-20 block h-full sm:w-[100px] ">
<button className="center h-full w-full rounded-md bg-black text-white">
Airdrop!
</button>
</div>
</Link>
</Transition>
);
};

const CategoriesMenu = ({ onSelect }: { onSelect: () => void }) => {
const { data: counts } = useCategoryCounts();

Expand Down
20 changes: 11 additions & 9 deletions lib/state/polkadot-api.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { ApiPromise, WsProvider } from "@polkadot/api";
import { atom, useAtom } from "jotai";
import { loadable } from "jotai/utils";
import { ChainName, CHAINS } from "lib/constants/chains";
import { useSdkv2 } from "lib/hooks/useSdkv2";
import { environment } from "lib/constants";

const endpoints = [
"wss://rpc.polkadot.io",
"wss://polkadot-rpc.dwellir.com",
"wss://polkadot.public.curie.radiumblock.co/ws",
"wss://1rpc.io/dot",
"wss://rpc-polkadot.luckyfriday.io",
];
const endpoints =
environment === "production"
? [
"wss://rpc.polkadot.io",
"wss://polkadot-rpc.dwellir.com",
"wss://polkadot.public.curie.radiumblock.co/ws",
"wss://1rpc.io/dot",
"wss://rpc-polkadot.luckyfriday.io",
]
: ["wss://rococo-rpc.polkadot.io"];

const polkadotApiAtom = loadable(
atom(async () => {
Expand Down
284 changes: 284 additions & 0 deletions pages/claim.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import { decodeAddress, encodeAddress } from "@polkadot/keyring";
import { useNotifications } from "lib/state/notifications";
import { usePolkadotApi } from "lib/state/polkadot-api";
import { useWallet } from "lib/state/wallet";
import { extrinsicCallback, signAndSend } from "lib/util/tx";
import { NextPage } from "next";
import { ChangeEvent, useState } from "react";
import airdrop from "../public/airdrop.json";
import { environment } from "lib/constants";
import NotFoundPage from "./404";

const TOTAL_AIRDROP_ZTG = 1_000_000;
const ZTG_PER_ADDRESS = TOTAL_AIRDROP_ZTG / airdrop.length;
const AIRDROP_REMARK_PREFIX = "zeitgeist.airdrop-1";

const ClaimPage: NextPage = () => {
if (process.env.NEXT_PUBLIC_SHOW_AIRDROP !== "true") {
return <NotFoundPage />;
}
const [showEligibility, setShowEligibility] = useState(false);
const [polkadotAddress, setPolkadotAddress] = useState("");

const isValidPolkadotAddress =
polkadotAddress == "" || validateAddress(polkadotAddress, 0);

return (
<div className="relative mt-10 flex items-center justify-center">
<div
className="absolute z-[-1] h-full w-full overflow-hidden"
style={{
background:
"radial-gradient(50% 50% at 50% 50%, rgba(254, 207, 255, 0.3) 20.83%, rgba(205, 222, 255, 0.3) 54.17%, rgba(201, 232, 255, 0.3) 57.29%, rgba(245, 245, 245, 0) 100%)",
}}
></div>
<div className="flex max-w-[850px] flex-col items-center justify-center gap-y-5">
<div className="flex w-full gap-x-10">
<div className="w-full text-4xl font-bold sm:text-5xl sm:!leading-[77px] md:text-6xl">
Find out if you are eligible for the Airdrop
</div>
<img
className="relative mr-auto hidden w-2/5 scale-110 sm:block"
src="/airdrop.svg"
alt="Airdrop"
/>
</div>
<div className="w-full whitespace-pre-wrap text-lg">
This airdrop is designed for those who have actively participated in
Polkadot's OpenGov by voting before the start of{" "}
<a href="https://polkadot.polkassembly.io/referenda/502">
Referendum 502
</a>
. The snapshot was taken February 14th, 2024 (22:14:54 UTC). Only
wallets that voted on Polkadot's OpenGov before the snapshot will be
eligible. Claims will be open until July 1st, 2024.
</div>
{showEligibility === false ? (
<>
<div className="w-full text-xl font-bold">
Enter your Polkadot address below to check your eligibility:
</div>
<div className="flex w-full flex-col gap-4 rounded-md bg-[#DFE5ED] p-7 sm:flex-row">
<div className="relative flex w-full flex-col">
<input
className="w-full rounded-md bg-white p-2"
placeholder="Enter Polkadot address"
spellCheck={false}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setPolkadotAddress(event.target.value);
}}
/>
{isValidPolkadotAddress === false && (
<div className="absolute top-10 text-xs text-red-600">
Invalid Polkadot address
</div>
)}
</div>

<button
className="h-[40px] w-full rounded-md bg-[#2468E2] text-white disabled:opacity-50 sm:w-[200px]"
onClick={() => {
setShowEligibility(true);
}}
disabled={
polkadotAddress === "" ||
polkadotAddress == null ||
isValidPolkadotAddress === false
}
>
Check Eligibility
</button>
</div>
</>
) : (
<Eligibility
polkadotAddress={polkadotAddress}
onCheckAgain={() => {
setShowEligibility(false);
setPolkadotAddress("");
}}
/>
)}
</div>
</div>
);
};

const Eligibility = ({
polkadotAddress,
onCheckAgain,
}: {
polkadotAddress: string;
onCheckAgain: () => void;
}) => {
const wallet = useWallet();
const notifications = useNotifications();

const [claimAddress, setClaimAddress] = useState<string | null>(null);
const { api } = usePolkadotApi();

const isEligible = airdrop.some(
(airdropAddress) => airdropAddress === polkadotAddress,
);

const isValid = claimAddress === null || validateAddress(claimAddress, 73);
const tx = api?.tx.system.remark(`${AIRDROP_REMARK_PREFIX}-${claimAddress}`);

const connectedWalletMatchesPolkadotAddress = addressesMatch(
polkadotAddress,
wallet.realAddress ?? "",
);

const txHex = tx?.toHex();

const submitClaim = () => {
if (!tx || !api) return;

const signer = wallet.getSigner();

if (!signer) return;

signAndSend(
tx,
signer,
extrinsicCallback({
api: api,
notifications,
broadcastCallback: () => {
notifications?.pushNotification("Broadcasting transaction...", {
autoRemove: true,
});
},
successCallback: (data) => {
notifications?.pushNotification(`Successfully claimed`, {
autoRemove: true,
type: "Success",
});
},
failCallback: (error) => {
notifications.pushNotification(error, { type: "Error" });
},
}),
).catch((error) => {
notifications.pushNotification(error?.toString() ?? "Unknown Error", {
type: "Error",
});
});
};

return (
<>
{isEligible ? (
<>
{connectedWalletMatchesPolkadotAddress ? (
<div className="w-full text-xl font-bold">
You are eligible for at least {Math.floor(ZTG_PER_ADDRESS)} ZTG,
enter Zeitgeist address to claim
</div>
) : (
<div className="w-full text-xl font-bold">
This address is eligible for at least{" "}
{Math.floor(ZTG_PER_ADDRESS)} ZTG, enter Zeitgeist address to
claim
</div>
)}
<div className="flex w-full flex-col">
<div className="flex w-full flex-col gap-4 rounded-md bg-[#DFE5ED] p-7 sm:flex-row">
<div className="relative flex w-full flex-col">
<input
className="w-full rounded-md bg-white p-2"
placeholder="Zeitgeist Address"
spellCheck={false}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setClaimAddress(event.target.value);
}}
/>
{isValid === false && (
<div className="absolute top-10 text-xs text-red-600">
Invalid Zeitgeist address
</div>
)}
{isValid === true &&
claimAddress != null &&
connectedWalletMatchesPolkadotAddress === false && (
<div className="absolute top-10 text-xs text-red-600">
Connected wallet doesn't match Polkadot address
</div>
)}
</div>
<button
className="h-[40px] w-full rounded-md bg-[#2468E2] text-white disabled:opacity-50 sm:w-[200px]"
disabled={
claimAddress === null ||
isValid === false ||
wallet.connected === false ||
connectedWalletMatchesPolkadotAddress === false
}
onClick={() => submitClaim()}
>
Claim Airdrop
</button>
</div>
<a
href={`https://polkadot.js.org/apps/?rpc=wss%3A%2F%2F${
environment === "production"
? "rpc.polkadot.io"
: "rococo-rpc.polkadot.io"
}#/extrinsics/decode/${txHex}`}
target="_blank"
rel="noreferrer"
className="mt-3 text-sm text-blue-700"
>
Wallet not supported? Enter Zeitgeist address and sign with
Polkadot.js/apps
</a>
</div>
</>
) : (
<div className="w-full text-xl font-bold">
You are not eligible for this airdrop
</div>
)}
<div className="flex w-full">
<button
className="h-[40px] w-[200px] rounded-md bg-[#2468E2] text-white"
onClick={() => onCheckAgain()}
>
Check another address
</button>
</div>
</>
);
};

const validateAddress = (address: string, ss58Format: number) => {
try {
const encodedAddress = encodeAddress(
decodeAddress(address),
ss58Format,
).toString();

return encodedAddress === address;
} catch {
return false;
}
};

const addressesMatch = (address1: string, address2: string) => {
try {
const encodedAddress1 = encodeAddress(
decodeAddress(address1),
0,
).toString();
const encodedAddress2 = encodeAddress(
decodeAddress(address2),
0,
).toString();

return encodedAddress1 === encodedAddress2;
} catch {
return false;
}
};

export default ClaimPage;
Loading
Loading