feat: usernames (#657)
* describe new endpoint * add spinner icon and dropdown loading state * loading avatar state * use ricardo's endpoints * better error handling for linked-addresses check * create jwt with jose * add loading state to registration UI * dedupe some registrar controller references * fix dropdown max height * rename constant * name -> names page * refactor to use AddressMap * undo docs change * rm console.log * set docs data to state on master * remove classname conditional logic in favor of headless ui transition * fix early return hook count error * Extract Learn More Modal * add dynamic og images for usernames * clean up og image and url setup * individually call each endpoint for attestation data--use linked accounts to check for existing registrations * useAttestations * fix linkedAddresses undefined error * clean up api result * tie in discount state to copy in registration form * Add ShareUsernameModal for post-claim flow * ShareUsernameModal: font weight and padding tweaks * resolve conflicts * useActiveDiscountValidators() * ShareUsernameModal: design update * update proof endpoints * use response types, fix wrong network breaking name search * ensure uniform utilization of the network check for contract args construction * usernames: Update learn more modal * Shelley feedback 1/6 * Shelley feedback 2/6 * Shelley feedback 3/6 * Brian doyle/zora tutorial (#588) * Partial: Build with zora * Stash progress * Progress stash * Add zora premint tutorial * Apply feedback * Fix typo * Update title * Shopify Storefront with Coinbase Commerce checkout (#562) * Shopify Storefront with Coinbase Commerce checkout * Implement @briandoyle81CB feedback * Update Commerce section * fix formatting * use backticks for buttons * add admonitions * add a tip for checking out with crypto * swap should with will * update conclusion * maintenance(node): Notate new requirement, NVME (#596) * Swap typescript Code Blocks to tsx (#597) * Start * Switch typescript code blocks to tsx * Feat/add events to docs (#587) * Created logEvent utility * Added event tracking to OCS Banner * Created CustomNavbarLink with event tracking * created custom navbar dropdown link with event tracking * added event tracking to P0 navbar elements * Added eventDetail parameter to event tracking * Updated Bootcamp link * enforced cursor pointer on custom navbar links * updated navbar elements to include event tracking * event tracking for navbar social links * added target property to custom navbar elements * made links to off-domain locations open in new tab * updated tutorials data * implemented tracking on connect wallet buttons * removed duplicate connect button from StudentProgress component * Links now send events with high importance * Cleaned up StudentProgress component * Social clicks use useCallback * bugfix for logEvent * created callbacks for social click handlers * improved social click callbacks * updated event names * updated connect wallet event names on base.org * added full typing to logEvent utility * refactored logEvent typing * added userId to event data options * updated base-docs logEvent immplementations * updated Connect Wallet event for base.org * changed component_type to the expected * Updated event logging for onchain summer banner * fixed key errors on tutorials page (#598) * chore: updated node versioning to be latest of 18 (hydrogen) (#560) * updated node versioning to be latest of 18 (hydrogen) lts * check if build version update works w/ gh action * chore: add a pre-commit hook for linting (#601) * add a precommit hook for linting * test * Revert "test" This reverts commit f5329bd73c927424777cbac81606d19103a1b616. * add eslintcache to gitignore * Shelley feedback 4/6 * fix lint * Shelley feedback 5/6 * Shelley feedback 6/6 * use chain query param * talk to the individual discount validators * fix parse cdp key issues * add error messages * fix register name callback types * remove some linkedAddresses stuff from the frontend * Mobile pass and update contract addresses * useBaseEnsName hook * fake hasUsedADiscount * return error * return error * return error * add back hasRegisteredWithDiscount from backend * restrict name to 20 chars * UsernameProfileForm, TextArea, Fieldset, Label, Hint and more * UsernameProfileForm: use useEnsText and rename bio to description * utilize proofs apis for checking validity of discounts * add (discounted)pricing hooks * useWriteMultipleBaseEnsTextRecords: hooks to multicall * update register name callback to include non-discounted registrations * render eth price correctly * minor cleanup * fiddling with register call & viem * clean up discount/non price passthrough * Move hooks into /src/hooks, clean up useFocusWithin * fix import path * Add USD price display * Fix multiple imports * Better placeholder * cleanup * .json->.ts abi * fix import * Update abis to consistent format * clean + add validateEnsDomainName * show error message * reset values when user skip * format search to avoid space * button loading state & emoji lenght fix * add support for discounted registration * move to profile after registration * update address type * add keywords * check for previously registered addresses on cb.id endpoint * add states and state switcher * determine if the name registration is free * small refactor of RegistrationProvider * move selectedName to provider * move hash to provider * fix broken imports * rm console.log * clean up and design fixes for success stage * connect discounts to explainer modal state * rm console.log * Profile page light * fix pill animation * update default expire * redirect to user profile after registration * fix expiration time * fix ellipsis * fix colors * rename registration-specific profile form * move brand logic to component * move components from file to folder * remove button in the username search input * fix redirect if no field was edited, rename variables * update card links and order * fix some styles * addAddr and fix the resolvers * test full flow, reload data * add correct styling to discounted price * User can edit profile * add social links on Profile page * css cleanup * fix styles from merge * fix tooltip opacity * re-arrange layout to avoid overflow * wip: fetch coinbase verifications badges * add usernames early access endpoint * add name is free pricing subtext * Fix text on kv errors * layout fixes and animation * Add EAS badges * fix key rendering issue * Don't display verifications if we have none * return error on hasPreviouslyClaimed proofs * rename area of expertise to skill * add analytics * use a provider for sanity * context chaining * update analytics * move discount logic to registration context * add analytics to user profile page * allow for 2 discounts to create a signature on sybil resistance * set chain based on env * switch chain * fix frogmonkee * add error states and dropdown components * fix spacing * design and usability improvements * fix navigation and registration profile form * rm dev controls * default to env chain * unify logic for chain check * fix getAttestations chain logic * add real name suggestions with cb-gpt * use cb1 discounts first, if available * address some TODOs and fix copy-to-clipboard UX in dropdown * fix domain url, enable metadata and svg card support * get chain client correctly * add twemoji support * Fix css-loader build error * add endpoints for contract-uri * add redirect when json is missing * ECO-75: Fix input focused state with blue background * ECO-81: Discount modal: external link open in a new tab * switch key to static value on ens pills * ECO-93: add <ImageWithLoading> component for nice image load * ECO-80: add nice animation to the Modal component * ECO-73: align blue dot * ECO-72: use Basenames for the pages titles * ECO-84: transition between registration profile form steps * ECO-76: Fix jumping layout when changing years * automatic redirect when missing domain * fix focus error causing blue reset * upgrade tanstack * Add Guild badges * minimal yarn.lock changes * Coerce schema type * Feat/local paymaster (#645) * paymaster working with url * Wallet type verification complete * Packages updated * configure smart wallet * Cleanup * Update yarn.lock * vercel error fixed * Build fixed * Resolved Comments * Resolved chains --------- Co-authored-by: Jordan Frankfurt <jordan.frankfurt@coinbase.com> * remove nav menu item for EA * fix avatar overflow on pills * tentative onchainkit implementation * fix package * fix padding and ECO-100 * ECO-60: fix validation * ECO-12: remove perk value * ECO-12-2: rename * ECO-16: change tooltip copy * update to latest onchainkit * Fix non member found from Base Guild * fix lock file * Stub talentprotocol, fix verified country badge * fix path * avatar support with Vercel Blob * lock * Add TalentProtocol API endpoint * Remove CSP for talentprotocol * add radix tooltip pkg w/ refactor and add ai tooltip text * use universal resolver address, check the avatar upload path * reset address resolver for now * error state & 404 page * Display talent protocol score in badge * add new discounts to discount modal * add missing images and EA discount * remove error stringifying from transactionError component * ECO-85: skill badge design fix * fix avatar resolution * log avatar errors * add EA contracts and validators * update contract * rename key * debug early access checks * add message if user is not eligible for early access * don't discounts modal during early access * connect wallet fixes * dual chain support * Unearned badges * improve testnet/mainnet banner messaging * proper chain resolution * Move badge images * Fix redirect after customizing profile * better handling of EA roadblock * fix cdp get vulnerability * encode talentprotol URI * fix cdp get vulnerability * revert encodeUriComponent * Fix a bunch of a11y errors * alt image * fix label * Fix yarn lint issues * lint error on getLinkedAddresses * refactor paymaster code to fix ts errors (#680) * add example env vars for paymaster links * Remove .env * gitignore .env --------- Co-authored-by: Léo Galley <contact@kirkas.ch> Co-authored-by: Ricardo Moguel <ricardo.moguel@coinbase.com> Co-authored-by: Matthew Bunday <matthew.bunday@coinbase.com> Co-authored-by: Brian Doyle <brian.doyle@coinbase.com> Co-authored-by: Pat <patrick.hughes@coinbase.com> Co-authored-by: wbnns <hello@wbnns.com> Co-authored-by: Brendan from DeFi <brendan.forster@coinbase.com> Co-authored-by: Keshav Singhal <107991050+Keshavrajsinghal@users.noreply.github.com>
@@ -26,6 +26,7 @@ module.exports = {
|
||||
'plugin:relay/strict',
|
||||
],
|
||||
rules: {
|
||||
'import/extensions': ['error', 'never'],
|
||||
'react/destructuring-assignment': 'off',
|
||||
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx', '.mdx'] }],
|
||||
|
||||
|
||||
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.env
|
||||
# Node stuff
|
||||
node_modules
|
||||
yarn-debug.log*
|
||||
@@ -71,4 +72,4 @@ persisted_queries.json
|
||||
**/*.graphql.ts
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
.eslintcache
|
||||
|
||||
8
.vscode/settings.json
vendored
@@ -25,5 +25,11 @@
|
||||
{ "pattern": "examples/*" },
|
||||
{ "pattern": "libs/*" },
|
||||
{ "pattern": "packages/*" }
|
||||
]
|
||||
],
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[svg]": {
|
||||
"editor.defaultFormatter": "jock.svg"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,6 @@ Subsquid offers a powerful toolkit for creating custom data extraction and proce
|
||||
|
||||
To get started, visit the [documentation](https://docs.subsquid.io/) or see this [quickstart with examples](https://docs.subsquid.io/sdk/examples/) on how to easily create subgraphs via Subsquid.
|
||||
|
||||
|
||||
#### Supported Networks
|
||||
|
||||
- Base Mainnet
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@rainbow-me/rainbowkit": "^2.1.3",
|
||||
"@tanstack/react-query": "^5.29.0",
|
||||
"@tanstack/react-query": "^5.51.11",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"base-ui": "0.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
|
||||
@@ -12,12 +12,12 @@ import styles from './styles.module.css';
|
||||
type BannerName = `${string}Banner`;
|
||||
|
||||
type BannerProps = {
|
||||
href: string;
|
||||
text: string;
|
||||
bannerName: BannerName;
|
||||
href: string;
|
||||
text: string;
|
||||
bannerName: BannerName;
|
||||
};
|
||||
|
||||
export default function Banner({ href, text, bannerName}: BannerProps) {
|
||||
export default function Banner({ href, text, bannerName }: BannerProps) {
|
||||
const [isBannerVisible, setIsBannerVisible] = useLocalStorage(`${bannerName}Visible'`, true);
|
||||
|
||||
const linkClick = useCallback(() => {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import logEvent, {
|
||||
AnalyticsEventData,
|
||||
AnalyticsEventImportance,
|
||||
CCAEventData,
|
||||
} from 'base-ui/utils/logEvent';
|
||||
import logEvent, { AnalyticsEventData } from 'base-ui/utils/logEvent';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type FooterLinkType = {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import logEvent, {
|
||||
AnalyticsEventData,
|
||||
AnalyticsEventImportance,
|
||||
CCAEventData,
|
||||
} from 'base-ui/utils/logEvent';
|
||||
import logEvent, { AnalyticsEventData } from 'base-ui/utils/logEvent';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
type ImageCardProps = {
|
||||
|
||||
@@ -2,11 +2,7 @@ import React, { useCallback } from 'react';
|
||||
import Icon from '../Icon';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
import logEvent, {
|
||||
AnalyticsEventData,
|
||||
AnalyticsEventImportance,
|
||||
CCAEventData,
|
||||
} from 'base-ui/utils/logEvent';
|
||||
import logEvent, { AnalyticsEventData } from 'base-ui/utils/logEvent';
|
||||
|
||||
type TextCardProps = {
|
||||
title: string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@media (min-width: 997px) {
|
||||
.menuHtmlItem {
|
||||
padding: var(--ifm-menu-link-padding-vertical)
|
||||
var(--ifm-menu-link-padding-horizontal);
|
||||
padding: var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@bugsnag/plugin-react": "^7.19.0",
|
||||
"@eth-optimism/contracts-ts": "^0.16.2",
|
||||
"@eth-optimism/indexer-api": "^0.0.4",
|
||||
"@headlessui/react": "latest",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@rainbow-me/rainbowkit": "^1",
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
|
||||
@@ -219,13 +219,22 @@ function MobileMenu({ color }: MobileMenuProps) {
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
title="Join us on Farcaster"
|
||||
aria-label="Join us on Farcaster"
|
||||
>
|
||||
<Icon name="farcaster" />
|
||||
</a>
|
||||
<a href="https://discord.com/invite/buildonbase" title="Join us on Discord">
|
||||
<a
|
||||
href="https://discord.com/invite/buildonbase"
|
||||
title="Join us on Discord"
|
||||
aria-label="Join us on Discord"
|
||||
>
|
||||
<Icon name="discord" />
|
||||
</a>
|
||||
<a href="https://twitter.com/base" title="Join us on Twitter">
|
||||
<a
|
||||
href="https://twitter.com/base"
|
||||
title="Join us on Twitter"
|
||||
aria-label="Join us on Twitter"
|
||||
>
|
||||
<Icon name="twitter" />
|
||||
</a>
|
||||
<a
|
||||
@@ -233,6 +242,7 @@ function MobileMenu({ color }: MobileMenuProps) {
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
title="Join us on Github"
|
||||
aria-label="Join us on Github"
|
||||
>
|
||||
<Icon name="github" />
|
||||
</a>
|
||||
|
||||
@@ -9,3 +9,11 @@ MAINNET_LAUNCH_BLOG_POST_URL=https://base.mirror.xyz
|
||||
MAINNET_LAUNCH_FLAG=false
|
||||
NEXT_PUBLIC_ECOSYSTEM_LAUNCH_FLAG=false
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=
|
||||
VERIFICATION_VERIFIED_ACCOUNT_SCHEMA_ID=
|
||||
VERIFICATION_VERIFIED_CB1_ACCOUNT_SCHEMA_ID=
|
||||
TRUSTED_SIGNER_ADDRESS=
|
||||
TRUSTED_SIGNER_PRIVATE_KEY=
|
||||
|
||||
NEXT_PUBLIC_USERNAMES_EARLY_ACCESS=
|
||||
NEXT_PUBLIC_BASE_SEPOLIA_PAYMASTER_SERVICE=
|
||||
NEXT_PUBLIC_BASE_PAYMASTER_SERVICE=
|
||||
59
apps/web/contexts/Analytics.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import logEvent, {
|
||||
ActionType,
|
||||
AnalyticsEventImportance,
|
||||
CCAEventData,
|
||||
} from 'libs/base-ui/utils/logEvent';
|
||||
import { ReactNode, createContext, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
export type AnalyticsContextProps = {
|
||||
logEventWithContext: (eventName: string, action: ActionType, eventData?: CCAEventData) => void;
|
||||
fullContext: string;
|
||||
};
|
||||
|
||||
export const AnalyticsContext = createContext<AnalyticsContextProps>({
|
||||
logEventWithContext: function () {
|
||||
return undefined;
|
||||
},
|
||||
fullContext: '',
|
||||
});
|
||||
|
||||
export function useAnalytics() {
|
||||
const context = useContext(AnalyticsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAnalytics must be used within a AnalyticsProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
type AnalyticsProviderProps = {
|
||||
children?: ReactNode;
|
||||
context: string; // TODO: This could be an enum in CCAEventData
|
||||
};
|
||||
|
||||
export default function AnalyticsProvider({ children, context }: AnalyticsProviderProps) {
|
||||
const { fullContext: previousContext } = useAnalytics();
|
||||
|
||||
const fullContext = [previousContext, context].filter((c) => !!c).join('_');
|
||||
const logEventWithContext = useCallback(
|
||||
(eventName: string, action: ActionType, eventData?: CCAEventData) => {
|
||||
const sanitizedEventName = eventName.toLocaleLowerCase();
|
||||
logEvent(
|
||||
sanitizedEventName, // TODO: Do we want context here?
|
||||
{
|
||||
action: action,
|
||||
context: fullContext,
|
||||
page_path: window.location.pathname,
|
||||
...eventData,
|
||||
},
|
||||
AnalyticsEventImportance.high,
|
||||
);
|
||||
},
|
||||
[fullContext],
|
||||
);
|
||||
|
||||
const values = useMemo(() => {
|
||||
return { logEventWithContext, context, fullContext };
|
||||
}, [context, fullContext, logEventWithContext]);
|
||||
|
||||
return <AnalyticsContext.Provider value={values}>{children}</AnalyticsContext.Provider>;
|
||||
}
|
||||
1
apps/web/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@@ -76,6 +76,8 @@ const contentSecurityPolicy = {
|
||||
],
|
||||
'connect-src': [
|
||||
"'self'",
|
||||
'https://blob.vercel-storage.com', // Vercel File storage
|
||||
'https://zku9gdedgba48lmr.public.blob.vercel-storage.com', // Vercel File storage
|
||||
walletconnectDomains,
|
||||
sprigDomains,
|
||||
greenhouseDomains,
|
||||
@@ -84,9 +86,14 @@ const contentSecurityPolicy = {
|
||||
'wss://www.walletlink.org/rpc', // coinbase wallet connection
|
||||
'https://analytics-service-dev.cbhq.net',
|
||||
'mainnet.base.org',
|
||||
'sepolia.base.org',
|
||||
'https://cloudflare-eth.com',
|
||||
'https://i.seadn.io/', // ens avatars
|
||||
'https://api.opensea.io', // enables getting ENS avatars
|
||||
'https://ipfs.io', // ipfs ens avatar resolution
|
||||
'wss://www.walletlink.org',
|
||||
'https://base.easscan.org/graphql',
|
||||
'https://api.guild.xyz/',
|
||||
isLocalDevelopment ? 'ws://localhost:3000/' : '',
|
||||
isLocalDevelopment ? 'http://localhost:3000/' : '',
|
||||
'https://flag.lab.amplitude.com/sdk/v2/flags',
|
||||
@@ -96,9 +103,13 @@ const contentSecurityPolicy = {
|
||||
'form-action': ["'self'", baseXYZDomains],
|
||||
'img-src': [
|
||||
"'self'",
|
||||
'blob:',
|
||||
'https://blob.vercel-storage.com', // Vercel File storage
|
||||
'https://zku9gdedgba48lmr.public.blob.vercel-storage.com', // Vercel File storage
|
||||
'data:',
|
||||
'https://*.walletconnect.com/', // WalletConnect
|
||||
'https://i.seadn.io/', // ens avatars
|
||||
'https://ipfs.io', // ipfs ens avatar resolution
|
||||
],
|
||||
};
|
||||
|
||||
@@ -154,6 +165,24 @@ module.exports = extendBaseConfig(
|
||||
protocol: 'https',
|
||||
hostname: 'i.seadn.io',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'ipfs.io',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cf-ipfs.com',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'blob.vercel-storage.com',
|
||||
port: '',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'zku9gdedgba48lmr.public.blob.vercel-storage.com',
|
||||
port: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
async headers() {
|
||||
|
||||
@@ -13,17 +13,29 @@
|
||||
"dependencies": {
|
||||
"@coinbase/cookie-banner": "^1.0.3",
|
||||
"@coinbase/cookie-manager": "^1.1.1",
|
||||
"@coinbase/onchainkit": "^0.6.0",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@coinbase/onchainkit": "^0.26.3",
|
||||
"@guildxyz/sdk": "2.6.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@next/font": "^13.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@rainbow-me/rainbowkit": "^2.1.3",
|
||||
"@tanstack/react-query": "^5.29.2",
|
||||
"@tanstack/react-query": "^5.51.11",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@vercel/blob": "^0.23.4",
|
||||
"@vercel/kv": "^1.0.1",
|
||||
"@vercel/og": "^0.6.2",
|
||||
"@vercel/postgres-kysely": "^0.8.0",
|
||||
"base-ui": "0.1.1",
|
||||
"classnames": "^2.5.1",
|
||||
"ethers": "5.7.2",
|
||||
"framer-motion": "^8.5.5",
|
||||
"jose": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "^0.27.3",
|
||||
"next": "^13.2.0",
|
||||
"next": "^13.5.6",
|
||||
"node-fetch": "^3.3.0",
|
||||
"permissionless": "^0.1.41",
|
||||
"pg": "^8.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-blockies": "^1.4.1",
|
||||
@@ -31,21 +43,24 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intersection-observer": "^9.10.2",
|
||||
"react-intl": "^6.2.1",
|
||||
"satori": "^0.10.14",
|
||||
"sharp": "^0.33.4",
|
||||
"twemoji": "^14.0.2",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"uuidv4": "^6.2.13",
|
||||
"viem": "2.x",
|
||||
"wagmi": "^2.5.20"
|
||||
"wagmi": "^2.11.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.18",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/react": "^18",
|
||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"csv-parser": "^3.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint-config-next": "^13.1.6",
|
||||
"node-fetch": "^3.3.0",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||
"tailwindcss": "^3.2.4",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '@coinbase/onchainkit/styles.css';
|
||||
import './global.css';
|
||||
|
||||
import {
|
||||
@@ -6,6 +7,8 @@ import {
|
||||
TrackingCategory,
|
||||
TrackingPreference,
|
||||
} from '@coinbase/cookie-manager';
|
||||
import { OnchainKitProvider } from '@coinbase/onchainkit';
|
||||
import { Provider as TooltipProvider } from '@radix-ui/react-tooltip';
|
||||
import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit';
|
||||
import '@rainbow-me/rainbowkit/styles.css';
|
||||
import {
|
||||
@@ -16,14 +19,16 @@ import {
|
||||
walletConnectWallet,
|
||||
} from '@rainbow-me/rainbowkit/wallets';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { UserAvatar } from 'apps/web/src/components/ConnectWalletButton/UserAvatar';
|
||||
import useSprig from 'base-ui/hooks/useSprig';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { NextPage } from 'next';
|
||||
import { AppProps } from 'next/app';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ReactElement, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createConfig, http, WagmiProvider } from 'wagmi';
|
||||
import { base, baseSepolia, mainnet, sepolia } from 'wagmi/chains';
|
||||
import ClientAnalyticsScript from '../src/components/ClientAnalyticsScript/ClientAnalyticsScript';
|
||||
import { Layout } from '../src/components/Layout/Layout';
|
||||
import { Layout, NavigationType } from '../src/components/Layout/Layout';
|
||||
import { cookieManagerConfig } from '../src/utils/cookieManagerConfig';
|
||||
import ExperimentsProvider from 'base-ui/contexts/Experiments';
|
||||
|
||||
@@ -57,11 +62,18 @@ const config = createConfig({
|
||||
},
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const sprigEnvironmentId = process.env.NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID;
|
||||
|
||||
export default function StaticApp({ Component, pageProps }: AppProps) {
|
||||
export type NextPageWithLayout<P = unknown, IP = P> = NextPage<P, IP> & {
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
};
|
||||
|
||||
type AppPropsWithLayout = AppProps & {
|
||||
Component: NextPageWithLayout;
|
||||
};
|
||||
|
||||
export default function StaticApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
// Cookie Manager Provider Configuration
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const trackingPreference = useRef<TrackingPreference | undefined>();
|
||||
@@ -99,6 +111,10 @@ export default function StaticApp({ Component, pageProps }: AppProps) {
|
||||
|
||||
useSprig(sprigEnvironmentId);
|
||||
|
||||
const getLayout =
|
||||
Component.getLayout ??
|
||||
((page) => <Layout navigationType={NavigationType.Default}>{page}</Layout>);
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
@@ -115,13 +131,18 @@ export default function StaticApp({ Component, pageProps }: AppProps) {
|
||||
<ClientAnalyticsScript />
|
||||
<WagmiProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RainbowKitProvider modalSize="compact">
|
||||
<ExperimentsProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</ExperimentsProvider>
|
||||
</RainbowKitProvider>
|
||||
<OnchainKitProvider
|
||||
chain={baseSepolia}
|
||||
apiKey={process.env.NEXT_PUBLIC_ONCHAINKIT_API_KEY}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<ExperimentsProvider>
|
||||
<RainbowKitProvider modalSize="compact" avatar={UserAvatar}>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</RainbowKitProvider>
|
||||
</ExperimentsProvider>
|
||||
</TooltipProvider>
|
||||
</OnchainKitProvider>
|
||||
</QueryClientProvider>
|
||||
</WagmiProvider>
|
||||
</MotionConfig>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getProofsByNamespaceAndAddress, ProofTableNamespace } from 'apps/web/src/utils/proofs';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'method not allowed' });
|
||||
}
|
||||
const { address, namespace } = req.query;
|
||||
if (!address) {
|
||||
return res.status(400).json({ error: 'address is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await getProofsByNamespaceAndAddress(
|
||||
address as string,
|
||||
namespace as ProofTableNamespace,
|
||||
);
|
||||
|
||||
return res.status(200).json({ result: content });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return res.status(404).json({ error: 'address is not eligible for this project' });
|
||||
}
|
||||
111
apps/web/pages/api/basenames/[name]/assets/cardImage.svg.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import satori from 'satori';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { getUserNamePicture } from 'apps/web/src/utils/usernames';
|
||||
import twemoji from 'twemoji';
|
||||
|
||||
const emojiCache: Record<string, Promise<string>> = {};
|
||||
|
||||
export async function loadEmoji(emojiString: string) {
|
||||
const code = twemoji.convert.toCodePoint(emojiString);
|
||||
|
||||
if (code in emojiCache) {
|
||||
return emojiCache[code];
|
||||
}
|
||||
|
||||
// TODO: Is this okay? Vercel's OG image already does these calls
|
||||
const url = `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${code.toLowerCase()}.svg`;
|
||||
|
||||
return (emojiCache[code] = fetch(url).then(async (r) => r.text()));
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
};
|
||||
|
||||
// TODO: Do we want to check if the name actually exists?
|
||||
export default async function handler(request: NextRequest) {
|
||||
const fontData = await fetch(
|
||||
new URL('../../../../../src/fonts/CoinbaseDisplay-Regular.ttf', import.meta.url),
|
||||
).then(async (res) => res.arrayBuffer());
|
||||
|
||||
// TODO: Check this works in live/production
|
||||
const url = new URL(request.url);
|
||||
const username = url.searchParams.get('name') ?? 'yourname';
|
||||
const domainName = `${url.protocol}//${url.host}`;
|
||||
const profilePicture = getUserNamePicture(username);
|
||||
|
||||
// Using Satori for a SVG response
|
||||
const svg = await satori(
|
||||
<div
|
||||
style={{
|
||||
color: 'black',
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
background: '#F7F7F7',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
backgroundColor: '#0455FF',
|
||||
borderRadius: '5rem',
|
||||
padding: '7%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
maxHeight: '55%',
|
||||
maxWidth: '90%',
|
||||
boxShadow:
|
||||
'0px 8px 16px 0px rgba(0,82,255,0.32),inset 0px 8px 16px 0px rgba(255,255,255,0.25) ',
|
||||
}}
|
||||
>
|
||||
<figure style={{ borderRadius: '100%', overflow: 'hidden', height: 120, width: 120 }}>
|
||||
<img src={domainName + profilePicture.src} height={120} width={120} alt={username} />
|
||||
</figure>
|
||||
<span
|
||||
style={{
|
||||
color: 'white',
|
||||
paddingBottom: '0.75rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 'auto',
|
||||
fontSize: '5rem',
|
||||
}}
|
||||
>
|
||||
{username}
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
fonts: [
|
||||
{
|
||||
name: 'CoinbaseDisplay',
|
||||
data: fontData,
|
||||
weight: 500,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
loadAdditionalAsset: async (code: string, segment: string) => {
|
||||
if (code === 'emoji') {
|
||||
return `data:image/svg+xml;base64,${btoa(await loadEmoji(segment))}`;
|
||||
}
|
||||
|
||||
return code;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return new Response(svg, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
import { getUserNamePicture } from 'apps/web/src/utils/usernames';
|
||||
import { NextRequest } from 'next/server';
|
||||
import tempPendingAnimation from 'apps/web/src/components/Basenames/tempPendingAnimation.png';
|
||||
import { openGraphImageHeight, openGraphImageWidth } from 'apps/web/src/utils/opengraphs';
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
};
|
||||
|
||||
// TODO: Do we want to check if the name actually exists?
|
||||
export default async function handler(request: NextRequest) {
|
||||
const fontData = await fetch(
|
||||
new URL('../../../../../src/fonts/CoinbaseDisplay-Regular.ttf', import.meta.url),
|
||||
).then(async (res) => res.arrayBuffer());
|
||||
|
||||
// TODO: Check this works in live/production
|
||||
const url = new URL(request.url);
|
||||
const username = url.searchParams.get('name') ?? 'yourname';
|
||||
const domainName = `${url.protocol}//${url.host}`;
|
||||
const profilePicture = getUserNamePicture(username);
|
||||
|
||||
// Using vercel's OG image for a PNG response
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundImage: `url(${domainName + tempPendingAnimation.src})`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: '100% 100%',
|
||||
padding: '1.5rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#0455FF',
|
||||
borderRadius: '5rem',
|
||||
padding: '1rem',
|
||||
paddingRight: '1.5rem',
|
||||
fontSize: '5rem',
|
||||
maxWidth: '100%',
|
||||
boxShadow:
|
||||
'0px 8px 16px 0px rgba(0,82,255,0.32),inset 0px 8px 16px 0px rgba(255,255,255,0.25) ',
|
||||
}}
|
||||
>
|
||||
<figure style={{ borderRadius: '100%', overflow: 'hidden' }}>
|
||||
<img src={domainName + profilePicture.src} height={80} width={80} alt={username} />
|
||||
</figure>
|
||||
<span
|
||||
style={{
|
||||
color: 'white',
|
||||
paddingBottom: '0.75rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 'auto',
|
||||
}}
|
||||
>
|
||||
{username}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: openGraphImageWidth,
|
||||
height: openGraphImageHeight,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Typewriter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
51
apps/web/pages/api/basenames/avatar/upload.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { handleUpload, type HandleUploadBody } from '@vercel/blob/client';
|
||||
import type { NextApiResponse, NextApiRequest } from 'next';
|
||||
|
||||
export const ALLOWED_IMAGE_TYPE = [
|
||||
'image/svg+xml',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
];
|
||||
|
||||
export const MAX_IMAGE_SIZE_IN_MB = 1; // max 1mb
|
||||
|
||||
export default async function handler(request: NextApiRequest, response: NextApiResponse) {
|
||||
const body = request.body as HandleUploadBody;
|
||||
const username = request.query.username;
|
||||
if (!username) return;
|
||||
|
||||
try {
|
||||
const jsonResponse = await handleUpload({
|
||||
body,
|
||||
request,
|
||||
onBeforeGenerateToken: async (pathname) => {
|
||||
// TODO: We can maybe compare username to an address for additional security
|
||||
// Currently this endpoints allows anonymous upload
|
||||
|
||||
// This should prevent random upload(s), but does not authenticate the source
|
||||
if (!pathname.includes(`basenames/avatar/${username}`)) {
|
||||
throw new Error('Issue with upload');
|
||||
}
|
||||
|
||||
return {
|
||||
pathname: pathname,
|
||||
allowedContentTypes: ALLOWED_IMAGE_TYPE,
|
||||
maximumSizeInBytes: MAX_IMAGE_SIZE_IN_MB * (1024 * 1024),
|
||||
tokenPayload: JSON.stringify({
|
||||
username,
|
||||
pathname,
|
||||
}),
|
||||
};
|
||||
},
|
||||
onUploadCompleted: async () => {
|
||||
// TODO: Maybe analytics?
|
||||
},
|
||||
});
|
||||
|
||||
return response.status(200).json(jsonResponse);
|
||||
} catch (error) {
|
||||
return response.status(400).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
29
apps/web/pages/api/basenames/contract-uri.json.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { base } from 'viem/chains';
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
};
|
||||
|
||||
export default async function GET(request: Request) {
|
||||
// TODO: Check this works in live/production
|
||||
const url = new URL(request.url);
|
||||
const domainName = `${url.protocol}//${url.host}`;
|
||||
|
||||
const chainId = url.searchParams.get('chainId') ?? base.id;
|
||||
if (!chainId) return NextResponse.json({ error: '406: chainId is missing' }, { status: 406 });
|
||||
|
||||
const name = Number(chainId) === base.id ? 'Basename' : 'Basename (Sepolia testnet)';
|
||||
|
||||
const tokenMedata = {
|
||||
name: name,
|
||||
description: 'TODO',
|
||||
image: 'TODO', // TODO
|
||||
banner_image: 'TODO.png', // TODO
|
||||
featured_image: 'TODO.png', // TODO
|
||||
external_link: `${domainName}/name`,
|
||||
collaborators: [],
|
||||
};
|
||||
|
||||
return NextResponse.json(tokenMedata);
|
||||
}
|
||||
17
apps/web/pages/api/basenames/contract-uri.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { base } from 'viem/chains';
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
};
|
||||
|
||||
export default async function GET(request: Request) {
|
||||
// TODO: Check this works in live/production
|
||||
const url = new URL(request.url);
|
||||
const chainId = url.searchParams.get('chainId') ?? base.id;
|
||||
if (!chainId) return NextResponse.json({ error: '406: chainId is missing' }, { status: 406 });
|
||||
|
||||
return NextResponse.redirect(
|
||||
new URL(`/api/basenames/contract-uri.json?chainId=${chainId}`, request.url),
|
||||
);
|
||||
}
|
||||
63
apps/web/pages/api/basenames/metadata/[tokenId].ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import L2Resolver from 'apps/web/src/abis/L2Resolver';
|
||||
import { USERNAME_L2_RESOLVER_ADDRESSES } from 'apps/web/src/addresses/usernames';
|
||||
import { getBasenamePublicClient } from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { USERNAME_DOMAINS } from 'apps/web/src/utils/usernames';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { encodePacked, keccak256, namehash, toHex } from 'viem';
|
||||
import { base } from 'viem/chains';
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
};
|
||||
|
||||
export default async function GET(request: Request) {
|
||||
// TODO: Check this works in live/production
|
||||
const url = new URL(request.url);
|
||||
const domainName = `${url.protocol}//${url.host}`;
|
||||
let tokenId = url.searchParams.get('tokenId');
|
||||
if (tokenId?.endsWith('.json')) tokenId = tokenId.slice(0, -5);
|
||||
|
||||
const chainId = url.searchParams.get('chainId') ?? base.id;
|
||||
const baseDomainName = USERNAME_DOMAINS[Number(chainId)];
|
||||
|
||||
if (!tokenId) return NextResponse.json({ error: '406: tokenId is missing' }, { status: 406 });
|
||||
if (!chainId) return NextResponse.json({ error: '406: chainId is missing' }, { status: 406 });
|
||||
if (!baseDomainName)
|
||||
return NextResponse.json({ error: '406: base domain name is missing' }, { status: 406 });
|
||||
|
||||
// Get labelhash from tokenId
|
||||
const labelHash = toHex(BigInt(tokenId));
|
||||
|
||||
// Convert labelhash to namehash
|
||||
const namehashNode = keccak256(
|
||||
encodePacked(['bytes32', 'bytes32'], [namehash(baseDomainName), labelHash]),
|
||||
);
|
||||
|
||||
const client = getBasenamePublicClient(Number(chainId));
|
||||
const basename = await client.readContract({
|
||||
abi: L2Resolver,
|
||||
address: USERNAME_L2_RESOLVER_ADDRESSES[Number(chainId)],
|
||||
args: [namehashNode],
|
||||
functionName: 'name',
|
||||
});
|
||||
|
||||
if (!basename) return NextResponse.json({ error: '404: Basename not found' }, { status: 404 });
|
||||
|
||||
const tokenMedata = {
|
||||
// This is the URL to the image of the item.
|
||||
image: `${domainName}/api/basenames/${basename}/assets/cardImage.svg`,
|
||||
|
||||
// This is the URL that will appear below the asset's image on OpenSea and will allow users to leave OpenSea and view the item on your site.
|
||||
external_url: `${domainName}/name/${basename}`,
|
||||
|
||||
// A human-readable description of the item. Markdown is supported.
|
||||
description: `${basename}, a Basename`,
|
||||
|
||||
// A human-readable description of the item. Markdown is supported.
|
||||
name: basename,
|
||||
|
||||
// TODO: attributes?
|
||||
};
|
||||
|
||||
return NextResponse.json(tokenMedata);
|
||||
}
|
||||
35
apps/web/pages/api/basenames/talentprotocol/[address].ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') {
|
||||
res.status(405).json({ error: 'method not allowed' });
|
||||
return;
|
||||
}
|
||||
const { address } = req.query;
|
||||
|
||||
if (typeof address !== 'string') {
|
||||
res.status(400).json({ error: 'address is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.talentprotocol.com/api/v2/passports/${encodeURIComponent(address || '')}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-API-KEY': process.env.TALENT_PROTOCOL_API_KEY,
|
||||
},
|
||||
},
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data) {
|
||||
return res.status(200).json(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return res.status(404).json({ error: 'address not found' });
|
||||
}
|
||||
62
apps/web/pages/api/name/[alreadyClaimedName].ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { queryCbGpt } from 'apps/web/src/cdp/api/cb-gpt';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export type NameSuggestionResponseData = {
|
||||
suggestion: string[];
|
||||
};
|
||||
|
||||
type ErrorResponseData = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
type ApiResponse = NameSuggestionResponseData | ErrorResponseData;
|
||||
|
||||
const NAME_COUNT = 3;
|
||||
const SYSTEM_PROMPT = `You are an AI assistant tasked with providing alternative username recommendations
|
||||
for the Ethereum Name Service (ENS). Users come to you when their desired ENS name is unavailable,
|
||||
and you need to suggest alternative names that are both desirable and likely to be available.
|
||||
You will be given one input. This is the name that the user originally wanted but is unavailable.
|
||||
Follow these guidelines when generating alternative names:
|
||||
1. Create names that are similar in style or
|
||||
meaning to the unavailable name.
|
||||
2. Suggestions should be very unlikely to already be taken.
|
||||
3. You may use emoji in your suggestions.
|
||||
4. Do not include any suffixes (i.e., .ens) in your suggestions.
|
||||
5. Keep the names short and memorable.
|
||||
6. Be creative and think of unique variations that the user might find appealing.
|
||||
Your output should be a JSON array containing exactly ${NAME_COUNT} alternative name suggestions. Do not
|
||||
include any explanation or additional text outside of the JSON array.
|
||||
Remember, the goal is to provide alternatives that users will find desirable and that are likely to be available on ENS.
|
||||
Focus on quality and creativity in your suggestions.`;
|
||||
const chatLlm = 'claude-3-5-sonnet@20240620';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResponse>) {
|
||||
const { alreadyClaimedName } = req.query;
|
||||
if (typeof alreadyClaimedName !== 'string') {
|
||||
res.status(400).json({ error: 'name must be a string' });
|
||||
return;
|
||||
}
|
||||
if (alreadyClaimedName.length > 50) {
|
||||
res.status(400).json({ error: 'name too long to fetch recommendations' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const suggestion = await queryCbGpt({
|
||||
taskConfig: {
|
||||
actionLlm: {
|
||||
chatLlm: chatLlm,
|
||||
},
|
||||
action_prompt_template: {
|
||||
init_llm_chain: SYSTEM_PROMPT,
|
||||
},
|
||||
},
|
||||
query: alreadyClaimedName,
|
||||
});
|
||||
res.status(200).json({ suggestion: JSON.parse(suggestion.response) as string[] });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
res.status(500).json({ error: `failed to generate suggestions ${e.message}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
40
apps/web/pages/api/paymaster/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { UserOperation } from 'permissionless';
|
||||
import { paymasterClient } from '../../../src/utils/paymasterConfig';
|
||||
import { willSponsor } from '../../../src/utils/paymasterSponsor';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type RequestBody = {
|
||||
params: [
|
||||
UserOperation<'v0.6'>, // userOp
|
||||
string, // endpoint
|
||||
number, // chainId
|
||||
];
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const method = req.method;
|
||||
if (!req.body?.params && !Array.isArray(req.body?.params)) {
|
||||
return res.status(400).json({ error: 'invalid body' });
|
||||
}
|
||||
const { params } = req.body as RequestBody;
|
||||
const [userOp, entrypoint, chainId] = params;
|
||||
try {
|
||||
const willSponsorDecision = await willSponsor({ chainId, entrypoint, userOp });
|
||||
if (!willSponsorDecision) {
|
||||
return res.json({ error: 'Not a sponsorable operation' });
|
||||
}
|
||||
|
||||
if (method === 'pm_getPaymasterStubData') {
|
||||
// @ts-expect-error verified functional by @keshavSinghal
|
||||
const result = await paymasterClient.getPaymasterStubData({ userOperation: userOp });
|
||||
return res.json({ result });
|
||||
} else if (method === 'pm_getPaymasterData') {
|
||||
// @ts-expect-error verified functional by @keshavSinghal
|
||||
const result = await paymasterClient.getPaymasterData({ userOperation: userOp });
|
||||
return res.json({ result });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.status(500).json({ error: 'something went wrong validating ' });
|
||||
}
|
||||
}
|
||||
70
apps/web/pages/api/proofs/cb1/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { trustedSignerPKey } from 'apps/web/src/constants';
|
||||
import { isBasenameSupportedChain } from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { DiscountType } from 'apps/web/src/utils/proofs';
|
||||
import { sybilResistantUsernameSigning } from 'apps/web/src/utils/proofs/sybil_resistance';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { isAddress } from 'viem';
|
||||
|
||||
/**
|
||||
* This endpoint checks if the provided address has access to the cb1 attestation.
|
||||
*
|
||||
* Possible Error Responses:
|
||||
* - 400: Invalid address or missing verifications.
|
||||
* - 405: Unauthorized method.
|
||||
* - 409: User has already claimed a username.
|
||||
* - 500: Internal server error.
|
||||
*
|
||||
* @returns {Object} - An object with the signed message, attestations, and discount validator address.
|
||||
* Example response:
|
||||
* {
|
||||
* "signedMessage": "0x0000000000000000000000009c02e8e28d8b706f67dcf0fc7f46a9ee1f9649fa000000000000000000000000000000000000000000000000000000000000012c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000416f4b871a02406ddddbf6f6df1c58416830c5ce45becad5b4f30cf32f74ee39a5559659f9e29479bc76bb1ebf40fffc7119d09ed7c8dcaf6075956f83935263851b00000000000000000000000000000000000000000000000000000000000000",
|
||||
* "attestations": [
|
||||
* {
|
||||
* "name": "verifiedCoinbaseOne",
|
||||
* "type": "bool",
|
||||
* "signature": "bool verifiedCoinbaseOne",
|
||||
* "value": {
|
||||
* "name": "verifiedCoinbaseOne",
|
||||
* "type": "bool",
|
||||
* "value": true
|
||||
* }
|
||||
* }
|
||||
* ],
|
||||
* "discountValidatorAddress": "0x502df754f25f492cad45ed85a4de0ee7540717e7"
|
||||
* }
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') {
|
||||
res.status(405).json({ error: 'method not allowed' });
|
||||
return;
|
||||
}
|
||||
const { address, chain } = req.query;
|
||||
|
||||
if (!address || Array.isArray(address) || !isAddress(address)) {
|
||||
return res.status(400).json({ error: 'valid address is required' });
|
||||
}
|
||||
|
||||
if (!trustedSignerPKey) {
|
||||
return res.status(500).json({ error: 'currently unable to sign' });
|
||||
}
|
||||
if (!chain || Array.isArray(chain)) {
|
||||
return res.status(400).json({ error: 'chain must be a single value' });
|
||||
}
|
||||
let parsedChain = parseInt(chain);
|
||||
if (!isBasenameSupportedChain(parsedChain)) {
|
||||
return res.status(400).json({ error: 'chain must be Base or Base Sepolia' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await sybilResistantUsernameSigning(address, DiscountType.CB1, parsedChain);
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
return res.status(409).json({ error: error.message });
|
||||
}
|
||||
|
||||
// If error is not an instance of Error, return a generic error message
|
||||
return res.status(409).json({ error: 'An unexpected error occurred' });
|
||||
}
|
||||
}
|
||||
68
apps/web/pages/api/proofs/cbid/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import {
|
||||
getProofsByNamespaceAndAddress,
|
||||
hasRegisteredWithDiscount,
|
||||
ProofTableNamespace,
|
||||
} from 'apps/web/src/utils/proofs';
|
||||
import { Address, isAddress } from 'viem';
|
||||
import { USERNAME_CB_ID_DISCOUNT_VALIDATORS } from 'apps/web/src/addresses/usernames';
|
||||
import { isBasenameSupportedChain } from 'apps/web/src/hooks/useBasenameChain';
|
||||
|
||||
export type CBIDProofResponse = {
|
||||
discountValidatorAddress: Address;
|
||||
address: Address;
|
||||
namespace: string;
|
||||
proofs: `0x${string}`[];
|
||||
};
|
||||
|
||||
/*
|
||||
this endpoint returns whether or not the account has a cb.id
|
||||
if result array is empty, user has no cb.id
|
||||
example return:
|
||||
{
|
||||
"address": "0xB18e4C959bccc8EF86D78DC297fb5efA99550d85",
|
||||
"namespace": "usernames",
|
||||
"proofs": "[0x56ce3bbc909b90035ae373d32c56a9d81d26bb505dd935cdee6afc384bcaed8d, 0x99e940ed9482bf59ba5ceab7df0948798978a1acaee0ecb41f64fe7f40eedd17]"
|
||||
"discountValidatorAddress": "0x..."
|
||||
}
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'method not allowed' });
|
||||
}
|
||||
const { address, chain } = req.query;
|
||||
if (!address || Array.isArray(address) || !isAddress(address)) {
|
||||
return res.status(400).json({ error: 'A single valid address is required' });
|
||||
}
|
||||
|
||||
if (!chain || Array.isArray(chain)) {
|
||||
return res.status(400).json({ error: 'invalid chain' });
|
||||
}
|
||||
let parsedChain = parseInt(chain);
|
||||
if (!isBasenameSupportedChain(parsedChain)) {
|
||||
return res.status(400).json({ error: 'chain must be Base or Base Sepolia' });
|
||||
}
|
||||
|
||||
try {
|
||||
const hasPreviouslyRegistered = await hasRegisteredWithDiscount([address], parsedChain);
|
||||
// if any linked address registered previously return an error
|
||||
if (hasPreviouslyRegistered) {
|
||||
return res.status(400).json({ error: 'This address has already claimed a username.' });
|
||||
}
|
||||
const [content] = await getProofsByNamespaceAndAddress(address, ProofTableNamespace.Usernames);
|
||||
const proofs = content?.proofs ? (JSON.parse(content.proofs) as `0x${string}`[]) : [];
|
||||
if (proofs.length === 0) {
|
||||
return res.status(404).json({ error: 'address is not eligible for a cbid discount' });
|
||||
}
|
||||
const responseData: CBIDProofResponse = {
|
||||
...content,
|
||||
proofs,
|
||||
discountValidatorAddress: USERNAME_CB_ID_DISCOUNT_VALIDATORS[parsedChain],
|
||||
};
|
||||
return res.status(200).json(responseData);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return res.status(404).json({ error: 'address is not eligible for a cbid discount' });
|
||||
}
|
||||
71
apps/web/pages/api/proofs/coinbase/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { trustedSignerPKey } from 'apps/web/src/constants';
|
||||
import { isBasenameSupportedChain } from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { DiscountType, VerifiedAccount } from 'apps/web/src/utils/proofs';
|
||||
import { sybilResistantUsernameSigning } from 'apps/web/src/utils/proofs/sybil_resistance';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { Address, isAddress } from 'viem';
|
||||
|
||||
// Coinbase verified account *and* CB1 structure
|
||||
export type CoinbaseProofResponse = {
|
||||
signedMessage?: string;
|
||||
attestations: VerifiedAccount[];
|
||||
discountValidatorAddress: Address;
|
||||
expires?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint reports whether or not the provided access has access to the verified account attestation
|
||||
*
|
||||
* Error responses:
|
||||
* 400: if address is invalid or missing verifications
|
||||
* 405: for unauthorized methods
|
||||
* 409: if user has already claimed a username
|
||||
* 500: for internal server errors
|
||||
*
|
||||
* @param req
|
||||
* {
|
||||
* address: address to check if user is allowed to claim a new username with discount
|
||||
* }
|
||||
* @param res
|
||||
* {
|
||||
* signedMessage: this is to be passed into the contract to claim a username
|
||||
* attestations: will show the attestations that the user has for verified account and verified cb1 account
|
||||
* }
|
||||
* @returns
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') {
|
||||
res.status(405).json({ error: 'method not allowed' });
|
||||
return;
|
||||
}
|
||||
const { address, chain } = req.query;
|
||||
|
||||
if (!address || Array.isArray(address) || !isAddress(address)) {
|
||||
return res.status(400).json({ error: 'valid address is required' });
|
||||
}
|
||||
|
||||
if (!trustedSignerPKey) {
|
||||
return res.status(500).json({ error: 'currently unable to sign' });
|
||||
}
|
||||
|
||||
if (!chain || Array.isArray(chain)) {
|
||||
return res.status(400).json({ error: 'chain must be a single value' });
|
||||
}
|
||||
let parsedChain = parseInt(chain);
|
||||
if (!isBasenameSupportedChain(parsedChain)) {
|
||||
return res.status(400).json({ error: 'chain must be Base or Base Sepolia' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await sybilResistantUsernameSigning(address, DiscountType.CB, parsedChain);
|
||||
return res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
return res.status(409).json({ error: error.message });
|
||||
}
|
||||
|
||||
// If error is not an instance of Error, return a generic error message
|
||||
return res.status(409).json({ error: 'An unexpected error occurred' });
|
||||
}
|
||||
}
|
||||
81
apps/web/pages/api/proofs/earlyAccess/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { USERNAME_EA_DISCOUNT_VALIDATORS } from 'apps/web/src/addresses/usernames';
|
||||
import { isBasenameSupportedChain } from 'apps/web/src/hooks/useBasenameChain';
|
||||
import {
|
||||
getProofsByNamespaceAndAddress,
|
||||
hasRegisteredWithDiscount,
|
||||
ProofTableNamespace,
|
||||
} from 'apps/web/src/utils/proofs';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { Address, isAddress } from 'viem';
|
||||
|
||||
export type EarlyAccessProofResponse = {
|
||||
discountValidatorAddress: Address;
|
||||
address: Address;
|
||||
namespace: string;
|
||||
proofs: `0x${string}`[];
|
||||
};
|
||||
|
||||
/*
|
||||
this endpoint returns whether or not the account has a cb.id
|
||||
if result array is empty, user has no cb.id
|
||||
example return:
|
||||
{
|
||||
"address": "0xB18e4C959bccc8EF86D78DC297fb5efA99550d85",
|
||||
"namespace": "usernames",
|
||||
"proofs": "[0x56ce3bbc909b90035ae373d32c56a9d81d26bb505dd935cdee6afc384bcaed8d, 0x99e940ed9482bf59ba5ceab7df0948798978a1acaee0ecb41f64fe7f40eedd17]"
|
||||
"discountValidatorAddress": "0x..."
|
||||
}
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== 'GET') {
|
||||
return res.status(405).json({ error: 'method not allowed' });
|
||||
}
|
||||
const { address, chain } = req.query;
|
||||
if (!address || Array.isArray(address) || !isAddress(address)) {
|
||||
return res.status(400).json({ error: 'A single valid address is required' });
|
||||
}
|
||||
|
||||
if (!chain || Array.isArray(chain)) {
|
||||
return res.status(400).json({ error: 'invalid chain' });
|
||||
}
|
||||
|
||||
let parsedChain = parseInt(chain);
|
||||
if (!isBasenameSupportedChain(parsedChain)) {
|
||||
return res.status(400).json({ error: 'chain must be Base or Base Sepolia' });
|
||||
}
|
||||
|
||||
if (!isBasenameSupportedChain(parsedChain)) {
|
||||
return res.status(400).json({ error: 'chain must be Base or Base Sepolia' });
|
||||
}
|
||||
|
||||
try {
|
||||
const hasPreviouslyRegistered = await hasRegisteredWithDiscount([address], parsedChain);
|
||||
|
||||
// if any linked address registered previously return an error
|
||||
if (hasPreviouslyRegistered) {
|
||||
return res.status(400).json({ error: 'This address has already claimed a username.' });
|
||||
}
|
||||
const [content] = await getProofsByNamespaceAndAddress(
|
||||
address,
|
||||
ProofTableNamespace.UsernamesEarlyAccess,
|
||||
);
|
||||
|
||||
const proofs = content?.proofs ? (JSON.parse(content.proofs) as `0x${string}`[]) : [];
|
||||
if (proofs.length === 0) {
|
||||
return res.status(404).json({ error: 'address is not eligible for early access' });
|
||||
}
|
||||
|
||||
const responseData: EarlyAccessProofResponse = {
|
||||
...content,
|
||||
proofs,
|
||||
discountValidatorAddress: USERNAME_EA_DISCOUNT_VALIDATORS[parsedChain],
|
||||
};
|
||||
|
||||
console.log({ responseData });
|
||||
return res.status(200).json(responseData);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return res.status(404).json({ error: 'address is not eligible for early access' });
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import EcosystemHeroLogos from 'apps/web/public/images/ecosystem-hero-logos-new.png';
|
||||
import { Divider } from 'apps/web/src/components/Divider/Divider';
|
||||
import { List } from 'apps/web/src/components/Ecosystem/List';
|
||||
import { Button } from 'apps/web/src/components/Button/Button';
|
||||
import { Button, ButtonVariants } from 'apps/web/src/components/Button/Button';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -21,7 +21,7 @@ function EcosystemHero() {
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<Button variant="primary" className="w-full md:w-64">
|
||||
<Button variant={ButtonVariants.Primary} className="w-full md:w-64">
|
||||
Apply
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Features } from '../src/components/Features/Features';
|
||||
import { Hero } from '../src/components/Home/Hero';
|
||||
import { JoinTheCommunity } from '../src/components/JoinTheCommunity/JoinTheCommunity';
|
||||
import { Partnerships } from '../src/components/Partnerships/Partnerships';
|
||||
import { FrameButtonMetadata, FrameMetadata } from '@coinbase/onchainkit';
|
||||
import { FrameButtonMetadata, FrameMetadata } from '@coinbase/onchainkit/frame';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export default function Home() {
|
||||
|
||||
53
apps/web/pages/name/[username].tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import AnalyticsProvider from 'apps/web/contexts/Analytics';
|
||||
import UsernameProfile from 'apps/web/src/components/Basenames/UsernameProfile';
|
||||
import UsernameProfileProvider from 'apps/web/src/components/Basenames/UsernameProfileContext';
|
||||
import { Layout, NavigationType } from 'apps/web/src/components/Layout/Layout';
|
||||
import {
|
||||
openGraphImageHeight,
|
||||
openGraphImageType,
|
||||
openGraphImageWidth,
|
||||
} from 'apps/web/src/utils/opengraphs';
|
||||
import { BaseName } from 'apps/web/src/utils/usernames';
|
||||
import { NextPageContext } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
// Do not change this unless you know what you're doing (it'll break analytics)
|
||||
const usernameProfileAnalyticContext = 'username_profile';
|
||||
|
||||
export function Username({ domain }: { domain: string }) {
|
||||
const params = useParams<{ username: BaseName }>();
|
||||
const profileUsername = params?.username;
|
||||
const ogImageUrl = `${domain}/api/basenames/${profileUsername}/assets/coverImage.png`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Basenames | {profileUsername}</title>
|
||||
<meta property="og:image" content={ogImageUrl} />
|
||||
<meta property="og:image:secure_url" content={ogImageUrl} />
|
||||
<meta property="og:image:type" content={openGraphImageType} />
|
||||
<meta property="og:image:width" content={openGraphImageWidth.toString()} />
|
||||
<meta property="og:image:height" content={openGraphImageHeight.toString()} />
|
||||
<meta property="og:image:alt" content={`Base profile `} />
|
||||
</Head>
|
||||
<AnalyticsProvider context={usernameProfileAnalyticContext}>
|
||||
<UsernameProfileProvider>
|
||||
<UsernameProfile />
|
||||
</UsernameProfileProvider>
|
||||
</AnalyticsProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Username.getInitialProps = async ({ req }: NextPageContext) => {
|
||||
const domain = req?.headers.host ?? '';
|
||||
return { domain };
|
||||
};
|
||||
|
||||
Username.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout navigationType={NavigationType.Username}>{page}</Layout>;
|
||||
};
|
||||
|
||||
export default Username;
|
||||
34
apps/web/pages/name/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import AnalyticsProvider from 'apps/web/contexts/Analytics';
|
||||
import RegistrationProvider from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import RegistrationFlow from 'apps/web/src/components/Basenames/RegistrationFlow';
|
||||
import { Layout, NavigationType } from 'apps/web/src/components/Layout/Layout';
|
||||
import Head from 'next/head';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
// Do not change this unless you know what you're doing (it'll break analytics)
|
||||
const usernameRegistrationAnalyticContext = 'username_registration';
|
||||
|
||||
export function Usernames() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Basenames</title>
|
||||
<meta
|
||||
content="Base is a secure, low-cost, builder-friendly Ethereum L2 built to bring the next billion users onchain."
|
||||
name="description"
|
||||
/>
|
||||
</Head>
|
||||
<AnalyticsProvider context={usernameRegistrationAnalyticContext}>
|
||||
<RegistrationProvider>
|
||||
<RegistrationFlow />
|
||||
</RegistrationProvider>
|
||||
</AnalyticsProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Usernames.getLayout = function getLayout(page: ReactElement) {
|
||||
return <Layout navigationType={NavigationType.Username}>{page}</Layout>;
|
||||
};
|
||||
|
||||
export default Usernames;
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 0V3H7V2H14V10H13V12H16V0H5Z" fill="white"/>
|
||||
<path d="M10 6V14H3V6H10ZM12 4H1V16H12V4Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 217 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_588_7121)">
|
||||
<path d="M40 0H0V40H40V0Z" fill="#B388F5"/>
|
||||
<path d="M29.5171 15.755C29.5171 20.9687 25.3125 25.2137 20.0853 25.2137C14.8864 25.2137 10.6534 20.9972 10.6534 15.755H29.5171Z" fill="#D058C1"/>
|
||||
<path d="M10.6534 32.0513C10.6534 26.8377 14.858 22.5927 20.0853 22.5927C25.3125 22.5927 29.5171 26.8092 29.5171 32.0513H10.6534Z" fill="white"/>
|
||||
<path d="M20.0852 25.1852C18.3523 25.1852 16.7045 24.7008 15.3125 23.8746C16.7045 23.0484 18.3523 22.5641 20.0852 22.5641C21.8182 22.5641 23.4659 23.0484 24.858 23.8746C23.4659 24.7293 21.8466 25.1852 20.0852 25.1852Z" fill="#0A0B0D"/>
|
||||
<path d="M19.5739 14.416C21.3637 14.416 22.841 12.963 22.841 11.1396C22.841 9.31627 21.3921 7.86328 19.5739 7.86328C17.7557 7.86328 16.3069 9.31627 16.3069 11.1396C16.3069 12.963 17.7842 14.416 19.5739 14.416Z" fill="#0052FF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_588_7121">
|
||||
<rect width="40" height="40" rx="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
BIN
apps/web/public/images/avatars/aflock.eth.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
apps/web/public/images/avatars/dcj.eth.avif
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/web/public/images/avatars/ianlakes.eth.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
apps/web/public/images/avatars/jfrankfurt.eth.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
1
apps/web/public/images/avatars/johnpalmer.eth.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
apps/web/public/images/avatars/wilsoncusack.eth.png
Normal file
|
After Width: | Height: | Size: 936 KiB |
BIN
apps/web/public/images/avatars/zencephalon.eth.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
200
apps/web/src/abis/AddrResolver.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
export default [
|
||||
{
|
||||
type: 'function',
|
||||
name: 'addr',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address',
|
||||
internalType: 'address payable',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'addr',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'coinType',
|
||||
type: 'uint256',
|
||||
internalType: 'uint256',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes',
|
||||
internalType: 'bytes',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'clearRecords',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'recordVersions',
|
||||
inputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint64',
|
||||
internalType: 'uint64',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'setAddr',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'coinType',
|
||||
type: 'uint256',
|
||||
internalType: 'uint256',
|
||||
},
|
||||
{
|
||||
name: 'a',
|
||||
type: 'bytes',
|
||||
internalType: 'bytes',
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'setAddr',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'a',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'supportsInterface',
|
||||
inputs: [
|
||||
{
|
||||
name: 'interfaceID',
|
||||
type: 'bytes4',
|
||||
internalType: 'bytes4',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool',
|
||||
internalType: 'bool',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
name: 'AddrChanged',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
indexed: true,
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'a',
|
||||
type: 'address',
|
||||
indexed: false,
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
name: 'AddressChanged',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
indexed: true,
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'coinType',
|
||||
type: 'uint256',
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
},
|
||||
{
|
||||
name: 'newAddress',
|
||||
type: 'bytes',
|
||||
indexed: false,
|
||||
internalType: 'bytes',
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
name: 'VersionChanged',
|
||||
inputs: [
|
||||
{
|
||||
name: 'node',
|
||||
type: 'bytes32',
|
||||
indexed: true,
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'newVersion',
|
||||
type: 'uint64',
|
||||
indexed: false,
|
||||
internalType: 'uint64',
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
] as const;
|
||||
315
apps/web/src/abis/AttestationValidator.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
export default [
|
||||
{
|
||||
type: 'constructor',
|
||||
inputs: [
|
||||
{
|
||||
name: 'owner_',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: 'signer_',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: 'schemaID_',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'indexer_',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'cancelOwnershipHandover',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'completeOwnershipHandover',
|
||||
inputs: [
|
||||
{
|
||||
name: 'pendingOwner',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'indexer',
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address',
|
||||
internalType: 'contract IAttestationIndexer',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'isValidDiscountRegistration',
|
||||
inputs: [
|
||||
{
|
||||
name: 'claimer',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: 'validationData',
|
||||
type: 'bytes',
|
||||
internalType: 'bytes',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool',
|
||||
internalType: 'bool',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'owner',
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'result',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'ownershipHandoverExpiresAt',
|
||||
inputs: [
|
||||
{
|
||||
name: 'pendingOwner',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'result',
|
||||
type: 'uint256',
|
||||
internalType: 'uint256',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'renounceOwnership',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'requestOwnershipHandover',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'setSigner',
|
||||
inputs: [
|
||||
{
|
||||
name: 'signer_',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'transferOwnership',
|
||||
inputs: [
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
name: 'IndexerUpdated',
|
||||
inputs: [
|
||||
{
|
||||
name: 'previousIndexer',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: 'updatedIndexer',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
name: 'OwnershipHandoverCanceled',
|
||||
inputs: [
|
||||
{
|
||||
name: 'pendingOwner',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
name: 'OwnershipHandoverRequested',
|
||||
inputs: [
|
||||
{
|
||||
name: 'pendingOwner',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
{
|
||||
type: 'event',
|
||||
name: 'OwnershipTransferred',
|
||||
inputs: [
|
||||
{
|
||||
name: 'oldOwner',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address',
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
anonymous: false,
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'AlreadyInitialized',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'AttestationExpired',
|
||||
inputs: [
|
||||
{
|
||||
name: 'attestationUid',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'expirationTime',
|
||||
type: 'uint256',
|
||||
internalType: 'uint256',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'AttestationInvariantViolation',
|
||||
inputs: [
|
||||
{
|
||||
name: 'reason',
|
||||
type: 'string',
|
||||
internalType: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'AttestationNotFound',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'AttestationRevoked',
|
||||
inputs: [
|
||||
{
|
||||
name: 'attestationUid',
|
||||
type: 'bytes32',
|
||||
internalType: 'bytes32',
|
||||
},
|
||||
{
|
||||
name: 'revocationTime',
|
||||
type: 'uint256',
|
||||
internalType: 'uint256',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'ClaimerAddressMismatch',
|
||||
inputs: [
|
||||
{
|
||||
name: 'expectedClaimer',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: 'claimer',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'InvalidIndexer',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'NewOwnerIsZeroAddress',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'NoHandoverRequest',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'SignatureExpired',
|
||||
inputs: [],
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
name: 'Unauthorized',
|
||||
inputs: [],
|
||||
},
|
||||
] as const;
|
||||
108
apps/web/src/abis/CBIdDiscountValidator.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
export default [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'owner_', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'root_', type: 'bytes32' },
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'constructor',
|
||||
},
|
||||
{ inputs: [], name: 'AlreadyInitialized', type: 'error' },
|
||||
{ inputs: [], name: 'NewOwnerIsZeroAddress', type: 'error' },
|
||||
{ inputs: [], name: 'NoHandoverRequest', type: 'error' },
|
||||
{ inputs: [], name: 'Unauthorized', type: 'error' },
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverCanceled',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverRequested',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'oldOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'cancelOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'completeOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'claimer', type: 'address' },
|
||||
{ internalType: 'bytes', name: 'validationData', type: 'bytes' },
|
||||
],
|
||||
name: 'isValidDiscountRegistration',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: 'result', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'ownershipHandoverExpiresAt',
|
||||
outputs: [{ internalType: 'uint256', name: 'result', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'renounceOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'requestOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'root',
|
||||
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'root_', type: 'bytes32' }],
|
||||
name: 'setRoot',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
] as const;
|
||||
42
apps/web/src/abis/ERC1155DiscountValidator.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export default [
|
||||
{
|
||||
type: 'constructor',
|
||||
inputs: [
|
||||
{
|
||||
name: 'tokenAddress',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: 'tokenId_',
|
||||
type: 'uint256',
|
||||
internalType: 'uint256',
|
||||
},
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
name: 'isValidDiscountRegistration',
|
||||
inputs: [
|
||||
{
|
||||
name: 'claimer',
|
||||
type: 'address',
|
||||
internalType: 'address',
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes',
|
||||
internalType: 'bytes',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool',
|
||||
internalType: 'bool',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
},
|
||||
] as const;
|
||||
108
apps/web/src/abis/EarlyAccessValidator.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
export default [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'owner_', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'root_', type: 'bytes32' },
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'constructor',
|
||||
},
|
||||
{ inputs: [], name: 'AlreadyInitialized', type: 'error' },
|
||||
{ inputs: [], name: 'NewOwnerIsZeroAddress', type: 'error' },
|
||||
{ inputs: [], name: 'NoHandoverRequest', type: 'error' },
|
||||
{ inputs: [], name: 'Unauthorized', type: 'error' },
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverCanceled',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverRequested',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'oldOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'cancelOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'completeOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'claimer', type: 'address' },
|
||||
{ internalType: 'bytes', name: 'validationData', type: 'bytes' },
|
||||
],
|
||||
name: 'isValidDiscountRegistration',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: 'result', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'ownershipHandoverExpiresAt',
|
||||
outputs: [{ internalType: 'uint256', name: 'result', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'renounceOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'requestOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'root',
|
||||
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'root_', type: 'bytes32' }],
|
||||
name: 'setRoot',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
] as const;
|
||||
574
apps/web/src/abis/L2Resolver.ts
Normal file
@@ -0,0 +1,574 @@
|
||||
export default [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'contract ENS', name: 'ens_', type: 'address' },
|
||||
{ internalType: 'address', name: 'registrarController_', type: 'address' },
|
||||
{ internalType: 'address', name: 'reverseRegistrar_', type: 'address' },
|
||||
{ internalType: 'address', name: 'owner_', type: 'address' },
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'constructor',
|
||||
},
|
||||
{ inputs: [], name: 'AlreadyInitialized', type: 'error' },
|
||||
{ inputs: [], name: 'CantSetSelfAsDelegate', type: 'error' },
|
||||
{ inputs: [], name: 'CantSetSelfAsOperator', type: 'error' },
|
||||
{ inputs: [], name: 'NewOwnerIsZeroAddress', type: 'error' },
|
||||
{ inputs: [], name: 'NoHandoverRequest', type: 'error' },
|
||||
{ inputs: [], name: 'Unauthorized', type: 'error' },
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: true, internalType: 'uint256', name: 'contentType', type: 'uint256' },
|
||||
],
|
||||
name: 'ABIChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'address', name: 'a', type: 'address' },
|
||||
],
|
||||
name: 'AddrChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'uint256', name: 'coinType', type: 'uint256' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'newAddress', type: 'bytes' },
|
||||
],
|
||||
name: 'AddressChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'owner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'operator', type: 'address' },
|
||||
{ indexed: false, internalType: 'bool', name: 'approved', type: 'bool' },
|
||||
],
|
||||
name: 'ApprovalForAll',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'address', name: 'owner', type: 'address' },
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: true, internalType: 'address', name: 'delegate', type: 'address' },
|
||||
{ indexed: true, internalType: 'bool', name: 'approved', type: 'bool' },
|
||||
],
|
||||
name: 'Approved',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'hash', type: 'bytes' },
|
||||
],
|
||||
name: 'ContenthashChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'name', type: 'bytes' },
|
||||
{ indexed: false, internalType: 'uint16', name: 'resource', type: 'uint16' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'record', type: 'bytes' },
|
||||
],
|
||||
name: 'DNSRecordChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'name', type: 'bytes' },
|
||||
{ indexed: false, internalType: 'uint16', name: 'resource', type: 'uint16' },
|
||||
],
|
||||
name: 'DNSRecordDeleted',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'lastzonehash', type: 'bytes' },
|
||||
{ indexed: false, internalType: 'bytes', name: 'zonehash', type: 'bytes' },
|
||||
],
|
||||
name: 'DNSZonehashChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: true, internalType: 'bytes4', name: 'interfaceID', type: 'bytes4' },
|
||||
{ indexed: false, internalType: 'address', name: 'implementer', type: 'address' },
|
||||
],
|
||||
name: 'InterfaceChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'string', name: 'name', type: 'string' },
|
||||
],
|
||||
name: 'NameChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverCanceled',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverRequested',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'oldOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'bytes32', name: 'x', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'bytes32', name: 'y', type: 'bytes32' },
|
||||
],
|
||||
name: 'PubkeyChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'newRegistrarController', type: 'address' },
|
||||
],
|
||||
name: 'RegistrarControllerUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'newReverseRegistrar', type: 'address' },
|
||||
],
|
||||
name: 'ReverseRegistrarUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: true, internalType: 'string', name: 'indexedKey', type: 'string' },
|
||||
{ indexed: false, internalType: 'string', name: 'key', type: 'string' },
|
||||
{ indexed: false, internalType: 'string', name: 'value', type: 'string' },
|
||||
],
|
||||
name: 'TextChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ indexed: false, internalType: 'uint64', name: 'newVersion', type: 'uint64' },
|
||||
],
|
||||
name: 'VersionChanged',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'contentTypes', type: 'uint256' },
|
||||
],
|
||||
name: 'ABI',
|
||||
outputs: [
|
||||
{ internalType: 'uint256', name: '', type: 'uint256' },
|
||||
{ internalType: 'bytes', name: '', type: 'bytes' },
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'node', type: 'bytes32' }],
|
||||
name: 'addr',
|
||||
outputs: [{ internalType: 'address payable', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'coinType', type: 'uint256' },
|
||||
],
|
||||
name: 'addr',
|
||||
outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'address', name: 'delegate', type: 'address' },
|
||||
{ internalType: 'bool', name: 'approved', type: 'bool' },
|
||||
],
|
||||
name: 'approve',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'cancelOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'node', type: 'bytes32' }],
|
||||
name: 'clearRecords',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'completeOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'node', type: 'bytes32' }],
|
||||
name: 'contenthash',
|
||||
outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes32', name: 'name', type: 'bytes32' },
|
||||
{ internalType: 'uint16', name: 'resource', type: 'uint16' },
|
||||
],
|
||||
name: 'dnsRecord',
|
||||
outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'ens',
|
||||
outputs: [{ internalType: 'contract ENS', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes32', name: 'name', type: 'bytes32' },
|
||||
],
|
||||
name: 'hasDNSRecords',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes4', name: 'interfaceID', type: 'bytes4' },
|
||||
],
|
||||
name: 'interfaceImplementer',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'owner', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'address', name: 'delegate', type: 'address' },
|
||||
],
|
||||
name: 'isApprovedFor',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'account', type: 'address' },
|
||||
{ internalType: 'address', name: 'operator', type: 'address' },
|
||||
],
|
||||
name: 'isApprovedForAll',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' }],
|
||||
name: 'multicall',
|
||||
outputs: [{ internalType: 'bytes[]', name: 'results', type: 'bytes[]' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'nodehash', type: 'bytes32' },
|
||||
{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' },
|
||||
],
|
||||
name: 'multicallWithNodeCheck',
|
||||
outputs: [{ internalType: 'bytes[]', name: 'results', type: 'bytes[]' }],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'node', type: 'bytes32' }],
|
||||
name: 'name',
|
||||
outputs: [{ internalType: 'string', name: '', type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: 'result', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'ownershipHandoverExpiresAt',
|
||||
outputs: [{ internalType: 'uint256', name: 'result', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'node', type: 'bytes32' }],
|
||||
name: 'pubkey',
|
||||
outputs: [
|
||||
{ internalType: 'bytes32', name: 'x', type: 'bytes32' },
|
||||
{ internalType: 'bytes32', name: 'y', type: 'bytes32' },
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
|
||||
name: 'recordVersions',
|
||||
outputs: [{ internalType: 'uint64', name: '', type: 'uint64' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'registrarController',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'renounceOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'requestOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes', name: '', type: 'bytes' },
|
||||
{ internalType: 'bytes', name: 'data', type: 'bytes' },
|
||||
],
|
||||
name: 'resolve',
|
||||
outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'reverseRegistrar',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'contentType', type: 'uint256' },
|
||||
{ internalType: 'bytes', name: 'data', type: 'bytes' },
|
||||
],
|
||||
name: 'setABI',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'coinType', type: 'uint256' },
|
||||
{ internalType: 'bytes', name: 'a', type: 'bytes' },
|
||||
],
|
||||
name: 'setAddr',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'address', name: 'a', type: 'address' },
|
||||
],
|
||||
name: 'setAddr',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: 'operator', type: 'address' },
|
||||
{ internalType: 'bool', name: 'approved', type: 'bool' },
|
||||
],
|
||||
name: 'setApprovalForAll',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'hash', type: 'bytes' },
|
||||
],
|
||||
name: 'setContenthash',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'data', type: 'bytes' },
|
||||
],
|
||||
name: 'setDNSRecords',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes4', name: 'interfaceID', type: 'bytes4' },
|
||||
{ internalType: 'address', name: 'implementer', type: 'address' },
|
||||
],
|
||||
name: 'setInterface',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'string', name: 'newName', type: 'string' },
|
||||
],
|
||||
name: 'setName',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes32', name: 'x', type: 'bytes32' },
|
||||
{ internalType: 'bytes32', name: 'y', type: 'bytes32' },
|
||||
],
|
||||
name: 'setPubkey',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'registrarController_', type: 'address' }],
|
||||
name: 'setRegistrarController',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'reverseRegistrar_', type: 'address' }],
|
||||
name: 'setReverseRegistrar',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'string', name: 'key', type: 'string' },
|
||||
{ internalType: 'string', name: 'value', type: 'string' },
|
||||
],
|
||||
name: 'setText',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'hash', type: 'bytes' },
|
||||
],
|
||||
name: 'setZonehash',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes4', name: 'interfaceID', type: 'bytes4' }],
|
||||
name: 'supportsInterface',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'node', type: 'bytes32' },
|
||||
{ internalType: 'string', name: 'key', type: 'string' },
|
||||
],
|
||||
name: 'text',
|
||||
outputs: [{ internalType: 'string', name: '', type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'node', type: 'bytes32' }],
|
||||
name: 'zonehash',
|
||||
outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
] as const;
|
||||
445
apps/web/src/abis/RegistrarControllerABI.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
export default [
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'contract BaseRegistrar', name: 'base_', type: 'address' },
|
||||
{ internalType: 'contract IPriceOracle', name: 'prices_', type: 'address' },
|
||||
{ internalType: 'contract IReverseRegistrar', name: 'reverseRegistrar_', type: 'address' },
|
||||
{ internalType: 'address', name: 'owner_', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'rootNode_', type: 'bytes32' },
|
||||
{ internalType: 'string', name: 'rootName_', type: 'string' },
|
||||
{ internalType: 'address', name: 'paymentReceiver_', type: 'address' },
|
||||
],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'constructor',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'target', type: 'address' }],
|
||||
name: 'AddressEmptyCode',
|
||||
type: 'error',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
|
||||
name: 'AddressInsufficientBalance',
|
||||
type: 'error',
|
||||
},
|
||||
{ inputs: [], name: 'AlreadyInitialized', type: 'error' },
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'sender', type: 'address' }],
|
||||
name: 'AlreadyRegisteredWithDiscount',
|
||||
type: 'error',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'uint256', name: 'duration', type: 'uint256' }],
|
||||
name: 'DurationTooShort',
|
||||
type: 'error',
|
||||
},
|
||||
{ inputs: [], name: 'FailedInnerCall', type: 'error' },
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'key', type: 'bytes32' }],
|
||||
name: 'InactiveDiscount',
|
||||
type: 'error',
|
||||
},
|
||||
{ inputs: [], name: 'InsufficientValue', type: 'error' },
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'key', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'data', type: 'bytes' },
|
||||
],
|
||||
name: 'InvalidDiscount',
|
||||
type: 'error',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'key', type: 'bytes32' }],
|
||||
name: 'InvalidDiscountAmount',
|
||||
type: 'error',
|
||||
},
|
||||
{ inputs: [], name: 'InvalidPaymentReceiver', type: 'error' },
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'bytes32', name: 'key', type: 'bytes32' },
|
||||
{ internalType: 'address', name: 'validator', type: 'address' },
|
||||
],
|
||||
name: 'InvalidValidator',
|
||||
type: 'error',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'string', name: 'name', type: 'string' }],
|
||||
name: 'NameNotAvailable',
|
||||
type: 'error',
|
||||
},
|
||||
{ inputs: [], name: 'NewOwnerIsZeroAddress', type: 'error' },
|
||||
{ inputs: [], name: 'NoHandoverRequest', type: 'error' },
|
||||
{ inputs: [], name: 'ResolverRequiredWhenDataSupplied', type: 'error' },
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'token', type: 'address' }],
|
||||
name: 'SafeERC20FailedOperation',
|
||||
type: 'error',
|
||||
},
|
||||
{ inputs: [], name: 'TransferFailed', type: 'error' },
|
||||
{ inputs: [], name: 'Unauthorized', type: 'error' },
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'registrant', type: 'address' },
|
||||
{ indexed: true, internalType: 'bytes32', name: 'discountKey', type: 'bytes32' },
|
||||
],
|
||||
name: 'DiscountApplied',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'bytes32', name: 'discountKey', type: 'bytes32' },
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'bool', name: 'active', type: 'bool' },
|
||||
{ internalType: 'address', name: 'discountValidator', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'key', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'discount', type: 'uint256' },
|
||||
],
|
||||
indexed: false,
|
||||
internalType: 'struct EARegistrarController.DiscountDetails',
|
||||
name: 'details',
|
||||
type: 'tuple',
|
||||
},
|
||||
],
|
||||
name: 'DiscountUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'payee', type: 'address' },
|
||||
{ indexed: false, internalType: 'uint256', name: 'price', type: 'uint256' },
|
||||
],
|
||||
name: 'ETHPaymentProcessed',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'string', name: 'name', type: 'string' },
|
||||
{ indexed: true, internalType: 'bytes32', name: 'label', type: 'bytes32' },
|
||||
{ indexed: true, internalType: 'address', name: 'owner', type: 'address' },
|
||||
{ indexed: false, internalType: 'uint256', name: 'expires', type: 'uint256' },
|
||||
],
|
||||
name: 'NameRegistered',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverCanceled',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: true, internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'OwnershipHandoverRequested',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: true, internalType: 'address', name: 'oldOwner', type: 'address' },
|
||||
{ indexed: true, internalType: 'address', name: 'newOwner', type: 'address' },
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'address', name: 'newPaymentReceiver', type: 'address' },
|
||||
],
|
||||
name: 'PaymentReceiverUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [{ indexed: false, internalType: 'address', name: 'newPrices', type: 'address' }],
|
||||
name: 'PriceOracleUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{ indexed: false, internalType: 'address', name: 'newReverseRegistrar', type: 'address' },
|
||||
],
|
||||
name: 'ReverseRegistrarUpdated',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'MIN_NAME_LENGTH',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'MIN_REGISTRATION_DURATION',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'string', name: 'name', type: 'string' }],
|
||||
name: 'available',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'cancelOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'completeOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'string', name: 'name', type: 'string' },
|
||||
{ internalType: 'address', name: 'owner', type: 'address' },
|
||||
{ internalType: 'uint256', name: 'duration', type: 'uint256' },
|
||||
{ internalType: 'address', name: 'resolver', type: 'address' },
|
||||
{ internalType: 'bytes[]', name: 'data', type: 'bytes[]' },
|
||||
{ internalType: 'bool', name: 'reverseRecord', type: 'bool' },
|
||||
],
|
||||
internalType: 'struct EARegistrarController.RegisterRequest',
|
||||
name: 'request',
|
||||
type: 'tuple',
|
||||
},
|
||||
{ internalType: 'bytes32', name: 'discountKey', type: 'bytes32' },
|
||||
{ internalType: 'bytes', name: 'validationData', type: 'bytes' },
|
||||
],
|
||||
name: 'discountedRegister',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'string', name: 'name', type: 'string' },
|
||||
{ internalType: 'uint256', name: 'duration', type: 'uint256' },
|
||||
{ internalType: 'bytes32', name: 'discountKey', type: 'bytes32' },
|
||||
],
|
||||
name: 'discountedRegisterPrice',
|
||||
outputs: [{ internalType: 'uint256', name: 'price', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'registrant', type: 'address' }],
|
||||
name: 'discountedRegistrants',
|
||||
outputs: [{ internalType: 'bool', name: 'hasRegisteredWithDiscount', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'bytes32', name: 'key', type: 'bytes32' }],
|
||||
name: 'discounts',
|
||||
outputs: [
|
||||
{ internalType: 'bool', name: 'active', type: 'bool' },
|
||||
{ internalType: 'address', name: 'discountValidator', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'key', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'discount', type: 'uint256' },
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'getActiveDiscounts',
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'bool', name: 'active', type: 'bool' },
|
||||
{ internalType: 'address', name: 'discountValidator', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'key', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'discount', type: 'uint256' },
|
||||
],
|
||||
internalType: 'struct EARegistrarController.DiscountDetails[]',
|
||||
name: '',
|
||||
type: 'tuple[]',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address[]', name: 'addresses', type: 'address[]' }],
|
||||
name: 'hasRegisteredWithDiscount',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [{ internalType: 'address', name: 'result', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'pendingOwner', type: 'address' }],
|
||||
name: 'ownershipHandoverExpiresAt',
|
||||
outputs: [{ internalType: 'uint256', name: 'result', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'paymentReceiver',
|
||||
outputs: [{ internalType: 'address', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'prices',
|
||||
outputs: [{ internalType: 'contract IPriceOracle', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'address', name: '_token', type: 'address' },
|
||||
{ internalType: 'address', name: '_to', type: 'address' },
|
||||
{ internalType: 'uint256', name: '_amount', type: 'uint256' },
|
||||
],
|
||||
name: 'recoverFunds',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'string', name: 'name', type: 'string' },
|
||||
{ internalType: 'uint256', name: 'duration', type: 'uint256' },
|
||||
],
|
||||
name: 'registerPrice',
|
||||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'renounceOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{ internalType: 'string', name: 'name', type: 'string' },
|
||||
{ internalType: 'uint256', name: 'duration', type: 'uint256' },
|
||||
],
|
||||
name: 'rentPrice',
|
||||
outputs: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'uint256', name: 'base', type: 'uint256' },
|
||||
{ internalType: 'uint256', name: 'premium', type: 'uint256' },
|
||||
],
|
||||
internalType: 'struct IPriceOracle.Price',
|
||||
name: 'price',
|
||||
type: 'tuple',
|
||||
},
|
||||
],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'requestOwnershipHandover',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'reverseRegistrar',
|
||||
outputs: [{ internalType: 'contract IReverseRegistrar', name: '', type: 'address' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'rootName',
|
||||
outputs: [{ internalType: 'string', name: '', type: 'string' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [],
|
||||
name: 'rootNode',
|
||||
outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{
|
||||
components: [
|
||||
{ internalType: 'bool', name: 'active', type: 'bool' },
|
||||
{ internalType: 'address', name: 'discountValidator', type: 'address' },
|
||||
{ internalType: 'bytes32', name: 'key', type: 'bytes32' },
|
||||
{ internalType: 'uint256', name: 'discount', type: 'uint256' },
|
||||
],
|
||||
internalType: 'struct EARegistrarController.DiscountDetails',
|
||||
name: 'details',
|
||||
type: 'tuple',
|
||||
},
|
||||
],
|
||||
name: 'setDiscountDetails',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'paymentReceiver_', type: 'address' }],
|
||||
name: 'setPaymentReceiver',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'contract IPriceOracle', name: 'prices_', type: 'address' }],
|
||||
name: 'setPriceOracle',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'contract IReverseRegistrar', name: 'reverse_', type: 'address' }],
|
||||
name: 'setReverseRegistrar',
|
||||
outputs: [],
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
stateMutability: 'payable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
inputs: [{ internalType: 'string', name: 'name', type: 'string' }],
|
||||
name: 'valid',
|
||||
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
|
||||
stateMutability: 'pure',
|
||||
type: 'function',
|
||||
},
|
||||
{ inputs: [], name: 'withdrawETH', outputs: [], stateMutability: 'nonpayable', type: 'function' },
|
||||
] as const;
|
||||
713
apps/web/src/abis/UniswapV2Pair.ts
Normal file
@@ -0,0 +1,713 @@
|
||||
export default [
|
||||
{
|
||||
inputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'constructor',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'owner',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'spender',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'value',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
name: 'Approval',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'sender',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount0',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount1',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'Burn',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'sender',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount0',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount1',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
name: 'Mint',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'sender',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount0In',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount1In',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount0Out',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'amount1Out',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'Swap',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint112',
|
||||
name: 'reserve0',
|
||||
type: 'uint112',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint112',
|
||||
name: 'reserve1',
|
||||
type: 'uint112',
|
||||
},
|
||||
],
|
||||
name: 'Sync',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'from',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
internalType: 'uint256',
|
||||
name: 'value',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
name: 'Transfer',
|
||||
type: 'event',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'DOMAIN_SEPARATOR',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'bytes32',
|
||||
name: '',
|
||||
type: 'bytes32',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'MINIMUM_LIQUIDITY',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'PERMIT_TYPEHASH',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'bytes32',
|
||||
name: '',
|
||||
type: 'bytes32',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'allowance',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'spender',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'value',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
name: 'approve',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'bool',
|
||||
name: '',
|
||||
type: 'bool',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'balanceOf',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'burn',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'amount0',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'amount1',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'decimals',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint8',
|
||||
name: '',
|
||||
type: 'uint8',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'factory',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getReserves',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint112',
|
||||
name: '_reserve0',
|
||||
type: 'uint112',
|
||||
},
|
||||
{
|
||||
internalType: 'uint112',
|
||||
name: '_reserve1',
|
||||
type: 'uint112',
|
||||
},
|
||||
{
|
||||
internalType: 'uint32',
|
||||
name: '_blockTimestampLast',
|
||||
type: 'uint32',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '_token0',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '_token1',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'kLast',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'mint',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'liquidity',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'name',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'string',
|
||||
name: '',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'nonces',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'owner',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'spender',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'value',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'deadline',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
internalType: 'uint8',
|
||||
name: 'v',
|
||||
type: 'uint8',
|
||||
},
|
||||
{
|
||||
internalType: 'bytes32',
|
||||
name: 'r',
|
||||
type: 'bytes32',
|
||||
},
|
||||
{
|
||||
internalType: 'bytes32',
|
||||
name: 's',
|
||||
type: 'bytes32',
|
||||
},
|
||||
],
|
||||
name: 'permit',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'price0CumulativeLast',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'price1CumulativeLast',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
name: 'skim',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'amount0Out',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'amount1Out',
|
||||
type: 'uint256',
|
||||
},
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'bytes',
|
||||
name: 'data',
|
||||
type: 'bytes',
|
||||
},
|
||||
],
|
||||
name: 'swap',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'symbol',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'string',
|
||||
name: '',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [],
|
||||
name: 'sync',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'token0',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'token1',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: '',
|
||||
type: 'address',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'totalSupply',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: '',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'value',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
name: 'transfer',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'bool',
|
||||
name: '',
|
||||
type: 'bool',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'from',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'address',
|
||||
name: 'to',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
internalType: 'uint256',
|
||||
name: 'value',
|
||||
type: 'uint256',
|
||||
},
|
||||
],
|
||||
name: 'transferFrom',
|
||||
outputs: [
|
||||
{
|
||||
internalType: 'bool',
|
||||
name: '',
|
||||
type: 'bool',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function',
|
||||
},
|
||||
] as const;
|
||||
43
apps/web/src/addresses/usernames.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Address } from 'viem';
|
||||
import { base, baseSepolia } from 'viem/chains';
|
||||
|
||||
type AddressMap = Record<number, Address>;
|
||||
|
||||
export const USERNAME_L2_RESOLVER_ADDRESSES: AddressMap = {
|
||||
[baseSepolia.id]: '0x6533C94869D28fAA8dF77cc63f9e2b2D6Cf77eBA',
|
||||
[base.id]: '0x',
|
||||
};
|
||||
|
||||
export const USERNAME_REGISTRAR_CONTROLLER_ADDRESSES: AddressMap = {
|
||||
[baseSepolia.id]: '0x3a0e8c2a0a28f396a5e5b69edb2e630311f1517a',
|
||||
[base.id]: '0x',
|
||||
};
|
||||
|
||||
export const USERNAME_CB_ID_DISCOUNT_VALIDATORS: AddressMap = {
|
||||
[baseSepolia.id]: '0x1079eF978d3c2A6CD4db142118D3C904E0Ac4Fc7',
|
||||
[base.id]: '0x',
|
||||
};
|
||||
|
||||
export const USERNAME_CB1_DISCOUNT_VALIDATORS: AddressMap = {
|
||||
[baseSepolia.id]: '0x502df754f25f492cad45ed85a4de0ee7540717e7',
|
||||
[base.id]: '0x',
|
||||
};
|
||||
|
||||
export const USERNAME_CB_DISCOUNT_VALIDATORS: AddressMap = {
|
||||
[baseSepolia.id]: '0x87B6Bb5d4F43f7bfF78fcFAE7227B2d918828a92',
|
||||
[base.id]: '0x',
|
||||
};
|
||||
|
||||
export const USERNAME_EA_DISCOUNT_VALIDATORS: AddressMap = {
|
||||
[baseSepolia.id]: '0x4944a8Ea7ec6fA356B159a2c363d83076B8f276D',
|
||||
[base.id]: '0x',
|
||||
};
|
||||
|
||||
export const USERNAME_1155_DISCOUNT_VALIDATORS: AddressMap = {
|
||||
[baseSepolia.id]: '0xE41Cd25f429E10744938d5048646E721ac630aF3',
|
||||
[base.id]: '0x',
|
||||
};
|
||||
|
||||
export const UNISWAP_USDC_WETH_POOL: AddressMap = {
|
||||
[base.id]: '0x88A43bbDF9D098eEC7bCEda4e2494615dfD9bB9C',
|
||||
};
|
||||
56
apps/web/src/cdp/api/cb-gpt.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { cdpPost } from 'apps/web/src/cdp/utils';
|
||||
|
||||
type ErrorResponse = {
|
||||
code: number;
|
||||
};
|
||||
|
||||
type QueryCbGptResponse = {
|
||||
response: string;
|
||||
chat_session_id: string;
|
||||
num_tokens_used: number;
|
||||
output_tokens_used: number;
|
||||
};
|
||||
|
||||
type CbGptQuery = {
|
||||
taskConfig: CbGptTaskConfig;
|
||||
query: string;
|
||||
};
|
||||
|
||||
type CbGptTaskConfig = {
|
||||
actionLlm: { chatLlm: string };
|
||||
action_prompt_template: { init_llm_chain: string };
|
||||
};
|
||||
|
||||
export async function queryCbGpt(query: CbGptQuery): Promise<QueryCbGptResponse> {
|
||||
try {
|
||||
const response = await cdpPost(`cb-gpt-api/v1/query`, query, true);
|
||||
|
||||
if (response.ok) {
|
||||
const res = (await response.json()) as QueryCbGptResponse;
|
||||
return res;
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
let errorResponse: ErrorResponse | string;
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
errorResponse = (await response.json()) as ErrorResponse;
|
||||
} else {
|
||||
errorResponse = await response.text();
|
||||
}
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error('Unauthorized access: ensure the calling IP is permitted.');
|
||||
}
|
||||
|
||||
if (response.status === 500 && typeof errorResponse !== 'string' && errorResponse.code === 13) {
|
||||
throw new Error('No user found');
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unexpected error: ${response.statusText}, Response: ${JSON.stringify(errorResponse)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error querying cb-gpt:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
48
apps/web/src/cdp/api/get_linked_addresses.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { cdpGet } from 'apps/web/src/cdp/utils';
|
||||
import { Address, isAddress } from 'viem';
|
||||
|
||||
export type LinkedAddresses = {
|
||||
idemKey: string;
|
||||
linkedAddresses: Address[];
|
||||
};
|
||||
|
||||
type ErrorResponse = {
|
||||
code: number;
|
||||
data: LinkedAddresses;
|
||||
};
|
||||
|
||||
export async function getLinkedAddresses(address: string): Promise<LinkedAddresses> {
|
||||
if (!isAddress(address)) {
|
||||
throw new Error('A valid address is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await cdpGet(`verifications/v1/recipients/${address}/linked-addresses`, true);
|
||||
|
||||
if (response.ok) {
|
||||
return (await response.json()) as LinkedAddresses;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
let errorResponse: ErrorResponse | string;
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
errorResponse = (await response.json()) as ErrorResponse;
|
||||
} else {
|
||||
errorResponse = await response.text();
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error('Unauthorized access: ensure the calling IP is permitted.');
|
||||
}
|
||||
|
||||
if (response.status === 500 && typeof errorResponse !== 'string' && errorResponse.code === 13) {
|
||||
throw new Error('No user found');
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected error: ${response.statusText}, Response: ${errorResponse}`);
|
||||
} catch (error) {
|
||||
console.error('Error fetching linked addresses:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
1
apps/web/src/cdp/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './get_linked_addresses';
|
||||
6
apps/web/src/cdp/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const cdpKeySecret = process.env.CDP_KEY_SECRET ?? '';
|
||||
export const cdpKeyName = process.env.CDP_KEY_NAME ?? '';
|
||||
export const cdpBaseRpcEndpoint = process.env.CDP_BASE_RPC_ENDPOINT ?? 'https://mainnet.base.org';
|
||||
export const cdpBaseSepoliaRpcEndpoint =
|
||||
process.env.CDP_BASE_SEPOLIA_RPC_ENDPOINT ?? 'https://sepolia.base.org';
|
||||
export const cdpBaseUri = process.env.CDP_BASE_URI ?? 'api.coinbase.com';
|
||||
36
apps/web/src/cdp/jwt.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { SignJWT } from 'jose';
|
||||
import crypto from 'crypto';
|
||||
import { cdpBaseUri, cdpKeyName, cdpKeySecret } from 'apps/web/src/cdp/constants';
|
||||
|
||||
const algorithm = 'ES256';
|
||||
|
||||
type APIKeyClaims = {
|
||||
iss: string;
|
||||
sub: string;
|
||||
nbf: number;
|
||||
exp: number;
|
||||
uri: string;
|
||||
aud: string[];
|
||||
};
|
||||
|
||||
export async function generateCdpJwt(requestMethod: string, requestPath: string): Promise<string> {
|
||||
const uri = `${requestMethod} ${cdpBaseUri}/${requestPath}`;
|
||||
const nonce = crypto.randomBytes(16).toString('hex');
|
||||
const claims: APIKeyClaims = {
|
||||
iss: 'cdp',
|
||||
sub: cdpKeyName,
|
||||
nbf: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 60,
|
||||
uri: uri,
|
||||
aud: ['cb-gpt-api'],
|
||||
};
|
||||
const key = crypto.createPrivateKey(cdpKeySecret.replace(/\\n/g, '\n'));
|
||||
|
||||
const jwt = await new SignJWT(claims)
|
||||
.setProtectedHeader({ alg: algorithm, kid: cdpKeyName, nonce })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('60s')
|
||||
.sign(key);
|
||||
|
||||
return jwt;
|
||||
}
|
||||
31
apps/web/src/cdp/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cdpBaseUri } from 'apps/web/src/cdp/constants';
|
||||
import { generateCdpJwt } from 'apps/web/src/cdp/jwt';
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
export async function cdpGet(endpoint: string, authed: boolean): Promise<Response> {
|
||||
const headers = new Headers();
|
||||
|
||||
const uri = `https://${cdpBaseUri}/${endpoint}`;
|
||||
if (authed) {
|
||||
const jwt = await generateCdpJwt('GET', endpoint);
|
||||
headers.set('Authorization', `Bearer ${jwt}`);
|
||||
}
|
||||
return fetch(uri, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
export async function cdpPost(endpoint: string, body: unknown, authed: boolean): Promise<Response> {
|
||||
const uri = `https://${cdpBaseUri}/${endpoint}`;
|
||||
const headers = new Headers();
|
||||
if (authed) {
|
||||
const jwt = await generateCdpJwt('POST', endpoint);
|
||||
headers.set('Authorization', `Bearer ${jwt}`);
|
||||
}
|
||||
return fetch(uri, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers,
|
||||
});
|
||||
}
|
||||
200
apps/web/src/components/Basenames/FloatingENSPills.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
registrationTransitionDuration,
|
||||
useRegistration,
|
||||
} from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import classNames from 'classnames';
|
||||
import Image from 'next/image';
|
||||
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const useMousePosition = () => {
|
||||
const [position, setPosition] = useState({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
|
||||
useEffect(() => {
|
||||
let animationFrameId: number;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
const updatePosition = () => {
|
||||
setPosition({ x: event.clientX, y: event.clientY });
|
||||
animationFrameId = requestAnimationFrame(updatePosition);
|
||||
};
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
animationFrameId = requestAnimationFrame(updatePosition);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return position;
|
||||
};
|
||||
|
||||
const NAMES = [
|
||||
{ name: 'ianlakes', avatar: '/images/avatars/ianlakes.eth.png' },
|
||||
{ name: 'wilsoncusack', avatar: '/images/avatars/wilsoncusack.eth.png' },
|
||||
{ name: 'aflock', avatar: '/images/avatars/aflock.eth.png' },
|
||||
{ name: 'johnpalmer', avatar: '/images/avatars/johnpalmer.eth.svg' },
|
||||
{ name: 'jfrankfurt', avatar: '/images/avatars/jfrankfurt.eth.jpeg' },
|
||||
{ name: 'lsr', avatar: '/images/avatars/lsr.eth.png' },
|
||||
{ name: 'dcj', avatar: '/images/avatars/dcj.eth.avif' },
|
||||
{ name: 'zencephalon', avatar: '/images/avatars/zencephalon.eth.webp' },
|
||||
];
|
||||
const PILL_COUNT = NAMES.length;
|
||||
const initialBlurStates = Array.from({ length: PILL_COUNT }).map((_, index) => index % 2 === 0);
|
||||
const intervals = [2000, 4000, 6000];
|
||||
const useBlurCycle = () => {
|
||||
const [blurredIndices, setBlurredIndices] = useState(initialBlurStates);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutIds: ReturnType<typeof setTimeout>[] = [];
|
||||
|
||||
const toggleBlur = (index: number) => {
|
||||
setBlurredIndices((prev) => prev.map((blurred, i) => (i === index ? !blurred : blurred)));
|
||||
|
||||
const randomInterval = intervals[Math.floor(Math.random() * intervals.length)];
|
||||
timeoutIds[index] = setTimeout(() => toggleBlur(index), randomInterval);
|
||||
};
|
||||
|
||||
for (let i = 0; i < PILL_COUNT; i++) {
|
||||
const randomInterval = intervals[Math.floor(Math.random() * intervals.length)];
|
||||
timeoutIds[i] = setTimeout(() => toggleBlur(i), randomInterval);
|
||||
}
|
||||
|
||||
return () => {
|
||||
timeoutIds.forEach(clearTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return blurredIndices;
|
||||
};
|
||||
|
||||
type PillProps = {
|
||||
name: string;
|
||||
avatar: string;
|
||||
x: number;
|
||||
y: number;
|
||||
isBlurred: boolean;
|
||||
transform: string;
|
||||
};
|
||||
const Pill = forwardRef(
|
||||
({ avatar, name, x, y, isBlurred, transform }: PillProps, ref: React.Ref<HTMLDivElement>) => {
|
||||
const { searchInputFocused, searchInputHovered } = useRegistration();
|
||||
|
||||
const pillClasses = classNames(
|
||||
'absolute flex items-center justify-center rounded-full border opacity-60',
|
||||
'transition-all duration-500',
|
||||
'border-[#d9dce2] text-[#666]',
|
||||
'px-2 py-2 gap-2 text-sm',
|
||||
'sm:px-3 sm:py-2 sm:gap-2 sm:text-sm',
|
||||
'md:px-4 md:py-3 md:gap-3 md:text-base',
|
||||
{
|
||||
'blur-sm': isBlurred,
|
||||
'bg-blue-600/10 border-blue-600/20 text-blue-600':
|
||||
!searchInputFocused && searchInputHovered,
|
||||
'bg-white/20 border-white/80 text-white': searchInputFocused,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={pillClasses} style={{ top: `${y}px`, left: `${x}px`, transform }}>
|
||||
<Image
|
||||
src={avatar}
|
||||
className="flex-shrink-0 rounded-full"
|
||||
alt={`${name}-avatar`}
|
||||
quality={1}
|
||||
priority
|
||||
width={34}
|
||||
height={34}
|
||||
/>
|
||||
<p className="max-w-[calc(100%-20px)] truncate">{name}.base.eth</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Pill.displayName = 'Pill';
|
||||
|
||||
const X_VECTOR_SCALER = 0.4;
|
||||
const Y_VECTOR_SCALER = 0.32;
|
||||
export function FloatingENSPills() {
|
||||
const [radiusX, setRadiusX] = useState(window.innerWidth * X_VECTOR_SCALER);
|
||||
const [radiusY, setRadiusY] = useState(window.innerHeight * Y_VECTOR_SCALER);
|
||||
const pillRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setRadiusX(window.innerWidth * X_VECTOR_SCALER);
|
||||
setRadiusY(window.innerHeight * Y_VECTOR_SCALER);
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const centerX = window.innerWidth / 2;
|
||||
const centerY = window.innerHeight / 2;
|
||||
const angleStep = (2 * Math.PI) / PILL_COUNT;
|
||||
const mousePosition = useMousePosition();
|
||||
|
||||
const pills = Array.from({ length: PILL_COUNT }).map((_, index) => {
|
||||
const pill = pillRefs.current[index];
|
||||
const pillWidth = pill ? pill.offsetWidth : 170;
|
||||
const pillHeight = pill ? pill.offsetHeight : 170;
|
||||
|
||||
const angle = index * angleStep;
|
||||
const x = centerX + radiusX * Math.cos(angle) - pillWidth / 2;
|
||||
const y = centerY + radiusY * Math.sin(angle) - pillHeight / 2;
|
||||
|
||||
const dx = x + pillWidth / 2 - mousePosition.x;
|
||||
const dy = y + pillHeight / 2 - mousePosition.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Normalize the direction vector
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
const nx = dx / length;
|
||||
const ny = dy / length;
|
||||
|
||||
const rotationAngle = Math.min(distance / 20, 20);
|
||||
const transform = `rotate3d(${-ny}, ${-nx}, 0, ${rotationAngle}deg)`;
|
||||
const { name, avatar } = NAMES[index];
|
||||
return { name, avatar, x, y, transform };
|
||||
});
|
||||
|
||||
const blurredIndices = useBlurCycle();
|
||||
const setRef = useCallback((index: number, el: HTMLDivElement | null) => {
|
||||
if (!el) return;
|
||||
pillRefs.current[index] = el;
|
||||
}, []);
|
||||
|
||||
const { searchInputFocused } = useRegistration();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'pointer-events-none absolute inset-0 -z-10 overflow-hidden',
|
||||
'transition-all',
|
||||
registrationTransitionDuration,
|
||||
{
|
||||
'bg-blue-600': searchInputFocused,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{pills.map(({ avatar, name, x, y, transform }, i) => (
|
||||
<Pill
|
||||
key={name}
|
||||
avatar={avatar}
|
||||
name={name}
|
||||
isBlurred={blurredIndices[i]}
|
||||
x={x}
|
||||
y={y}
|
||||
transform={transform}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
||||
ref={(el) => setRef(i, el)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { FloatingENSPills } from 'apps/web/src/components/Basenames/FloatingENSPills';
|
||||
import {
|
||||
RegistrationSteps,
|
||||
registrationTransitionDuration,
|
||||
useRegistration,
|
||||
} from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import tempPendingAnimation from 'apps/web/src/components/Basenames/tempPendingAnimation.png';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function RegistrationBackground() {
|
||||
const { registrationStep } = useRegistration();
|
||||
|
||||
const isSearch = registrationStep === RegistrationSteps.Search;
|
||||
const isClaim = registrationStep === RegistrationSteps.Claim;
|
||||
const isPending = registrationStep === RegistrationSteps.Pending;
|
||||
const isSuccess = registrationStep === RegistrationSteps.Success;
|
||||
|
||||
const claimBackgroundClasses = classNames(
|
||||
'pointer-events-none absolute inset-0 w-full h-full bg-cover bg-center -z-10',
|
||||
);
|
||||
|
||||
const successBackgroundClasses = classNames(
|
||||
'pointer-events-none absolute inset-0 w-full h-full bg-cover bg-center -z-10 bg-blue-600',
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
appear
|
||||
show={isSearch}
|
||||
className={classNames('transition-opacity', registrationTransitionDuration)}
|
||||
enterFrom={classNames('opacity-0')}
|
||||
enterTo={classNames('opacity-100')}
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<FloatingENSPills />
|
||||
</Transition>
|
||||
<Transition
|
||||
appear
|
||||
show={isClaim || isPending}
|
||||
className={classNames('transition-opacity', registrationTransitionDuration)}
|
||||
enterFrom={classNames('opacity-0')}
|
||||
enterTo={classNames('opacity-100')}
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{/* TODO: Lottie animation file */}
|
||||
<div
|
||||
className={claimBackgroundClasses}
|
||||
style={{ backgroundImage: `url(${tempPendingAnimation.src})` }}
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
appear
|
||||
show={isSuccess}
|
||||
className={classNames('transition-opacity', registrationTransitionDuration)}
|
||||
enterFrom={classNames('opacity-0')}
|
||||
enterTo={classNames('opacity-100')}
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{/* TODO: Lottie animation file */}
|
||||
<div className={successBackgroundClasses} />
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { useRegistration } from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
|
||||
const SEARCH_LABEL_COPY_STRINGS = ['Build your Based profile', 'Connect with Based builders'];
|
||||
|
||||
const useRotatingText = (strings: string[]) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
useInterval(() => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % strings.length);
|
||||
}, 3000);
|
||||
return strings[currentIndex];
|
||||
};
|
||||
|
||||
export default function RegistrationBrand() {
|
||||
const rotatingText = useRotatingText(SEARCH_LABEL_COPY_STRINGS);
|
||||
const { searchInputFocused } = useRegistration();
|
||||
return (
|
||||
<div className="relative flex w-full flex-row">
|
||||
<div className="flex items-center items-center gap-1">
|
||||
<span
|
||||
className={classNames('pt-[1px]', {
|
||||
'text-blue-600': !searchInputFocused,
|
||||
'text-white': searchInputFocused,
|
||||
})}
|
||||
>
|
||||
<Icon name="blueCircle" color="currentColor" width={15} height={15} />
|
||||
</span>
|
||||
<h1 className="text-md font-bold md:text-xl">Basenames</h1>
|
||||
</div>
|
||||
{SEARCH_LABEL_COPY_STRINGS.map((string) => (
|
||||
<Transition
|
||||
key={string}
|
||||
show={rotatingText === string}
|
||||
className="transition-opacity"
|
||||
enter="delay-500"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<p className="text-md absolute right-0 md:text-xl ">{string}</p>
|
||||
</Transition>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
228
apps/web/src/components/Basenames/RegistrationContext.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import {
|
||||
DiscountData,
|
||||
findFirstValidDiscount,
|
||||
useAggregatedDiscountValidators,
|
||||
} from 'apps/web/src/hooks/useAggregatedDiscountValidators';
|
||||
import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { Discount, isValidDiscount } from 'apps/web/src/utils/usernames';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import {
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Address, TransactionReceipt } from 'viem';
|
||||
import { useAccount, useWaitForTransactionReceipt } from 'wagmi';
|
||||
|
||||
export enum RegistrationSteps {
|
||||
Search = 'search',
|
||||
Claim = 'claim',
|
||||
Pending = 'pending',
|
||||
Success = 'success',
|
||||
Profile = 'profile',
|
||||
}
|
||||
|
||||
export type RegistrationContextProps = {
|
||||
searchInputFocused: boolean;
|
||||
setSearchInputFocused: Dispatch<SetStateAction<boolean>>;
|
||||
searchInputHovered: boolean;
|
||||
setSearchInputHovered: Dispatch<SetStateAction<boolean>>;
|
||||
registrationStep: RegistrationSteps;
|
||||
setRegistrationStep: Dispatch<SetStateAction<RegistrationSteps>>;
|
||||
selectedName: string;
|
||||
setSelectedName: Dispatch<SetStateAction<string>>;
|
||||
registerNameTransactionHash: `0x${string}` | undefined;
|
||||
setRegisterNameTransactionHash: Dispatch<SetStateAction<`0x${string}` | undefined>>;
|
||||
loadingDiscounts: boolean;
|
||||
discount: DiscountData | undefined;
|
||||
allActiveDiscounts: Set<Discount>;
|
||||
transactionData: TransactionReceipt | undefined;
|
||||
transactionError: unknown | null;
|
||||
};
|
||||
|
||||
export const RegistrationContext = createContext<RegistrationContextProps>({
|
||||
searchInputFocused: false,
|
||||
searchInputHovered: false,
|
||||
registrationStep: RegistrationSteps.Search,
|
||||
selectedName: '',
|
||||
setSearchInputFocused: function () {
|
||||
return undefined;
|
||||
},
|
||||
setSearchInputHovered: function () {
|
||||
return undefined;
|
||||
},
|
||||
setRegistrationStep: function () {
|
||||
return undefined;
|
||||
},
|
||||
setSelectedName: function () {
|
||||
return undefined;
|
||||
},
|
||||
registerNameTransactionHash: '0x',
|
||||
setRegisterNameTransactionHash: function () {
|
||||
return undefined;
|
||||
},
|
||||
loadingDiscounts: true,
|
||||
discount: undefined,
|
||||
allActiveDiscounts: new Set(),
|
||||
transactionData: undefined,
|
||||
transactionError: null,
|
||||
});
|
||||
|
||||
type RegistrationProviderProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
// Maybe not the best place for this
|
||||
export const registrationTransitionDuration = 'duration-700';
|
||||
|
||||
export default function RegistrationProvider({ children }: RegistrationProviderProps) {
|
||||
// UI state
|
||||
const [searchInputFocused, setSearchInputFocused] = useState<boolean>(false);
|
||||
const [searchInputHovered, setSearchInputHovered] = useState<boolean>(false);
|
||||
const [selectedName, setSelectedName] = useState<string>('');
|
||||
const [registrationStep, setRegistrationStep] = useState<RegistrationSteps>(
|
||||
RegistrationSteps.Search,
|
||||
);
|
||||
|
||||
const { basenameChain } = useBasenameChain();
|
||||
|
||||
// Analytics
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
|
||||
// Web3 data
|
||||
const { address } = useAccount();
|
||||
const { refetch: baseEnsNameRefetch } = useBaseEnsName({
|
||||
address,
|
||||
});
|
||||
|
||||
// Username discount states
|
||||
const { data: discounts, loading: loadingDiscounts } = useAggregatedDiscountValidators();
|
||||
const discount = findFirstValidDiscount(discounts);
|
||||
|
||||
const allActiveDiscounts = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
Object.keys(discounts)
|
||||
.filter(isValidDiscount)
|
||||
.map((key) => Discount[key]),
|
||||
),
|
||||
[discounts],
|
||||
);
|
||||
|
||||
// TODO: Not a big fan of this, I think ideally we'd have useRegisterNameCallback here
|
||||
const [registerNameTransactionHash, setRegisterNameTransactionHash] = useState<
|
||||
Address | undefined
|
||||
>();
|
||||
|
||||
// Wait for text record transaction to be processed
|
||||
const {
|
||||
data: transactionData,
|
||||
isFetching: transactionIsFetching,
|
||||
isSuccess: transactionIsSuccess,
|
||||
error: transactionError,
|
||||
} = useWaitForTransactionReceipt({
|
||||
hash: registerNameTransactionHash,
|
||||
chainId: basenameChain.id,
|
||||
query: {
|
||||
enabled: !!registerNameTransactionHash,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (transactionIsFetching) {
|
||||
logEventWithContext('register_name_transaction_processing', ActionType.change);
|
||||
|
||||
setRegistrationStep(RegistrationSteps.Pending);
|
||||
}
|
||||
|
||||
if (transactionIsSuccess) {
|
||||
if (transactionData.status === 'success') {
|
||||
logEventWithContext('register_name_transaction_success', ActionType.change);
|
||||
// Reload current ENS name
|
||||
baseEnsNameRefetch()
|
||||
.then(() => {
|
||||
setRegistrationStep(RegistrationSteps.Success);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (transactionData.status === 'reverted') {
|
||||
logEventWithContext('register_name_transaction_reverted', ActionType.change, {
|
||||
error: `Transaction reverted: ${transactionData.transactionHash}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
baseEnsNameRefetch,
|
||||
logEventWithContext,
|
||||
setRegistrationStep,
|
||||
transactionData,
|
||||
transactionIsFetching,
|
||||
transactionIsSuccess,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedName.length) {
|
||||
setRegistrationStep(RegistrationSteps.Claim);
|
||||
}
|
||||
}, [selectedName.length]);
|
||||
|
||||
// Log user moving through the flow
|
||||
useEffect(() => {
|
||||
logEventWithContext(`step_${registrationStep}`, ActionType.change);
|
||||
}, [logEventWithContext, registrationStep]);
|
||||
|
||||
// Log user selecting a name
|
||||
useEffect(() => {
|
||||
if (!selectedName) return;
|
||||
logEventWithContext('selected_name', ActionType.change);
|
||||
}, [logEventWithContext, selectedName]);
|
||||
|
||||
const values = useMemo(() => {
|
||||
return {
|
||||
searchInputFocused,
|
||||
searchInputHovered,
|
||||
setSearchInputFocused,
|
||||
setSearchInputHovered,
|
||||
selectedName,
|
||||
setSelectedName,
|
||||
registrationStep,
|
||||
setRegistrationStep,
|
||||
registerNameTransactionHash,
|
||||
setRegisterNameTransactionHash,
|
||||
loadingDiscounts,
|
||||
discount,
|
||||
allActiveDiscounts,
|
||||
transactionData,
|
||||
transactionError,
|
||||
};
|
||||
}, [
|
||||
allActiveDiscounts,
|
||||
discount,
|
||||
loadingDiscounts,
|
||||
registerNameTransactionHash,
|
||||
registrationStep,
|
||||
searchInputFocused,
|
||||
searchInputHovered,
|
||||
selectedName,
|
||||
transactionData,
|
||||
transactionError,
|
||||
]);
|
||||
|
||||
return <RegistrationContext.Provider value={values}>{children}</RegistrationContext.Provider>;
|
||||
}
|
||||
|
||||
export function useRegistration() {
|
||||
const context = useContext(RegistrationContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useCount must be used within a CountProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
275
apps/web/src/components/Basenames/RegistrationFlow.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import RegistrationBackground from 'apps/web/src/components/Basenames/RegistrationBackground';
|
||||
import RegistrationBrand from 'apps/web/src/components/Basenames/RegistrationBrand';
|
||||
import {
|
||||
RegistrationSteps,
|
||||
registrationTransitionDuration,
|
||||
useRegistration,
|
||||
} from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import RegistrationForm from 'apps/web/src/components/Basenames/RegistrationForm';
|
||||
import RegistrationProfileForm from 'apps/web/src/components/Basenames/RegistrationProfileForm';
|
||||
import RegistrationSearchInput, {
|
||||
RegistrationSearchInputVariant,
|
||||
} from 'apps/web/src/components/Basenames/RegistrationSearchInput';
|
||||
import RegistrationSuccessMessage from 'apps/web/src/components/Basenames/RegistrationSuccessMessage';
|
||||
import { UsernamePill, UsernamePillVariants } from 'apps/web/src/components/Basenames/UsernamePill';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { formatBaseEthDomain, USERNAME_DOMAINS } from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/16/solid';
|
||||
import { InformationCircleIcon } from '@heroicons/react/16/solid';
|
||||
import { useAccount } from 'wagmi';
|
||||
|
||||
/*
|
||||
test addresses w/ different verifications
|
||||
0xB18e4C959bccc8EF86D78DC297fb5efA99550d85 - cb.id
|
||||
0xB18e4C959bccc8EF86D78DC297fb5efA99550d85, 0xB6944B3074F40959E1166fe010a3F86B02cF2b7c- verified account
|
||||
0x9C02E8E28D8b706F67dcf0FC7F46A9ee1f9649FA - cb1
|
||||
*/
|
||||
|
||||
export const claimQueryKey = 'claim';
|
||||
|
||||
export function RegistrationFlow() {
|
||||
const { isConnected } = useAccount();
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const searchParams = useSearchParams();
|
||||
const { discount, registrationStep, searchInputFocused, selectedName, setSelectedName } =
|
||||
useRegistration();
|
||||
const { basenameChain } = useBasenameChain();
|
||||
const isEarlyAccess = process.env.NEXT_PUBLIC_USERNAMES_EARLY_ACCESS == 'true';
|
||||
const isSearch = registrationStep === RegistrationSteps.Search;
|
||||
const isClaim = registrationStep === RegistrationSteps.Claim;
|
||||
const isPending = registrationStep === RegistrationSteps.Pending;
|
||||
const isSuccess = registrationStep === RegistrationSteps.Success;
|
||||
const isProfile = registrationStep === RegistrationSteps.Profile;
|
||||
|
||||
const layoutPadding = 'px-4 md:px-8';
|
||||
const absoluteLayoutPosition = 'top-[40vh] md:top-[50vh]';
|
||||
|
||||
const mainClasses = classNames(
|
||||
'w-full relative min-h-screen pb-40',
|
||||
'transition-[padding]',
|
||||
layoutPadding,
|
||||
registrationTransitionDuration,
|
||||
{
|
||||
'pt-[calc(40vh-24px)] md:pt-[calc(50vh-24px)]': isSearch || isClaim || isPending || isSuccess,
|
||||
'delay-500': isSuccess || isProfile,
|
||||
'pt-32 md:pt-40': isProfile,
|
||||
},
|
||||
);
|
||||
|
||||
const currentUsernamePillVariant = isProfile
|
||||
? UsernamePillVariants.Card
|
||||
: UsernamePillVariants.Inline;
|
||||
|
||||
useEffect(() => {
|
||||
logEventWithContext('initial_render', ActionType.render);
|
||||
}, [logEventWithContext]);
|
||||
|
||||
useEffect(() => {
|
||||
const claimQuery = searchParams?.get(claimQueryKey);
|
||||
if (claimQuery) {
|
||||
setSelectedName(claimQuery.replace(`.${USERNAME_DOMAINS[basenameChain.id]}`, ''));
|
||||
}
|
||||
}, [basenameChain.id, searchParams, setSelectedName]);
|
||||
|
||||
return (
|
||||
<main className={mainClasses}>
|
||||
{/* 1. Brand & Search */}
|
||||
<Transition
|
||||
appear
|
||||
show={isSearch}
|
||||
className={classNames(
|
||||
'absolute left-1/2 z-20 mx-auto w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 transform transition-opacity',
|
||||
registrationTransitionDuration,
|
||||
absoluteLayoutPosition,
|
||||
{
|
||||
'text-white': searchInputFocused,
|
||||
'text-blue-600': searchInputFocused,
|
||||
},
|
||||
)}
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className={classNames('relative bottom-full w-full pb-4', layoutPadding)}>
|
||||
<RegistrationBrand />
|
||||
</div>
|
||||
<Transition
|
||||
appear
|
||||
show={isSearch}
|
||||
className={classNames(
|
||||
'mx-auto transition-[max-width] ',
|
||||
registrationTransitionDuration,
|
||||
layoutPadding,
|
||||
)}
|
||||
leaveFrom="max-w-full"
|
||||
leaveTo="max-w-0"
|
||||
>
|
||||
<RegistrationSearchInput
|
||||
variant={RegistrationSearchInputVariant.Large}
|
||||
placeholder="Search for a name"
|
||||
/>
|
||||
</Transition>
|
||||
</Transition>
|
||||
{/* 2 - Username Pill */}
|
||||
<div className="relative flex w-full max-w-full max-w-full flex-col items-center justify-center ">
|
||||
<Transition
|
||||
appear
|
||||
show={!isSearch}
|
||||
className={classNames(
|
||||
'relative z-40 transition-opacity',
|
||||
registrationTransitionDuration,
|
||||
{
|
||||
'w-full max-w-[26rem]': isProfile,
|
||||
'max-w-full': !isProfile,
|
||||
},
|
||||
)}
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{/* 2.1 - Small search input - positioned based on username pill, only for claim */}
|
||||
<Transition
|
||||
appear
|
||||
show={isClaim}
|
||||
className={classNames(
|
||||
'absolute left-1/2 z-40 mx-auto w-full max-w-[14rem] -translate-x-1/2 -translate-y-20 transition-opacity',
|
||||
registrationTransitionDuration,
|
||||
)}
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<RegistrationSearchInput
|
||||
variant={RegistrationSearchInputVariant.Small}
|
||||
placeholder="Find another name"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
{/* 2.2 - The pill */}
|
||||
<Transition
|
||||
appear
|
||||
show={!isSearch}
|
||||
className={classNames(
|
||||
'transition-[max-width, transform] mx-auto',
|
||||
registrationTransitionDuration,
|
||||
{
|
||||
'scale-90 animate-pulse': isPending,
|
||||
},
|
||||
)}
|
||||
enterFrom="max-w-0"
|
||||
enterTo="max-w-full"
|
||||
>
|
||||
<UsernamePill
|
||||
variant={currentUsernamePillVariant}
|
||||
username={formatBaseEthDomain(selectedName, basenameChain.id)}
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
{/* 2.2 - Pending registration - positioned based on username pill, only visible when registration is pending*/}
|
||||
<Transition
|
||||
appear
|
||||
show={isPending}
|
||||
className={classNames(
|
||||
'absolute left-1/2 top-full mt-6 -translate-x-1/2 transform animate-pulse text-center transition-opacity ',
|
||||
registrationTransitionDuration,
|
||||
)}
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{isPending && (
|
||||
<p className=" text-line text-center font-bold uppercase text-gray-60">
|
||||
Registering...
|
||||
</p>
|
||||
)}
|
||||
</Transition>
|
||||
</Transition>
|
||||
|
||||
{/* 3. Registration Form */}
|
||||
<Transition
|
||||
appear
|
||||
show={isClaim}
|
||||
className={classNames(
|
||||
'relative z-40 transition-opacity',
|
||||
'mx-auto w-full max-w-[50rem]',
|
||||
registrationTransitionDuration,
|
||||
)}
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{!isEarlyAccess || (isEarlyAccess && discount) ? (
|
||||
<div className="mt-20">
|
||||
<RegistrationForm />
|
||||
</div>
|
||||
) : isConnected ? (
|
||||
<div className="z-10 mt-8 flex flex-row items-center justify-center ">
|
||||
<ExclamationCircleIcon width={12} height={12} className="fill-state-n-hovered" />
|
||||
<p className="ml-2 text-state-n-hovered">
|
||||
The connected wallet is not eligible for early access
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="z-10 mt-8 flex flex-row items-center justify-center ">
|
||||
<InformationCircleIcon width={12} height={12} className="fill-gray-40" />
|
||||
<p className="ml-2 text-gray-40">Connect a wallet to register a name</p>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
{/* 4. Registration Success Message */}
|
||||
<Transition
|
||||
appear
|
||||
show={isSuccess}
|
||||
className={classNames(
|
||||
'top-full z-40 pt-20 transition-opacity',
|
||||
'mx-auto w-full max-w-[50rem]',
|
||||
registrationTransitionDuration,
|
||||
)}
|
||||
enter={classNames('transition-opacity', registrationTransitionDuration)}
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave={classNames('transition-opacity', 'duration-200 absolute')}
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<RegistrationSuccessMessage />
|
||||
</Transition>
|
||||
</div>
|
||||
{/* 5. Registration: Edit Profile flow */}
|
||||
<Transition
|
||||
appear
|
||||
show={isProfile}
|
||||
className={classNames(
|
||||
'relative z-50 mx-auto mt-8 transition-opacity',
|
||||
'w-full max-w-[26rem]',
|
||||
registrationTransitionDuration,
|
||||
)}
|
||||
enter="delay-1000"
|
||||
enterFrom={classNames('opacity-0')}
|
||||
enterTo={classNames('opacity-100')}
|
||||
leave="transition-all "
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0 "
|
||||
>
|
||||
<RegistrationProfileForm />
|
||||
</Transition>
|
||||
|
||||
{/* Misc: Animated background for each steps */}
|
||||
<RegistrationBackground />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default RegistrationFlow;
|
||||
244
apps/web/src/components/Basenames/RegistrationForm/index.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { MinusIcon, PlusIcon } from '@heroicons/react/16/solid';
|
||||
import { ConnectButton, useConnectModal } from '@rainbow-me/rainbowkit';
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import { useRegistration } from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import RegistrationLearnMoreModal from 'apps/web/src/components/Basenames/RegistrationLearnMoreModal';
|
||||
import { Button, ButtonSizes, ButtonVariants } from 'apps/web/src/components/Button/Button';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import TransactionError from 'apps/web/src/components/TransactionError';
|
||||
import TransactionStatus from 'apps/web/src/components/TransactionStatus';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { useEthPriceFromUniswap } from 'apps/web/src/hooks/useEthPriceFromUniswap';
|
||||
import {
|
||||
useDiscountedNameRegistrationPrice,
|
||||
useNameRegistrationPrice,
|
||||
} from 'apps/web/src/hooks/useNameRegistrationPrice';
|
||||
import { useRegisterNameCallback } from 'apps/web/src/hooks/useRegisterNameCallback';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { formatEther } from 'viem';
|
||||
|
||||
function formatEtherPrice(price?: bigint) {
|
||||
if (price === undefined) {
|
||||
return '...';
|
||||
}
|
||||
const value = parseFloat(formatEther(price));
|
||||
if (value < 0.001) {
|
||||
return parseFloat(value.toFixed(4));
|
||||
} else {
|
||||
return parseFloat(value.toFixed(3));
|
||||
}
|
||||
}
|
||||
|
||||
function formatUsdPrice(price: bigint, ethUsdPrice: number) {
|
||||
if (price === 0n) return '0';
|
||||
const parsed = (parseFloat(formatEther(price)) * Number(ethUsdPrice)).toFixed(2);
|
||||
if (parsed === '0.00') return '0';
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export default function RegistrationForm() {
|
||||
const { openConnectModal } = useConnectModal();
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const { basenameChain } = useBasenameChain();
|
||||
|
||||
const {
|
||||
transactionData,
|
||||
transactionError,
|
||||
selectedName,
|
||||
setRegisterNameTransactionHash,
|
||||
discount,
|
||||
loadingDiscounts,
|
||||
} = useRegistration();
|
||||
const [years, setYears] = useState(1);
|
||||
|
||||
const [learnMoreModalOpen, setLearnMoreModalOpen] = useState(false);
|
||||
|
||||
const toggleLearnMoreModal = useCallback(() => {
|
||||
logEventWithContext('open_learn_more_modal', ActionType.change);
|
||||
setLearnMoreModalOpen((open) => !open);
|
||||
}, [logEventWithContext]);
|
||||
|
||||
const increment = useCallback(() => {
|
||||
logEventWithContext('registration_form_increment_year', ActionType.click);
|
||||
|
||||
setYears((n) => n + 1);
|
||||
}, [logEventWithContext]);
|
||||
|
||||
const decrement = useCallback(() => {
|
||||
logEventWithContext('registration_form_decement_year', ActionType.click);
|
||||
|
||||
setYears((n) => (n > 1 ? n - 1 : n));
|
||||
}, [logEventWithContext]);
|
||||
|
||||
const ethUsdPrice = useEthPriceFromUniswap();
|
||||
const { data: initialPrice } = useNameRegistrationPrice(selectedName, years);
|
||||
const { data: discountedPrice } = useDiscountedNameRegistrationPrice(
|
||||
selectedName,
|
||||
years,
|
||||
discount?.discountKey,
|
||||
);
|
||||
|
||||
const price = discountedPrice ?? initialPrice;
|
||||
|
||||
const {
|
||||
callback: registerName,
|
||||
data: registerNameTransactionHash,
|
||||
error,
|
||||
isPending: registerNameTransactionIsPending,
|
||||
error: registerNameError,
|
||||
} = useRegisterNameCallback(
|
||||
selectedName,
|
||||
price,
|
||||
years,
|
||||
discount?.discountKey,
|
||||
discount?.validationData,
|
||||
);
|
||||
|
||||
if (error) {
|
||||
// todo: handle this error in the UI
|
||||
// it is likely the user doesn't have sufficient funds to register and they've failed simulation
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (registerNameTransactionHash) {
|
||||
logEventWithContext('register_name_transaction_approved', ActionType.change);
|
||||
|
||||
setRegisterNameTransactionHash(registerNameTransactionHash);
|
||||
}
|
||||
}, [logEventWithContext, registerNameTransactionHash, setRegisterNameTransactionHash]);
|
||||
|
||||
const registerNameCallback = useCallback(() => {
|
||||
registerName()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}, [registerName]);
|
||||
|
||||
const usdPrice =
|
||||
price !== undefined && ethUsdPrice !== undefined ? formatUsdPrice(price, ethUsdPrice) : '--.--';
|
||||
const nameIsFree = price === 0n;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="transition-all duration-500">
|
||||
<div className="z-10 flex flex-col justify-between gap-4 rounded-2xl bg-[#F7F7F7] p-8 text-gray-60 shadow-xl md:flex-row">
|
||||
<div>
|
||||
<p className="text-line mb-2 text-sm font-bold uppercase">Claim for</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={decrement}
|
||||
disabled={years === 1}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-[#DEE1E7]"
|
||||
aria-label="Decrement years"
|
||||
>
|
||||
<MinusIcon width="14" height="14" className="fill-[#32353D]" />
|
||||
</button>
|
||||
<span className="flex w-32 items-center justify-center text-3xl text-black">
|
||||
{years} year{years > 1 && 's'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={increment}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-[#DEE1E7]"
|
||||
aria-label="Increment years"
|
||||
>
|
||||
<PlusIcon width="14" height="14" className="fill-[#32353D]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-[14rem] text-left">
|
||||
<p className="text-line mb-2 text-sm font-bold uppercase">Amount</p>
|
||||
<div className="flex items-baseline justify-start gap-4">
|
||||
{discountedPrice !== undefined ? (
|
||||
<div className=" flex flex-row items-baseline justify-around gap-2">
|
||||
<p className="whitespace-nowrap text-3xl text-black line-through">
|
||||
{formatEtherPrice(initialPrice)}
|
||||
</p>
|
||||
<p className="whitespace-nowrap text-3xl text-green-50">
|
||||
{formatEtherPrice(discountedPrice)} ETH
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className=" whitespace-nowrap text-3xl text-black">
|
||||
{formatEtherPrice(price)} ETH
|
||||
</p>
|
||||
)}
|
||||
{loadingDiscounts ? (
|
||||
<div className="flex h-4 items-center justify-center">
|
||||
<Icon name="spinner" color="currentColor" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="whitespace-nowrap text-xl text-gray-60">${usdPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
{nameIsFree && <p className="text-sm text-green-50">Free with your verification</p>}
|
||||
</div>
|
||||
|
||||
<ConnectButton.Custom>
|
||||
{({ account, chain, mounted }) => {
|
||||
const ready = mounted;
|
||||
const connected = ready && account && chain;
|
||||
|
||||
if (!connected) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={ButtonVariants.Black}
|
||||
size={ButtonSizes.Small}
|
||||
onClick={openConnectModal}
|
||||
rounded
|
||||
>
|
||||
Connect wallet
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={registerNameCallback}
|
||||
type="button"
|
||||
variant={ButtonVariants.Black}
|
||||
size={ButtonSizes.Small}
|
||||
disabled={registerNameTransactionIsPending}
|
||||
isLoading={registerNameTransactionIsPending}
|
||||
rounded
|
||||
>
|
||||
Register name
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
</ConnectButton.Custom>
|
||||
</div>
|
||||
|
||||
{transactionError !== null && (
|
||||
<TransactionError className="mt-4 text-center" error={transactionError} />
|
||||
)}
|
||||
{registerNameError && (
|
||||
<TransactionError className="mt-4 text-center" error={registerNameError} />
|
||||
)}
|
||||
{transactionData && transactionData.status === 'reverted' && (
|
||||
<TransactionStatus
|
||||
className="mt-4 text-center"
|
||||
transaction={transactionData}
|
||||
chainId={basenameChain.id}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-6 flex w-full justify-center">
|
||||
<p className="text mr-2 text-center font-bold uppercase text-[#5B616E]">
|
||||
{nameIsFree ? "You've qualified for a free name! " : 'Unlock your username for free! '}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-line font-bold uppercase underline"
|
||||
onClick={toggleLearnMoreModal}
|
||||
>
|
||||
Learn more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<RegistrationLearnMoreModal isOpen={learnMoreModalOpen} toggleModal={toggleLearnMoreModal} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8134_24705)">
|
||||
<rect width="32" height="32" rx="8" fill="#FF8DCF" />
|
||||
<path
|
||||
d="M4 14C4 9.58172 7.58172 6 12 6L22.0289 6C22.3149 6 22.5757 6.075 22.8112 6.225C23.0467 6.35833 23.2318 6.54167 23.3663 6.775C23.5177 7.00833 23.5934 7.26667 23.5934 7.55V14.5C23.5934 14.8 23.5177 15.075 23.3663 15.325C23.2318 15.5583 23.0467 15.75 22.8112 15.9C22.5757 16.0333 22.3149 16.1 22.0289 16.1H6.68446C6.54639 16.1 6.43447 16.2119 6.43447 16.35C6.43447 16.4881 6.54639 16.6 6.68446 16.6H23.7196C24.3252 16.6 24.8719 16.75 25.3598 17.05C25.8645 17.3333 26.2598 17.725 26.5458 18.225C26.8486 18.7083 27 19.2417 27 19.825V22.725C27 23.3083 26.8486 23.8583 26.5458 24.375C26.2598 24.875 25.8645 25.275 25.3598 25.575C24.8551 25.8583 24.3084 26 23.7196 26H12C7.58172 26 4 22.4183 4 18V14ZM10.2952 25.025C10.9681 25.025 11.5906 24.8583 12.1625 24.525C12.7345 24.1917 13.1887 23.7417 13.5252 23.175C13.8616 22.6083 14.0298 21.9917 14.0298 21.325C14.0298 20.6583 13.8616 20.0417 13.5252 19.475C13.1887 18.9083 12.7345 18.4583 12.1625 18.125C11.5906 17.775 10.9681 17.6 10.2952 17.6C9.62234 17.6 8.9999 17.775 8.42793 18.125C7.85597 18.4583 7.40176 18.9083 7.06531 19.475C6.72886 20.0417 6.56063 20.6583 6.56063 21.325C6.56063 21.9917 6.72886 22.6083 7.06531 23.175C7.40176 23.7417 7.85597 24.1917 8.42793 24.525C8.9999 24.8583 9.62234 25.025 10.2952 25.025ZM20.2373 11.725C20.8766 11.725 21.4233 11.5 21.8775 11.05C22.3486 10.5833 22.5841 10.025 22.5841 9.375C22.5841 8.70833 22.3486 8.15 21.8775 7.7C21.4233 7.23333 20.8766 7 20.2373 7C19.5644 7 18.9925 7.23333 18.5214 7.7C18.0672 8.15 17.8401 8.70833 17.8401 9.375C17.8401 10.025 18.0672 10.5833 18.5214 11.05C18.9925 11.5 19.5644 11.725 20.2373 11.725Z"
|
||||
fill="white" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8134_24705">
|
||||
<rect width="32" height="32" rx="8" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4283_55832)">
|
||||
<rect width="32" height="32" rx="8" fill="#73A2FF"/>
|
||||
<path d="M16.0456 10.7359C17.4856 10.7359 18.6741 9.56853 18.6741 8.10358C18.6741 6.63863 17.4856 5.49414 16.0456 5.49414C14.6056 5.49414 13.417 6.66152 13.417 8.12647C13.417 9.59142 14.5827 10.7359 16.0456 10.7359Z" fill="#0052FF"/>
|
||||
<path d="M8.38867 19.5704C8.38867 15.3358 11.8172 11.9023 16.0458 11.9023C20.2744 11.9023 23.703 15.3358 23.703 19.5704H8.38867Z" fill="#FFD200"/>
|
||||
<path d="M19.6801 26.5075H12.3887V18.5418C12.3887 16.5275 14.0115 14.9023 16.023 14.9023C18.0344 14.9023 19.6572 16.5275 19.6572 18.5418L19.6801 26.5075Z" fill="#0052FF"/>
|
||||
<path d="M12.3887 19.5719V18.5418C12.3887 16.5275 14.0115 14.9023 16.023 14.9023C18.0344 14.9023 19.6572 16.5275 19.6572 18.5418V19.5719H12.3887Z" fill="#0A0B0D"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4283_55832">
|
||||
<rect width="32" height="32" rx="8" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="8" fill="url(#paint0_linear_8134_24688)" />
|
||||
<path
|
||||
d="M15.7522 4C9.12354 4 3.75195 9.38751 3.75195 16.0349C3.75195 22.243 8.44067 27.3523 14.4605 28V18.8357C16.9086 17.8029 18.9136 15.9342 20.1266 13.5821V27.2253C24.5667 25.4596 27.752 21.118 27.752 16.0349C27.752 9.38751 22.3804 4 15.7522 4ZM8.90341 18.779V13.2824C11.8007 13.2824 14.1986 11.1621 14.6525 8.38522H20.1683C19.684 14.2036 14.8295 18.779 8.90341 18.779Z"
|
||||
fill="white" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_8134_24688" x1="2.08616e-07" y1="16" x2="32" y2="16"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#266EFF" />
|
||||
<stop offset="1" stop-color="#45E1E5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 835 B |
@@ -0,0 +1,11 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" rx="8" fill="#0052FF"/>
|
||||
<g clip-path="url(#clip0_4283_55809)">
|
||||
<path d="M15.7745 22C12.4533 22 9.76322 19.315 9.76322 16C9.76322 12.685 12.4533 10 15.7745 10C18.7501 10 21.2197 12.165 21.6956 15H27.752C27.241 8.84 22.0763 4 15.7745 4C9.13705 4 3.75195 9.375 3.75195 16C3.75195 22.625 9.13705 28 15.7745 28C22.0763 28 27.241 23.16 27.752 17H21.6956C21.2197 19.835 18.7501 22 15.7745 22Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4283_55809">
|
||||
<rect width="24" height="24" fill="white" transform="translate(3.75195 4)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 676 B |
@@ -0,0 +1,16 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8134_24712)">
|
||||
<rect width="32" height="32" rx="8" fill="#0052FF" />
|
||||
<path
|
||||
d="M28 16C28 22.6274 22.6274 28 16 28C9.37258 28 4 22.6274 4 16C4 9.37258 9.37258 4 16 4C22.6274 4 28 9.37258 28 16ZM16.4867 25.5879C17.8404 22.9446 18.5995 20.0854 18.7642 17.2H13.2358C13.4004 20.0854 14.1596 22.9446 15.5132 25.5879C15.6745 25.5959 15.8368 25.6 16 25.6C16.1632 25.6 16.3255 25.5959 16.4867 25.5879ZM19.4119 24.976C22.6709 23.7365 25.0802 20.7732 25.5257 17.2H21.1677C21.032 19.8492 20.4468 22.4806 19.4119 24.976ZM21.1677 14.8H25.5257C25.0802 11.2268 22.6709 8.26349 19.412 7.02401C20.4468 9.5194 21.0321 12.1508 21.1677 14.8ZM16.4868 6.41213C16.3255 6.40407 16.1632 6.4 16 6.4C15.8368 6.4 15.6745 6.40407 15.5133 6.41212C14.1596 9.05538 13.4005 11.9146 13.2358 14.8H18.7642C18.5996 11.9146 17.8404 9.05539 16.4868 6.41213ZM10.8323 17.2H6.47427C6.91984 20.7732 9.3291 23.7365 12.588 24.976C11.5532 22.4806 10.9679 19.8492 10.8323 17.2ZM10.8323 14.8C10.968 12.1508 11.5532 9.51938 12.5881 7.02399C9.32913 8.26346 6.91984 11.2268 6.47427 14.8H10.8323Z"
|
||||
fill="white" />
|
||||
<path
|
||||
d="M28 16C28 22.6274 22.6274 28 16 28C9.37258 28 4 22.6274 4 16C4 9.37258 9.37258 4 16 4C22.6274 4 28 9.37258 28 16ZM16.4867 25.5879C17.8404 22.9446 18.5995 20.0854 18.7642 17.2H13.2358C13.4004 20.0854 14.1596 22.9446 15.5132 25.5879C15.6745 25.5959 15.8368 25.6 16 25.6C16.1632 25.6 16.3255 25.5959 16.4867 25.5879ZM19.4119 24.976C22.6709 23.7365 25.0802 20.7732 25.5257 17.2H21.1677C21.032 19.8492 20.4468 22.4806 19.4119 24.976ZM21.1677 14.8H25.5257C25.0802 11.2268 22.6709 8.26349 19.412 7.02401C20.4468 9.5194 21.0321 12.1508 21.1677 14.8ZM16.4868 6.41213C16.3255 6.40407 16.1632 6.4 16 6.4C15.8368 6.4 15.6745 6.40407 15.5133 6.41212C14.1596 9.05538 13.4005 11.9146 13.2358 14.8H18.7642C18.5996 11.9146 17.8404 9.05539 16.4868 6.41213ZM10.8323 17.2H6.47427C6.91984 20.7732 9.3291 23.7365 12.588 24.976C11.5532 22.4806 10.9679 19.8492 10.8323 17.2ZM10.8323 14.8C10.968 12.1508 11.5532 9.51938 12.5881 7.02399C9.32913 8.26346 6.91984 11.2268 6.47427 14.8H10.8323Z"
|
||||
fill="white" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_8134_24712">
|
||||
<rect width="32" height="32" rx="8" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,196 @@
|
||||
import { InformationCircleIcon } from '@heroicons/react/20/solid';
|
||||
import { useRegistration } from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import Modal from 'apps/web/src/components/Modal';
|
||||
import Tooltip from 'apps/web/src/components/Tooltip';
|
||||
import { Discount } from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import baseBuildathonParticipant from './images/base-buildathon-participant.svg';
|
||||
import summerPassLvl3 from './images/summer-pass-lvl-3.svg';
|
||||
import cbidVerification from './images/cbid-verification.svg';
|
||||
import coinbaseOneVerification from './images/coinbase-one-verification.svg';
|
||||
import coinbaseVerification from './images/coinbase-verification.svg';
|
||||
import { StaticImageData } from 'next/dist/shared/lib/get-img-props';
|
||||
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
|
||||
|
||||
function InfoIcon() {
|
||||
return <InformationCircleIcon width={12} height={12} className="fill-gray-40" />;
|
||||
}
|
||||
|
||||
export default function RegistrationLearnMoreModal({
|
||||
isOpen,
|
||||
toggleModal,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
toggleModal: () => void;
|
||||
}) {
|
||||
const { allActiveDiscounts } = useRegistration();
|
||||
|
||||
const hasDiscount = allActiveDiscounts.size > 0;
|
||||
const rowClasses = 'flex flex-row items-center justify-start';
|
||||
const CBRowClasses = classNames(rowClasses, {
|
||||
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.COINBASE_VERIFIED_ACCOUNT),
|
||||
});
|
||||
const CB1RowClasses = classNames(rowClasses, {
|
||||
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.CB1),
|
||||
});
|
||||
const CBIDRowClasses = classNames(rowClasses, {
|
||||
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.CBID),
|
||||
});
|
||||
const BuildathonRowClasses = classNames(rowClasses, {
|
||||
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.BASE_BUILDATHON_PARTICIPANT),
|
||||
});
|
||||
const SummerPassRowClasses = classNames(rowClasses, {
|
||||
'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.SUMMER_PASS_LVL_3),
|
||||
});
|
||||
|
||||
const qualifiedClasses = classNames(
|
||||
'flex flex-row items-center justify-center py-3 px-1 h-5 text-xs bg-green-0 rounded ml-3',
|
||||
);
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={toggleModal} title="">
|
||||
<div className="flex max-w-prose flex-1 flex-col place-content-center place-items-center gap-3">
|
||||
<span className="w-full text-2xl font-bold">
|
||||
{hasDiscount ? "You're getting a discounted name" : 'Register for free'}
|
||||
</span>
|
||||
<p className="mb-3 text-illoblack">
|
||||
{hasDiscount
|
||||
? "You're receiving your name for free (5+ characters for 1 year) because your wallet has one of the following:"
|
||||
: "You'll receive a name for free (5+ characters for 1 year) if your wallet has any of the following:"}
|
||||
</p>
|
||||
<ul className="mb-3 flex flex-col gap-3 self-start">
|
||||
<li className="flex items-center gap-3">
|
||||
<Tooltip content="Verifies you have a valid trading account on Coinbase">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<ImageWithLoading
|
||||
src={coinbaseVerification as StaticImageData}
|
||||
alt="criteria icon"
|
||||
width={30}
|
||||
height={30}
|
||||
wrapperClassName="rounded-lg"
|
||||
imageClassName={CBRowClasses}
|
||||
/>
|
||||
<p className={classNames(CBRowClasses)}>Coinbase verification</p>
|
||||
<InfoIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{allActiveDiscounts.has(Discount.COINBASE_VERIFIED_ACCOUNT) && (
|
||||
<div className={qualifiedClasses}>
|
||||
<p className="text-green-60">Qualified</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<Tooltip content="Verifies you have an active Coinbase One subscription">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<ImageWithLoading
|
||||
src={coinbaseOneVerification as StaticImageData}
|
||||
alt="criteria icon"
|
||||
width={30}
|
||||
height={30}
|
||||
wrapperClassName="rounded-lg"
|
||||
imageClassName={CBRowClasses}
|
||||
/>
|
||||
<p className={classNames(CB1RowClasses)}>Coinbase One verification</p>
|
||||
<InfoIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{allActiveDiscounts.has(Discount.CB1) && (
|
||||
<div className={qualifiedClasses}>
|
||||
<p className="text-green-60">Qualified</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<Tooltip content="cb.id must have been claimed prior to Basenames launch">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<ImageWithLoading
|
||||
src={cbidVerification as StaticImageData}
|
||||
alt="criteria icon"
|
||||
width={30}
|
||||
height={30}
|
||||
wrapperClassName="rounded-lg"
|
||||
imageClassName={CBRowClasses}
|
||||
/>
|
||||
<p className={classNames(CBIDRowClasses)}>A cb.id username</p>
|
||||
<InfoIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{allActiveDiscounts.has(Discount.CBID) && (
|
||||
<div className={qualifiedClasses}>
|
||||
<p className="text-green-60">Qualified</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<Tooltip content="Available for anyone holding a Base Buildathon participant NFT.">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<ImageWithLoading
|
||||
src={baseBuildathonParticipant as StaticImageData}
|
||||
alt="criteria icon"
|
||||
width={30}
|
||||
height={30}
|
||||
wrapperClassName="rounded-lg"
|
||||
imageClassName={CBRowClasses}
|
||||
/>
|
||||
<p className={classNames(BuildathonRowClasses)}>Base buildathon participant</p>
|
||||
<InfoIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{allActiveDiscounts.has(Discount.BASE_BUILDATHON_PARTICIPANT) && (
|
||||
<div className={qualifiedClasses}>
|
||||
<p className="text-green-60">Qualified</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<Tooltip content="Available for anyone holding a Summer Pass Level 3 NFT. Go to wallet.coinbase.com/ocs to get your Summer Pass">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<ImageWithLoading
|
||||
src={summerPassLvl3 as StaticImageData}
|
||||
alt="criteria icon"
|
||||
width={30}
|
||||
height={30}
|
||||
wrapperClassName="rounded-lg"
|
||||
imageClassName={CBRowClasses}
|
||||
/>
|
||||
<p className={classNames(SummerPassRowClasses)}>Summer Pass Level 3</p>
|
||||
<InfoIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
{allActiveDiscounts.has(Discount.SUMMER_PASS_LVL_3) && (
|
||||
<div className={qualifiedClasses}>
|
||||
<p className="text-green-60">Qualified</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
{!hasDiscount && (
|
||||
<>
|
||||
<p className="mb-3 w-full text-illoblack">
|
||||
Your registration will be gasless with{' '}
|
||||
<Link
|
||||
href="http://wallet.coinbase.com/smart-wallet"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
a smart wallet
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<div className="text-md w-full rounded-xl border border-[#CED2DB] bg-palette-backgroundAlternate p-4 font-medium text-illoblack">
|
||||
Don't have any of these?{' '}
|
||||
<Link
|
||||
href="https://www.coinbase.com/onchain-verify"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
Get a verification
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import {
|
||||
registrationTransitionDuration,
|
||||
useRegistration,
|
||||
} from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import UsernameDescriptionField from 'apps/web/src/components/Basenames/UsernameDescriptionField';
|
||||
import UsernameKeywordsField from 'apps/web/src/components/Basenames/UsernameKeywordsField';
|
||||
import UsernameTextRecordInlineField from 'apps/web/src/components/Basenames/UsernameTextRecordInlineField';
|
||||
import { Button, ButtonVariants } from 'apps/web/src/components/Button/Button';
|
||||
import Fieldset from 'apps/web/src/components/Fieldset';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import Label from 'apps/web/src/components/Label';
|
||||
import TransactionError from 'apps/web/src/components/TransactionError';
|
||||
import TransactionStatus from 'apps/web/src/components/TransactionStatus';
|
||||
import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords';
|
||||
import useWriteBaseEnsTextRecords from 'apps/web/src/hooks/useWriteBaseEnsTextRecords';
|
||||
import {
|
||||
UsernameTextRecords,
|
||||
UsernameTextRecordKeys,
|
||||
textRecordsSocialFieldsEnabled,
|
||||
formatBaseEthDomain,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useAccount, useWaitForTransactionReceipt } from 'wagmi';
|
||||
|
||||
export enum FormSteps {
|
||||
Description = 'description',
|
||||
Socials = 'socials',
|
||||
Keywords = 'keywords',
|
||||
}
|
||||
|
||||
export default function RegistrationProfileForm() {
|
||||
const [currentFormStep, setCurrentFormStep] = useState<FormSteps>(FormSteps.Description);
|
||||
const [transitionStep, setTransitionStep] = useState<boolean>(false);
|
||||
const { selectedName } = useRegistration();
|
||||
const { address } = useAccount();
|
||||
const router = useRouter();
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const { basenameChain } = useBasenameChain();
|
||||
const { data: baseEnsName } = useBaseEnsName({
|
||||
address,
|
||||
});
|
||||
|
||||
const {
|
||||
existingTextRecords,
|
||||
existingTextRecordsIsLoading,
|
||||
refetchExistingTextRecords,
|
||||
existingTextRecordsError,
|
||||
} = useReadBaseEnsTextRecords({
|
||||
address: address,
|
||||
username: baseEnsName,
|
||||
});
|
||||
|
||||
// Write text records
|
||||
const {
|
||||
writeTextRecords,
|
||||
writeTextRecordsIsPending,
|
||||
writeTextRecordsTransactionHash,
|
||||
writeTextRecordsError,
|
||||
} = useWriteBaseEnsTextRecords({
|
||||
address: address,
|
||||
username: baseEnsName,
|
||||
});
|
||||
|
||||
// Wait for text record transaction to be processed
|
||||
const {
|
||||
data: transactionData,
|
||||
isFetching: transactionIsFetching,
|
||||
error: transactionError,
|
||||
} = useWaitForTransactionReceipt({
|
||||
hash: writeTextRecordsTransactionHash,
|
||||
chainId: basenameChain.id,
|
||||
query: {
|
||||
enabled: !!writeTextRecordsTransactionHash,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
});
|
||||
|
||||
const [textRecords, setTextRecords] = useState<UsernameTextRecords>(existingTextRecords);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transactionData) return;
|
||||
|
||||
if (transactionData.status === 'success') {
|
||||
logEventWithContext('update_text_records_transaction_success', ActionType.change);
|
||||
|
||||
refetchExistingTextRecords()
|
||||
.then(() => {
|
||||
router.push(`name/${formatBaseEthDomain(selectedName, basenameChain.id)}`);
|
||||
})
|
||||
.catch(() => {
|
||||
// console.log({ error });
|
||||
});
|
||||
}
|
||||
|
||||
if (transactionData.status === 'reverted') {
|
||||
logEventWithContext('update_text_records_transaction_reverted', ActionType.change, {
|
||||
error: `Transaction reverted: ${transactionData.transactionHash}`,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
logEventWithContext,
|
||||
refetchExistingTextRecords,
|
||||
router,
|
||||
baseEnsName,
|
||||
transactionData,
|
||||
selectedName,
|
||||
basenameChain.id,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (transactionIsFetching) {
|
||||
logEventWithContext('update_text_records_transaction_processing', ActionType.change);
|
||||
}
|
||||
}, [logEventWithContext, transactionIsFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
setTextRecords(existingTextRecords);
|
||||
}, [existingTextRecords]);
|
||||
|
||||
const updateTextRecords = useCallback((key: UsernameTextRecordKeys, value: string) => {
|
||||
setTextRecords((previousTextRecords) => {
|
||||
return {
|
||||
...previousTextRecords,
|
||||
[key]: value,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const transitionFormOpacity = useCallback((callbackFunction: () => void) => {
|
||||
// Hide the form
|
||||
setTransitionStep(true);
|
||||
|
||||
setTimeout(() => {
|
||||
// Display the next step
|
||||
callbackFunction();
|
||||
setTimeout(() => {
|
||||
// Show the form
|
||||
setTransitionStep(false);
|
||||
}, 700);
|
||||
}, 700);
|
||||
}, []);
|
||||
|
||||
const onClickSave = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
if (currentFormStep === FormSteps.Description) {
|
||||
transitionFormOpacity(() => setCurrentFormStep(FormSteps.Socials));
|
||||
}
|
||||
|
||||
if (currentFormStep === FormSteps.Socials) {
|
||||
transitionFormOpacity(() => setCurrentFormStep(FormSteps.Keywords));
|
||||
}
|
||||
|
||||
if (currentFormStep === FormSteps.Keywords) {
|
||||
logEventWithContext('update_text_records_transaction_initiated', ActionType.change);
|
||||
|
||||
writeTextRecords(textRecords)
|
||||
.then((result) => {
|
||||
// We updated some text records
|
||||
if (result) {
|
||||
logEventWithContext('update_text_records_transaction_approved', ActionType.change);
|
||||
} else {
|
||||
// no text records had to be updated, simply go to profile
|
||||
router.push(`names/${formatBaseEthDomain(selectedName, basenameChain.id)}`);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
},
|
||||
[
|
||||
basenameChain.id,
|
||||
currentFormStep,
|
||||
logEventWithContext,
|
||||
router,
|
||||
selectedName,
|
||||
textRecords,
|
||||
transitionFormOpacity,
|
||||
writeTextRecords,
|
||||
],
|
||||
);
|
||||
|
||||
const onChangeTextRecord = useCallback(
|
||||
(key: UsernameTextRecordKeys, value: string) => {
|
||||
updateTextRecords(key, value);
|
||||
},
|
||||
[updateTextRecords],
|
||||
);
|
||||
|
||||
const formClasses = classNames(
|
||||
'flex flex-col justify-between gap-4 text-gray-60 md:items-center rounded-3xl shadow-xl p-8 transition-all',
|
||||
registrationTransitionDuration,
|
||||
{ 'opacity-0': transitionStep, 'opacity-100': !transitionStep },
|
||||
);
|
||||
|
||||
const descriptionLabelChildren = (
|
||||
<div className="flex w-full cursor-pointer flex-col">
|
||||
<p className="flex flex-row justify-between text-black">
|
||||
<div className="flex flex-row items-center gap-1 text-blue-500">
|
||||
<Icon name="blueCircle" color="currentColor" height="0.8rem" width="0.8rem" />
|
||||
<strong className="text-black">Add Bio</strong>
|
||||
</div>
|
||||
<span className="font-normal">Step 1 of 3</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const socialsLabelChildren = (
|
||||
<div className="flex w-full cursor-pointer flex-col">
|
||||
<p className="flex flex-row justify-between text-black">
|
||||
<div className="flex flex-row items-center gap-1 text-blue-500">
|
||||
<Icon name="blueCircle" color="currentColor" height="0.8rem" width="0.8rem" />
|
||||
<strong className="text-black">Add Socials</strong>
|
||||
</div>
|
||||
<span className="font-normal">Step 2 of 3</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const keywordsLabelChildren = (
|
||||
<div className="mb-2 flex w-full cursor-pointer flex-col">
|
||||
<p className="flex flex-row justify-between text-black">
|
||||
<div className="flex flex-row items-center gap-1 text-blue-500">
|
||||
<Icon name="blueCircle" color="currentColor" height="0.8rem" width="0.8rem" />
|
||||
<strong className="text-black">Add areas of expertise</strong>
|
||||
</div>
|
||||
<span className="font-normal">Step 3 of 3</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
existingTextRecordsIsLoading || writeTextRecordsIsPending || transactionIsFetching;
|
||||
|
||||
useEffect(() => {
|
||||
logEventWithContext(`registration_profile_form_step_${currentFormStep}`, ActionType.change);
|
||||
}, [currentFormStep, logEventWithContext]);
|
||||
|
||||
return (
|
||||
<form className={formClasses}>
|
||||
{currentFormStep === FormSteps.Description && (
|
||||
<UsernameDescriptionField
|
||||
labelChildren={descriptionLabelChildren}
|
||||
onChange={onChangeTextRecord}
|
||||
value={textRecords[UsernameTextRecordKeys.Description]}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
{currentFormStep === FormSteps.Socials && (
|
||||
<Fieldset>
|
||||
<Label>{socialsLabelChildren}</Label>
|
||||
{textRecordsSocialFieldsEnabled.map((textRecordKey) => (
|
||||
<UsernameTextRecordInlineField
|
||||
key={textRecordKey}
|
||||
textRecordKey={textRecordKey}
|
||||
onChange={onChangeTextRecord}
|
||||
value={textRecords[textRecordKey]}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
))}
|
||||
</Fieldset>
|
||||
)}
|
||||
{currentFormStep === FormSteps.Keywords && (
|
||||
<div className="mb-2">
|
||||
<UsernameKeywordsField
|
||||
labelChildren={keywordsLabelChildren}
|
||||
onChange={onChangeTextRecord}
|
||||
value={textRecords[UsernameTextRecordKeys.Keywords]}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant={ButtonVariants.Black}
|
||||
rounded
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onClickSave}
|
||||
>
|
||||
{currentFormStep === FormSteps.Keywords ? "I'm done" : 'Next'}
|
||||
</Button>
|
||||
{writeTextRecordsError && <TransactionError error={writeTextRecordsError} />}
|
||||
{existingTextRecordsError && <TransactionError error={existingTextRecordsError} />}
|
||||
{transactionError && <TransactionError error={transactionError} />}
|
||||
{transactionData && transactionData.status === 'reverted' && (
|
||||
<TransactionStatus transaction={transactionData} chainId={transactionData.chainId} />
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import { useRegistration } from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import Input from 'apps/web/src/components/Input';
|
||||
import { useAlternativeNameSuggestions } from 'apps/web/src/hooks/useAlternativeNameSuggestions';
|
||||
import { useFocusWithin } from 'apps/web/src/hooks/useFocusWithin';
|
||||
import { useIsNameAvailable } from 'apps/web/src/hooks/useIsNameAvailable';
|
||||
import { formatBaseEthDomain, validateEnsDomainName } from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
import Tooltip from 'apps/web/src/components/Tooltip';
|
||||
import { InformationCircleIcon } from '@heroicons/react/16/solid';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
|
||||
export enum RegistrationSearchInputVariant {
|
||||
Small,
|
||||
Large,
|
||||
}
|
||||
|
||||
type RegistrationSearchInputProps = {
|
||||
variant: RegistrationSearchInputVariant;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export default function RegistrationSearchInput({
|
||||
variant,
|
||||
placeholder,
|
||||
}: RegistrationSearchInputProps) {
|
||||
const { ref, focused } = useFocusWithin<HTMLFieldSetElement>();
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
|
||||
const [debouncedSearch] = useDebounceValue(search, 400);
|
||||
const { basenameChain } = useBasenameChain();
|
||||
const {
|
||||
isLoading: isLoadingNameAvailability,
|
||||
data: isNameAvailable,
|
||||
isError: errorCheckingNameAvailability,
|
||||
isFetching,
|
||||
} = useIsNameAvailable(debouncedSearch);
|
||||
const {
|
||||
data: suggestions,
|
||||
error: alternativeNameSuggestionError,
|
||||
isLoading: isLoadingAlternatives,
|
||||
} = useAlternativeNameSuggestions(debouncedSearch, isNameAvailable === false);
|
||||
const isLoading = isLoadingAlternatives || isLoadingNameAvailability;
|
||||
|
||||
const { valid, message } = validateEnsDomainName(debouncedSearch);
|
||||
const invalidWithMessage = !valid && !!message;
|
||||
|
||||
const { setSearchInputFocused, setSearchInputHovered, setSelectedName } = useRegistration();
|
||||
|
||||
const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
setSearch(value.replace(/\s/g, ''));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDropdownOpen(valid || invalidWithMessage);
|
||||
}, [debouncedSearch, invalidWithMessage, valid]);
|
||||
|
||||
const RegistrationSearchInputClasses = classNames(
|
||||
'relative z-10 transition-all duration-500 w-full mx-auto group text-black',
|
||||
);
|
||||
|
||||
// This will change/animate the border when hovering the whole component
|
||||
const groupBorderClasses = classNames('transition-colors', {
|
||||
'border-2 border-gray-40/20 group-hover:border-blue-600 ':
|
||||
variant === RegistrationSearchInputVariant.Large,
|
||||
'border border-transparent group-hover:border-gray-40/20 ':
|
||||
variant === RegistrationSearchInputVariant.Small,
|
||||
|
||||
'shadow-lg': variant === RegistrationSearchInputVariant.Large,
|
||||
});
|
||||
|
||||
const inputClasses = classNames(
|
||||
'w-full outline-0 placeholder:uppercase peer ',
|
||||
// Padding & Font sizes
|
||||
{
|
||||
'py-7 pl-6 pr-16 text-2xl': variant === RegistrationSearchInputVariant.Large,
|
||||
'py-2 pl-3 pr-6': variant === RegistrationSearchInputVariant.Small,
|
||||
},
|
||||
// Background
|
||||
{
|
||||
'bg-white': variant === RegistrationSearchInputVariant.Large,
|
||||
'bg-transparent focus:bg-white': variant === RegistrationSearchInputVariant.Small,
|
||||
},
|
||||
// border colors
|
||||
{
|
||||
'border-gray-40/20 focus:border-blue-600 ': variant === RegistrationSearchInputVariant.Large,
|
||||
'focus:border-gray-40/20 hover:border-gray-40/20':
|
||||
variant === RegistrationSearchInputVariant.Small,
|
||||
},
|
||||
// Borders Radius
|
||||
{
|
||||
'rounded-3xl': variant === RegistrationSearchInputVariant.Large,
|
||||
'rounded-xl': variant === RegistrationSearchInputVariant.Small,
|
||||
'rounded-b-none border-b-none border-b-0': dropdownOpen,
|
||||
},
|
||||
groupBorderClasses,
|
||||
);
|
||||
|
||||
const dropdownClasses = classNames(
|
||||
'flex flex-col items-start bg-white text-black',
|
||||
'absolute left-0 right-0 top-full z-10 border-t-0 ',
|
||||
groupBorderClasses,
|
||||
// radius, Padding & Font sizes
|
||||
{
|
||||
'pb-4 rounded-b-3xl': variant === RegistrationSearchInputVariant.Large,
|
||||
'pb-2 rounded-b-xl': variant === RegistrationSearchInputVariant.Small,
|
||||
},
|
||||
// border colors
|
||||
{
|
||||
'border-2 peer-focus:border-blue-600': variant === RegistrationSearchInputVariant.Large,
|
||||
'border peer-focus:border-gray-40/20': variant === RegistrationSearchInputVariant.Small,
|
||||
},
|
||||
// Visible or not
|
||||
{
|
||||
'opacity-0': !dropdownOpen,
|
||||
'opacity-1': dropdownOpen,
|
||||
},
|
||||
|
||||
// // Animation
|
||||
'transition-all overflow-scroll ',
|
||||
{
|
||||
'max-h-[20rem]': dropdownOpen,
|
||||
'max-h-0 p-0 overflow-hidden border-none': !dropdownOpen,
|
||||
},
|
||||
);
|
||||
|
||||
const dropdownLabelClasses = classNames(
|
||||
'w-full uppercase text-gray-60 font-bold pointer-events-none',
|
||||
{
|
||||
'text-sm ml-6 mb-4 mt-4': variant === RegistrationSearchInputVariant.Large,
|
||||
'text-xs ml-3 mb-2 mt-2': variant === RegistrationSearchInputVariant.Small,
|
||||
},
|
||||
);
|
||||
|
||||
const buttonClasses = classNames(
|
||||
'flex w-full flex-row items-center justify-between transition-colors hover:bg-[#F9F9F9] active:bg-[#EAEAEB] text-ellipsis',
|
||||
{
|
||||
'px-6 py-3 text': variant === RegistrationSearchInputVariant.Large,
|
||||
'px-3 py-2 text-sm': variant === RegistrationSearchInputVariant.Small,
|
||||
},
|
||||
);
|
||||
|
||||
const inputIconClasses = classNames('absolute top-1/2 z-20 flex -translate-y-1/2 items-center', {
|
||||
'right-8': variant === RegistrationSearchInputVariant.Large,
|
||||
'right-3': variant === RegistrationSearchInputVariant.Small,
|
||||
});
|
||||
|
||||
const lineClasses = classNames('w-full', {
|
||||
'px-6': variant === RegistrationSearchInputVariant.Large,
|
||||
'px-3': variant === RegistrationSearchInputVariant.Small,
|
||||
});
|
||||
|
||||
const mutedMessage = classNames('text-gray-60', {
|
||||
'px-6 py-4 text': variant === RegistrationSearchInputVariant.Large,
|
||||
'px-3 py-2 text-sm': variant === RegistrationSearchInputVariant.Small,
|
||||
});
|
||||
|
||||
const spinnerWrapperClasses = classNames('flex w-full items-center justify-center', {
|
||||
// Equivalent to the dropdown when one name is available
|
||||
'h-[6.75rem]': variant === RegistrationSearchInputVariant.Large,
|
||||
'h-[4.25rem]': variant === RegistrationSearchInputVariant.Small,
|
||||
});
|
||||
|
||||
const iconSize = variant === RegistrationSearchInputVariant.Large ? 24 : 16;
|
||||
|
||||
const inputId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
if (!focused) {
|
||||
setDropdownOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (focused && valid) {
|
||||
setDropdownOpen(true);
|
||||
}
|
||||
}, [focused, valid]);
|
||||
|
||||
const handleSelectName = useCallback(
|
||||
(name: string) => {
|
||||
setDropdownOpen(false);
|
||||
setSelectedName(name.trim());
|
||||
},
|
||||
[setSelectedName],
|
||||
);
|
||||
|
||||
const resetSearch = useCallback(() => {
|
||||
setSearch('');
|
||||
}, []);
|
||||
|
||||
const onMouseEnterFieldset = useCallback(
|
||||
() => setSearchInputHovered(true),
|
||||
[setSearchInputHovered],
|
||||
);
|
||||
const onMouseLeaveFieldset = useCallback(
|
||||
() => setSearchInputHovered(false),
|
||||
[setSearchInputHovered],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchInputFocused(focused);
|
||||
}, [focused, setSearchInputFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!invalidWithMessage) return;
|
||||
|
||||
// Log invalid
|
||||
logEventWithContext('search_available_name_invalid', ActionType.error, { error: message });
|
||||
}, [invalidWithMessage, logEventWithContext, message, setSearchInputFocused]);
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
className={RegistrationSearchInputClasses}
|
||||
onMouseEnter={onMouseEnterFieldset}
|
||||
onMouseLeave={onMouseLeaveFieldset}
|
||||
ref={ref}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={placeholder}
|
||||
className={inputClasses}
|
||||
id={inputId}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<div className={dropdownClasses}>
|
||||
<div className={lineClasses}>
|
||||
<div className="w-full border-t border-gray-40/20 " />
|
||||
</div>
|
||||
{invalidWithMessage ? (
|
||||
<p className={mutedMessage}>{message}</p>
|
||||
) : isNameAvailable === true ? (
|
||||
<>
|
||||
<p className={dropdownLabelClasses}>Available</p>
|
||||
<button
|
||||
className={buttonClasses}
|
||||
type="button"
|
||||
onClick={() => handleSelectName(debouncedSearch)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{formatBaseEthDomain(debouncedSearch, basenameChain.id)}
|
||||
</span>
|
||||
<ChevronRightIcon width={iconSize} height={iconSize} />
|
||||
</button>
|
||||
</>
|
||||
) : isLoading || isFetching ? (
|
||||
<div className={spinnerWrapperClasses}>
|
||||
<Icon name="spinner" color="currentColor" />
|
||||
</div>
|
||||
) : errorCheckingNameAvailability ? (
|
||||
<p className={mutedMessage}>
|
||||
There was an error checking if your desired name is available
|
||||
</p>
|
||||
) : alternativeNameSuggestionError ? (
|
||||
<p className={mutedMessage}>
|
||||
There was an error coming up with alternative name suggestions.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className={mutedMessage}>
|
||||
{formatBaseEthDomain(debouncedSearch, basenameChain.id)} is not available
|
||||
</p>
|
||||
{suggestions.length > 0 ? (
|
||||
<>
|
||||
<Tooltip content="Suggestions are generated by AI. Do not type in any sensitive information.">
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<p className={dropdownLabelClasses}>Suggested</p>
|
||||
<InformationCircleIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="fill-gray-40 transition-colors hover:fill-gray-dark"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{suggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
className={buttonClasses}
|
||||
type="button"
|
||||
onClick={() => handleSelectName(suggestion)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{formatBaseEthDomain(suggestion, basenameChain.id)}
|
||||
</span>
|
||||
<Icon name="chevronRight" width={iconSize} height={iconSize} />
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className={mutedMessage}>
|
||||
We are currently unable to offer alternative name suggestions
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<span className={inputIconClasses}>
|
||||
{search.length > 0 ? (
|
||||
<button onClick={resetSearch} type="button" aria-label="Reset search">
|
||||
<Icon name="cross" color="currentColor" height={iconSize} width={iconSize} />
|
||||
</button>
|
||||
) : (
|
||||
<span className="pointer-events-none">
|
||||
<Icon name="search" color="currentColor" height={iconSize} width={iconSize} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import {
|
||||
RegistrationSteps,
|
||||
useRegistration,
|
||||
} from 'apps/web/src/components/Basenames/RegistrationContext';
|
||||
import ShareUsernameModal from 'apps/web/src/components/Basenames/ShareUsernameModal';
|
||||
import { Button, ButtonVariants } from 'apps/web/src/components/Button/Button';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { formatBaseEthDomain } from 'apps/web/src/utils/usernames';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
export default function RegistrationSuccessMessage() {
|
||||
const { setRegistrationStep, selectedName } = useRegistration();
|
||||
const router = useRouter();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const { basenameChain } = useBasenameChain();
|
||||
const openModal = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
logEventWithContext('open_share_on_social_modal', ActionType.click);
|
||||
setIsOpen(true);
|
||||
},
|
||||
[logEventWithContext],
|
||||
);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const customizeProfileOnClick = useCallback(() => {
|
||||
logEventWithContext('customize_profile', ActionType.click);
|
||||
setRegistrationStep(RegistrationSteps.Profile);
|
||||
}, [logEventWithContext, setRegistrationStep]);
|
||||
|
||||
const goToProfileOnClick = useCallback(() => {
|
||||
logEventWithContext('go_to_profile', ActionType.click);
|
||||
router.push(`name/${formatBaseEthDomain(selectedName, basenameChain.id)}`);
|
||||
}, [basenameChain.id, logEventWithContext, router, selectedName]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="items-left mx-auto flex w-full max-w-[50rem] flex-col justify-between gap-6 rounded-3xl border border-[#266EFF] bg-blue-600 p-10 shadow-xl transition-all duration-500 md:flex-row md:items-center">
|
||||
<h1 className="text-3xl font-bold tracking-wider text-white">
|
||||
Congrats! This name is yours!
|
||||
</h1>
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<Button rounded fullWidth variant={ButtonVariants.Secondary} onClick={goToProfileOnClick}>
|
||||
Go to Profile
|
||||
</Button>
|
||||
|
||||
<Button rounded fullWidth onClick={customizeProfileOnClick}>
|
||||
Customize Profile
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ShareUsernameModal selectedName={selectedName} isOpen={isOpen} toggleModal={closeModal} />
|
||||
<p className="mt-6 text-center">
|
||||
<Link
|
||||
href="#share"
|
||||
className="font-bold uppercase tracking-wider text-white underline underline-offset-4"
|
||||
onClick={openModal}
|
||||
>
|
||||
Share your name on socials
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
138
apps/web/src/components/Basenames/ShareUsernameModal/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Button, ButtonVariants } from 'apps/web/src/components/Button/Button';
|
||||
import Modal from 'apps/web/src/components/Modal';
|
||||
import { useCallback, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
SocialMediaShareParams,
|
||||
SocialPlatform,
|
||||
socialPlatformCtaForDisplay,
|
||||
socialPlatformHandle,
|
||||
socialPlatformIconName,
|
||||
socialPlatformShareLinkFunction,
|
||||
socialPlatformsNameForDisplay,
|
||||
} from 'apps/web/src/utils/socialPlatforms';
|
||||
import { openGraphImageHeight, openGraphImageWidth } from 'apps/web/src/utils/opengraphs';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import { formatBaseEthDomain } from 'apps/web/src/utils/usernames';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import Image from 'next/image';
|
||||
|
||||
export const socialPlatformsEnabled = [SocialPlatform.Twitter, SocialPlatform.Farcaster];
|
||||
|
||||
function SocialPlatformButton({
|
||||
socialPlatform,
|
||||
openPopup,
|
||||
}: {
|
||||
socialPlatform: SocialPlatform;
|
||||
openPopup: (socialPlatform: SocialPlatform) => void;
|
||||
}) {
|
||||
const onClick = useCallback(() => {
|
||||
openPopup(socialPlatform);
|
||||
}, [socialPlatform, openPopup]);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Button onClick={onClick} variant={ButtonVariants.Gray} rounded fullWidth>
|
||||
<Icon
|
||||
name={socialPlatformIconName[socialPlatform]}
|
||||
color="currentColor"
|
||||
height="1rem"
|
||||
width="1rem"
|
||||
/>
|
||||
{socialPlatformCtaForDisplay[socialPlatform]} on{' '}
|
||||
{socialPlatformsNameForDisplay[socialPlatform]}
|
||||
</Button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ShareUsernameModal({
|
||||
isOpen,
|
||||
toggleModal,
|
||||
selectedName,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
toggleModal: () => void;
|
||||
selectedName: string;
|
||||
}) {
|
||||
const [imageIsLoading, setImageIsLoading] = useState<boolean>(true);
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const coverImageWrapperClasses = classNames(
|
||||
'min-h-[10.25rem] w-full overflow-hidden rounded-2xl border border-gray-40/20 bg-gray-40/10',
|
||||
{
|
||||
'animate-pulse': imageIsLoading,
|
||||
},
|
||||
);
|
||||
|
||||
const { basenameChain } = useBasenameChain();
|
||||
|
||||
const coverImageClasses = classNames('transition-opacity duration-500', {
|
||||
'opacity-0': imageIsLoading,
|
||||
'opacity-100': !imageIsLoading,
|
||||
});
|
||||
|
||||
const onLoadImage = useCallback(() => {
|
||||
setImageIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const popupWidth = 600;
|
||||
const popupHeight = 600;
|
||||
|
||||
const openPopup = (socialPlatform: SocialPlatform) => {
|
||||
const socialMediaShareParams: SocialMediaShareParams = {
|
||||
text: `I just got a name from ${socialPlatformHandle[socialPlatform]} during Onchain Summer! You can get yours too at base.org/name`,
|
||||
url: `https://base.org/name/${selectedName}`,
|
||||
};
|
||||
const shareLinkFunction = socialPlatformShareLinkFunction[socialPlatform];
|
||||
if (shareLinkFunction) {
|
||||
logEventWithContext(`share_on_social_${socialPlatform}`, ActionType.click);
|
||||
|
||||
const shareLink = shareLinkFunction(socialMediaShareParams);
|
||||
const left = window.innerWidth / 2 - popupWidth / 2;
|
||||
const top = window.innerHeight / 2 - popupHeight / 2;
|
||||
const options = `width=${popupWidth},height=${popupHeight},left=${left},top=${top}`;
|
||||
|
||||
window.open(shareLink, '_blank', options);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={toggleModal}
|
||||
title="Share your name on socials"
|
||||
titleAlign="left"
|
||||
>
|
||||
<div className="mt-4 flex w-full flex-col gap-4 rounded-3xl border border-gray-40/20 p-6">
|
||||
<p>
|
||||
I just got a name from <span className="text-blue-500">@base</span> during Onchain Summer!
|
||||
You can get yours too at <span className="text-blue-500">base.org/name</span>
|
||||
</p>
|
||||
<figure className={coverImageWrapperClasses}>
|
||||
<Image
|
||||
src={`/api/basenames/${formatBaseEthDomain(
|
||||
selectedName,
|
||||
basenameChain.id,
|
||||
)}/assets/coverImage.png`}
|
||||
alt={selectedName}
|
||||
onLoad={onLoadImage}
|
||||
className={coverImageClasses}
|
||||
width={openGraphImageWidth}
|
||||
height={openGraphImageHeight}
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
<ul className="mt-4 flex w-full flex-col gap-4">
|
||||
{socialPlatformsEnabled.map((socialPlatform) => (
|
||||
<SocialPlatformButton
|
||||
socialPlatform={socialPlatform}
|
||||
openPopup={openPopup}
|
||||
key={socialPlatform}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2.29492" y="2.39111" width="38.1083" height="38.1083" rx="19.0542" fill="#E0E2E7"/>
|
||||
<rect x="2.29492" y="2.39111" width="38.1083" height="38.1083" rx="19.0542" stroke="white" stroke-width="3"/>
|
||||
<path d="M18.682 15.4453H24.0153L24.9042 17.4453H29.3486V27.4453H13.3486V17.4453H17.7931L18.682 15.4453ZM23.3486 22.1953C23.3486 23.2999 22.4532 24.1953 21.3486 24.1953C20.2441 24.1953 19.3486 23.2999 19.3486 22.1953C19.3486 21.0907 20.2441 20.1953 21.3486 20.1953C22.4532 20.1953 23.3486 21.0907 23.3486 22.1953ZM24.8486 22.1953C24.8486 20.2623 23.2816 18.6953 21.3486 18.6953C19.4156 18.6953 17.8486 20.2623 17.8486 22.1953C17.8486 24.1283 19.4156 25.6953 21.3486 25.6953C23.2816 25.6953 24.8486 24.1283 24.8486 22.1953Z" fill="#0A0B0D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 847 B |
115
apps/web/src/components/Basenames/UsernameAvatarField/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import Fieldset from 'apps/web/src/components/Fieldset';
|
||||
import FileInput from 'apps/web/src/components/FileInput';
|
||||
import Hint, { HintVariants } from 'apps/web/src/components/Hint';
|
||||
import Label from 'apps/web/src/components/Label';
|
||||
import { getUserNamePicture } from 'apps/web/src/utils/usernames';
|
||||
import { ChangeEvent, useCallback, useEffect, useId, useState } from 'react';
|
||||
import {
|
||||
ALLOWED_IMAGE_TYPE,
|
||||
MAX_IMAGE_SIZE_IN_MB,
|
||||
} from 'apps/web/pages/api/basenames/avatar/upload';
|
||||
import Image, { StaticImageData } from 'next/image';
|
||||
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
|
||||
import cameraIcon from './cameraIcon.svg';
|
||||
|
||||
export type UsernameAvatarFieldProps = {
|
||||
onChange: (file: File | undefined) => void;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export function validateAvatarUpload(file: File) {
|
||||
if (!ALLOWED_IMAGE_TYPE.includes(file.type)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Only supported image are PNG, SVG, JPEG & WebP',
|
||||
};
|
||||
}
|
||||
const bytes = file.size;
|
||||
const bytesToMegaBytes = bytes / (1024 * 1024);
|
||||
|
||||
if (bytesToMegaBytes > MAX_IMAGE_SIZE_IN_MB) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Max image size is 1Mb',
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Validate a square-ish image, with a width/height ratio of minimum 0.8
|
||||
return {
|
||||
valid: true,
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
export default function UsernameAvatarField({
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
username,
|
||||
}: UsernameAvatarFieldProps) {
|
||||
const [error, setError] = useState<string>();
|
||||
const [avatarFile, setAvatarFile] = useState<File>();
|
||||
const onChangeAvatar = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files) return;
|
||||
|
||||
const singleFile = files[0];
|
||||
if (!singleFile) return;
|
||||
|
||||
setAvatarFile(singleFile);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!avatarFile) return;
|
||||
const validationResult = validateAvatarUpload(avatarFile);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
onChange(undefined);
|
||||
setError(validationResult.message);
|
||||
return;
|
||||
} else {
|
||||
onChange(avatarFile);
|
||||
return setError('');
|
||||
}
|
||||
}, [avatarFile, onChange]);
|
||||
|
||||
const usernameAvatarFieldId = useId();
|
||||
|
||||
const defaultSelectedProfilePicture = getUserNamePicture(username);
|
||||
const currentAvatar = value; // TODO: parse IPFS
|
||||
|
||||
const avatarSrc =
|
||||
avatarFile && !error
|
||||
? URL.createObjectURL(avatarFile)
|
||||
: currentAvatar || defaultSelectedProfilePicture;
|
||||
|
||||
return (
|
||||
<Fieldset>
|
||||
<Label htmlFor={usernameAvatarFieldId} className="group relative inline-block max-w-[10rem]">
|
||||
<ImageWithLoading
|
||||
src={avatarSrc}
|
||||
alt={username}
|
||||
wrapperClassName="rounded-full h-[10rem] max-h-[10rem] min-h-[10rem] w-[10rem] min-w-[10rem] max-w-[10rem] border-4 border-white group-hover:border-blue-500"
|
||||
backgroundClassName="bg-blue-500"
|
||||
imageClassName="object-cover h-full w-full"
|
||||
width={320}
|
||||
height={320}
|
||||
/>
|
||||
<Image
|
||||
src={cameraIcon as StaticImageData}
|
||||
alt="Upload an avatar"
|
||||
className="absolute bottom-0 right-0"
|
||||
/>
|
||||
<FileInput
|
||||
id={usernameAvatarFieldId}
|
||||
onChange={onChangeAvatar}
|
||||
disabled={disabled}
|
||||
className="hidden"
|
||||
/>
|
||||
</Label>
|
||||
{error && <Hint variant={HintVariants.Error}>{error}</Hint>}
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import Fieldset from 'apps/web/src/components/Fieldset';
|
||||
import Hint from 'apps/web/src/components/Hint';
|
||||
import Label from 'apps/web/src/components/Label';
|
||||
import TextArea from 'apps/web/src/components/TextArea';
|
||||
import {
|
||||
textRecordsKeysPlaceholderForDisplay,
|
||||
USERNAME_DESCRIPTION_MAX_LENGTH,
|
||||
UsernameTextRecordKeys,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import { ChangeEvent, ReactNode, useCallback, useId } from 'react';
|
||||
|
||||
export type UsernameDescriptionFieldProps = {
|
||||
labelChildren?: ReactNode;
|
||||
onChange: (key: UsernameTextRecordKeys, value: string) => void;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function UsernameDescriptionField({
|
||||
labelChildren = 'Description',
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
}: UsernameDescriptionFieldProps) {
|
||||
const descriptionLength = value.length;
|
||||
|
||||
const onChangeDescription = useCallback(
|
||||
(event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const description = event.target.value;
|
||||
if (description.length > USERNAME_DESCRIPTION_MAX_LENGTH) {
|
||||
event.preventDefault();
|
||||
} else {
|
||||
// setDescription(value);
|
||||
if (onChange) onChange(UsernameTextRecordKeys.Description, description);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const usernameDescriptionFieldId = useId();
|
||||
const descriptionCharactersRemaining = USERNAME_DESCRIPTION_MAX_LENGTH - descriptionLength;
|
||||
|
||||
return (
|
||||
<Fieldset>
|
||||
{labelChildren && <Label htmlFor={usernameDescriptionFieldId}>{labelChildren}</Label>}
|
||||
<TextArea
|
||||
id={usernameDescriptionFieldId}
|
||||
placeholder={textRecordsKeysPlaceholderForDisplay[UsernameTextRecordKeys.Description]}
|
||||
maxLength={USERNAME_DESCRIPTION_MAX_LENGTH}
|
||||
onChange={onChangeDescription}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
/>
|
||||
<Hint>
|
||||
{String(descriptionCharactersRemaining)}{' '}
|
||||
{descriptionCharactersRemaining === 1 ? 'character' : 'characters'} remaining
|
||||
</Hint>
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import Fieldset from 'apps/web/src/components/Fieldset';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import Label from 'apps/web/src/components/Label';
|
||||
|
||||
import {
|
||||
UsernameTextRecordKeys,
|
||||
textRecordsCommunnicationKeywords,
|
||||
textRecordsCreativesKeywords,
|
||||
textRecordsEngineersKeywords,
|
||||
textRecordsKeysForDisplay,
|
||||
textRecordsKeywords,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
import { ReactNode, useCallback, useEffect, useId, useState } from 'react';
|
||||
|
||||
export type UsernameKeywordsFieldProps = {
|
||||
labelChildren?: ReactNode;
|
||||
onChange: (key: UsernameTextRecordKeys, value: string) => void;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function UsernameKeywordsField({
|
||||
labelChildren = textRecordsKeysForDisplay[UsernameTextRecordKeys.Keywords],
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
}: UsernameKeywordsFieldProps) {
|
||||
const [keywords, setKeywords] = useState<string[]>(
|
||||
value.split(',').filter((keyword) => !!keyword),
|
||||
);
|
||||
|
||||
const onClickKeyword = useCallback(
|
||||
(keyword: string) => {
|
||||
if (keywords.includes(keyword)) {
|
||||
setKeywords((previousKeywords) => previousKeywords.filter((k) => k !== keyword));
|
||||
} else {
|
||||
setKeywords([...keywords, keyword]);
|
||||
}
|
||||
},
|
||||
[keywords],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(UsernameTextRecordKeys.Keywords, keywords.join(','));
|
||||
}, [keywords, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setKeywords(value.split(',').filter((keyword) => !!keyword));
|
||||
}, [value]);
|
||||
|
||||
const usernameKeywordsFieldId = useId();
|
||||
|
||||
const renderKeyword = useCallback(
|
||||
(keyword: string) => {
|
||||
const keywordSelected = keywords.includes(keyword);
|
||||
const keywordClasses = classNames(
|
||||
'flex items-center gap-2 rounded-xl border px-3 py-2 text-sm font-bold transition-all',
|
||||
{
|
||||
'bg-white hover:bg-gray-40/5 border-gray-40/20 text-black': !keywordSelected,
|
||||
'border-[#7FD057] bg-[#7FD057]/20 text-[#195D29]':
|
||||
keywordSelected && textRecordsEngineersKeywords.includes(keyword),
|
||||
'border-[#F8BDF5] bg-[#F8BDF5]/20 text-[#741A66]':
|
||||
keywordSelected && textRecordsCreativesKeywords.includes(keyword),
|
||||
'border-[#45E1E5] bg-[#45E1E5]/20 text-[#004774]':
|
||||
keywordSelected && textRecordsCommunnicationKeywords.includes(keyword),
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={keyword}>
|
||||
<button
|
||||
type="button"
|
||||
className={keywordClasses}
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
||||
onClick={() => onClickKeyword(keyword)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{keyword}
|
||||
|
||||
<Icon
|
||||
name={keywords.includes(keyword) ? 'cross' : 'plus'}
|
||||
color="currentColor"
|
||||
width="0.75rem"
|
||||
height="0.75rem"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
},
|
||||
[disabled, keywords, onClickKeyword],
|
||||
);
|
||||
|
||||
return (
|
||||
<Fieldset>
|
||||
{labelChildren && <Label htmlFor={usernameKeywordsFieldId}>{labelChildren}</Label>}
|
||||
|
||||
<ul className="flex flex-wrap gap-2">{textRecordsKeywords.map(renderKeyword)}</ul>
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
96
apps/web/src/components/Basenames/UsernamePill/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import Dropdown from 'apps/web/src/components/Dropdown';
|
||||
import DropdownItem from 'apps/web/src/components/DropdownItem';
|
||||
import DropdownMenu from 'apps/web/src/components/DropdownMenu';
|
||||
import DropdownToggle from 'apps/web/src/components/DropdownToggle';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
|
||||
import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords';
|
||||
import { BaseName, getUserNamePicture } from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
import { Address } from 'viem';
|
||||
|
||||
export enum UsernamePillVariants {
|
||||
Inline = 'inline',
|
||||
Card = 'card',
|
||||
}
|
||||
|
||||
type UsernamePillProps = {
|
||||
variant: UsernamePillVariants;
|
||||
username: BaseName;
|
||||
address?: Address;
|
||||
};
|
||||
|
||||
export function UsernamePill({ variant, username, address }: UsernamePillProps) {
|
||||
const transitionClasses = 'transition-all duration-700 ease-in-out';
|
||||
|
||||
const pillNameClasses = classNames(
|
||||
'bg-blue-500 text-white relative leading-[2em] overflow-hidden text-ellipsis max-w-full',
|
||||
'shadow-[0px_8px_16px_0px_rgba(0,82,255,0.32),inset_0px_8px_16px_0px_rgba(255,255,255,0.25)]',
|
||||
transitionClasses,
|
||||
{
|
||||
// Note: If you change this py-5, it won't match the dropdown's height
|
||||
'rounded-[5rem] py-5 px-8 w-fit': variant === UsernamePillVariants.Inline,
|
||||
'rounded-[2rem] py-8 px-10 pt-40 w-full': variant === UsernamePillVariants.Card,
|
||||
},
|
||||
);
|
||||
|
||||
const avatarClasses = classNames(
|
||||
'flex items-center justify-center overflow-hidden rounded-full',
|
||||
'absolute',
|
||||
transitionClasses,
|
||||
{
|
||||
'h-[4rem] max-h-[4rem] min-h-[4rem] w-[4rem] min-w-[4rem] max-w-[4rem] top-4 left-4':
|
||||
variant === UsernamePillVariants.Inline,
|
||||
'h-[3rem] max-h-[3rem] min-h-[3rem] w-[3rem] min-w-[3rem] max-w-[3rem] top-10 left-10':
|
||||
variant === UsernamePillVariants.Card,
|
||||
},
|
||||
);
|
||||
|
||||
const userNameClasses = classNames(
|
||||
'overflow-y-hidden text-ellipsis whitespace-nowrap',
|
||||
transitionClasses,
|
||||
{
|
||||
'text-5xl pl-[4rem]': variant === UsernamePillVariants.Inline,
|
||||
'text-3xl pl-0 mt-20': variant === UsernamePillVariants.Card,
|
||||
},
|
||||
);
|
||||
|
||||
const { existingTextRecords, existingTextRecordsIsLoading } = useReadBaseEnsTextRecords({
|
||||
address: address,
|
||||
username: username,
|
||||
});
|
||||
|
||||
const selectedProfilePicture = existingTextRecords.avatar || getUserNamePicture(username);
|
||||
|
||||
return (
|
||||
<div className={pillNameClasses}>
|
||||
<ImageWithLoading
|
||||
src={selectedProfilePicture}
|
||||
alt={username}
|
||||
title={username}
|
||||
wrapperClassName={avatarClasses}
|
||||
imageClassName="object-cover w-full h-full"
|
||||
backgroundClassName="bg-blue-500"
|
||||
width={4 * 16}
|
||||
height={4 * 16}
|
||||
forceIsLoading={existingTextRecordsIsLoading}
|
||||
/>
|
||||
<span className={userNameClasses}>{username}</span>
|
||||
{address && (
|
||||
<div className="absolute right-4 top-4">
|
||||
<Dropdown>
|
||||
<DropdownToggle>
|
||||
<span className="inline-block p-2 opacity-50 hover:opacity-100">
|
||||
<Icon name="caret" color="currentColor" width="1.5rem" height="1.5rem" />
|
||||
</span>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<DropdownItem copyValue={username}>{username}</DropdownItem>
|
||||
<DropdownItem copyValue={address}>{address}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
apps/web/src/components/Basenames/UsernameProfile/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import UsernameProfileContent from 'apps/web/src/components/Basenames/UsernameProfileContent';
|
||||
import UsernameProfileSidebar from 'apps/web/src/components/Basenames/UsernameProfileSidebar';
|
||||
import { useUsernameProfile } from 'apps/web/src/components/Basenames/UsernameProfileContext';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { formatBaseEthDomain, USERNAME_DOMAINS } from 'apps/web/src/utils/usernames';
|
||||
import UsernameProfileNotFound from 'apps/web/src/components/Basenames/UsernameProfileNotFound';
|
||||
import classNames from 'classnames';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
|
||||
export default function UsernameProfile() {
|
||||
const { profileAddress, profileUsername, profileAddressIsLoading } = useUsernameProfile();
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const router = useRouter();
|
||||
const { basenameChain } = useBasenameChain();
|
||||
const usernameProfilePageClasses = classNames(
|
||||
'mx-auto mt-32 flex min-h-screen w-full max-w-[1440px] flex-col justify-between gap-10 px-4 px-4 pb-40 md:flex-row md:px-8',
|
||||
);
|
||||
|
||||
if (!profileUsername.endsWith(USERNAME_DOMAINS[basenameChain.id])) {
|
||||
router.push(formatBaseEthDomain(profileUsername, basenameChain.id));
|
||||
}
|
||||
|
||||
if (profileAddressIsLoading) {
|
||||
logEventWithContext('page_loading', ActionType.render);
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center">
|
||||
<Icon name="spinner" color="currentColor" height="2rem" width="2rem" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!profileAddress) {
|
||||
logEventWithContext('page_unavailable', ActionType.error, { error: 'No address resolved' });
|
||||
|
||||
return (
|
||||
<main className={classNames(usernameProfilePageClasses, 'items-center justify-center')}>
|
||||
<UsernameProfileNotFound username={profileUsername} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
logEventWithContext('page_loaded', ActionType.render);
|
||||
|
||||
return (
|
||||
<main className={usernameProfilePageClasses}>
|
||||
<div className="w-full md:max-w-[25rem]">
|
||||
<UsernameProfileSidebar />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<UsernameProfileContent />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useUsernameProfile } from 'apps/web/src/components/Basenames/UsernameProfileContext';
|
||||
import { Icon } from 'apps/web/src/components/Icon/Icon';
|
||||
import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords';
|
||||
import {
|
||||
formatSocialFieldForDisplay,
|
||||
formatSocialFieldUrl,
|
||||
textRecordsSocialFieldsEnabled,
|
||||
textRecordsSocialFieldsEnabledIcons,
|
||||
UsernameTextRecordKeys,
|
||||
UsernameTextRecords,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function UsernameProfileCard() {
|
||||
const { profileUsername, profileAddress } = useUsernameProfile();
|
||||
|
||||
const { existingTextRecords } = useReadBaseEnsTextRecords({
|
||||
address: profileAddress,
|
||||
username: profileUsername,
|
||||
});
|
||||
|
||||
const textRecordDescription = existingTextRecords[UsernameTextRecordKeys.Description];
|
||||
|
||||
const textRecordsSocial: UsernameTextRecords = textRecordsSocialFieldsEnabled.reduce(
|
||||
(previousValue, textRecordKey) => {
|
||||
previousValue[textRecordKey] = existingTextRecords[textRecordKey];
|
||||
return previousValue;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// TODO: Empty state / CTA to edit if owner
|
||||
const hasTextRecordsToDisplay =
|
||||
!!textRecordDescription || Object.values(textRecordsSocial).filter((v) => !!v).length > 0;
|
||||
|
||||
if (!hasTextRecordsToDisplay) {
|
||||
return;
|
||||
}
|
||||
|
||||
textRecordsSocialFieldsEnabled.map((textRecordKey) => (
|
||||
<li key={textRecordDescription}>{existingTextRecords[textRecordKey]}</li>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-2xl bg-[#EEF0F3] p-8 shadow-xl">
|
||||
{!!textRecordDescription && (
|
||||
<p className="text-lg font-bold text-[#5B616E]">{textRecordDescription}</p>
|
||||
)}
|
||||
|
||||
<ul className="flex flex-col gap-2">
|
||||
{textRecordsSocialFieldsEnabled.map(
|
||||
(textRecordKey) =>
|
||||
!!existingTextRecords[textRecordKey] && (
|
||||
<li key={textRecordKey}>
|
||||
<Link
|
||||
href={formatSocialFieldUrl(textRecordKey, existingTextRecords[textRecordKey])}
|
||||
target="_blank"
|
||||
className="flex items-center gap-2 text-gray-40 hover:text-blue-500"
|
||||
>
|
||||
<Icon
|
||||
name={textRecordsSocialFieldsEnabledIcons[textRecordKey]}
|
||||
height="1rem"
|
||||
width="1rem"
|
||||
color="currentColor"
|
||||
/>
|
||||
{formatSocialFieldForDisplay(textRecordKey, existingTextRecords[textRecordKey])}
|
||||
</Link>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import UsernameProfileSectionBadges from 'apps/web/src/components/Basenames/UsernameProfileSectionBadges';
|
||||
import UsernameProfileSectionExplore from 'apps/web/src/components/Basenames/UsernameProfileSectionExplore';
|
||||
|
||||
export default function UsernameProfileContent() {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 rounded-2xl border border-[#EBEBEB] p-8 shadow-lg">
|
||||
<UsernameProfileSectionBadges />
|
||||
<UsernameProfileSectionExplore />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { USERNAME_L2_RESOLVER_ADDRESSES } from 'apps/web/src/addresses/usernames';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import { BaseName } from 'apps/web/src/utils/usernames';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ReactNode, createContext, useContext, useMemo } from 'react';
|
||||
import { Address } from 'viem';
|
||||
import { useAccount, useEnsAddress } from 'wagmi';
|
||||
|
||||
export enum UsernameProfileSteps {}
|
||||
|
||||
export type UsernameProfileContextProps = {
|
||||
profileUsername: BaseName;
|
||||
profileAddressIsLoading: boolean;
|
||||
profileAddress?: Address;
|
||||
currentWalletIsOwner?: boolean;
|
||||
};
|
||||
|
||||
export const UsernameProfileContext = createContext<UsernameProfileContextProps>({
|
||||
profileUsername: 'default.basetest.eth',
|
||||
profileAddressIsLoading: true,
|
||||
profileAddress: undefined,
|
||||
currentWalletIsOwner: false,
|
||||
});
|
||||
|
||||
type UsernameProfileProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function UsernameProfileProvider({ children }: UsernameProfileProviderProps) {
|
||||
const params = useParams<{ username: BaseName }>();
|
||||
const profileUsername = params?.username;
|
||||
|
||||
const { address } = useAccount();
|
||||
|
||||
const { basenameChain } = useBasenameChain();
|
||||
|
||||
const { data: profileAddress, isLoading: profileAddressIsLoading } = useEnsAddress({
|
||||
name: profileUsername,
|
||||
chainId: basenameChain.id,
|
||||
universalResolverAddress: USERNAME_L2_RESOLVER_ADDRESSES[basenameChain.id],
|
||||
query: {
|
||||
enabled: !!profileUsername,
|
||||
},
|
||||
});
|
||||
|
||||
const currentWalletIsOwner = address === profileAddress;
|
||||
|
||||
const values = useMemo(() => {
|
||||
return {
|
||||
profileAddress: profileAddress?.toString() as Address,
|
||||
profileAddressIsLoading,
|
||||
profileUsername,
|
||||
currentWalletIsOwner,
|
||||
};
|
||||
}, [currentWalletIsOwner, profileAddress, profileAddressIsLoading, profileUsername]);
|
||||
|
||||
return (
|
||||
<UsernameProfileContext.Provider value={values}>{children}</UsernameProfileContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUsernameProfile() {
|
||||
const context = useContext(UsernameProfileContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useCount must be used within a CountProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
import { upload } from '@vercel/blob/client';
|
||||
import { useAnalytics } from 'apps/web/contexts/Analytics';
|
||||
import UsernameAvatarField from 'apps/web/src/components/Basenames/UsernameAvatarField';
|
||||
import UsernameDescriptionField from 'apps/web/src/components/Basenames/UsernameDescriptionField';
|
||||
import UsernameKeywordsField from 'apps/web/src/components/Basenames/UsernameKeywordsField';
|
||||
import { useUsernameProfile } from 'apps/web/src/components/Basenames/UsernameProfileContext';
|
||||
import UsernameTextRecordInlineField from 'apps/web/src/components/Basenames/UsernameTextRecordInlineField';
|
||||
import { Button, ButtonVariants } from 'apps/web/src/components/Button/Button';
|
||||
import Fieldset from 'apps/web/src/components/Fieldset';
|
||||
import Label from 'apps/web/src/components/Label';
|
||||
import Modal, { ModalSizes } from 'apps/web/src/components/Modal';
|
||||
import TransactionError from 'apps/web/src/components/TransactionError';
|
||||
import TransactionStatus from 'apps/web/src/components/TransactionStatus';
|
||||
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
|
||||
import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords';
|
||||
import useWriteBaseEnsTextRecords from 'apps/web/src/hooks/useWriteBaseEnsTextRecords';
|
||||
import {
|
||||
textRecordsSocialFieldsEnabled,
|
||||
UsernameTextRecordKeys,
|
||||
UsernameTextRecords,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
import { ActionType } from 'libs/base-ui/utils/logEvent';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useWaitForTransactionReceipt } from 'wagmi';
|
||||
|
||||
export default function UsernameProfileEditModal({
|
||||
isOpen,
|
||||
toggleModal,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
toggleModal: () => void;
|
||||
}) {
|
||||
const { profileUsername, profileAddress, currentWalletIsOwner } = useUsernameProfile();
|
||||
const [avatarFile, setAvatarFile] = useState<File | undefined>();
|
||||
const { logEventWithContext } = useAnalytics();
|
||||
const { basenameChain } = useBasenameChain();
|
||||
|
||||
const {
|
||||
existingTextRecords,
|
||||
existingTextRecordsIsLoading,
|
||||
refetchExistingTextRecords,
|
||||
existingTextRecordsError,
|
||||
} = useReadBaseEnsTextRecords({
|
||||
address: profileAddress,
|
||||
username: profileUsername,
|
||||
});
|
||||
|
||||
// Write text records
|
||||
const {
|
||||
writeTextRecords,
|
||||
writeTextRecordsIsPending,
|
||||
writeTextRecordsTransactionHash,
|
||||
writeTextRecordsError,
|
||||
} = useWriteBaseEnsTextRecords({
|
||||
address: profileAddress,
|
||||
username: profileUsername,
|
||||
});
|
||||
|
||||
// Wait for text record transaction to be processed
|
||||
const {
|
||||
data: transactionData,
|
||||
isFetching: transactionIsFetching,
|
||||
isSuccess: transactionIsSuccess,
|
||||
error: transactionError,
|
||||
} = useWaitForTransactionReceipt({
|
||||
hash: writeTextRecordsTransactionHash,
|
||||
chainId: basenameChain.id,
|
||||
query: {
|
||||
enabled: !!writeTextRecordsTransactionHash,
|
||||
},
|
||||
});
|
||||
|
||||
const [textRecords, setTextRecords] = useState<UsernameTextRecords>(existingTextRecords);
|
||||
|
||||
useEffect(() => {
|
||||
if (transactionIsFetching) {
|
||||
logEventWithContext('update_text_records_transaction_processing', ActionType.change);
|
||||
}
|
||||
if (!transactionData) return;
|
||||
|
||||
if (transactionData.status === 'success') {
|
||||
logEventWithContext('update_text_records_transaction_success', ActionType.change);
|
||||
|
||||
// TODO: Call to remove the previous avatar for vercel's blob
|
||||
refetchExistingTextRecords()
|
||||
.then(() => {
|
||||
toggleModal();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (transactionData.status === 'reverted') {
|
||||
logEventWithContext('update_text_records_transaction_reverted', ActionType.change, {
|
||||
error: `Transaction reverted: ${transactionData.transactionHash}`,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
refetchExistingTextRecords,
|
||||
transactionIsSuccess,
|
||||
transactionData,
|
||||
logEventWithContext,
|
||||
transactionIsFetching,
|
||||
toggleModal,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setTextRecords(existingTextRecords);
|
||||
}, [existingTextRecords]);
|
||||
|
||||
const updateTextRecords = useCallback((key: UsernameTextRecordKeys, value: string) => {
|
||||
setTextRecords((previousTextRecords) => {
|
||||
return {
|
||||
...previousTextRecords,
|
||||
[key]: value,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const uploadAvatar = useCallback(
|
||||
async (file: File | undefined) => {
|
||||
if (!file) return Promise.resolve();
|
||||
if (!currentWalletIsOwner) return false;
|
||||
|
||||
logEventWithContext('avatar_upload_initiated', ActionType.change);
|
||||
|
||||
// TODO: Rename .name to username.[jpeg/webp/svg/png]
|
||||
const timestamp = Date.now();
|
||||
const newBlob = await upload(
|
||||
`basenames/avatar/${profileUsername}/${timestamp}/${file.name}`,
|
||||
file,
|
||||
{
|
||||
access: 'public',
|
||||
handleUploadUrl: `/api/basenames/avatar/upload?username=${profileUsername}`,
|
||||
},
|
||||
);
|
||||
|
||||
updateTextRecords(UsernameTextRecordKeys.Avatar, newBlob.url);
|
||||
|
||||
return newBlob;
|
||||
},
|
||||
[currentWalletIsOwner, logEventWithContext, profileUsername, updateTextRecords],
|
||||
);
|
||||
|
||||
const onClickSave = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
// TODO: We can't really get to this steps, but we should show an error
|
||||
if (!currentWalletIsOwner) return false;
|
||||
|
||||
let writeTextRecordsRequest = { ...textRecords };
|
||||
|
||||
// TODO: Clean this up
|
||||
// Upload the avatar first
|
||||
uploadAvatar(avatarFile)
|
||||
.then((result) => {
|
||||
// set the uploaded result as the url
|
||||
if (result) {
|
||||
logEventWithContext('avatar_upload_success', ActionType.change);
|
||||
writeTextRecordsRequest[UsernameTextRecordKeys.Avatar] = result.url;
|
||||
}
|
||||
|
||||
// Write the records
|
||||
writeTextRecords(writeTextRecordsRequest)
|
||||
.then((transactionResult) => {
|
||||
// We updated some text records
|
||||
if (transactionResult) {
|
||||
logEventWithContext('update_text_records_transaction_approved', ActionType.change);
|
||||
} else {
|
||||
// No text records had to be updated, simply go to profile
|
||||
toggleModal();
|
||||
}
|
||||
})
|
||||
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
logEventWithContext('update_text_records_transaction_canceled', ActionType.click);
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
logEventWithContext('avatar_upload_failed', ActionType.error);
|
||||
});
|
||||
|
||||
logEventWithContext('update_text_records_transaction_initiated', ActionType.change);
|
||||
},
|
||||
[
|
||||
avatarFile,
|
||||
currentWalletIsOwner,
|
||||
logEventWithContext,
|
||||
uploadAvatar,
|
||||
textRecords,
|
||||
toggleModal,
|
||||
writeTextRecords,
|
||||
],
|
||||
);
|
||||
|
||||
const onChangeTextRecord = useCallback(
|
||||
(key: UsernameTextRecordKeys, value: string) => {
|
||||
updateTextRecords(key, value);
|
||||
},
|
||||
[updateTextRecords],
|
||||
);
|
||||
|
||||
const onChangeAvatar = useCallback((file: File | undefined) => {
|
||||
setAvatarFile(file);
|
||||
}, []);
|
||||
|
||||
const formClasses = classNames(
|
||||
'flex flex-col justify-between gap-8 text-gray/60 md:items-center mt-6',
|
||||
);
|
||||
|
||||
const isLoading =
|
||||
existingTextRecordsIsLoading || writeTextRecordsIsPending || transactionIsFetching;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={toggleModal}
|
||||
title="Manage Profile"
|
||||
titleAlign="left"
|
||||
modalAlign="top"
|
||||
size={ModalSizes.Large}
|
||||
>
|
||||
{!currentWalletIsOwner ? (
|
||||
<p>You don't have the permission to edit this profile</p>
|
||||
) : (
|
||||
<form className={formClasses}>
|
||||
<UsernameAvatarField
|
||||
onChange={onChangeAvatar}
|
||||
value={textRecords[UsernameTextRecordKeys.Avatar]}
|
||||
disabled={isLoading}
|
||||
username={profileUsername}
|
||||
/>
|
||||
<UsernameDescriptionField
|
||||
onChange={onChangeTextRecord}
|
||||
value={textRecords[UsernameTextRecordKeys.Description]}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Fieldset>
|
||||
<Label>Socials</Label>
|
||||
{textRecordsSocialFieldsEnabled.map((textRecordKey) => (
|
||||
<UsernameTextRecordInlineField
|
||||
key={textRecordKey}
|
||||
textRecordKey={textRecordKey}
|
||||
onChange={onChangeTextRecord}
|
||||
value={textRecords[textRecordKey]}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
))}
|
||||
</Fieldset>
|
||||
<div className="mb-2">
|
||||
<UsernameKeywordsField
|
||||
onChange={onChangeTextRecord}
|
||||
value={textRecords[UsernameTextRecordKeys.Keywords]}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant={ButtonVariants.Black}
|
||||
rounded
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onClickSave}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
{writeTextRecordsError && <TransactionError error={writeTextRecordsError} />}
|
||||
{existingTextRecordsError && <TransactionError error={existingTextRecordsError} />}
|
||||
{transactionError && <TransactionError error={existingTextRecordsError} />}
|
||||
{transactionData && transactionData.status === 'reverted' && (
|
||||
<TransactionStatus transaction={transactionData} chainId={transactionData.chainId} />
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
textRecordsCommunnicationKeywords,
|
||||
textRecordsCreativesKeywords,
|
||||
textRecordsEngineersKeywords,
|
||||
textRecordsKeysForDisplay,
|
||||
UsernameTextRecordKeys,
|
||||
} from 'apps/web/src/utils/usernames';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type UsernameProfileKeywordsProps = {
|
||||
keywords: string;
|
||||
};
|
||||
|
||||
export default function UsernameProfileKeywords({ keywords }: UsernameProfileKeywordsProps) {
|
||||
const keywordsArray = keywords.split(',').filter((k) => !!k);
|
||||
|
||||
const renderKeyword = (keyword: string) => {
|
||||
const keywordClasses = classNames(
|
||||
'flex items-center gap-2 rounded-xl border px-3 py-2 text-sm font-bold transition-all',
|
||||
{
|
||||
'border-[#7FD057] bg-[#7FD057]/20 text-[#195D29]':
|
||||
textRecordsEngineersKeywords.includes(keyword),
|
||||
'border-[#F8BDF5] bg-[#F8BDF5]/20 text-[#741A66]':
|
||||
textRecordsCreativesKeywords.includes(keyword),
|
||||
'border-[#45E1E5] bg-[#45E1E5]/20 text-[#004774]':
|
||||
textRecordsCommunnicationKeywords.includes(keyword),
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={keyword}>
|
||||
<span className={keywordClasses}>{keyword}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<h2 className="font-bold uppercase text-[#5B616E]">
|
||||
{textRecordsKeysForDisplay[UsernameTextRecordKeys.Keywords]}
|
||||
</h2>
|
||||
<ul className="mt-4 flex flex-wrap gap-2">{keywordsArray.map(renderKeyword)}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { BaseName } from 'apps/web/src/utils/usernames';
|
||||
import notFoundIllustration from './notFoundIllustration.svg';
|
||||
import Image, { StaticImageData } from 'next/image';
|
||||
import { Button, ButtonVariants } from 'apps/web/src/components/Button/Button';
|
||||
import Link from 'next/link';
|
||||
import { claimQueryKey } from 'apps/web/src/components/Basenames/RegistrationFlow';
|
||||
|
||||
export default function UsernameProfileNotFound({ username }: { username: BaseName }) {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center gap-8 text-center">
|
||||
<Image src={notFoundIllustration as StaticImageData} alt="404 Illustration" />
|
||||
<h2 className="break-all text-3xl font-bold ">{username} is not found</h2>
|
||||
<p className="text-lg">
|
||||
There's no profile associated with this name, but it could be yours!
|
||||
</p>
|
||||
<Link href={`/name?${claimQueryKey}=${username}`}>
|
||||
<Button variant={ButtonVariants.Black} rounded>
|
||||
Register name
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<svg width="428" height="424" viewBox="0 0 428 424" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="219.5" cy="256.5" r="49.5" fill="#0A0B0D"/>
|
||||
<path d="M242.208 244.354C258.807 226.509 247.774 215.479 229.925 232.074C228.33 233.559 227.339 235.594 227.199 237.769C227.199 237.814 227.194 237.854 227.189 237.899C226.844 243.124 231.16 247.439 236.386 247.094C236.432 247.094 236.472 247.089 236.517 247.084C238.692 246.944 240.727 245.954 242.213 244.359L242.208 244.354Z" fill="white"/>
|
||||
<path d="M195.076 266.93C178.477 284.775 189.51 295.805 207.359 279.21C208.954 277.725 209.944 275.69 210.084 273.515C210.084 273.47 210.089 273.43 210.094 273.385C210.439 268.16 206.124 263.845 200.897 264.19C200.852 264.19 200.812 264.195 200.767 264.2C198.592 264.34 196.556 265.33 195.071 266.925L195.076 266.93Z" fill="white"/>
|
||||
<path d="M207.355 232.077C189.511 215.475 178.477 226.512 195.076 244.359C196.561 245.954 198.595 246.944 200.77 247.084C200.815 247.084 200.855 247.089 200.9 247.094C206.125 247.439 210.439 243.124 210.094 237.898C210.094 237.853 210.089 237.813 210.084 237.768C209.944 235.593 208.955 233.558 207.36 232.072L207.355 232.077Z" fill="white"/>
|
||||
<path d="M229.929 279.208C247.775 295.807 258.805 284.774 242.21 266.925C240.725 265.33 238.69 264.34 236.515 264.2C236.47 264.2 236.43 264.195 236.385 264.19C231.159 263.845 226.844 268.16 227.189 273.387C227.189 273.432 227.194 273.472 227.199 273.517C227.339 275.692 228.329 277.728 229.924 279.213L229.929 279.208Z" fill="white"/>
|
||||
<path d="M227.327 230.992C226.442 206.659 210.843 206.659 209.958 230.992C209.878 233.165 210.618 235.303 212.058 236.941C212.088 236.976 212.113 237.006 212.143 237.041C215.593 240.978 221.692 240.978 225.142 237.041C225.172 237.006 225.197 236.976 225.227 236.941C226.667 235.308 227.402 233.17 227.327 230.992Z" fill="white"/>
|
||||
<path d="M193.992 246.957C169.659 247.842 169.659 263.441 193.992 264.326C196.165 264.406 198.303 263.666 199.941 262.226C199.971 262.196 200.006 262.171 200.041 262.141C203.977 258.692 203.977 252.592 200.041 249.142C200.006 249.112 199.976 249.087 199.941 249.057C198.308 247.617 196.17 246.882 193.992 246.957Z" fill="white"/>
|
||||
<path d="M237.343 249.058C237.308 249.088 237.278 249.113 237.243 249.143C233.306 252.593 233.306 258.692 237.243 262.142C237.278 262.172 237.308 262.197 237.343 262.227C238.976 263.667 241.114 264.402 243.292 264.327C267.625 263.442 267.625 247.843 243.292 246.958C241.119 246.878 238.981 247.618 237.343 249.058Z" fill="white"/>
|
||||
<path d="M209.957 280.292C210.842 304.625 226.441 304.625 227.326 280.292C227.406 278.119 226.666 275.981 225.226 274.343C225.196 274.308 225.171 274.278 225.141 274.243C221.692 270.306 215.592 270.306 212.142 274.243C212.112 274.278 212.087 274.308 212.057 274.343C210.617 275.976 209.882 278.114 209.957 280.292Z" fill="white"/>
|
||||
<circle cx="218.659" cy="255.659" r="29.2545" fill="white"/>
|
||||
<path d="M201.218 247L200 248.218L206.201 254.42L200 260.621L201.218 261.839L207.42 255.638L213.621 261.839L214.839 260.621L208.638 254.42L214.839 248.218L213.621 247L207.42 253.201L201.218 247Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M225.379 247L224.161 248.218L230.362 254.42L224.161 260.621L225.379 261.839L231.581 255.638L237.782 261.839L239 260.621L232.799 254.42L239 248.218L237.782 247L231.581 253.201L225.379 247Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<circle cx="105.048" cy="152.048" r="77.6067" transform="rotate(-15 105.048 152.048)" fill="#0A0B0D"/>
|
||||
<path d="M162.875 148.415C162.336 144.672 161.577 140.916 160.574 137.17C159.57 133.425 158.351 129.8 156.944 126.282C164.256 118.885 163.018 105.132 151.875 100.528C147.225 98.6067 142.415 97.0404 137.485 95.866C133.998 92.1936 130.242 88.8013 126.253 85.7352C116.696 78.3947 104.166 84.1916 101.415 94.2222C97.6722 94.7611 93.9165 95.5195 90.1705 96.5232C86.4245 97.527 82.8003 98.746 79.2822 100.153C71.8847 92.8413 58.1321 94.0789 53.528 105.222C51.6067 109.872 50.0405 114.682 48.866 119.611C45.1937 123.099 41.8013 126.855 38.7352 130.844C31.3947 140.4 37.1917 152.931 47.2222 155.682C47.7611 159.425 48.5195 163.18 49.5233 166.926C50.527 170.672 51.746 174.297 53.1526 177.815C45.8413 185.212 47.0789 198.965 58.2219 203.569C62.8716 205.49 67.682 207.056 72.6115 208.231C76.0988 211.903 79.855 215.295 83.8437 218.362C93.4005 225.702 105.931 219.905 108.682 209.875C112.425 209.336 116.18 208.577 119.926 207.574C123.672 206.57 127.297 205.351 130.815 203.944C138.212 211.256 151.965 210.018 156.569 198.875C158.49 194.225 160.056 189.415 161.231 184.485C164.903 180.998 168.296 177.242 171.362 173.253C178.702 163.696 172.905 151.165 162.875 148.415Z" fill="white"/>
|
||||
<path d="M69.9678 142.933L68.3387 145.613L81.9818 153.907L73.688 167.55L76.3678 169.179L84.6615 155.536L98.3045 163.83L99.9336 161.15L86.2905 152.856L94.5843 139.213L91.9045 137.584L83.6108 151.227L69.9678 142.933Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M112.701 132.513L111.072 135.193L124.715 143.486L116.421 157.129L119.101 158.758L127.395 145.115L141.038 153.409L142.667 150.729L129.024 142.436L137.317 128.793L134.638 127.163L126.344 140.807L112.701 132.513Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<circle cx="298.162" cy="107.162" r="94.838" fill="#0A0B0D"/>
|
||||
<path d="M350.111 107.162C396.076 121.547 386.842 132.459 341.316 125.336C369.318 149.477 361.023 155.588 328.176 137.165C346.605 170.006 340.488 178.312 316.341 150.316C323.464 195.847 312.552 205.076 298.162 159.111C283.778 205.076 272.866 195.842 279.988 150.316C255.848 178.318 249.736 170.023 268.16 137.176C235.318 155.605 227.012 149.488 255.009 125.341C209.477 132.464 200.249 121.552 246.213 107.162C200.249 92.7775 209.483 81.8655 255.009 88.9883C227.006 64.8474 235.301 58.7358 268.149 77.1595C249.719 44.3179 255.836 36.0117 279.983 64.0085C272.86 18.4769 283.772 9.24836 298.162 55.2133C312.547 9.24836 323.459 18.4825 316.336 64.0085C340.477 36.0061 346.588 44.3013 328.165 77.1484C361.006 58.7191 369.313 64.8363 341.316 88.9827C386.847 81.8599 396.076 92.7719 350.111 107.162Z" fill="white"/>
|
||||
<path d="M268.968 95L267 96.9677L277.018 106.985L267 117.003L268.968 118.971L278.985 108.953L289.003 118.971L290.971 117.003L280.953 106.985L290.971 96.9677L289.003 95L278.985 105.018L268.968 95Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M307.997 95L306.029 96.9677L316.047 106.985L306.029 117.003L307.997 118.971L318.015 108.953L328.032 118.971L330 117.003L319.982 106.985L330 96.9677L328.032 95L318.015 105.018L307.997 95Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<circle cx="125.565" cy="347.565" r="56" transform="rotate(-28.3225 125.565 347.565)" fill="#0A0B0D"/>
|
||||
<path d="M165.433 326.077C169.231 331.703 167.847 338.269 166.202 343.663C164.393 349.206 162.326 353.575 161.008 358.175C159.579 362.743 158.901 367.538 157.364 373.162C155.77 378.574 153.316 384.82 147.052 387.433C141.426 391.232 134.86 389.848 129.466 388.202C123.923 386.393 119.554 384.326 114.954 383.008C110.386 381.579 105.591 380.901 99.9666 379.364C94.5548 377.77 88.3095 375.316 85.696 369.052C81.8975 363.426 83.2814 356.86 84.9273 351.466C86.7356 345.924 88.8028 341.555 90.1211 336.954C91.5496 332.386 92.2278 327.591 93.765 321.967C95.3572 316.552 97.8114 310.307 104.077 307.696C109.703 303.898 116.269 305.282 121.663 306.927C127.206 308.736 131.574 310.803 136.175 312.121C140.743 313.55 145.538 314.228 151.162 315.765C156.574 317.359 162.819 319.813 165.433 326.077Z" fill="white"/>
|
||||
<path d="M98.1267 349.969L97.4013 352.257L109.05 355.95L105.357 367.599L107.645 368.324L111.338 356.675L122.987 360.368L123.713 358.08L112.064 354.387L115.757 342.738L113.469 342.013L109.776 353.662L98.1267 349.969Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M128.013 334.471L127.288 336.759L138.937 340.452L135.244 352.101L137.532 352.826L141.225 341.177L152.874 344.87L153.599 342.582L141.95 338.889L145.643 327.24L143.355 326.515L139.662 338.164L128.013 334.471Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<circle cx="347.79" cy="277.791" r="55.103" transform="rotate(18.513 347.79 277.791)" fill="#0A0B0D"/>
|
||||
<path d="M307.484 292.603C317.231 292.672 328.376 292.548 335.16 290.388C340.441 288.88 343.362 286.269 345.439 282.866C345.864 282.171 346.693 282.449 346.611 283.258C346.22 287.221 346.98 291.065 350.287 295.453C354.402 301.263 363.229 308.074 371.049 313.888C379.527 319.736 381.109 321.269 379.834 315.922C378.858 310.997 375.03 299.212 379.518 288.02C379.525 287.999 379.532 287.977 379.541 287.952C382.982 276.49 393.056 269.679 396.602 266.477C400.649 263.135 398.15 263.411 387.872 262.993C378.12 262.928 367.066 263.068 360.332 265.235C355.102 266.745 352.664 267.955 350.603 271.335C350.178 272.029 349.349 271.752 349.431 270.942C349.822 266.999 348.603 264.569 345.338 260.214C341.266 254.429 332.525 247.661 324.7 241.84C316.243 235.985 314.417 234.261 315.633 239.365C316.535 244.056 320.475 255.564 316.316 266.791C316.309 266.812 316.302 266.833 316.293 266.859C313.137 278.497 302.983 285.6 299.239 288.944C295.002 292.442 297.187 292.175 307.478 292.611L307.484 292.603Z" fill="white"/>
|
||||
<path d="M332.978 237.484C332.909 247.231 333.033 258.376 335.193 265.16C336.701 270.441 339.312 273.362 342.715 275.439C343.41 275.864 343.132 276.693 342.323 276.611C338.36 276.22 334.516 276.98 330.128 280.287C324.318 284.402 317.507 293.229 311.693 301.049C305.845 309.527 304.312 311.109 309.66 309.834C314.584 308.858 326.369 305.03 337.561 309.518C337.582 309.525 337.604 309.532 337.63 309.541C349.091 312.982 355.902 323.056 359.104 326.602C362.446 330.649 362.17 328.15 362.588 317.872C362.653 308.12 362.513 297.066 360.346 290.332C358.836 285.102 357.626 282.664 354.246 280.603C353.552 280.178 353.829 279.349 354.639 279.431C358.582 279.823 361.012 278.603 365.367 275.338C371.152 271.266 377.921 262.525 383.741 254.7C389.597 246.243 391.32 244.417 386.216 245.633C381.525 246.535 370.017 250.475 358.791 246.316C358.769 246.309 358.748 246.302 358.722 246.293C347.084 243.137 339.981 232.983 336.637 229.239C333.14 225.002 333.406 227.187 332.97 237.478L332.978 237.484Z" fill="white"/>
|
||||
<path d="M329.011 260.242L326.967 261.226L331.974 271.631L321.569 276.638L322.552 278.682L332.958 273.675L337.965 284.081L340.009 283.097L335.002 272.691L345.407 267.684L344.424 265.641L334.018 270.648L329.011 260.242Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M359.035 270.758L356.992 271.742L361.999 282.148L351.593 287.155L352.577 289.198L362.982 284.191L367.989 294.597L370.033 293.613L365.026 283.208L375.432 278.201L374.448 276.157L364.042 281.164L359.035 270.758Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<circle cx="269" cy="366" r="31" transform="rotate(15 269 366)" fill="#0A0B0D"/>
|
||||
<path d="M291.3 365.714C290.315 364.957 289.408 364.193 288.569 363.411C289.177 362.439 289.854 361.464 290.61 360.479C294.158 355.861 290.345 349.256 284.572 350.02C283.34 350.183 282.157 350.284 281.012 350.322C280.754 349.207 280.546 348.037 280.383 346.805C279.628 341.031 272.26 339.057 268.719 343.68C267.963 344.665 267.198 345.571 266.416 346.411C265.444 345.803 264.47 345.126 263.484 344.37C258.866 340.822 252.261 344.635 253.026 350.408C253.188 351.64 253.29 352.823 253.328 353.968C252.212 354.226 251.042 354.434 249.81 354.596C244.037 355.352 242.062 362.72 246.685 366.261C247.67 367.017 248.577 367.782 249.417 368.564C248.808 369.536 248.132 370.51 247.375 371.496C243.827 376.113 247.64 382.719 253.414 381.954C254.645 381.792 255.828 381.69 256.973 381.652C257.231 382.768 257.439 383.938 257.602 385.17C258.357 390.943 265.725 392.918 269.266 388.295C270.023 387.31 270.787 386.403 271.569 385.563C272.541 386.172 273.516 386.848 274.501 387.605C279.119 391.153 285.724 387.34 284.96 381.566C284.797 380.335 284.696 379.152 284.658 378.007C285.773 377.749 286.943 377.54 288.175 377.378C293.949 376.623 295.923 369.255 291.3 365.714ZM272.478 371.807C272.416 371.844 272.353 371.88 272.29 371.916C271.515 372.378 270.686 372.657 269.848 372.777C269.032 372.865 268.188 372.804 267.346 372.567C267.275 372.547 267.206 372.529 267.134 372.51C266.287 372.295 265.523 371.922 264.862 371.441C264.197 370.918 263.618 370.262 263.178 369.475C263.142 369.412 263.105 369.349 263.069 369.286C262.608 368.511 262.329 367.682 262.208 366.845C262.12 366.031 262.182 365.184 262.419 364.342C262.438 364.271 262.456 364.202 262.476 364.131C262.691 363.283 263.064 362.519 263.544 361.858C264.067 361.194 264.724 360.615 265.511 360.174C265.574 360.138 265.636 360.102 265.699 360.065C266.474 359.604 267.304 359.325 268.141 359.204C268.954 359.116 269.802 359.178 270.643 359.415C270.715 359.434 270.783 359.453 270.855 359.472C271.702 359.687 272.466 360.06 273.127 360.541C273.792 361.064 274.371 361.72 274.811 362.507C274.847 362.57 274.884 362.633 274.92 362.695C275.381 363.47 275.66 364.3 275.781 365.137C275.869 365.953 275.808 366.798 275.57 367.639C275.551 367.711 275.533 367.78 275.514 367.851C275.298 368.698 274.926 369.462 274.445 370.123C273.922 370.788 273.266 371.367 272.478 371.807Z" fill="white"/>
|
||||
<circle cx="268.831" cy="364.714" r="9.15533" transform="rotate(15 268.831 364.714)" fill="white"/>
|
||||
<path d="M260.706 352.39L259.567 352.836L261.841 358.638L256.04 360.912L256.486 362.051L262.288 359.777L264.562 365.579L265.701 365.132L263.427 359.33L269.229 357.056L268.782 355.917L262.98 358.191L260.706 352.39Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<path d="M276.438 359.26L275.298 359.707L277.573 365.509L271.771 367.783L272.218 368.922L278.019 366.648L280.294 372.449L281.433 372.003L279.159 366.201L284.96 363.927L284.514 362.788L278.712 365.062L276.438 359.26Z" fill="#0A0B0D" stroke="black" stroke-linecap="square" stroke-linejoin="round"/>
|
||||
<circle cx="169.5" cy="31.5" r="31.5" fill="#89909E"/>
|
||||
<circle cx="31.5" cy="273.5" r="31.5" fill="#89909E"/>
|
||||
<circle cx="409.5" cy="199.5" r="18.5" fill="#89909E"/>
|
||||
<circle cx="204.5" cy="404.5" r="18.5" fill="#89909E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |