From e7e63f2aea7a936e9f1c84aa03dc4d638d40b68a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Galley?=
Date: Mon, 4 Nov 2024 11:28:17 -0500
Subject: [PATCH 01/83] Github action: check for large files (#1199)
* github action: check for large files
* fix action
* comment setup
* fix file checker
---
.github/workflows/file-size-checker.yml | 128 ++++++++++++++++++++++++
1 file changed, 128 insertions(+)
create mode 100644 .github/workflows/file-size-checker.yml
diff --git a/.github/workflows/file-size-checker.yml b/.github/workflows/file-size-checker.yml
new file mode 100644
index 0000000000..18fbc30fc9
--- /dev/null
+++ b/.github/workflows/file-size-checker.yml
@@ -0,0 +1,128 @@
+name: File Size Checker
+
+# Add required permissions
+permissions:
+ contents: read
+ pull-requests: write
+ statuses: write
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+
+jobs:
+ check-file-sizes:
+ name: File Size Check
+ runs-on: ubuntu-latest
+
+ steps:
+ # - name: Setup environment
+ # run: |
+ # apt-get update
+ # apt-get install -y git bc
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check file sizes
+ id: check-sizes
+ run: |
+ # Initialize variables for tracking findings
+ large_files=""
+ huge_files=""
+
+ # Get all files in the PR
+ echo "Files changed in PR:"
+ git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}
+
+ for file in $(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}); do
+ if [ -f "$file" ]; then
+ size=$(stat -c%s "$file")
+ size_mb=$(echo "scale=2; $size/1048576" | bc)
+
+ echo "Checking $file: ${size_mb}MB"
+
+ # Check for files over 40MB
+ if (( $(echo "$size_mb > 40" | bc -l) )); then
+ huge_files="${huge_files}* ${file} (${size_mb}MB)\n"
+ # Check for files over 10MB
+ elif (( $(echo "$size_mb > 10" | bc -l) )); then
+ large_files="${large_files}* ${file} (${size_mb}MB)\n"
+ fi
+ fi
+ done
+
+ # Print findings for debugging
+ echo "Large files found:"
+ echo -e "$large_files"
+ echo "Huge files found:"
+ echo -e "$huge_files"
+
+ # Set outputs for use in next steps
+ echo "large_files<> $GITHUB_OUTPUT
+ echo -e "$large_files" >> $GITHUB_OUTPUT
+ echo "EOF" >> $GITHUB_OUTPUT
+
+ echo "huge_files<> $GITHUB_OUTPUT
+ echo -e "$huge_files" >> $GITHUB_OUTPUT
+ echo "EOF" >> $GITHUB_OUTPUT
+
+ # Fail if huge files are found
+ if [ ! -z "$huge_files" ]; then
+ echo "❌ Files over 40MB found!"
+ exit 1
+ fi
+
+ - name: Update Status and Comment
+ if: always()
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const hugeFiles = `${{ steps.check-sizes.outputs.huge_files }}`;
+ const largeFiles = `${{ steps.check-sizes.outputs.large_files }}`;
+
+ try {
+ console.log('Repository:', context.payload.repository.name);
+ console.log('Owner:', context.payload.repository.owner.login);
+ console.log('SHA:', context.payload.pull_request.head.sha);
+
+ // Set status check that will be used by branch protection
+ await github.rest.repos.createCommitStatus({
+ owner: context.payload.repository.owner.login,
+ repo: context.payload.repository.name,
+ sha: context.payload.pull_request.head.sha,
+ state: hugeFiles ? 'failure' : 'success',
+ context: 'File Size Check',
+ description: hugeFiles ? 'Files over 40MB found' : 'All files within size limits',
+ target_url: `https://github.com/${context.payload.repository.owner.login}/${context.payload.repository.name}/actions/runs/${context.runId}`
+ });
+
+ // Only comment if issues were found
+ if (hugeFiles || largeFiles) {
+ let comment = '## ⚠️ File Size Check Results\n\n';
+
+ if (hugeFiles) {
+ comment += '### 🚫 Files over 40MB (Not Allowed):\n' + hugeFiles + '\n';
+ comment += '**These files must be removed from git history before the PR can be merged.**\n\n';
+ }
+
+ if (largeFiles) {
+ comment += '### ⚠️ Large Files (Over 10MB):\n' + largeFiles + '\n';
+ comment += 'Consider reducing the size of these files if possible.\n';
+ }
+
+ await github.rest.issues.createComment({
+ issue_number: context.payload.pull_request.number,
+ owner: context.payload.repository.owner.login,
+ repo: context.payload.repository.name,
+ body: comment
+ });
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ console.error('Context:', JSON.stringify(context.payload, null, 2));
+ core.setFailed(error.message);
+ }
From dc16fec1cd016be8cb25862cc9e2dcf9ff05755a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Galley?=
Date: Mon, 4 Nov 2024 15:33:14 -0500
Subject: [PATCH 02/83] Feat: IPFS + Cloudinary integration (#1201)
* IPFS + Cloudinary integration
* faster proxy
* lint
* mb feedback
---
.../name/[username]/opengraph-image.tsx | 36 ++++++++++-----
.../basenames/[name]/assets/cardImage.svg.tsx | 45 ++++++++++++-------
.../RegistrationProfileForm/index.tsx | 3 --
.../Basenames/UsernameProfileCard/index.tsx | 3 +-
.../Basenames/UsernameProfileCasts/index.tsx | 3 +-
.../UsernameProfileSectionFrames/Context.tsx | 3 +-
.../UsernameProfileSettingsAvatar/index.tsx | 3 +-
.../index.tsx | 3 +-
.../UsernameProfileSidebar/index.tsx | 1 -
.../ConnectWalletButton/UserAvatar.tsx | 4 --
.../src/components/WalletIdentity/index.tsx | 4 --
apps/web/src/hooks/useBaseEnsAvatar.ts | 39 ++++++++--------
.../src/hooks/useReadBaseEnsTextRecords.ts | 13 +-----
.../src/hooks/useWriteBaseEnsTextRecords.ts | 5 +--
apps/web/src/utils/images.ts | 9 +++-
apps/web/src/utils/pinata.ts | 2 +-
apps/web/src/utils/urls.ts | 34 ++++++++------
apps/web/src/utils/usernames.ts | 2 +-
18 files changed, 114 insertions(+), 98 deletions(-)
diff --git a/apps/web/app/(basenames)/name/[username]/opengraph-image.tsx b/apps/web/app/(basenames)/name/[username]/opengraph-image.tsx
index bcd0ef107a..073788206c 100644
--- a/apps/web/app/(basenames)/name/[username]/opengraph-image.tsx
+++ b/apps/web/app/(basenames)/name/[username]/opengraph-image.tsx
@@ -7,11 +7,16 @@ import { isDevelopment } from 'apps/web/src/constants';
import {
formatBaseEthDomain,
getBasenameImage,
+ getChainForBasename,
USERNAME_DOMAINS,
+ UsernameTextRecordKeys,
} from 'apps/web/src/utils/usernames';
import { base, baseSepolia } from 'viem/chains';
import { USERNAME_L2_RESOLVER_ADDRESSES } from 'apps/web/src/addresses/usernames';
-import { CLOUDFARE_IPFS_PROXY } from 'apps/web/src/utils/urls';
+import { getIpfsGatewayUrl, IpfsUrl, IsValidIpfsUrl } from 'apps/web/src/utils/urls';
+import { Basename } from '@coinbase/onchainkit/identity';
+import { getCloudinaryMediaUrl } from 'apps/web/src/utils/images';
+import { logger } from 'apps/web/src/utils/logger';
export const runtime = 'edge';
const size = {
@@ -63,24 +68,35 @@ export default async function OpenGraphImage(props: ImageRouteProps) {
const domainName = isDevelopment ? `http://localhost:3000` : 'https://www.base.org';
const profilePicture = getBasenameImage(username);
+ const chain = getChainForBasename(username as Basename);
let imageSource = domainName + profilePicture.src;
// NOTE: Do we want to fail if the name doesn't exists?
try {
- const client = getBasenamePublicClient(base.id);
- const avatar = await client.getEnsAvatar({
+ const client = getBasenamePublicClient(chain.id);
+ const avatar = await client.getEnsText({
name: username,
- universalResolverAddress: USERNAME_L2_RESOLVER_ADDRESSES[base.id],
- assetGatewayUrls: {
- ipfs: CLOUDFARE_IPFS_PROXY,
- },
+ key: UsernameTextRecordKeys.Avatar,
+ universalResolverAddress: USERNAME_L2_RESOLVER_ADDRESSES[chain.id],
});
- // Satori Doesn't support webp
- if (avatar && !avatar.endsWith('.webp')) {
+ if (!avatar) return;
+
+ // IPFS Resolution
+ if (IsValidIpfsUrl(avatar)) {
+ const ipfsUrl = getIpfsGatewayUrl(avatar as IpfsUrl);
+ if (ipfsUrl) {
+ imageSource = ipfsUrl;
+ }
+ } else {
imageSource = avatar;
}
- } catch (error) {}
+
+ // Cloudinary resize / fetch
+ imageSource = getCloudinaryMediaUrl({ media: imageSource, format: 'png', width: 80 });
+ } catch (error) {
+ logger.error('Error fetching basename Avatar:', error);
+ }
return new ImageResponse(
(
diff --git a/apps/web/pages/api/basenames/[name]/assets/cardImage.svg.tsx b/apps/web/pages/api/basenames/[name]/assets/cardImage.svg.tsx
index 8119f167c0..deabe8d199 100644
--- a/apps/web/pages/api/basenames/[name]/assets/cardImage.svg.tsx
+++ b/apps/web/pages/api/basenames/[name]/assets/cardImage.svg.tsx
@@ -1,13 +1,20 @@
import satori from 'satori';
import { NextRequest } from 'next/server';
-import { getBasenameImage } from 'apps/web/src/utils/usernames';
+import {
+ getBasenameImage,
+ getChainForBasename,
+ UsernameTextRecordKeys,
+} from 'apps/web/src/utils/usernames';
import twemoji from 'twemoji';
-import { base } from 'viem/chains';
import { getBasenamePublicClient } from 'apps/web/src/hooks/useBasenameChain';
import { USERNAME_L2_RESOLVER_ADDRESSES } from 'apps/web/src/addresses/usernames';
import { isDevelopment } from 'apps/web/src/constants';
import ImageRaw from 'apps/web/src/components/ImageRaw';
-import { CLOUDFARE_IPFS_PROXY } from 'apps/web/src/utils/urls';
+import { getIpfsGatewayUrl, IpfsUrl, IsValidIpfsUrl } from 'apps/web/src/utils/urls';
+import { logger } from 'apps/web/src/utils/logger';
+import { Basename } from '@coinbase/onchainkit/identity';
+import { getCloudinaryMediaUrl } from 'apps/web/src/utils/images';
+
const emojiCache: Record> = {};
export async function loadEmoji(emojiString: string) {
@@ -37,26 +44,34 @@ export default async function handler(request: NextRequest) {
const username = url.searchParams.get('name') ?? 'yourname';
const domainName = isDevelopment ? `${url.protocol}//${url.host}` : 'https://www.base.org';
const profilePicture = getBasenameImage(username);
- const chainIdFromParams = url.searchParams.get('chainId');
- const chainId = chainIdFromParams ? Number(chainIdFromParams) : base.id;
+ const chain = getChainForBasename(username as Basename);
let imageSource = domainName + profilePicture.src;
- // NOTE: Do we want to fail if the name doesn't exists?
try {
- const client = getBasenamePublicClient(chainId);
- const avatar = await client.getEnsAvatar({
+ const client = getBasenamePublicClient(chain.id);
+ const avatar = await client.getEnsText({
name: username,
- universalResolverAddress: USERNAME_L2_RESOLVER_ADDRESSES[chainId],
- assetGatewayUrls: {
- ipfs: CLOUDFARE_IPFS_PROXY,
- },
+ key: UsernameTextRecordKeys.Avatar,
+ universalResolverAddress: USERNAME_L2_RESOLVER_ADDRESSES[chain.id],
});
- // Satori Doesn't support webp
- if (avatar && !avatar.endsWith('.webp')) {
+ if (!avatar) return;
+
+ // IPFS Resolution
+ if (IsValidIpfsUrl(avatar)) {
+ const ipfsUrl = getIpfsGatewayUrl(avatar as IpfsUrl);
+ if (ipfsUrl) {
+ imageSource = ipfsUrl;
+ }
+ } else {
imageSource = avatar;
}
- } catch (error) {}
+
+ // Cloudinary resize / fetch
+ imageSource = getCloudinaryMediaUrl({ media: imageSource, format: 'png', width: 120 });
+ } catch (error) {
+ logger.error('Error fetching basename Avatar:', error);
+ }
// Using Satori for a SVG response
const svg = await satori(
diff --git a/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx b/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx
index 5af3e7d881..3856b5bfe1 100644
--- a/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx
+++ b/apps/web/src/components/Basenames/RegistrationProfileForm/index.tsx
@@ -20,7 +20,6 @@ import {
import classNames from 'classnames';
import { ActionType } from 'libs/base-ui/utils/logEvent';
import { useCallback, useEffect, useState } from 'react';
-import { useAccount } from 'wagmi';
export enum FormSteps {
Description = 'description',
@@ -33,7 +32,6 @@ export default function RegistrationProfileForm() {
const [transitionStep, setTransitionStep] = useState(false);
const { logError } = useErrors();
const { redirectToProfile, selectedNameFormatted } = useRegistration();
- const { address } = useAccount();
const { logEventWithContext } = useAnalytics();
const {
@@ -43,7 +41,6 @@ export default function RegistrationProfileForm() {
writeTextRecordsIsPending,
writeTextRecordsError,
} = useWriteBaseEnsTextRecords({
- address: address,
username: selectedNameFormatted,
onSuccess: () => {
redirectToProfile();
diff --git a/apps/web/src/components/Basenames/UsernameProfileCard/index.tsx b/apps/web/src/components/Basenames/UsernameProfileCard/index.tsx
index c6a5cdf9b3..5f12f8bb87 100644
--- a/apps/web/src/components/Basenames/UsernameProfileCard/index.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileCard/index.tsx
@@ -12,10 +12,9 @@ import {
import Link from 'next/link';
export default function UsernameProfileCard() {
- const { profileUsername, profileAddress } = useUsernameProfile();
+ const { profileUsername } = useUsernameProfile();
const { existingTextRecords } = useReadBaseEnsTextRecords({
- address: profileAddress,
username: profileUsername,
});
diff --git a/apps/web/src/components/Basenames/UsernameProfileCasts/index.tsx b/apps/web/src/components/Basenames/UsernameProfileCasts/index.tsx
index c64edd6113..cb96899230 100644
--- a/apps/web/src/components/Basenames/UsernameProfileCasts/index.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileCasts/index.tsx
@@ -6,10 +6,9 @@ import NeymarCast from 'apps/web/src/components/NeymarCast';
import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords';
export default function UsernameProfileCasts() {
- const { profileUsername, profileAddress } = useUsernameProfile();
+ const { profileUsername } = useUsernameProfile();
const { existingTextRecords } = useReadBaseEnsTextRecords({
- address: profileAddress,
username: profileUsername,
});
const casts = existingTextRecords.casts.split(',').filter((cast) => !!cast);
diff --git a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx
index 9dfb63af6a..d92cad22a7 100644
--- a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx
@@ -89,10 +89,9 @@ export function FramesProvider({ children }: FramesProviderProps) {
const { logEventWithContext } = useAnalytics();
const { address } = useAccount();
const { logError } = useErrors();
- const { profileUsername, profileAddress, currentWalletIsProfileOwner } = useUsernameProfile();
+ const { profileUsername, currentWalletIsProfileOwner } = useUsernameProfile();
const { existingTextRecords, existingTextRecordsIsLoading, refetchExistingTextRecords } =
useReadBaseEnsTextRecords({
- address: profileAddress,
username: profileUsername,
refetchInterval: currentWalletIsProfileOwner ? 1000 * 5 : Infinity,
});
diff --git a/apps/web/src/components/Basenames/UsernameProfileSettingsAvatar/index.tsx b/apps/web/src/components/Basenames/UsernameProfileSettingsAvatar/index.tsx
index b119fc38dc..5caa3e98c0 100644
--- a/apps/web/src/components/Basenames/UsernameProfileSettingsAvatar/index.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileSettingsAvatar/index.tsx
@@ -13,7 +13,7 @@ import { Icon } from 'apps/web/src/components/Icon/Icon';
import { PinResponse } from 'pinata';
export default function UsernameProfileSettingsAvatar() {
- const { profileUsername, profileAddress, currentWalletIsProfileEditor } = useUsernameProfile();
+ const { profileUsername, currentWalletIsProfileEditor } = useUsernameProfile();
const [avatarFile, setAvatarFile] = useState();
const [avatarIsLoading, setAvatarIsLoading] = useState(false);
@@ -30,7 +30,6 @@ export default function UsernameProfileSettingsAvatar() {
writeTextRecordsIsPending,
hasChanged,
} = useWriteBaseEnsTextRecords({
- address: profileAddress,
username: profileUsername,
onSuccess: () => {
setAvatarFile(undefined);
diff --git a/apps/web/src/components/Basenames/UsernameProfileSettingsManageProfile/index.tsx b/apps/web/src/components/Basenames/UsernameProfileSettingsManageProfile/index.tsx
index 9940f8a267..ca9d6941c2 100644
--- a/apps/web/src/components/Basenames/UsernameProfileSettingsManageProfile/index.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileSettingsManageProfile/index.tsx
@@ -24,7 +24,7 @@ const settingTabClass = classNames(
);
export default function UsernameProfileSettingsManageProfile() {
- const { profileUsername, profileAddress, currentWalletIsProfileEditor, setShowProfileSettings } =
+ const { profileUsername, currentWalletIsProfileEditor, setShowProfileSettings } =
useUsernameProfile();
const { logError } = useErrors();
@@ -42,7 +42,6 @@ export default function UsernameProfileSettingsManageProfile() {
writeTextRecordsError,
hasChanged,
} = useWriteBaseEnsTextRecords({
- address: profileAddress,
username: profileUsername,
onSuccess: closeSettings,
});
diff --git a/apps/web/src/components/Basenames/UsernameProfileSidebar/index.tsx b/apps/web/src/components/Basenames/UsernameProfileSidebar/index.tsx
index c202f00b93..a4e83c909f 100644
--- a/apps/web/src/components/Basenames/UsernameProfileSidebar/index.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileSidebar/index.tsx
@@ -44,7 +44,6 @@ export default function UsernameProfileSidebar() {
]);
const { existingTextRecords } = useReadBaseEnsTextRecords({
- address: profileAddress,
username: profileUsername,
});
diff --git a/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx b/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx
index 726d74b5cc..97158ff3d6 100644
--- a/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx
+++ b/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx
@@ -3,7 +3,6 @@ import { useAccount, useEnsAvatar, useEnsName } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
-import { CLOUDFARE_IPFS_PROXY } from 'apps/web/src/utils/urls';
import BasenameAvatar from 'apps/web/src/components/Basenames/BasenameAvatar';
export function UserAvatar() {
@@ -21,9 +20,6 @@ export function UserAvatar() {
const { data: ensAvatar, isLoading: ensAvatarIsLoading } = useEnsAvatar({
name: ensName ?? undefined,
chainId: mainnet.id,
- assetGatewayUrls: {
- ipfs: CLOUDFARE_IPFS_PROXY,
- },
query: {
retry: false,
},
diff --git a/apps/web/src/components/WalletIdentity/index.tsx b/apps/web/src/components/WalletIdentity/index.tsx
index 23dd79afa0..9e525e2874 100644
--- a/apps/web/src/components/WalletIdentity/index.tsx
+++ b/apps/web/src/components/WalletIdentity/index.tsx
@@ -4,7 +4,6 @@ import BasenameAvatar from 'apps/web/src/components/Basenames/BasenameAvatar';
import useBaseEnsAvatar from 'apps/web/src/hooks/useBaseEnsAvatar';
import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
-import { CLOUDFARE_IPFS_PROXY } from 'apps/web/src/utils/urls';
import { getBasenameImage } from 'apps/web/src/utils/usernames';
import { truncateMiddle } from 'libs/base-ui/utils/string';
import Image from 'next/image';
@@ -33,9 +32,6 @@ export default function WalletIdentity({ address }: { address: Address }) {
const { data: ensAvatar } = useEnsAvatar({
name: basename ?? undefined,
chainId: mainnet.id,
- assetGatewayUrls: {
- ipfs: CLOUDFARE_IPFS_PROXY,
- },
query: {
retry: false,
},
diff --git a/apps/web/src/hooks/useBaseEnsAvatar.ts b/apps/web/src/hooks/useBaseEnsAvatar.ts
index feb0cf4d43..231e3a6c69 100644
--- a/apps/web/src/hooks/useBaseEnsAvatar.ts
+++ b/apps/web/src/hooks/useBaseEnsAvatar.ts
@@ -1,8 +1,7 @@
-import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
import { Basename } from '@coinbase/onchainkit/identity';
-import { useEnsAvatar } from 'wagmi';
-import { USERNAME_L2_RESOLVER_ADDRESSES } from 'apps/web/src/addresses/usernames';
-import { CLOUDFARE_IPFS_PROXY } from 'apps/web/src/utils/urls';
+import { getIpfsGatewayUrl, IpfsUrl, IsValidIpfsUrl } from 'apps/web/src/utils/urls';
+import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextRecords';
+import { UsernameTextRecordKeys } from 'apps/web/src/utils/usernames';
export type UseBaseEnsNameProps = {
name?: BaseEnsNameData;
@@ -10,20 +9,24 @@ export type UseBaseEnsNameProps = {
export type BaseEnsNameData = Basename | undefined;
-// Wrapper around onchainkit's useName
export default function useBaseEnsAvatar({ name }: UseBaseEnsNameProps) {
- const { basenameChain } = useBasenameChain(name);
+ const { existingTextRecords, refetchExistingTextRecords, existingTextRecordsIsLoading } =
+ useReadBaseEnsTextRecords({
+ username: name,
+ });
- return useEnsAvatar({
- name: name,
- chainId: basenameChain.id,
- universalResolverAddress: USERNAME_L2_RESOLVER_ADDRESSES[basenameChain.id],
- assetGatewayUrls: {
- ipfs: CLOUDFARE_IPFS_PROXY,
- },
- query: {
- retry: false,
- enabled: !!name,
- },
- });
+ let avatar = existingTextRecords[UsernameTextRecordKeys.Avatar];
+
+ if (IsValidIpfsUrl(avatar)) {
+ const ipfsUrl = getIpfsGatewayUrl(avatar as IpfsUrl);
+ if (ipfsUrl) {
+ avatar = ipfsUrl;
+ }
+ }
+
+ return {
+ data: avatar,
+ refetch: refetchExistingTextRecords,
+ isLoading: existingTextRecordsIsLoading,
+ };
}
diff --git a/apps/web/src/hooks/useReadBaseEnsTextRecords.ts b/apps/web/src/hooks/useReadBaseEnsTextRecords.ts
index 979737d7a3..27a78aca4d 100644
--- a/apps/web/src/hooks/useReadBaseEnsTextRecords.ts
+++ b/apps/web/src/hooks/useReadBaseEnsTextRecords.ts
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { Address } from 'viem';
import {
UsernameTextRecords,
UsernameTextRecordKeys,
@@ -11,13 +10,11 @@ import { BaseEnsNameData } from 'apps/web/src/hooks/useBaseEnsName';
import useBasenameChain from 'apps/web/src/hooks/useBasenameChain';
export type UseReadBaseEnsTextRecordsProps = {
- address?: Address;
username: BaseEnsNameData;
refetchInterval?: number;
};
export default function useReadBaseEnsTextRecords({
- address,
username,
refetchInterval = Infinity,
}: UseReadBaseEnsTextRecordsProps) {
@@ -69,15 +66,9 @@ export default function useReadBaseEnsTextRecords({
refetch: refetchExistingTextRecords,
error: existingTextRecordsError,
} = useQuery({
- queryKey: [
- 'useReadBaseEnsTextRecords',
- address,
- textRecordsKeysEnabled,
- basenameChain.id,
- username,
- ],
+ queryKey: ['useReadBaseEnsTextRecords', textRecordsKeysEnabled, basenameChain.id, username],
queryFn: getExistingTextRecords,
- enabled: !!address && !!username,
+ enabled: !!username,
retry: false,
refetchInterval,
refetchOnWindowFocus: false,
diff --git a/apps/web/src/hooks/useWriteBaseEnsTextRecords.ts b/apps/web/src/hooks/useWriteBaseEnsTextRecords.ts
index d45c7f6fe9..0ea0a9d582 100644
--- a/apps/web/src/hooks/useWriteBaseEnsTextRecords.ts
+++ b/apps/web/src/hooks/useWriteBaseEnsTextRecords.ts
@@ -8,10 +8,9 @@ import useReadBaseEnsTextRecords from 'apps/web/src/hooks/useReadBaseEnsTextReco
import useWriteContractWithReceipt from 'apps/web/src/hooks/useWriteContractWithReceipt';
import { UsernameTextRecords, UsernameTextRecordKeys } from 'apps/web/src/utils/usernames';
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { namehash, encodeFunctionData, Address } from 'viem';
+import { namehash, encodeFunctionData } from 'viem';
export type UseWriteBaseEnsTextRecordsProps = {
- address?: Address;
username: BaseEnsNameData;
onSuccess?: () => void;
};
@@ -29,7 +28,6 @@ export type UseWriteBaseEnsTextRecordsProps = {
*/
export default function useWriteBaseEnsTextRecords({
- address,
username,
onSuccess,
}: UseWriteBaseEnsTextRecordsProps) {
@@ -39,7 +37,6 @@ export default function useWriteBaseEnsTextRecords({
// Fetch existing TextRecords
const { existingTextRecords, existingTextRecordsIsLoading, refetchExistingTextRecords } =
useReadBaseEnsTextRecords({
- address,
username,
});
diff --git a/apps/web/src/utils/images.ts b/apps/web/src/utils/images.ts
index 4f8f5252c1..809799640f 100644
--- a/apps/web/src/utils/images.ts
+++ b/apps/web/src/utils/images.ts
@@ -30,13 +30,18 @@ function isDataUrl(url: string) {
type GetCloudinaryMediaUrlParams = {
media: string;
width: number;
+ format?: 'webp' | 'png' | 'jpg';
};
-export function getCloudinaryMediaUrl({ media, width }: GetCloudinaryMediaUrlParams) {
+export function getCloudinaryMediaUrl({
+ media,
+ width,
+ format = 'webp',
+}: GetCloudinaryMediaUrlParams) {
if (isDataUrl(media)) return media;
const imageWidth = `w_${width * 2}`;
- const imageFormat = 'f_webp';
+ const imageFormat = `f_${format}`;
const imageUrl = encodeURIComponent(media);
const fetchOptions = [imageWidth, imageFormat, imageUrl].join('/');
diff --git a/apps/web/src/utils/pinata.ts b/apps/web/src/utils/pinata.ts
index 62555ec85c..403616bc63 100644
--- a/apps/web/src/utils/pinata.ts
+++ b/apps/web/src/utils/pinata.ts
@@ -2,5 +2,5 @@ import { PinataSDK } from 'pinata';
export const pinata = new PinataSDK({
pinataJwt: `${process.env.PINATA_API_KEY}`,
- pinataGateway: `${process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL}`,
+ pinataGateway: `${process.env.PINATA_GATEWAY_URL}`,
});
diff --git a/apps/web/src/utils/urls.ts b/apps/web/src/utils/urls.ts
index 2bcd097590..f3e3a4b83f 100644
--- a/apps/web/src/utils/urls.ts
+++ b/apps/web/src/utils/urls.ts
@@ -3,9 +3,9 @@ import { cid } from 'is-ipfs';
export type IpfsUrl = `ipfs://${string}`;
export const VERCEL_BLOB_HOSTNAME = 'zku9gdedgba48lmr.public.blob.vercel-storage.com';
export const IPFS_URI_PROTOCOL = 'ipfs://';
-export const CLOUDFARE_IPFS_PROXY = process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL
- ? `https://${process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL}`
- : 'https://cloudflare-ipfs.com';
+
+export const PINATA_GATEWAY_URL = process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL ?? undefined;
+export const PINATA_GATEWAY_KEY = process.env.NEXT_PUBLIC_PINATA_GATEWAY_KEY ?? undefined;
export type QueryParams = Record;
@@ -28,13 +28,17 @@ export function isValidUrl(string?: string) {
}
}
-export const IsValidIpfsUrl = (ipfsUrl: IpfsUrl): boolean => {
+export const IsValidIpfsUrl = (ipfsUrl: string): boolean => {
try {
const url = new URL(ipfsUrl);
- const ipfsCid = url.pathname.replace('//', '');
+ if (url.protocol !== 'ipfs:') return false;
+
+ // Get first path segment after hostname as CID
+ const ipfsCid = url.host;
+
+ // Validate the CID directly
const isValidCid = cid(ipfsCid);
- const isValidIpfsUrl = url.protocol === 'ipfs:' && isValidCid;
- return isValidIpfsUrl;
+ return isValidCid;
} catch (error) {
return false;
}
@@ -50,18 +54,20 @@ export const IsValidVercelBlobUrl = (source: string): boolean => {
}
};
-export const getIpfsGatewayUrl = (ipfsUrl?: IpfsUrl): string | undefined => {
+export const getIpfsGatewayUrl = (ipfsUrl: IpfsUrl): string | undefined => {
if (!ipfsUrl) return;
+ if (!IsValidIpfsUrl(ipfsUrl)) return;
try {
const url = new URL(ipfsUrl);
- const ipfsCid = url.pathname.replace('//', '');
-
- const isValidCid = cid(ipfsCid);
- const isValidIpfsUrl = url.protocol === 'ipfs:' && isValidCid;
- if (!isValidIpfsUrl) return;
+ const path = url.host;
+ const pathname = url.pathname;
- return `${CLOUDFARE_IPFS_PROXY}/ipfs/${ipfsCid}`;
+ if (PINATA_GATEWAY_URL && PINATA_GATEWAY_KEY) {
+ return `https://${PINATA_GATEWAY_URL}/ipfs/${path}${pathname}?pinataGatewayToken=${PINATA_GATEWAY_KEY}`;
+ } else {
+ return `https://ipfs.io/ipfs/${path}${pathname}`;
+ }
} catch (error) {
return;
}
diff --git a/apps/web/src/utils/usernames.ts b/apps/web/src/utils/usernames.ts
index 7ecb1ae17f..7403afeec7 100644
--- a/apps/web/src/utils/usernames.ts
+++ b/apps/web/src/utils/usernames.ts
@@ -508,7 +508,7 @@ export function validateBasenameAvatarUrl(source: string): ValidationResult {
const url = new URL(source);
if (url.protocol === 'ipfs:') {
- const isValid = IsValidIpfsUrl(source as IpfsUrl);
+ const isValid = IsValidIpfsUrl(source);
return {
valid: isValid,
From 76c7bc3cabaf3f868c8817a3e747c03ff9c40896 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Galley?=
Date: Tue, 5 Nov 2024 16:18:53 -0500
Subject: [PATCH 03/83] BAPP-765: ERC1155-v2 support for Basename (#1207)
* add address, attestations and hooks for testnet
* around the world
---
.../src/abis/ERC1155DiscountValidatorV2.ts | 58 ++++
apps/web/src/addresses/usernames.ts | 5 +
.../images/base-around-the-world-nft.svg | 20 ++
.../RegistrationLearnMoreModal/index.tsx | 300 +++++++-----------
.../hooks/useAggregatedDiscountValidators.ts | 13 +-
apps/web/src/hooks/useAttestations.ts | 46 +++
apps/web/src/utils/usernames.ts | 1 +
7 files changed, 250 insertions(+), 193 deletions(-)
create mode 100644 apps/web/src/abis/ERC1155DiscountValidatorV2.ts
create mode 100644 apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/base-around-the-world-nft.svg
diff --git a/apps/web/src/abis/ERC1155DiscountValidatorV2.ts b/apps/web/src/abis/ERC1155DiscountValidatorV2.ts
new file mode 100644
index 0000000000..d7caaae4c1
--- /dev/null
+++ b/apps/web/src/abis/ERC1155DiscountValidatorV2.ts
@@ -0,0 +1,58 @@
+export default [
+ {
+ type: 'constructor',
+ inputs: [
+ {
+ name: 'token_',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'tokenIds',
+ type: 'uint256[]',
+ internalType: 'uint256[]',
+ },
+ ],
+ stateMutability: 'nonpayable',
+ },
+ {
+ 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: 'error',
+ name: 'AddressEmptyCode',
+ inputs: [
+ {
+ name: 'target',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'FailedInnerCall',
+ inputs: [],
+ },
+] as const;
diff --git a/apps/web/src/addresses/usernames.ts b/apps/web/src/addresses/usernames.ts
index ec2077e4d0..43e6e2f9ad 100644
--- a/apps/web/src/addresses/usernames.ts
+++ b/apps/web/src/addresses/usernames.ts
@@ -96,3 +96,8 @@ export const TALENT_PROTOCOL_DISCOUNT_VALIDATORS: AddressMap = {
[baseSepolia.id]: '0x8b769A3fbC29AC02344218840602615B6c9200e7',
[base.id]: '0xb16A4f14A9dED9e27F0Fe59Dc907D245769de19E',
};
+
+export const BASE_WORLD_DISCOUNT_VALIDATORS: AddressMap = {
+ [baseSepolia.id]: '0xFa69f6167F40247fe3EFF2d8375B25C5d7834c48',
+ [base.id]: '0xfEb00a4EfF372a307fDc556Cf4359f7D679E4d11',
+};
diff --git a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/base-around-the-world-nft.svg b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/base-around-the-world-nft.svg
new file mode 100644
index 0000000000..b75c9e6e70
--- /dev/null
+++ b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/base-around-the-world-nft.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx
index 4b2011840d..5d8169c427 100644
--- a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx
+++ b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx
@@ -13,6 +13,7 @@ import BaseNFT from './images/base-nft.svg';
import TalentProtocolIcon from './images/TalentProtocol.svg';
import coinbaseOneVerification from './images/coinbase-one-verification.svg';
import coinbaseVerification from './images/coinbase-verification.svg';
+import BaseWorldNFT from './images/base-around-the-world-nft.svg';
import { StaticImageData } from 'next/dist/shared/lib/get-img-props';
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
@@ -20,6 +21,82 @@ function InfoIcon() {
return ;
}
+type DiscountItem = {
+ discount: Discount;
+ icon: StaticImageData;
+ alt: string;
+ label: string;
+ tooltipContent: string;
+};
+
+const DISCOUNT_ITEMS: DiscountItem[] = [
+ {
+ discount: Discount.COINBASE_VERIFIED_ACCOUNT,
+ icon: coinbaseVerification as StaticImageData,
+ alt: 'icon of coinbase',
+ label: 'Coinbase verification',
+ tooltipContent: 'Verifies you have a valid trading account on Coinbase',
+ },
+ {
+ discount: Discount.CB1,
+ icon: coinbaseOneVerification as StaticImageData,
+ alt: 'icon of coinbase one',
+ label: 'Coinbase One verification',
+ tooltipContent: 'Verifies you have an active Coinbase One subscription',
+ },
+ {
+ discount: Discount.CBID,
+ icon: cbidVerification as StaticImageData,
+ alt: 'icon of CBID',
+ label: 'A cb.id username',
+ tooltipContent: 'cb.id must have been claimed prior to August 9, 2024.',
+ },
+ {
+ discount: Discount.BASE_BUILDATHON_PARTICIPANT,
+ icon: baseBuildathonParticipant as StaticImageData,
+ alt: 'icon of base buildathon',
+ label: 'Base buildathon participant',
+ tooltipContent: 'Available for anyone holding a Base Buildathon participant NFT.',
+ },
+ {
+ discount: Discount.TALENT_PROTOCOL,
+ icon: TalentProtocolIcon as StaticImageData,
+ alt: 'icon of talent protocol',
+ label: 'Builder score 50+',
+ tooltipContent:
+ 'Available for anyone with an onchain builder score 50+. Go to passport.talentprotocol.com to mint yours.',
+ },
+ {
+ discount: Discount.SUMMER_PASS_LVL_3,
+ icon: summerPassLvl3 as StaticImageData,
+ alt: 'icon of summer pass',
+ label: 'Summer Pass Level 3',
+ tooltipContent:
+ 'Available for anyone holding a Summer Pass Level 3 NFT. Go to wallet.coinbase.com/ocs to get your Summer Pass',
+ },
+ {
+ discount: Discount.BNS_NAME,
+ icon: BNSOwnership,
+ alt: 'icon of BNS',
+ label: 'BNS username',
+ tooltipContent: 'BNS (.base) username holders are eligible for a 0.01 ETH discount',
+ },
+ {
+ discount: Discount.BASE_DOT_ETH_NFT,
+ icon: BaseNFT as StaticImageData,
+ alt: 'icon of Base',
+ label: 'Base.eth NFT',
+ tooltipContent: 'Available for anyone holding a base.eth NFT',
+ },
+ {
+ discount: Discount.BASE_WORLD,
+ icon: BaseWorldNFT as StaticImageData,
+ alt: 'icon of Base World',
+ label: 'Base around the world NFT',
+ tooltipContent: 'Available for anyone holding one of the Base around the world NFTs',
+ },
+];
+
export default function RegistrationLearnMoreModal({
isOpen,
toggleModal,
@@ -31,30 +108,6 @@ export default function RegistrationLearnMoreModal({
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 BNSRowClasses = classNames(rowClasses, {
- 'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.BNS_NAME),
- });
- const BaseDotEthNFTRowClasses = classNames(rowClasses, {
- 'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.BASE_DOT_ETH_NFT),
- });
- const TalentProtocolRowClasses = classNames(rowClasses, {
- 'opacity-40': hasDiscount && !allActiveDiscounts.has(Discount.TALENT_PROTOCOL),
- });
const qualifiedClasses = classNames(
'flex flex-row items-center justify-center py-3 px-1 h-5 text-xs bg-green-0 rounded ml-3',
@@ -71,174 +124,37 @@ export default function RegistrationLearnMoreModal({
: "You'll receive a name for free (5+ characters for 1 year) if your wallet has any of the following:"}
-
-
-
-
-
Coinbase verification
-
-
-
- {allActiveDiscounts.has(Discount.COINBASE_VERIFIED_ACCOUNT) && (
-
- )}
-
-
-
-
-
-
Coinbase One verification
-
-
-
- {allActiveDiscounts.has(Discount.CB1) && (
-
- )}
-
-
-
-
-
- {allActiveDiscounts.has(Discount.CBID) && (
-
- )}
-
-
-
-
-
-
Base buildathon participant
-
-
-
- {allActiveDiscounts.has(Discount.BASE_BUILDATHON_PARTICIPANT) && (
-
- )}
-
-
-
-
-
-
Builder score 50+
-
-
-
- {allActiveDiscounts.has(Discount.TALENT_PROTOCOL) && (
-
- )}
-
-
-
-
-
-
Summer Pass Level 3
-
-
-
- {allActiveDiscounts.has(Discount.SUMMER_PASS_LVL_3) && (
-
- )}
-
-
-
-
-
- {allActiveDiscounts.has(Discount.BNS_NAME) && (
-
- )}
-
-
-
-
-
- {allActiveDiscounts.has(Discount.BASE_DOT_ETH_NFT) && (
-
- )}
-
+ {DISCOUNT_ITEMS.map(({ discount, icon, alt, label, tooltipContent }) => (
+
+
+
+
+ {allActiveDiscounts.has(discount) && (
+
+ )}
+
+ ))}
{!hasDiscount && (
<>
diff --git a/apps/web/src/hooks/useAggregatedDiscountValidators.ts b/apps/web/src/hooks/useAggregatedDiscountValidators.ts
index b4f9e6b8dc..0e80c6023e 100644
--- a/apps/web/src/hooks/useAggregatedDiscountValidators.ts
+++ b/apps/web/src/hooks/useAggregatedDiscountValidators.ts
@@ -2,6 +2,7 @@ import {
AttestationData,
useBNSAttestations,
useBaseDotEthAttestations,
+ useBaseWorldAttestations,
useBuildathonAttestations,
useCheckCB1Attestations,
useCheckCBIDAttestations,
@@ -56,6 +57,7 @@ export function useAggregatedDiscountValidators(code?: string) {
useDiscountCodeAttestations(code);
const { data: TalentProtocolData, loading: loadingTalentProtocolAttestations } =
useTalentProtocolAttestations();
+ const { data: BaseWorldData, loading: loadingBaseWorld } = useBaseWorldAttestations();
const loadingDiscounts =
loadingCoinbaseAttestations ||
@@ -68,7 +70,8 @@ export function useAggregatedDiscountValidators(code?: string) {
loadingBaseDotEth ||
loadingBNS ||
loadingDiscountCode ||
- loadingTalentProtocolAttestations;
+ loadingTalentProtocolAttestations ||
+ loadingBaseWorld;
const discountsToAttestationData = useMemo(() => {
const discountMapping: MappedDiscountData = {};
@@ -143,6 +146,13 @@ export function useAggregatedDiscountValidators(code?: string) {
discountKey: validator.key,
};
}
+
+ if (BaseWorldData && validator.discountValidator === BaseWorldData.discountValidatorAddress) {
+ discountMapping[Discount.BASE_WORLD] = {
+ ...BaseWorldData,
+ discountKey: validator.key,
+ };
+ }
});
return discountMapping;
@@ -158,6 +168,7 @@ export function useAggregatedDiscountValidators(code?: string) {
BNSData,
DiscountCodeData,
TalentProtocolData,
+ BaseWorldData,
]);
return {
diff --git a/apps/web/src/hooks/useAttestations.ts b/apps/web/src/hooks/useAttestations.ts
index 19941dff21..194c5a88dd 100644
--- a/apps/web/src/hooks/useAttestations.ts
+++ b/apps/web/src/hooks/useAttestations.ts
@@ -5,10 +5,12 @@ import AttestationValidatorABI from 'apps/web/src/abis/AttestationValidator';
import CBIDValidatorABI from 'apps/web/src/abis/CBIdDiscountValidator';
import EarlyAccessValidatorABI from 'apps/web/src/abis/EarlyAccessValidator';
import ERC1155DiscountValidator from 'apps/web/src/abis/ERC1155DiscountValidator';
+import ERC1155DiscountValidatorV2 from 'apps/web/src/abis/ERC1155DiscountValidatorV2';
import ERC721ValidatorABI from 'apps/web/src/abis/ERC721DiscountValidator';
import TalentProtocolDiscountValidatorABI from 'apps/web/src/abis/TalentProtocolDiscountValidator';
import {
BASE_DOT_ETH_ERC721_DISCOUNT_VALIDATOR,
+ BASE_WORLD_DISCOUNT_VALIDATORS,
BUILDATHON_ERC721_DISCOUNT_VALIDATOR,
TALENT_PROTOCOL_DISCOUNT_VALIDATORS,
USERNAME_1155_DISCOUNT_VALIDATORS,
@@ -592,3 +594,47 @@ export function useTalentProtocolAttestations() {
}
return { data: null, loading: isLoading, error };
}
+
+const baseWorldTokenIds = [
+ BigInt(0),
+ BigInt(1),
+ BigInt(2),
+ BigInt(3),
+ BigInt(4),
+ BigInt(5),
+ BigInt(6),
+];
+
+export function useBaseWorldAttestations() {
+ const { address } = useAccount();
+ const { basenameChain } = useBasenameChain();
+
+ const discountValidatorAddress = BASE_WORLD_DISCOUNT_VALIDATORS[basenameChain.id];
+
+ const readContractArgs = useMemo(() => {
+ if (!address) {
+ return {};
+ }
+ return {
+ address: discountValidatorAddress,
+ abi: ERC1155DiscountValidatorV2,
+ functionName: 'isValidDiscountRegistration',
+ args: [address, encodeAbiParameters([{ type: 'uint256[]' }], [baseWorldTokenIds])],
+ };
+ }, [address, discountValidatorAddress]);
+
+ const { data: isValid, isLoading, error } = useReadContract({ ...readContractArgs, query: {} });
+ if (isValid && address) {
+ return {
+ data: {
+ discountValidatorAddress,
+ discount: Discount.BASE_WORLD,
+ validationData: '0x0' as `0x${string}`,
+ },
+ loading: false,
+ error: null,
+ };
+ }
+
+ return { data: null, loading: isLoading, error };
+}
diff --git a/apps/web/src/utils/usernames.ts b/apps/web/src/utils/usernames.ts
index 7403afeec7..dc8a783835 100644
--- a/apps/web/src/utils/usernames.ts
+++ b/apps/web/src/utils/usernames.ts
@@ -398,6 +398,7 @@ export enum Discount {
BASE_DOT_ETH_NFT = 'BASE_DOT_ETH_NFT',
DISCOUNT_CODE = 'DISCOUNT_CODE',
TALENT_PROTOCOL = 'TALENT_PROTOCOL',
+ BASE_WORLD = 'BASE_WORLD',
}
export function isValidDiscount(key: string): key is keyof typeof Discount {
From ca413fe6a09d9a0d3069d2634a87fcb65979a63b Mon Sep 17 00:00:00 2001
From: wbnns
Date: Wed, 6 Nov 2024 23:34:34 +0900
Subject: [PATCH 04/83] chore(ecosystem): Remove inactive project (#1215)
Removes BotFi; inactive since mid-summer (report via community)
---
apps/web/public/images/partners/botfi.webp | Bin 7330 -> 0 bytes
apps/web/src/data/ecosystem.json | 9 ---------
2 files changed, 9 deletions(-)
delete mode 100644 apps/web/public/images/partners/botfi.webp
diff --git a/apps/web/public/images/partners/botfi.webp b/apps/web/public/images/partners/botfi.webp
deleted file mode 100644
index 4a7267cb2ff21bc9560351d3910963280f3ea459..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 7330
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
Date: Wed, 6 Nov 2024 13:20:16 -0500
Subject: [PATCH 05/83] Fix 1155 Discount validator token encoding (#1217)
---
apps/web/src/hooks/useAttestations.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/hooks/useAttestations.ts b/apps/web/src/hooks/useAttestations.ts
index 194c5a88dd..aab973f6e1 100644
--- a/apps/web/src/hooks/useAttestations.ts
+++ b/apps/web/src/hooks/useAttestations.ts
@@ -629,7 +629,7 @@ export function useBaseWorldAttestations() {
data: {
discountValidatorAddress,
discount: Discount.BASE_WORLD,
- validationData: '0x0' as `0x${string}`,
+ validationData: encodeAbiParameters([{ type: 'uint256[]' }], [baseWorldTokenIds]),
},
loading: false,
error: null,
From d189f3a93d1b7bedb9069a65fb991f54c55678ba Mon Sep 17 00:00:00 2001
From: witty <131909329+0xwitty@users.noreply.github.com>
Date: Thu, 7 Nov 2024 18:30:51 +0300
Subject: [PATCH 06/83] Typo corrections (#1213)
* typo Update privacy-policy.md
The spelling error in the text.
* typo Update terms-of-service.md
There is one orthographic mistake.
---
apps/base-docs/docs/privacy-policy.md | 2 +-
apps/base-docs/docs/terms-of-service.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/base-docs/docs/privacy-policy.md b/apps/base-docs/docs/privacy-policy.md
index 765a0e4cde..22115fd66b 100644
--- a/apps/base-docs/docs/privacy-policy.md
+++ b/apps/base-docs/docs/privacy-policy.md
@@ -13,7 +13,7 @@ Last updated: July 12, 2023
At Base (referred to here as “**we**”, “**us**” or “**our**”), we respect and protect the privacy of those users and developers (“**you**” and “**your**” or “**Users**” and “**Developers**”, as relevant) who explore and use Base (“**Base**”) through the Base protocol or any other applications, tools, and features we operate (collectively, the “**Services**”).
-This Privacy Policy describes how we collect, use, and disclose personal information when you use our Services, which include the services offered on our website [https://base.org](https://base.org/) ( “**Site**”). This Privacy Policy does not apply to any processing which Base carries out as a processor on behalf of those Users and Developers who explore and use Base. Please note that we do not control websites, applications, or services operated by third parties, and we are not responsible for their actions. We encourage you to review the privacy policies of the other websites, decentralised applications, and services you use to access or interact with our Services.
+This Privacy Policy describes how we collect, use, and disclose personal information when you use our Services, which include the services offered on our website [https://base.org](https://base.org/) ( “**Site**”). This Privacy Policy does not apply to any processing which Base carries out as a processor on behalf of those Users and Developers who explore and use Base. Please note that we do not control websites, applications, or services operated by third parties, and we are not responsible for their actions. We encourage you to review the privacy policies of the other websites, decentralized applications, and services you use to access or interact with our Services.
# 1. WHAT INFORMATION WE COLLECT
diff --git a/apps/base-docs/docs/terms-of-service.md b/apps/base-docs/docs/terms-of-service.md
index 7bc6621333..595026449e 100644
--- a/apps/base-docs/docs/terms-of-service.md
+++ b/apps/base-docs/docs/terms-of-service.md
@@ -63,7 +63,7 @@ You agree that you will not use the Services in any manner or for any purpose ot
By using the Services, Base, or the Bridging Smart Contracts, you represent that you understand there are risks inherent in using cryptographic and public blockchain-based systems, including, but not limited, to the Services and digital assets such as bitcoin (BTC) and ether (ETH). You expressly agree that you assume all risks in connection with your access and use of Base, the Bridging Smart Contracts, Basenames, and the separate Services offered by Coinbase. That means, among other things, you understand and acknowledge that:
- The Base, the Bridging Smart Contracts, Basenames, and the separate Services may be subject to cyberattacks and exploits, which could result in the irrevocable loss or reduction in value of your digital assets or in additional copies of your digital assets being created or bridged without your consent.
-- Base is subject to periodic upgrades by the Optimism Collective. The Optimism Collective may approve a protocol upgrade that, if implemented, may significantly impacts Base, and may introduce other risks, bugs, malfunctions, cyberattack vectors, or other changes to Base that could disrupt the operation of Base, the Bridging Smart Contracts, Basenames, or the Services or otherwise cause you damage or loss.
+- Base is subject to periodic upgrades by the Optimism Collective. The Optimism Collective may approve a protocol upgrade that, if implemented, may significantly impact Base, and may introduce other risks, bugs, malfunctions, cyberattack vectors, or other changes to Base that could disrupt the operation of Base, the Bridging Smart Contracts, Basenames, or the Services or otherwise cause you damage or loss.
- If you lose your Wallet seed phrase, private keys, or password, you might permanently be unable to access your digital assets. You bear sole responsibility for safeguarding and ensuring the security of your Wallet.
You further expressly waive and release Coinbase, its parents, affiliates, related companies, their officers, directors, members, employees, consultants, representatives. agents, partners, licensors, and each of their respective successors and assigns (collectively, the “Coinbase Entities”) from any and all liability, claims, causes of action, or damages arising from or in any way related to your use of the Services, and your interaction with Base, the Bridging Smart Contracts, or Basenames. Also, to the extent applicable, you shall and hereby do waive the benefits and protections of California Civil Code § 1542, which provides: “[a] general release does not extend to claims that the creditor or releasing party does not know or suspect to exist in his or her favor at the time of executing the release and that, if known by him or her, would have materially affected his or her settlement with the debtor or released party.”
From 6336a6a3e7be071ed7880e5b7692b86440e29e91 Mon Sep 17 00:00:00 2001
From: Davion Selever
Date: Thu, 7 Nov 2024 18:31:40 +0300
Subject: [PATCH 07/83] Corrected errors in docs (#1211)
---
apps/base-docs/base-learn/docs/arrays/arrays-in-solidity.md | 2 +-
.../base-learn/docs/arrays/filtering-an-array-sbs.md | 2 +-
.../basic-functions-exercise.md | 4 ++--
.../hello-world-step-by-step.md | 4 ++--
.../docs/deployment-to-testnet/contract-verification-sbs.md | 2 +-
.../base-learn/docs/erc-20-token/erc-20-token-sbs.md | 4 ++--
apps/base-docs/base-learn/docs/error-triage/error-triage.md | 2 +-
.../base-learn/docs/hardhat-deploy/hardhat-deploy-sbs.md | 6 +++---
8 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/apps/base-docs/base-learn/docs/arrays/arrays-in-solidity.md b/apps/base-docs/base-learn/docs/arrays/arrays-in-solidity.md
index b01053975b..6e0b661200 100644
--- a/apps/base-docs/base-learn/docs/arrays/arrays-in-solidity.md
+++ b/apps/base-docs/base-learn/docs/arrays/arrays-in-solidity.md
@@ -69,7 +69,7 @@ contract StorageArray {
You cannot use a `storage` array as a function parameter, and you cannot write a function that `return`s a `storage` array.
-Storage arrays are dynamic, unless they are declared with an explicit size. However, their functionality is limited compared to other languages. The `.push(value)` function works as expected. the `.pop()` function removes the last value of an array, but it **does not** return that value. You also **may not** use `.pop()` with an index to remove an element from the middle of an array, or to remove more than one element.
+Storage arrays are dynamic, unless they are declared with an explicit size. However, their functionality is limited compared to other languages. The `.push(value)` function works as expected. The `.pop()` function removes the last value of an array, but it **does not** return that value. You also **may not** use `.pop()` with an index to remove an element from the middle of an array, or to remove more than one element.
You can use the `delete` keyword with an array. Doing so on an entire array will reset the array to zero length. Calling it on an element within the array will reset that value to its default. It **will not** resize the array!
diff --git a/apps/base-docs/base-learn/docs/arrays/filtering-an-array-sbs.md b/apps/base-docs/base-learn/docs/arrays/filtering-an-array-sbs.md
index 84f611fafb..7b5a53639a 100644
--- a/apps/base-docs/base-learn/docs/arrays/filtering-an-array-sbs.md
+++ b/apps/base-docs/base-learn/docs/arrays/filtering-an-array-sbs.md
@@ -170,7 +170,7 @@ uint[] public numbers;
uint numEven;
```
-Add a new function called `debugLoadArray` that takes a `uint` called `_number` as an argument, and fills the array by looping through `_numbers` times, pushing each number into the array. **For now, _don't_ update `numEven`**.
+Add a new function called `debugLoadArray` that takes a `uint` called `_number` as an argument, and fills the array by looping through `_number` times, pushing each number into the array. **For now, _don't_ update `numEven`**.
diff --git a/apps/base-docs/base-learn/docs/contracts-and-basic-functions/basic-functions-exercise.md b/apps/base-docs/base-learn/docs/contracts-and-basic-functions/basic-functions-exercise.md
index 57b7b80958..be026d9e4c 100644
--- a/apps/base-docs/base-learn/docs/contracts-and-basic-functions/basic-functions-exercise.md
+++ b/apps/base-docs/base-learn/docs/contracts-and-basic-functions/basic-functions-exercise.md
@@ -26,8 +26,8 @@ A function called `adder`. It must:
- Accept two `uint` arguments, called `_a` and `_b`
- Return a `uint` `sum` and a `bool` `error`
-- If `_a` + `_b` do not overflow, it should return the `sum` and an `error` of `false`
-- If `_a` + `_b` overflow, it should return `0` as the `sum`, and an `error` of `true`
+- If `_a` + `_b` does not overflow, it should return the `sum` and an `error` of `false`
+- If `_a` + `_b` overflows, it should return `0` as the `sum`, and an `error` of `true`
### Subtractor
diff --git a/apps/base-docs/base-learn/docs/contracts-and-basic-functions/hello-world-step-by-step.md b/apps/base-docs/base-learn/docs/contracts-and-basic-functions/hello-world-step-by-step.md
index 04929a660e..3c2c535d6b 100644
--- a/apps/base-docs/base-learn/docs/contracts-and-basic-functions/hello-world-step-by-step.md
+++ b/apps/base-docs/base-learn/docs/contracts-and-basic-functions/hello-world-step-by-step.md
@@ -77,7 +77,7 @@ Is `public` the most appropriate [visibility specifier]?
It would work, but you won't be calling this function from within the contract, so `external` is more appropriate.
-You also need to specify a return type, and we've decided this function should return a string. You'll learn more about this later, but in Solidity, many of the more complex types require you to specify if they are `storage` or `memory`. You can then have your function return a string of `"Hello World!`.
+You also need to specify a return type, and we've decided this function should return a string. You'll learn more about this later, but in Solidity, many of the more complex types require you to specify if they are `storage` or `memory`. You can then have your function return a string of `"Hello World!"`.
Don't forget your semicolon. They're mandatory in Solidity!
@@ -136,7 +136,7 @@ function Greeter(string memory _name) external pure returns (string memory) {
Unfortunately, this does not work in Solidity. The error message you receive is a little confusing:
-> TypeError: Operator + not compatible with types literal_string "Hello " and string memory.
+> TypeError: Operator + not compatible with types literal_string "Hello" and string memory.
You might think that there is some sort of type casting or conversion error that could be solved by explicitly casting the string literal to string memory, or vice versa. This is a great instinct. Solidity is a very explicit language.
diff --git a/apps/base-docs/base-learn/docs/deployment-to-testnet/contract-verification-sbs.md b/apps/base-docs/base-learn/docs/deployment-to-testnet/contract-verification-sbs.md
index 41e84cc731..3272ae93d1 100644
--- a/apps/base-docs/base-learn/docs/deployment-to-testnet/contract-verification-sbs.md
+++ b/apps/base-docs/base-learn/docs/deployment-to-testnet/contract-verification-sbs.md
@@ -4,7 +4,7 @@ description: Verify your contract and interact with it.
hide_table_of_contents: false
---
-Once your contract is deployed, you can verify it using a number of popular services. Doing so will let you users have confidence that your contract does what you claim, and will allow you to interact with it using a similar interface to what you used in Remix.
+Once your contract is deployed, you can verify it using a number of popular services. Doing so will let your users have confidence that your contract does what you claim, and will allow you to interact with it using a similar interface to what you used in Remix.
---
diff --git a/apps/base-docs/base-learn/docs/erc-20-token/erc-20-token-sbs.md b/apps/base-docs/base-learn/docs/erc-20-token/erc-20-token-sbs.md
index cca565b3c8..73f0364dcf 100644
--- a/apps/base-docs/base-learn/docs/erc-20-token/erc-20-token-sbs.md
+++ b/apps/base-docs/base-learn/docs/erc-20-token/erc-20-token-sbs.md
@@ -12,8 +12,8 @@ The ERC-20 is a standard that allows for the development of fungible tokens and
By the end of this lesson you should be able to:
-- Describe OpenZepplin
-- Import the OpenZepplin ERC-20 implementation
+- Describe OpenZeppelin
+- Import the OpenZeppelin ERC-20 implementation
- Describe the difference between the ERC-20 standard and OpenZeppelin's ERC20.sol
- Build and deploy an ERC-20 compliant token
diff --git a/apps/base-docs/base-learn/docs/error-triage/error-triage.md b/apps/base-docs/base-learn/docs/error-triage/error-triage.md
index 1234fb638b..d50e34bcfe 100644
--- a/apps/base-docs/base-learn/docs/error-triage/error-triage.md
+++ b/apps/base-docs/base-learn/docs/error-triage/error-triage.md
@@ -406,7 +406,7 @@ function badRandomLoopFixed() public view returns (uint) {
The `uint` type will _panic_ in the event of an overflow or underflow.
```solidity
-function badSubstraction() public pure returns (uint) {
+function badSubtraction() public pure returns (uint) {
uint first = 1;
uint second = 2;
return first - second;
diff --git a/apps/base-docs/base-learn/docs/hardhat-deploy/hardhat-deploy-sbs.md b/apps/base-docs/base-learn/docs/hardhat-deploy/hardhat-deploy-sbs.md
index bae9771ff1..063e6a8eec 100644
--- a/apps/base-docs/base-learn/docs/hardhat-deploy/hardhat-deploy-sbs.md
+++ b/apps/base-docs/base-learn/docs/hardhat-deploy/hardhat-deploy-sbs.md
@@ -159,8 +159,8 @@ Reuse `Lock__factory` but use the connect function and pass the address of the n
✔ should get the unlockTime value
✔ should have the right ether balance
✔ should have the right owner
- ✔ shouldn"t allow to withdraw before unlock time (51ms)
- ✔ shouldn"t allow to withdraw a non owner
+ ✔ shouldn't allow to withdraw before unlock time (51ms)
+ ✔ shouldn't allow to withdraw a non owner
✔ should allow to withdraw a owner
6 passing (2s)
@@ -170,7 +170,7 @@ Reuse `Lock__factory` but use the connect function and pass the address of the n
Deploying to a real test network involves configuring the network parameters in the hardhat config file. You need to include parameters such as:
-- The JSON RPC url
+- The JSON RPC URL
- The account you want to use
- Real test ether or the native Blockchain token for gas costs
From e8d2b458abfeee0188fe67f3195cc4c1d9666452 Mon Sep 17 00:00:00 2001
From: Pat
Date: Thu, 7 Nov 2024 09:32:16 -0600
Subject: [PATCH 08/83] docstutorial): update 'Pay' component to 'Checkout' for
consistency (#1209)
---
...tutorial.md => 2_ock-checkout-tutorial.md} | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
rename apps/base-docs/tutorials/docs/{2_ock-pay-tutorial.md => 2_ock-checkout-tutorial.md} (84%)
diff --git a/apps/base-docs/tutorials/docs/2_ock-pay-tutorial.md b/apps/base-docs/tutorials/docs/2_ock-checkout-tutorial.md
similarity index 84%
rename from apps/base-docs/tutorials/docs/2_ock-pay-tutorial.md
rename to apps/base-docs/tutorials/docs/2_ock-checkout-tutorial.md
index af3b81c93e..5dc83f2dc1 100644
--- a/apps/base-docs/tutorials/docs/2_ock-pay-tutorial.md
+++ b/apps/base-docs/tutorials/docs/2_ock-checkout-tutorial.md
@@ -1,6 +1,6 @@
---
title: 'Build a eCommerce App using Coinbase Commerce and OnchainKit'
-slug: /coinbase-commerce-payment-integration
+slug: /coinbase-commerce-onchainkit-checkout
description: Learn how to integrate Coinbase Commerce payments into your application using OnchainKit.
author: hughescoin
keywords: [
@@ -56,7 +56,7 @@ Here, you'll need to add a detailed description of the product or service you're
![pay-commerce-uuid](../../assets/images/onchainkit-tutorials/pay-create-product-details.png)
-Once your product is created you will be presented with a small popup that contains a link to your products hosted page. Click on the `View product` button. This will take you to a page with more details about your newly created product. Pay close attention to the URL of this page, as it contains a crucial piece of information: the product's UUID. You'll need to copy this UUID from the URL.
+After creating your product, click `View product` in the popup to access the product page and copy the UUID from its URL.
![pay-commerce-uuid](../../assets/images/onchainkit-tutorials/pay-copy-product-link.png)
@@ -142,12 +142,12 @@ export const NEXT_PUBLIC_WC_PROJECT_ID = process.env.NEXT_PUBLIC_WC_PROJECT_ID;
## Implementing the Payment Component
-To implement the payment component, start by opening the `src/app/page.tsx` file. You'll need to add some new imports at the top of the file: import the `Pay`, `PayButton`, and `PayStatus` components from '@coinbase/onchainkit', as well as the `Image` component from 'next/image'.
+To implement the payment component, start by opening the `src/app/page.tsx` file. You'll need to add some new imports at the top of the file: import the `Checkout`, `CheckoutButton`, `CheckoutStatus` components from '@coinbase/onchainkit', as well as the `Image` component from 'next/image'.
Next, create a constant for your product ID using the environment variable you set up earlier. This will allow you to easily reference your product in the payment component.
```typescript
-import { Pay, PayButton, PayStatus } from '@coinbase/onchainkit';
+import { Checkout, CheckoutButton, CheckoutStatus } from '@coinbase/onchainkit/checkout';
import Image from 'next/image';
const productId = process.env.NEXT_PUBLIC_PRODUCT_ID;
@@ -168,7 +168,7 @@ For visual appeal, add an image of your product to the `/public` folder. This im
When setting up the payment component, it's important to implement conditional rendering. This ensures that the payment button only appears once the user's wallet is connected. This approach provides a smoother user experience and prevents potential errors from attempting to initiate a payment before a wallet is available.
:::
-Finally, configure the Pay component within your JSX. Wrap the `PayButton` and `PayStatus` components inside the `Pay` component, passing your `productId` as a prop to the `Pay` component. Set the `coinbaseBranded` prop on the `PayButton` to true for consistent branding. This setup creates a complete payment flow, allowing users to initiate a payment and view its status all within your application.
+Finally, configure the Checkout component within your JSX. Wrap the `CheckoutButton` and `CheckoutStatus` components inside the `Checkout` component, passing your `productId` as a prop to the `Checkout` component. Set the `coinbaseBranded` prop on the `CheckoutButton` to true for consistent branding. This setup creates a complete payment flow, allowing users to initiate a payment and view its status all within your application.
```jsx
@@ -181,10 +181,10 @@ Finally, configure the Pay component within your JSX. Wrap the `PayButton` and `
{' '}
{/* Added spacing */}
{address ? (
-
-
-
-
+
+
+
+
) : (
)}
@@ -200,7 +200,7 @@ You may now test your implementation locally by running `bun run dev`
Congratulations! You've successfully integrated Coinbase Commerce payments into your application using OnchainKit. This is a significant achievement that opens up new possibilities for your business.
-As next steps, consider expanding your product catalog by adding more items to your site. Each new product can be seamlessly integrated using the same Pay component, allowing you to create a diverse and engaging e-commerce experience. Once you're satisfied with your application, you can easily deploy it using a service like Vercel, making your creation accessible to users worldwide. Keep exploring and building – the potential for your onchain commerce application is limitless!
+As next steps, consider expanding your product catalog by adding more items to your site. Each new product can be seamlessly integrated using the same Checkout component, allowing you to create a diverse and engaging e-commerce experience. Once you're satisfied with your application, you can easily deploy it using a service like Vercel, making your creation accessible to users worldwide. Keep exploring and building – the potential for your onchain commerce application is limitless!
---
From ac97665c4ac1049ce420ba5fd1de541911fdac24 Mon Sep 17 00:00:00 2001
From: Pavel Zaborskii
Date: Thu, 7 Nov 2024 17:15:17 +0100
Subject: [PATCH 09/83] correct quorum spelling in issue descriptions (#1212)
---
.../docs/reading-and-displaying-data/useReadContract.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/base-docs/base-learn/docs/reading-and-displaying-data/useReadContract.md b/apps/base-docs/base-learn/docs/reading-and-displaying-data/useReadContract.md
index 9630ba46e9..1431d4beaf 100644
--- a/apps/base-docs/base-learn/docs/reading-and-displaying-data/useReadContract.md
+++ b/apps/base-docs/base-learn/docs/reading-and-displaying-data/useReadContract.md
@@ -58,19 +58,19 @@ Add the following two issues:
```text
_issueDesc: We should enable light mode by default.
-_quorom: 2
+_quorum: 2
```
```text
_issueDesc: We should make inverted mouse controls the default selection.
-_quorom: 2
+_quorum: 2
```
Switch to a **different wallet address**. Claim your tokens with the new address, and add one more issue:
```text
_issueDesc: Two spaces, not four, not tabs!
-_quorom: 2
+_quorum: 2
```
Call the `getAllIssues` function under the `Read Contract` tab to make sure all three are there.
From ce51c6e33d8e30036e4f338cad4148d6265ff979 Mon Sep 17 00:00:00 2001
From: Matthew Bunday
Date: Thu, 7 Nov 2024 15:26:45 -0500
Subject: [PATCH 10/83] Multiname management (#1216)
* Scaffold page
* Add NamesList component
* API endppint for getUsernames
* Add NamesList component
* Move route
* Ugly list demo working
* Style it up a bit
* Manage names list styling and expiry display
* Style the header
* Add triple dot icon
* Triple dot dropdown menu
* Set as primary working
* Work on transfers, checkpoint
* Work on transfers
* Lint unused dep
* UI polish
* Add empty state
* Resolve type errors?
* Reolve types
* Slightly better empty state
* Remove console.log
* Only show My Basenames if the user has a wallet connected
* Improve dropdown mechanics
* Spacing feedback
* Error handling ala Leo
* Handle success / failure correctly
* Lint
* Add some mobile margin
* Fix mobile padding
---
.../api/basenames/getUsernames/route.ts | 29 +++++
.../web/app/(basenames)/manage-names/page.tsx | 32 +++++
apps/web/next-env.d.ts | 2 +-
apps/web/package.json | 1 +
.../Basenames/ManageNames/NameDisplay.tsx | 111 ++++++++++++++++++
.../Basenames/ManageNames/NamesList.tsx | 81 +++++++++++++
.../Basenames/ManageNames/hooks.tsx | 105 +++++++++++++++++
.../context.tsx | 8 +-
.../index.tsx | 5 +-
apps/web/src/components/Dropdown/index.tsx | 15 ++-
.../web/src/components/DropdownMenu/index.tsx | 8 +-
apps/web/src/components/Icon/Icon.tsx | 53 +++++++++
.../components/Layout/UsernameNav/index.tsx | 12 +-
apps/web/src/hooks/useSetPrimaryBasename.ts | 9 +-
apps/web/src/types/ManagedAddresses.ts | 17 +++
yarn.lock | 8 ++
16 files changed, 479 insertions(+), 17 deletions(-)
create mode 100644 apps/web/app/(basenames)/api/basenames/getUsernames/route.ts
create mode 100644 apps/web/app/(basenames)/manage-names/page.tsx
create mode 100644 apps/web/src/components/Basenames/ManageNames/NameDisplay.tsx
create mode 100644 apps/web/src/components/Basenames/ManageNames/NamesList.tsx
create mode 100644 apps/web/src/components/Basenames/ManageNames/hooks.tsx
create mode 100644 apps/web/src/types/ManagedAddresses.ts
diff --git a/apps/web/app/(basenames)/api/basenames/getUsernames/route.ts b/apps/web/app/(basenames)/api/basenames/getUsernames/route.ts
new file mode 100644
index 0000000000..b1baae10cf
--- /dev/null
+++ b/apps/web/app/(basenames)/api/basenames/getUsernames/route.ts
@@ -0,0 +1,29 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+import type { ManagedAddressesResponse } from 'apps/web/src/types/ManagedAddresses';
+
+export async function GET(request: NextRequest) {
+ const address = request.nextUrl.searchParams.get('address');
+ if (!address) {
+ return NextResponse.json({ error: 'No address provided' }, { status: 400 });
+ }
+
+ const network = request.nextUrl.searchParams.get('network') ?? 'base-mainnet';
+ if (network !== 'base-mainnet' && network !== 'base-sepolia') {
+ return NextResponse.json({ error: 'Invalid network provided' }, { status: 400 });
+ }
+
+ const response = await fetch(
+ `https://api.cdp.coinbase.com/platform/v1/networks/${network}/addresses/${address}/identity?limit=50`,
+ {
+ headers: {
+ Authorization: `Bearer ${process.env.CDP_BEARER_TOKEN}`,
+ 'Content-Type': 'application/json',
+ },
+ },
+ );
+
+ const data = (await response.json()) as ManagedAddressesResponse;
+
+ return NextResponse.json(data, { status: 200 });
+}
diff --git a/apps/web/app/(basenames)/manage-names/page.tsx b/apps/web/app/(basenames)/manage-names/page.tsx
new file mode 100644
index 0000000000..1aec6958aa
--- /dev/null
+++ b/apps/web/app/(basenames)/manage-names/page.tsx
@@ -0,0 +1,32 @@
+import ErrorsProvider from 'apps/web/contexts/Errors';
+import type { Metadata } from 'next';
+import { initialFrame } from 'apps/web/pages/api/basenames/frame/frameResponses';
+import NamesList from 'apps/web/src/components/Basenames/ManageNames/NamesList';
+
+export const metadata: Metadata = {
+ metadataBase: new URL('https://base.org'),
+ title: `Basenames`,
+ description:
+ 'Basenames are a core onchain building block that enables anyone to establish their identity on Base by registering human-readable names for their address(es). They are a fully onchain solution which leverages ENS infrastructure deployed on Base.',
+ openGraph: {
+ title: `Basenames`,
+ url: `/manage-names`,
+ },
+ twitter: {
+ site: '@base',
+ card: 'summary_large_image',
+ },
+ other: {
+ ...(initialFrame as Record),
+ },
+};
+
+export default async function Page() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
index fd36f9494e..725dd6f245 100644
--- a/apps/web/next-env.d.ts
+++ b/apps/web/next-env.d.ts
@@ -3,4 +3,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/apps/web/package.json b/apps/web/package.json
index 3c42a50734..718ac4131e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -45,6 +45,7 @@
"base-ui": "0.1.1",
"classnames": "^2.5.1",
"cloudinary": "^2.5.1",
+ "date-fns": "^4.1.0",
"dd-trace": "^5.21.0",
"ethers": "5.7.2",
"framer-motion": "^11.9.0",
diff --git a/apps/web/src/components/Basenames/ManageNames/NameDisplay.tsx b/apps/web/src/components/Basenames/ManageNames/NameDisplay.tsx
new file mode 100644
index 0000000000..0a1b44fd63
--- /dev/null
+++ b/apps/web/src/components/Basenames/ManageNames/NameDisplay.tsx
@@ -0,0 +1,111 @@
+'use client';
+
+import { useState, useCallback } from 'react';
+import UsernameProfileProvider from 'apps/web/src/components/Basenames/UsernameProfileContext';
+import ProfileTransferOwnershipProvider from 'apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/context';
+import UsernameProfileTransferOwnershipModal from 'apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal';
+import BasenameAvatar from 'apps/web/src/components/Basenames/BasenameAvatar';
+import { Basename } from '@coinbase/onchainkit/identity';
+import { formatDistanceToNow, parseISO } from 'date-fns';
+import { Icon } from 'apps/web/src/components/Icon/Icon';
+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 classNames from 'classnames';
+import {
+ useUpdatePrimaryName,
+ useRemoveNameFromUI,
+} from 'apps/web/src/components/Basenames/ManageNames/hooks';
+import Link from 'apps/web/src/components/Link';
+
+const transitionClasses = 'transition-all duration-700 ease-in-out';
+
+const pillNameClasses = classNames(
+ 'bg-blue-500 mx-auto 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,
+ 'rounded-[2rem] py-6 px-6 w-full',
+);
+
+const avatarClasses = classNames(
+ 'flex items-center justify-center overflow-hidden rounded-full',
+ transitionClasses,
+ 'h-[2.5rem] w-[2.5rem] md:h-[4rem] md:w-[4rem] top-3 md:top-4 left-4',
+);
+
+type NameDisplayProps = {
+ domain: string;
+ isPrimary: boolean;
+ tokenId: string;
+ expiresAt: string;
+};
+
+export default function NameDisplay({ domain, isPrimary, tokenId, expiresAt }: NameDisplayProps) {
+ const expirationText = formatDistanceToNow(parseISO(expiresAt), { addSuffix: true });
+
+ const { setPrimaryUsername } = useUpdatePrimaryName(domain as Basename);
+
+ const [isOpen, setIsOpen] = useState(false);
+ const openModal = useCallback(() => setIsOpen(true), []);
+ const closeModal = useCallback(() => setIsOpen(false), []);
+
+ const { removeNameFromUI } = useRemoveNameFromUI(domain as Basename);
+
+ return (
+
+
+
+
+
+
+
{domain}
+
Expires {expirationText}
+
+
+
+
+ {isPrimary && (
+ Primary
+ )}
+
+
+
+
+
+
+
+ Transfer
+ name
+
+
+ {!isPrimary ? (
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+
+
+ Set as
+ primary
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/Basenames/ManageNames/NamesList.tsx b/apps/web/src/components/Basenames/ManageNames/NamesList.tsx
new file mode 100644
index 0000000000..c4838f1da2
--- /dev/null
+++ b/apps/web/src/components/Basenames/ManageNames/NamesList.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import NameDisplay from './NameDisplay';
+import { useNameList } from 'apps/web/src/components/Basenames/ManageNames/hooks';
+import Link from 'apps/web/src/components/Link';
+import { Icon } from 'apps/web/src/components/Icon/Icon';
+import AnalyticsProvider from 'apps/web/contexts/Analytics';
+
+const usernameManagementListAnalyticContext = 'username_management_list';
+
+function NamesLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
My Basenames
+
+
+
+
+ {children}
+
+
+ );
+}
+
+export default function NamesList() {
+ const { namesData, isLoading, error } = useNameList();
+
+ if (error) {
+ return (
+
+
+ Failed to load names. Please try again later.
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+ Loading names...
+
+ );
+ }
+
+ if (!namesData?.data?.length) {
+ return (
+
+
+ No names found.
+
+
+
+ Get a Basename!
+
+
+
+ );
+ }
+
+ return (
+
+
+ {namesData.data.map((name) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/Basenames/ManageNames/hooks.tsx b/apps/web/src/components/Basenames/ManageNames/hooks.tsx
new file mode 100644
index 0000000000..63d1a1b355
--- /dev/null
+++ b/apps/web/src/components/Basenames/ManageNames/hooks.tsx
@@ -0,0 +1,105 @@
+import { useCallback, useEffect } from 'react';
+import { useErrors } from 'apps/web/contexts/Errors';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { useAccount, useChainId } from 'wagmi';
+import { ManagedAddressesResponse } from 'apps/web/src/types/ManagedAddresses';
+import useSetPrimaryBasename from 'apps/web/src/hooks/useSetPrimaryBasename';
+import { Basename } from '@coinbase/onchainkit/identity';
+
+export function useNameList() {
+ const { address } = useAccount();
+ const chainId = useChainId();
+ const { logError } = useErrors();
+
+ const network = chainId === 8453 ? 'base-mainnet' : 'base-sepolia';
+
+ const {
+ data: namesData,
+ isLoading,
+ error,
+ } = useQuery({
+ queryKey: ['usernames', address, network],
+ queryFn: async (): Promise => {
+ try {
+ const response = await fetch(
+ `/api/basenames/getUsernames?address=${address}&network=${network}`,
+ );
+ if (!response.ok) {
+ throw new Error(`Failed to fetch usernames: ${response.statusText}`);
+ }
+ return (await response.json()) as ManagedAddressesResponse;
+ } catch (err) {
+ logError(err, 'Failed to fetch usernames');
+ throw err;
+ }
+ },
+ enabled: !!address,
+ });
+
+ return { namesData, isLoading, error };
+}
+
+export function useRemoveNameFromUI(domain: Basename) {
+ const { address } = useAccount();
+ const chainId = useChainId();
+
+ const network = chainId === 8453 ? 'base-mainnet' : 'base-sepolia';
+ const queryClient = useQueryClient();
+
+ const removeNameFromUI = useCallback(() => {
+ queryClient.setQueryData(
+ ['usernames', address, network],
+ (prevData: ManagedAddressesResponse) => {
+ return { ...prevData, data: prevData.data.filter((name) => name.domain !== domain) };
+ },
+ );
+ }, [address, domain, network, queryClient]);
+
+ return { removeNameFromUI };
+}
+
+export function useUpdatePrimaryName(domain: Basename) {
+ const { address } = useAccount();
+ const chainId = useChainId();
+ const { logError } = useErrors();
+
+ const queryClient = useQueryClient();
+
+ const network = chainId === 8453 ? 'base-mainnet' : 'base-sepolia';
+
+ // Hook to update primary name
+ const { setPrimaryName, transactionIsSuccess } = useSetPrimaryBasename({
+ secondaryUsername: domain,
+ });
+
+ const setPrimaryUsername = useCallback(async () => {
+ try {
+ await setPrimaryName();
+ } catch (error) {
+ logError(error, 'Failed to update primary name');
+ throw error;
+ }
+ }, [logError, setPrimaryName]);
+
+ useEffect(() => {
+ if (transactionIsSuccess) {
+ queryClient.setQueryData(
+ ['usernames', address, network],
+ (prevData: ManagedAddressesResponse) => {
+ return {
+ ...prevData,
+ data: prevData.data.map((name) =>
+ name.domain === domain
+ ? { ...name, is_primary: true }
+ : name.is_primary
+ ? { ...name, is_primary: false }
+ : name,
+ ),
+ };
+ },
+ );
+ }
+ }, [transactionIsSuccess, address, domain, network, queryClient]);
+
+ return { setPrimaryUsername };
+}
diff --git a/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/context.tsx b/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/context.tsx
index 8a5dd1254d..04b785421a 100644
--- a/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/context.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/context.tsx
@@ -337,9 +337,11 @@ export default function ProfileTransferOwnershipProvider({
// Smart wallet: One transaction
batchCallsStatus === BatchCallsStatus.Success ||
// Other wallet: 4 Transactions are successfull
- ownershipSettings.every(
- (ownershipSetting) => ownershipSetting.status === WriteTransactionWithReceiptStatus.Success,
- ),
+ (ownershipSettings.length > 0 &&
+ ownershipSettings.every(
+ (ownershipSetting) =>
+ ownershipSetting.status === WriteTransactionWithReceiptStatus.Success,
+ )),
[batchCallsStatus, ownershipSettings],
);
diff --git a/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/index.tsx b/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/index.tsx
index 0a6c90fc42..d03eb2b539 100644
--- a/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/index.tsx
+++ b/apps/web/src/components/Basenames/UsernameProfileTransferOwnershipModal/index.tsx
@@ -28,11 +28,13 @@ const ownershipStepsTitleForDisplay = {
type UsernameProfileTransferOwnershipModalProps = {
isOpen: boolean;
onClose: () => void;
+ onSuccess?: () => void;
};
export default function UsernameProfileTransferOwnershipModal({
isOpen,
onClose,
+ onSuccess,
}: UsernameProfileTransferOwnershipModalProps) {
// Hooks
const { address } = useAccount();
@@ -103,8 +105,9 @@ export default function UsernameProfileTransferOwnershipModal({
useEffect(() => {
if (isSuccess) {
setCurrentOwnershipStep(OwnershipSteps.Success);
+ onSuccess?.();
}
- }, [isSuccess, setCurrentOwnershipStep]);
+ }, [isSuccess, setCurrentOwnershipStep, onSuccess]);
return (
{
- setOpen(false);
+ const timeoutId = setTimeout(() => {
+ setOpen(false);
+ }, 300);
+ return () => clearTimeout(timeoutId);
}, []);
const openDropdown = useCallback(() => {
@@ -56,7 +59,15 @@ export default function Dropdown({ children }: DropdownProps) {
return (
-
+
{children}
diff --git a/apps/web/src/components/DropdownMenu/index.tsx b/apps/web/src/components/DropdownMenu/index.tsx
index a2bdadcae3..0d17afd834 100644
--- a/apps/web/src/components/DropdownMenu/index.tsx
+++ b/apps/web/src/components/DropdownMenu/index.tsx
@@ -44,8 +44,8 @@ export default function DropdownMenu({
let dropdownStyle: CSSProperties = {};
if (dropdownToggleRef?.current) {
const { top, height, right } = dropdownToggleRef.current.getBoundingClientRect();
- dropdownStyle.top = top + height + 'px';
- dropdownStyle.left = `${right}px`;
+ dropdownStyle.top = top + height + window.scrollY + 'px';
+ dropdownStyle.left = `${right + window.scrollX}px`;
dropdownStyle.transform = `translateX(-100%)`;
}
@@ -61,8 +61,8 @@ export default function DropdownMenu({
let arrowStyle: CSSProperties = {};
if (dropdownToggleRef?.current) {
const { top, height, left, width } = dropdownToggleRef.current.getBoundingClientRect();
- arrowStyle.top = top + height + 'px';
- arrowStyle.left = `${left + width / 2}px`;
+ arrowStyle.top = top + height + window.scrollY + 'px';
+ arrowStyle.left = `${left + width / 2 + window.scrollX}px`;
}
return (
diff --git a/apps/web/src/components/Icon/Icon.tsx b/apps/web/src/components/Icon/Icon.tsx
index f68b34edfb..49e0a5fc45 100644
--- a/apps/web/src/components/Icon/Icon.tsx
+++ b/apps/web/src/components/Icon/Icon.tsx
@@ -538,6 +538,59 @@ const ICONS: Record
JSX.Element> = {
/>
),
+ list: ({ color, width, height }: SvgProps) => (
+
+
+
+
+
+
+
+
+ ),
+ verticalDots: ({ color, width, height }: SvgProps) => (
+
+
+
+ ),
+ transfer: ({ color, width, height }: SvgProps) => (
+
+
+
+ ),
};
export function Icon({ name, color = 'white', width = '24', height = '24' }: IconProps) {
diff --git a/apps/web/src/components/Layout/UsernameNav/index.tsx b/apps/web/src/components/Layout/UsernameNav/index.tsx
index 73d5e1a9bc..e042d9db71 100644
--- a/apps/web/src/components/Layout/UsernameNav/index.tsx
+++ b/apps/web/src/components/Layout/UsernameNav/index.tsx
@@ -1,6 +1,6 @@
'use client';
-import Link from 'next/link';
import usernameBaseLogo from './usernameBaseLogo.svg';
+import Link from 'apps/web/src/components/Link';
import {
ConnectWalletButton,
@@ -42,7 +42,7 @@ export default function UsernameNav() {
[switchChain],
);
- const walletStateClasses = classNames('p2 rounded', {
+ const walletStateClasses = classNames('p2 rounded flex items-center gap-6', {
'bg-white': isConnected,
});
@@ -111,6 +111,14 @@ export default function UsernameNav() {
+ {isConnected && (
+
+
+
+ My Basenames
+
+
+ )}
{
+ const setPrimaryName = useCallback(async (): Promise => {
// Already primary
- if (secondaryUsername === primaryUsername) return;
+ if (secondaryUsername === primaryUsername) return undefined;
// No user is connected
- if (!address) return;
+ if (!address) return undefined;
try {
await initiateTransaction({
@@ -81,6 +81,7 @@ export default function useSetPrimaryBasename({ secondaryUsername }: UseSetPrima
});
} catch (error) {
logError(error, 'Set primary name transaction canceled');
+ return undefined;
}
return true;
@@ -95,5 +96,5 @@ export default function useSetPrimaryBasename({ secondaryUsername }: UseSetPrima
const isLoading = transactionIsLoading || primaryUsernameIsLoading || primaryUsernameIsFetching;
- return { setPrimaryName, canSetUsernameAsPrimary, isLoading };
+ return { setPrimaryName, canSetUsernameAsPrimary, isLoading, transactionIsSuccess };
}
diff --git a/apps/web/src/types/ManagedAddresses.ts b/apps/web/src/types/ManagedAddresses.ts
new file mode 100644
index 0000000000..5ffc4aa53a
--- /dev/null
+++ b/apps/web/src/types/ManagedAddresses.ts
@@ -0,0 +1,17 @@
+export type ManagedAddressesData = {
+ domain: string;
+ expires_at: string;
+ is_primary: boolean;
+ manager_address: string;
+ network_id: string;
+ owner_address: string;
+ primary_address: string;
+ token_id: string;
+};
+
+export type ManagedAddressesResponse = {
+ data: ManagedAddressesData[];
+ has_more: boolean;
+ next_page: string;
+ total_count: number;
+};
diff --git a/yarn.lock b/yarn.lock
index aee8802291..5cd976b724 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -401,6 +401,7 @@ __metadata:
classnames: ^2.5.1
cloudinary: ^2.5.1
csv-parser: ^3.0.0
+ date-fns: ^4.1.0
dd-trace: ^5.21.0
dotenv: ^16.0.3
eslint-config-next: ^13.1.6
@@ -13298,6 +13299,13 @@ __metadata:
languageName: node
linkType: hard
+"date-fns@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "date-fns@npm:4.1.0"
+ checksum: fb681b242cccabed45494468f64282a7d375ea970e0adbcc5dcc92dcb7aba49b2081c2c9739d41bf71ce89ed68dd73bebfe06ca35129490704775d091895710b
+ languageName: node
+ linkType: hard
+
"dc-polyfill@npm:^0.1.4":
version: 0.1.6
resolution: "dc-polyfill@npm:0.1.6"
From 24dbe96e0d38e243bd014cd41dfbeaa30d1b8a3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Galley?=
Date: Thu, 7 Nov 2024 15:40:09 -0500
Subject: [PATCH 11/83] fix script (#1223)
---
.github/workflows/file-size-checker.yml | 37 +++++++++++++------------
1 file changed, 20 insertions(+), 17 deletions(-)
diff --git a/.github/workflows/file-size-checker.yml b/.github/workflows/file-size-checker.yml
index 18fbc30fc9..a8c438eb64 100644
--- a/.github/workflows/file-size-checker.yml
+++ b/.github/workflows/file-size-checker.yml
@@ -85,22 +85,20 @@ jobs:
const largeFiles = `${{ steps.check-sizes.outputs.large_files }}`;
try {
- console.log('Repository:', context.payload.repository.name);
- console.log('Owner:', context.payload.repository.owner.login);
- console.log('SHA:', context.payload.pull_request.head.sha);
-
- // Set status check that will be used by branch protection
- await github.rest.repos.createCommitStatus({
- owner: context.payload.repository.owner.login,
- repo: context.payload.repository.name,
- sha: context.payload.pull_request.head.sha,
- state: hugeFiles ? 'failure' : 'success',
- context: 'File Size Check',
- description: hugeFiles ? 'Files over 40MB found' : 'All files within size limits',
- target_url: `https://github.com/${context.payload.repository.owner.login}/${context.payload.repository.name}/actions/runs/${context.runId}`
- });
-
- // Only comment if issues were found
+ // Only create status check if we have permission (not a fork PR)
+ if (context.payload.pull_request.head.repo.full_name === context.payload.repository.full_name) {
+ await github.rest.repos.createCommitStatus({
+ owner: context.payload.repository.owner.login,
+ repo: context.payload.repository.name,
+ sha: context.payload.pull_request.head.sha,
+ state: hugeFiles ? 'failure' : 'success',
+ context: 'File Size Check',
+ description: hugeFiles ? 'Files over 40MB found' : 'All files within size limits',
+ target_url: `https://github.com/${context.payload.repository.owner.login}/${context.payload.repository.name}/actions/runs/${context.runId}`
+ });
+ }
+
+ // Comments should work for both fork and non-fork PRs
if (hugeFiles || largeFiles) {
let comment = '## ⚠️ File Size Check Results\n\n';
@@ -124,5 +122,10 @@ jobs:
} catch (error) {
console.error('Error:', error);
console.error('Context:', JSON.stringify(context.payload, null, 2));
- core.setFailed(error.message);
+ // Only ignore status check permission errors for fork PRs
+ if (error.status === 403 && context.payload.pull_request.head.repo.full_name !== context.payload.repository.full_name) {
+ console.log('Ignoring status check permission error for fork PR');
+ } else {
+ core.setFailed(error.message);
+ }
}
From 2ee1723ba2fe2be17395ccc072f39aca10350c3b Mon Sep 17 00:00:00 2001
From: NikolaiKryshnev <63440682+NikolaiKryshnev@users.noreply.github.com>
Date: Thu, 7 Nov 2024 23:50:38 +0300
Subject: [PATCH 12/83] Enhancements: Add comments in tos.ts and switch Twitter
icon to X (#1140)
* Update tos.ts: Add comments
Included comments explaining the purpose of the array and the meaning of each code.
* Update tos.ts
Removed unnecessary parenthetical comment
* Update index.tsx: switch Twitter icon to X
switch Twitter icon to X in SVG assets
* Update socialPlatforms.ts: rename Twitter to X
- Renaming [SocialPlatform.Twitter]: 'twitter' to [SocialPlatform.Twitter]: 'x'
- Modifying share link URL to `https://x.com/intent/tweet`
---
apps/bridge/pages/api/tos.ts | 58 ++++++++++++++-------------
apps/web/src/utils/socialPlatforms.ts | 4 +-
libs/base-ui/Icon/index.tsx | 16 ++++----
3 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/apps/bridge/pages/api/tos.ts b/apps/bridge/pages/api/tos.ts
index be54946c8d..2d7316c983 100644
--- a/apps/bridge/pages/api/tos.ts
+++ b/apps/bridge/pages/api/tos.ts
@@ -1,36 +1,38 @@
import type { NextApiRequest, NextApiResponse } from 'next';
+// Array of two-letter country codes of European Union members (according to ISO 3166-1 alpha-2)
const EU_COUNTRIES = [
- 'AT',
- 'BE',
- 'BG',
- 'CY',
- 'CZ',
- 'DE',
- 'DK',
- 'EE',
- 'ES',
- 'FI',
- 'FR',
- 'GB',
- 'GR',
- 'HU',
- 'HR',
- 'IE',
- 'IT',
- 'LT',
- 'LU',
- 'LV',
- 'MT',
- 'NL',
- 'PL',
- 'PT',
- 'RO',
- 'SE',
- 'SI',
- 'SK',
+ 'AT', // Austria
+ 'BE', // Belgium
+ 'BG', // Bulgaria
+ 'CY', // Cyprus
+ 'CZ', // Czech Republic
+ 'DE', // Germany
+ 'DK', // Denmark
+ 'EE', // Estonia
+ 'ES', // Spain
+ 'FI', // Finland
+ 'FR', // France
+ 'GB', // United Kingdom
+ 'GR', // Greece
+ 'HU', // Hungary
+ 'HR', // Croatia
+ 'IE', // Ireland
+ 'IT', // Italy
+ 'LT', // Lithuania
+ 'LU', // Luxembourg
+ 'LV', // Latvia
+ 'MT', // Malta
+ 'NL', // Netherlands
+ 'PL', // Poland
+ 'PT', // Portugal
+ 'RO', // Romania
+ 'SE', // Sweden
+ 'SI', // Slovenia
+ 'SK', // Slovakia
];
+
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const country = res.getHeader('x-cf-country') as string;
const tosRegion = EU_COUNTRIES.includes(country) ? 'EU' : 'US';
diff --git a/apps/web/src/utils/socialPlatforms.ts b/apps/web/src/utils/socialPlatforms.ts
index 0604bcd40f..c5795e6e11 100644
--- a/apps/web/src/utils/socialPlatforms.ts
+++ b/apps/web/src/utils/socialPlatforms.ts
@@ -25,7 +25,7 @@ export const socialPlatformCtaForDisplay = {
};
export const socialPlatformIconName: Record = {
- [SocialPlatform.Twitter]: 'twitter',
+ [SocialPlatform.Twitter]: 'x',
[SocialPlatform.Farcaster]: 'farcaster',
};
@@ -44,7 +44,7 @@ export const socialPlatformShareLinkFunction: SocialPlatformShareLinkFunction =
url: url,
};
- return urlWithQueryParams('https://twitter.com/intent/tweet', shareParams);
+ return urlWithQueryParams('https://x.com/intent/tweet', shareParams);
},
[SocialPlatform.Farcaster]: ({ text, url }: SocialMediaShareParams) => {
const shareParams: QueryParams = {
diff --git a/libs/base-ui/Icon/index.tsx b/libs/base-ui/Icon/index.tsx
index d080bc2ec6..4bd7770b21 100644
--- a/libs/base-ui/Icon/index.tsx
+++ b/libs/base-ui/Icon/index.tsx
@@ -29,18 +29,16 @@ const ICONS: Record JSX.Element> = {
),
twitter: ({ color, width, height }: SvgProps) => (
-
-
-
+
+
),
github: ({ color, width, height }: SvgProps) => (
Date: Fri, 8 Nov 2024 09:54:49 -0500
Subject: [PATCH 13/83] remove api call in in favor of exit 0 (#1227)
---
.github/workflows/file-size-checker.yml | 28 ++-----------------------
1 file changed, 2 insertions(+), 26 deletions(-)
diff --git a/.github/workflows/file-size-checker.yml b/.github/workflows/file-size-checker.yml
index a8c438eb64..71a0968c1c 100644
--- a/.github/workflows/file-size-checker.yml
+++ b/.github/workflows/file-size-checker.yml
@@ -16,11 +16,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- # - name: Setup environment
- # run: |
- # apt-get update
- # apt-get install -y git bc
-
- name: Checkout code
uses: actions/checkout@v4
with:
@@ -85,20 +80,7 @@ jobs:
const largeFiles = `${{ steps.check-sizes.outputs.large_files }}`;
try {
- // Only create status check if we have permission (not a fork PR)
- if (context.payload.pull_request.head.repo.full_name === context.payload.repository.full_name) {
- await github.rest.repos.createCommitStatus({
- owner: context.payload.repository.owner.login,
- repo: context.payload.repository.name,
- sha: context.payload.pull_request.head.sha,
- state: hugeFiles ? 'failure' : 'success',
- context: 'File Size Check',
- description: hugeFiles ? 'Files over 40MB found' : 'All files within size limits',
- target_url: `https://github.com/${context.payload.repository.owner.login}/${context.payload.repository.name}/actions/runs/${context.runId}`
- });
- }
-
- // Comments should work for both fork and non-fork PRs
+ // Only comment if issues were found
if (hugeFiles || largeFiles) {
let comment = '## ⚠️ File Size Check Results\n\n';
@@ -121,11 +103,5 @@ jobs:
}
} catch (error) {
console.error('Error:', error);
- console.error('Context:', JSON.stringify(context.payload, null, 2));
- // Only ignore status check permission errors for fork PRs
- if (error.status === 403 && context.payload.pull_request.head.repo.full_name !== context.payload.repository.full_name) {
- console.log('Ignoring status check permission error for fork PR');
- } else {
- core.setFailed(error.message);
- }
+ core.setFailed(error.message);
}
From 6bb9f92f792afefe72c2e3a42086781c2c23a679 Mon Sep 17 00:00:00 2001
From: Matthew Bunday
Date: Fri, 8 Nov 2024 13:53:54 -0500
Subject: [PATCH 14/83] Fix nav padding (#1230)
---
apps/web/src/components/Layout/UsernameNav/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/components/Layout/UsernameNav/index.tsx b/apps/web/src/components/Layout/UsernameNav/index.tsx
index e042d9db71..22cca65afe 100644
--- a/apps/web/src/components/Layout/UsernameNav/index.tsx
+++ b/apps/web/src/components/Layout/UsernameNav/index.tsx
@@ -42,7 +42,7 @@ export default function UsernameNav() {
[switchChain],
);
- const walletStateClasses = classNames('p2 rounded flex items-center gap-6', {
+ const walletStateClasses = classNames('p-2 rounded flex items-center gap-6', {
'bg-white': isConnected,
});
From 819ac6dd5698f753ad2c4ea6d2dc0a5fe7590900 Mon Sep 17 00:00:00 2001
From: Danyal Prout
Date: Fri, 8 Nov 2024 12:54:33 -0600
Subject: [PATCH 15/83] chore: add stats page w/ datadog iframe (#1224)
* chore: add stats page w/ datadog iframe
* fix linter
---
apps/web/app/(stats)/layout.tsx | 32 +++++++++++++++++++++++++++++
apps/web/app/(stats)/stats/page.tsx | 18 ++++++++++++++++
apps/web/next.config.js | 1 +
3 files changed, 51 insertions(+)
create mode 100644 apps/web/app/(stats)/layout.tsx
create mode 100644 apps/web/app/(stats)/stats/page.tsx
diff --git a/apps/web/app/(stats)/layout.tsx b/apps/web/app/(stats)/layout.tsx
new file mode 100644
index 0000000000..7baa778472
--- /dev/null
+++ b/apps/web/app/(stats)/layout.tsx
@@ -0,0 +1,32 @@
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ metadataBase: new URL('https://base.org'),
+ title: `Base`,
+ description:
+ 'Base is a secure, low-cost, builder-friendly Ethereum L2 built to bring the next billion users onchain.',
+ openGraph: {
+ type: 'website',
+ title: `Base`,
+ description:
+ 'Base is a secure, low-cost, builder-friendly Ethereum L2 built to bring the next billion users onchain.',
+ url: `/`,
+ images: ['https://base.org/images/base-open-graph.png'],
+ },
+ twitter: {
+ site: '@base',
+ card: 'summary_large_image',
+ },
+};
+
+export default async function StatsLayout({
+ children, // will be a page or nested layout
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/app/(stats)/stats/page.tsx b/apps/web/app/(stats)/stats/page.tsx
new file mode 100644
index 0000000000..bfa6a24a23
--- /dev/null
+++ b/apps/web/app/(stats)/stats/page.tsx
@@ -0,0 +1,18 @@
+import type { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ metadataBase: new URL('https://base.org'),
+ title: `Base | Stats`,
+ description: 'Live stats for the Base network',
+};
+
+export default async function Page() {
+ return (
+
+ );
+}
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index 9f259d7395..6c3a807f5f 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -119,6 +119,7 @@ const contentSecurityPolicy = {
'https://unpkg.com/@lottiefiles/dotlottie-web@0.31.1/dist/dotlottie-player.wasm', // lottie player
`https://${process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL}`,
],
+ 'frame-src': ["https://p.datadoghq.com"],
'frame-ancestors': ["'self'", baseXYZDomains],
'form-action': ["'self'", baseXYZDomains],
'img-src': [
From 362dac5d49264e532983c60cf1c2d963768aa3df Mon Sep 17 00:00:00 2001
From: Brendan from DeFi
Date: Fri, 8 Nov 2024 13:36:01 -0800
Subject: [PATCH 16/83] chore: limit usage of gas estimator and connect wallet
(#1232)
* moved crypto providers closer to their components
* import cleanup
* handling providers for /names
* handling builder anniversary nft
* mb fix
---
.../builder-anniversary-nft/page.tsx | 5 +-
apps/web/app/(basenames)/layout.tsx | 11 ++-
apps/web/app/AppProviders.tsx | 80 ++-----------------
apps/web/app/CryptoProviders.tsx | 74 +++++++++++++++++
.../ConnectWalletButton.tsx | 11 +++
.../shared/TopNavigation/GasPriceDropdown.tsx | 11 ++-
.../base-org/shared/TopNavigation/index.tsx | 26 ++++--
7 files changed, 132 insertions(+), 86 deletions(-)
create mode 100644 apps/web/app/CryptoProviders.tsx
diff --git a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
index 80efceb319..355dfc619e 100644
--- a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
+++ b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
@@ -1,3 +1,4 @@
+import CryptoProviders from 'apps/web/app/CryptoProviders';
import { BuilderNftHero } from 'apps/web/src/components/BuilderNft/BuilderNftHero';
import type { Metadata } from 'next';
@@ -13,7 +14,9 @@ export const metadata: Metadata = {
export default async function About() {
return (
-
+
+
+
);
}
diff --git a/apps/web/app/(basenames)/layout.tsx b/apps/web/app/(basenames)/layout.tsx
index 493d746931..87db756f35 100644
--- a/apps/web/app/(basenames)/layout.tsx
+++ b/apps/web/app/(basenames)/layout.tsx
@@ -1,3 +1,4 @@
+import CryptoProviders from 'apps/web/app/CryptoProviders';
import ErrorsProvider from 'apps/web/contexts/Errors';
import UsernameNav from 'apps/web/src/components/Layout/UsernameNav';
@@ -27,10 +28,12 @@ export default async function BasenameLayout({
}) {
return (
-
-
- {children}
-
+
+
+
+ {children}
+
+
);
}
diff --git a/apps/web/app/AppProviders.tsx b/apps/web/app/AppProviders.tsx
index 3f48d3ecb4..b720ff7c13 100644
--- a/apps/web/app/AppProviders.tsx
+++ b/apps/web/app/AppProviders.tsx
@@ -1,6 +1,4 @@
'use client';
-import '@rainbow-me/rainbowkit/styles.css';
-import '@coinbase/onchainkit/styles.css';
import {
Provider as CookieManagerProvider,
@@ -8,28 +6,14 @@ 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 {
- coinbaseWallet,
- metaMaskWallet,
- phantomWallet,
- rainbowWallet,
- uniswapWallet,
- walletConnectWallet,
-} from '@rainbow-me/rainbowkit/wallets';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ExperimentsProvider from 'base-ui/contexts/Experiments';
import useSprig from 'base-ui/hooks/useSprig';
import { useCallback, useRef } from 'react';
-import { createConfig, http, WagmiProvider } from 'wagmi';
-import { base, baseSepolia, mainnet } from 'wagmi/chains';
import { cookieManagerConfig } from '../src/utils/cookieManagerConfig';
import ClientAnalyticsScript from 'apps/web/src/components/ClientAnalyticsScript/ClientAnalyticsScript';
import dynamic from 'next/dynamic';
import ErrorsProvider from 'apps/web/contexts/Errors';
-import { isDevelopment } from 'apps/web/src/constants';
import { logger } from 'apps/web/src/utils/logger';
const DynamicCookieBannerWrapper = dynamic(
@@ -39,43 +23,6 @@ const DynamicCookieBannerWrapper = dynamic(
},
);
-coinbaseWallet.preference = 'all';
-
-const connectors = connectorsForWallets(
- [
- {
- groupName: 'Recommended',
- wallets: [
- coinbaseWallet,
- metaMaskWallet,
- uniswapWallet,
- rainbowWallet,
- phantomWallet,
- walletConnectWallet,
- ],
- },
- ],
- {
- projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id',
- walletConnectParameters: {},
- appName: 'Base.org',
- appDescription: '',
- appUrl: 'https://www.base.org/',
- appIcon: '',
- },
-);
-
-const config = createConfig({
- connectors,
- chains: [base, baseSepolia, mainnet],
- transports: {
- [base.id]: http(),
- [baseSepolia.id]: http(),
- [mainnet.id]: http(),
- },
- ssr: true,
-});
-const queryClient = new QueryClient();
const sprigEnvironmentId = process.env.NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID;
type AppProvidersProps = {
@@ -136,25 +83,14 @@ export default function AppProviders({ children }: AppProvidersProps) {
config={cookieManagerConfig}
>
-
-
-
-
-
-
- <>
- {children}
-
- >
-
-
-
-
-
-
+
+
+ <>
+ {children}
+
+ >
+
+
);
diff --git a/apps/web/app/CryptoProviders.tsx b/apps/web/app/CryptoProviders.tsx
new file mode 100644
index 0000000000..1e2ae0e5d2
--- /dev/null
+++ b/apps/web/app/CryptoProviders.tsx
@@ -0,0 +1,74 @@
+'use client';
+
+import '@rainbow-me/rainbowkit/styles.css';
+import '@coinbase/onchainkit/styles.css';
+
+import { OnchainKitProvider } from '@coinbase/onchainkit';
+import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { isDevelopment } from 'apps/web/src/constants';
+import { createConfig, http, WagmiProvider } from 'wagmi';
+import { base, baseSepolia, mainnet } from 'wagmi/chains';
+import {
+ coinbaseWallet,
+ metaMaskWallet,
+ phantomWallet,
+ rainbowWallet,
+ uniswapWallet,
+ walletConnectWallet,
+} from '@rainbow-me/rainbowkit/wallets';
+
+const connectors = connectorsForWallets(
+ [
+ {
+ groupName: 'Recommended',
+ wallets: [
+ coinbaseWallet,
+ metaMaskWallet,
+ uniswapWallet,
+ rainbowWallet,
+ phantomWallet,
+ walletConnectWallet,
+ ],
+ },
+ ],
+ {
+ projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id',
+ walletConnectParameters: {},
+ appName: 'Base.org',
+ appDescription: '',
+ appUrl: 'https://www.base.org/',
+ appIcon: '',
+ },
+);
+
+const config = createConfig({
+ connectors,
+ chains: [base, baseSepolia, mainnet],
+ transports: {
+ [base.id]: http(),
+ [baseSepolia.id]: http(),
+ [mainnet.id]: http(),
+ },
+ ssr: true,
+});
+const queryClient = new QueryClient();
+
+type CryptoProvidersProps = {
+ children: React.ReactNode;
+};
+
+export default function CryptoProviders({ children }: CryptoProvidersProps) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
index 6e936441ab..0834958c3a 100644
--- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
+++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
@@ -27,6 +27,7 @@ import { useCopyToClipboard } from 'usehooks-ts';
import { useAccount, useSwitchChain } from 'wagmi';
import ChainDropdown from 'apps/web/src/components/ChainDropdown';
import { useSearchParams } from 'next/navigation';
+import CryptoProviders from 'apps/web/app/CryptoProviders';
export enum ConnectWalletButtonVariants {
BaseOrg,
@@ -37,6 +38,16 @@ type ConnectWalletButtonProps = {
connectWalletButtonVariant: ConnectWalletButtonVariants;
};
+export function WrappedConnectWalletButton({
+ connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg,
+}: ConnectWalletButtonProps) {
+ return (
+
+
+
+ )
+}
+
export function ConnectWalletButton({
connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg,
}: ConnectWalletButtonProps) {
diff --git a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
index 27543de352..cee635c449 100644
--- a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
+++ b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
@@ -1,3 +1,4 @@
+import CryptoProviders from 'apps/web/app/CryptoProviders';
import Card from 'apps/web/src/components/base-org/Card';
import { Icon } from 'apps/web/src/components/Icon/Icon';
import { base, mainnet } from 'viem/chains';
@@ -9,7 +10,15 @@ const convertWeiToMwei = (weiValue: bigint): number => {
return Number(mweiValue.toFixed(2)); // Round to 2 decimal places
};
-export default function GasPriceDropdown() {
+export function WrappedGasPriceDropdown() {
+ return (
+
+
+
+ );
+}
+
+function GasPriceDropdown() {
const { data: baseGasPriceInWei } = useGasPrice({
chainId: base.id,
query: {
diff --git a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
index b7160b2cb8..06280970da 100644
--- a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
+++ b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
@@ -5,13 +5,14 @@ import Link from 'next/link';
import logo from './assets/logo.svg';
import Image, { StaticImageData } from 'next/image';
import {
- ConnectWalletButton,
+ WrappedConnectWalletButton,
ConnectWalletButtonVariants,
} from 'apps/web/src/components/ConnectWalletButton/ConnectWalletButton';
import MenuDesktop from 'apps/web/src/components/base-org/shared/TopNavigation/MenuDesktop';
import MenuMobile from 'apps/web/src/components/base-org/shared/TopNavigation/MenuMobile';
-import GasPriceDropdown from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown';
+import { WrappedGasPriceDropdown } from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown';
import { Suspense } from 'react';
+import { usePathname } from 'next/navigation';
export type SubItem = {
name: string;
@@ -92,6 +93,13 @@ const links: TopNavigationLink[] = [
];
export default function TopNavigation() {
+ const pathname = usePathname();
+ const showGasDropdownAndConnectWallet = ![
+ '/jobs',
+ '/about',
+ '/ecosystem',
+ '/getstarted',
+ ].includes(pathname ?? '');
return (
@@ -101,7 +109,7 @@ export default function TopNavigation() {
-
+ {showGasDropdownAndConnectWallet && }
@@ -114,11 +122,13 @@ export default function TopNavigation() {
{/* Connect Wallet button */}
-
-
-
+ {showGasDropdownAndConnectWallet && (
+
+
+
+ )}
From b75d9cc1d521bf19bef387a1ea3807ac4550bb63 Mon Sep 17 00:00:00 2001
From: omkar
Date: Sun, 10 Nov 2024 10:53:31 -0500
Subject: [PATCH 17/83] Revert "chore: limit usage of gas estimator and connect
wallet (#1232)" (#1242)
This reverts commit 362dac5d49264e532983c60cf1c2d963768aa3df.
---
.../builder-anniversary-nft/page.tsx | 5 +-
apps/web/app/(basenames)/layout.tsx | 11 +--
apps/web/app/AppProviders.tsx | 80 +++++++++++++++++--
apps/web/app/CryptoProviders.tsx | 74 -----------------
.../ConnectWalletButton.tsx | 11 ---
.../shared/TopNavigation/GasPriceDropdown.tsx | 11 +--
.../base-org/shared/TopNavigation/index.tsx | 26 ++----
7 files changed, 86 insertions(+), 132 deletions(-)
delete mode 100644 apps/web/app/CryptoProviders.tsx
diff --git a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
index 355dfc619e..80efceb319 100644
--- a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
+++ b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
@@ -1,4 +1,3 @@
-import CryptoProviders from 'apps/web/app/CryptoProviders';
import { BuilderNftHero } from 'apps/web/src/components/BuilderNft/BuilderNftHero';
import type { Metadata } from 'next';
@@ -14,9 +13,7 @@ export const metadata: Metadata = {
export default async function About() {
return (
-
-
-
+
);
}
diff --git a/apps/web/app/(basenames)/layout.tsx b/apps/web/app/(basenames)/layout.tsx
index 87db756f35..493d746931 100644
--- a/apps/web/app/(basenames)/layout.tsx
+++ b/apps/web/app/(basenames)/layout.tsx
@@ -1,4 +1,3 @@
-import CryptoProviders from 'apps/web/app/CryptoProviders';
import ErrorsProvider from 'apps/web/contexts/Errors';
import UsernameNav from 'apps/web/src/components/Layout/UsernameNav';
@@ -28,12 +27,10 @@ export default async function BasenameLayout({
}) {
return (
-
-
-
- {children}
-
-
+
+
+ {children}
+
);
}
diff --git a/apps/web/app/AppProviders.tsx b/apps/web/app/AppProviders.tsx
index b720ff7c13..3f48d3ecb4 100644
--- a/apps/web/app/AppProviders.tsx
+++ b/apps/web/app/AppProviders.tsx
@@ -1,4 +1,6 @@
'use client';
+import '@rainbow-me/rainbowkit/styles.css';
+import '@coinbase/onchainkit/styles.css';
import {
Provider as CookieManagerProvider,
@@ -6,14 +8,28 @@ 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 {
+ coinbaseWallet,
+ metaMaskWallet,
+ phantomWallet,
+ rainbowWallet,
+ uniswapWallet,
+ walletConnectWallet,
+} from '@rainbow-me/rainbowkit/wallets';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ExperimentsProvider from 'base-ui/contexts/Experiments';
import useSprig from 'base-ui/hooks/useSprig';
import { useCallback, useRef } from 'react';
+import { createConfig, http, WagmiProvider } from 'wagmi';
+import { base, baseSepolia, mainnet } from 'wagmi/chains';
import { cookieManagerConfig } from '../src/utils/cookieManagerConfig';
import ClientAnalyticsScript from 'apps/web/src/components/ClientAnalyticsScript/ClientAnalyticsScript';
import dynamic from 'next/dynamic';
import ErrorsProvider from 'apps/web/contexts/Errors';
+import { isDevelopment } from 'apps/web/src/constants';
import { logger } from 'apps/web/src/utils/logger';
const DynamicCookieBannerWrapper = dynamic(
@@ -23,6 +39,43 @@ const DynamicCookieBannerWrapper = dynamic(
},
);
+coinbaseWallet.preference = 'all';
+
+const connectors = connectorsForWallets(
+ [
+ {
+ groupName: 'Recommended',
+ wallets: [
+ coinbaseWallet,
+ metaMaskWallet,
+ uniswapWallet,
+ rainbowWallet,
+ phantomWallet,
+ walletConnectWallet,
+ ],
+ },
+ ],
+ {
+ projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id',
+ walletConnectParameters: {},
+ appName: 'Base.org',
+ appDescription: '',
+ appUrl: 'https://www.base.org/',
+ appIcon: '',
+ },
+);
+
+const config = createConfig({
+ connectors,
+ chains: [base, baseSepolia, mainnet],
+ transports: {
+ [base.id]: http(),
+ [baseSepolia.id]: http(),
+ [mainnet.id]: http(),
+ },
+ ssr: true,
+});
+const queryClient = new QueryClient();
const sprigEnvironmentId = process.env.NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID;
type AppProvidersProps = {
@@ -83,14 +136,25 @@ export default function AppProviders({ children }: AppProvidersProps) {
config={cookieManagerConfig}
>
-
-
- <>
- {children}
-
- >
-
-
+
+
+
+
+
+
+ <>
+ {children}
+
+ >
+
+
+
+
+
+
);
diff --git a/apps/web/app/CryptoProviders.tsx b/apps/web/app/CryptoProviders.tsx
deleted file mode 100644
index 1e2ae0e5d2..0000000000
--- a/apps/web/app/CryptoProviders.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import '@rainbow-me/rainbowkit/styles.css';
-import '@coinbase/onchainkit/styles.css';
-
-import { OnchainKitProvider } from '@coinbase/onchainkit';
-import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { isDevelopment } from 'apps/web/src/constants';
-import { createConfig, http, WagmiProvider } from 'wagmi';
-import { base, baseSepolia, mainnet } from 'wagmi/chains';
-import {
- coinbaseWallet,
- metaMaskWallet,
- phantomWallet,
- rainbowWallet,
- uniswapWallet,
- walletConnectWallet,
-} from '@rainbow-me/rainbowkit/wallets';
-
-const connectors = connectorsForWallets(
- [
- {
- groupName: 'Recommended',
- wallets: [
- coinbaseWallet,
- metaMaskWallet,
- uniswapWallet,
- rainbowWallet,
- phantomWallet,
- walletConnectWallet,
- ],
- },
- ],
- {
- projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id',
- walletConnectParameters: {},
- appName: 'Base.org',
- appDescription: '',
- appUrl: 'https://www.base.org/',
- appIcon: '',
- },
-);
-
-const config = createConfig({
- connectors,
- chains: [base, baseSepolia, mainnet],
- transports: {
- [base.id]: http(),
- [baseSepolia.id]: http(),
- [mainnet.id]: http(),
- },
- ssr: true,
-});
-const queryClient = new QueryClient();
-
-type CryptoProvidersProps = {
- children: React.ReactNode;
-};
-
-export default function CryptoProviders({ children }: CryptoProvidersProps) {
- return (
-
-
-
- {children}
-
-
-
- );
-}
diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
index 0834958c3a..6e936441ab 100644
--- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
+++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
@@ -27,7 +27,6 @@ import { useCopyToClipboard } from 'usehooks-ts';
import { useAccount, useSwitchChain } from 'wagmi';
import ChainDropdown from 'apps/web/src/components/ChainDropdown';
import { useSearchParams } from 'next/navigation';
-import CryptoProviders from 'apps/web/app/CryptoProviders';
export enum ConnectWalletButtonVariants {
BaseOrg,
@@ -38,16 +37,6 @@ type ConnectWalletButtonProps = {
connectWalletButtonVariant: ConnectWalletButtonVariants;
};
-export function WrappedConnectWalletButton({
- connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg,
-}: ConnectWalletButtonProps) {
- return (
-
-
-
- )
-}
-
export function ConnectWalletButton({
connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg,
}: ConnectWalletButtonProps) {
diff --git a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
index cee635c449..27543de352 100644
--- a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
+++ b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
@@ -1,4 +1,3 @@
-import CryptoProviders from 'apps/web/app/CryptoProviders';
import Card from 'apps/web/src/components/base-org/Card';
import { Icon } from 'apps/web/src/components/Icon/Icon';
import { base, mainnet } from 'viem/chains';
@@ -10,15 +9,7 @@ const convertWeiToMwei = (weiValue: bigint): number => {
return Number(mweiValue.toFixed(2)); // Round to 2 decimal places
};
-export function WrappedGasPriceDropdown() {
- return (
-
-
-
- );
-}
-
-function GasPriceDropdown() {
+export default function GasPriceDropdown() {
const { data: baseGasPriceInWei } = useGasPrice({
chainId: base.id,
query: {
diff --git a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
index 06280970da..b7160b2cb8 100644
--- a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
+++ b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
@@ -5,14 +5,13 @@ import Link from 'next/link';
import logo from './assets/logo.svg';
import Image, { StaticImageData } from 'next/image';
import {
- WrappedConnectWalletButton,
+ ConnectWalletButton,
ConnectWalletButtonVariants,
} from 'apps/web/src/components/ConnectWalletButton/ConnectWalletButton';
import MenuDesktop from 'apps/web/src/components/base-org/shared/TopNavigation/MenuDesktop';
import MenuMobile from 'apps/web/src/components/base-org/shared/TopNavigation/MenuMobile';
-import { WrappedGasPriceDropdown } from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown';
+import GasPriceDropdown from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown';
import { Suspense } from 'react';
-import { usePathname } from 'next/navigation';
export type SubItem = {
name: string;
@@ -93,13 +92,6 @@ const links: TopNavigationLink[] = [
];
export default function TopNavigation() {
- const pathname = usePathname();
- const showGasDropdownAndConnectWallet = ![
- '/jobs',
- '/about',
- '/ecosystem',
- '/getstarted',
- ].includes(pathname ?? '');
return (
@@ -109,7 +101,7 @@ export default function TopNavigation() {
- {showGasDropdownAndConnectWallet && }
+
@@ -122,13 +114,11 @@ export default function TopNavigation() {
{/* Connect Wallet button */}
- {showGasDropdownAndConnectWallet && (
-
-
-
- )}
+
+
+
From 99b491e89d0a8c5cc4582427de58df6970232eaa Mon Sep 17 00:00:00 2001
From: lucacurranCB <157145820+lucacurranCB@users.noreply.github.com>
Date: Mon, 11 Nov 2024 12:31:10 +0000
Subject: [PATCH 18/83] Update GetNoticed.tsx (#1241)
Change link on onchain registry from blog post to deform application
---
apps/web/src/components/GetStarted/GetNoticed.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/src/components/GetStarted/GetNoticed.tsx b/apps/web/src/components/GetStarted/GetNoticed.tsx
index c393745ac8..f3c889ffd1 100644
--- a/apps/web/src/components/GetStarted/GetNoticed.tsx
+++ b/apps/web/src/components/GetStarted/GetNoticed.tsx
@@ -16,7 +16,7 @@ export default async function GetNoticed() {
01}
topRight={ }
classnames="bg-pink-60 border-pink-60"
From 7f031defb0571b3739bd76e7b0f50418e639ef94 Mon Sep 17 00:00:00 2001
From: katzman
Date: Mon, 11 Nov 2024 17:16:01 -0800
Subject: [PATCH 19/83] feat(BAPP-790): Devcon discount validator (#1246)
---
apps/web/src/addresses/usernames.ts | 5 +++
.../images/devcon.png | Bin 0 -> 2815 bytes
.../RegistrationLearnMoreModal/index.tsx | 8 ++++
.../hooks/useAggregatedDiscountValidators.ts | 13 +++++-
apps/web/src/hooks/useAttestations.ts | 37 ++++++++++++++++++
apps/web/src/utils/usernames.ts | 1 +
6 files changed, 63 insertions(+), 1 deletion(-)
create mode 100644 apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/devcon.png
diff --git a/apps/web/src/addresses/usernames.ts b/apps/web/src/addresses/usernames.ts
index 43e6e2f9ad..110fd65b0b 100644
--- a/apps/web/src/addresses/usernames.ts
+++ b/apps/web/src/addresses/usernames.ts
@@ -101,3 +101,8 @@ export const BASE_WORLD_DISCOUNT_VALIDATORS: AddressMap = {
[baseSepolia.id]: '0xFa69f6167F40247fe3EFF2d8375B25C5d7834c48',
[base.id]: '0xfEb00a4EfF372a307fDc556Cf4359f7D679E4d11',
};
+
+export const DEVCON_DISCOUNT_VALIDATORS: AddressMap = {
+ [baseSepolia.id]: '0x5c81c392C22Cba477a70D809DE6d6Cd362A1c3DE',
+ [base.id]: '0xFca2EB54EaB56085e25a32BfF30fe8C452216c5F',
+};
diff --git a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/devcon.png b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/images/devcon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3b140e499dc238a14bab3f596dd34278bb695a84
GIT binary patch
literal 2815
zcmZ`*c|4Ts7k_6)b~RbTD8|?oGxiKd3_@g0mh25i3}&WgvUan!xY;g3C|j1ew&J4f
z>$O!Pg`e#rvL+$i8TnP8-~HX^{harl=bZ2Noadb9{o}o5dj2d2D~uHY01g9v9dnw7
z(}kIl_9prJe*yrIJsyiSHNaxwrbHh%ycZ4tBxA3}n4ANfakum)2v41nLD+^>aY8uc
zE`Bu92H$Zu-zpc#$YRXWXf5^NT0fC}5BemOXm*G79g~*X9Z!DU!g^h?S3&1q>>ZYQ%yKGoxlD~9
z@KHSjc2znzekmsv-yiOE%ZVsVON~0ckPiw{d%E)nP}O$huS)M1gI+b&Y@bHK6454o
z+HUQM;Ob1;yc2QO25!d2fHci!1`dHZ0SL_k(H;N<0}k!m0AL6b_->nnBo8oP0Eol`
z38WI-)Fwu6LGzrTDe@DYJAwO(d4A8Q`8W_;Dg)0$18-?fwl+3GyZU&`I$!p2!O2p+ed&1sbt;->dgI8>
zaH_W#frO@NAodYxnoV~j5%7Ho`HBX@+SnA1^Tm%Caa~6avEvhr`v0m)+3jI(i3m
z+D-%EK_>g6kw^-KB1=(_^&z?==8pcneSzMT_C#s8>8ARV+tYY<6~AmwCH$p1$p
z?f3PqoH{)j+7wU4dD-aTy=kMS9StLgQdQqC^N+}ngMMRPAmNBuA8#6wjQMd_2jriF
z|4ZC2Y5lJye~SFXq_==JA>wJ(oar;f$f+a$;XA;qBk2SFY2e>(bKgt5P7JF$^82TO
zVI?zUo&^9#djlOUOKX+~iFjX&L7ujdI4>E8u-f+a!q$jnzPwxx@dhYwUd-LOGfu~1n-538ca=3L(}i9NLG{*
zp!3*=YRSkqyP=MYYMFwmv;W|akH<1vFU2X`(_pACTP?lPk!Xq)Nsmg
z7n-}PmMM;6Yzj(Blt<9Hlqlxa;4oonrW=s5ZA1ny~+
z`bUYk*)t!ibo7>$=iSjZ+tCjcD|$|TQnj>+AkD4q9R-KXEueX+dgF|(K2))e8frhd)uYz9%b0@DHswF)!=nrH*o5%IGqz`pc0d+b;
zso~7ICDDc(iMhMN1%adm&2@Sp>`9LQgPKOZzx
z^G-_o{hud8Fs&ZdjeCnvrqu0jIY@=yyRWe6{|m>JdhAwWUi-)nVl6czv$*oNo3-8^
zJUNzDgxV6n6=9ddQ43HQCh&_AhM*i1tJbYxlC-v5aN+%jh1_YC3zr#&S1@ak^x>yG
zgU#N%pId9#Kc_BV4>(Ppb+bH)^YEz7
zjIIRo<8?v1#Ap%Z#qqAirgYQRp>9#4(CTWu(4J(0l?LJv_y(VL{-dtVorbaTuT{w}
zHLO}W_8!iEV6JLwm#J{-4FK{nsT-+d(?css7nYi~7t@GKV~>HTMLplyYbMKUNd@{g
zl5TS(;fa?@eBye2A1=R7;>6<@CR#$@QL`&P5vCTsKZlebCBbWr_;@X|b92krs^uAL
zwos0;ZkJZ&^Vx(|@=E)%#!uQ=T$pSmY1t`?NCOj@z9h5fi+-KgMEfpvLKuhm3E<^M1yJsMu)M1oykQ6edT;C45L1IlGI{tj0&GE2tGd
zkDPH;Mg7Ihe2*JaaB75b?-9F7R}!yI(Y4q|%Z}q}nr|++gr1}Sml)`t*D2O^y7@Q2
Cw4{{)
literal 0
HcmV?d00001
diff --git a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx
index 5d8169c427..62a8ba495d 100644
--- a/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx
+++ b/apps/web/src/components/Basenames/RegistrationLearnMoreModal/index.tsx
@@ -10,6 +10,7 @@ import summerPassLvl3 from './images/summer-pass-lvl-3.svg';
import cbidVerification from './images/cbid-verification.svg';
import BNSOwnership from './images/bns.jpg';
import BaseNFT from './images/base-nft.svg';
+import DevconPNG from './images/devcon.png';
import TalentProtocolIcon from './images/TalentProtocol.svg';
import coinbaseOneVerification from './images/coinbase-one-verification.svg';
import coinbaseVerification from './images/coinbase-verification.svg';
@@ -95,6 +96,13 @@ const DISCOUNT_ITEMS: DiscountItem[] = [
label: 'Base around the world NFT',
tooltipContent: 'Available for anyone holding one of the Base around the world NFTs',
},
+ {
+ discount: Discount.DEVCON,
+ icon: DevconPNG,
+ alt: 'icon of Devcon',
+ label: 'Devcon attendance NFT',
+ tooltipContent: 'Available for anyone holding one of the Base Devcon NFTs',
+ },
];
export default function RegistrationLearnMoreModal({
diff --git a/apps/web/src/hooks/useAggregatedDiscountValidators.ts b/apps/web/src/hooks/useAggregatedDiscountValidators.ts
index 0e80c6023e..d34263d162 100644
--- a/apps/web/src/hooks/useAggregatedDiscountValidators.ts
+++ b/apps/web/src/hooks/useAggregatedDiscountValidators.ts
@@ -8,6 +8,7 @@ import {
useCheckCBIDAttestations,
useCheckCoinbaseAttestations,
useCheckEAAttestations,
+ useDevconAttestations,
useDiscountCodeAttestations,
useSummerPassAttestations,
useTalentProtocolAttestations,
@@ -58,6 +59,7 @@ export function useAggregatedDiscountValidators(code?: string) {
const { data: TalentProtocolData, loading: loadingTalentProtocolAttestations } =
useTalentProtocolAttestations();
const { data: BaseWorldData, loading: loadingBaseWorld } = useBaseWorldAttestations();
+ const { data: DevconData, loading: loadingDevcon } = useDevconAttestations();
const loadingDiscounts =
loadingCoinbaseAttestations ||
@@ -71,7 +73,8 @@ export function useAggregatedDiscountValidators(code?: string) {
loadingBNS ||
loadingDiscountCode ||
loadingTalentProtocolAttestations ||
- loadingBaseWorld;
+ loadingBaseWorld ||
+ loadingDevcon;
const discountsToAttestationData = useMemo(() => {
const discountMapping: MappedDiscountData = {};
@@ -153,6 +156,13 @@ export function useAggregatedDiscountValidators(code?: string) {
discountKey: validator.key,
};
}
+
+ if (DevconData && validator.discountValidator === DevconData.discountValidatorAddress) {
+ discountMapping[Discount.DEVCON] = {
+ ...DevconData,
+ discountKey: validator.key,
+ };
+ }
});
return discountMapping;
@@ -169,6 +179,7 @@ export function useAggregatedDiscountValidators(code?: string) {
DiscountCodeData,
TalentProtocolData,
BaseWorldData,
+ DevconData,
]);
return {
diff --git a/apps/web/src/hooks/useAttestations.ts b/apps/web/src/hooks/useAttestations.ts
index aab973f6e1..a849032785 100644
--- a/apps/web/src/hooks/useAttestations.ts
+++ b/apps/web/src/hooks/useAttestations.ts
@@ -12,6 +12,7 @@ import {
BASE_DOT_ETH_ERC721_DISCOUNT_VALIDATOR,
BASE_WORLD_DISCOUNT_VALIDATORS,
BUILDATHON_ERC721_DISCOUNT_VALIDATOR,
+ DEVCON_DISCOUNT_VALIDATORS,
TALENT_PROTOCOL_DISCOUNT_VALIDATORS,
USERNAME_1155_DISCOUNT_VALIDATORS,
} from 'apps/web/src/addresses/usernames';
@@ -638,3 +639,39 @@ export function useBaseWorldAttestations() {
return { data: null, loading: isLoading, error };
}
+
+const devconTokenIds = [BigInt(100), BigInt(101)];
+
+export function useDevconAttestations() {
+ const { address } = useAccount();
+ const { basenameChain } = useBasenameChain();
+
+ const discountValidatorAddress = DEVCON_DISCOUNT_VALIDATORS[basenameChain.id];
+
+ const readContractArgs = useMemo(() => {
+ if (!address) {
+ return {};
+ }
+ return {
+ address: discountValidatorAddress,
+ abi: ERC1155DiscountValidatorV2,
+ functionName: 'isValidDiscountRegistration',
+ args: [address, encodeAbiParameters([{ type: 'uint256[]' }], [devconTokenIds])],
+ };
+ }, [address, discountValidatorAddress]);
+
+ const { data: isValid, isLoading, error } = useReadContract({ ...readContractArgs, query: {} });
+ if (isValid && address) {
+ return {
+ data: {
+ discountValidatorAddress,
+ discount: Discount.DEVCON,
+ validationData: encodeAbiParameters([{ type: 'uint256[]' }], [devconTokenIds]),
+ },
+ loading: false,
+ error: null,
+ };
+ }
+
+ return { data: null, loading: isLoading, error };
+}
diff --git a/apps/web/src/utils/usernames.ts b/apps/web/src/utils/usernames.ts
index dc8a783835..43041fbe88 100644
--- a/apps/web/src/utils/usernames.ts
+++ b/apps/web/src/utils/usernames.ts
@@ -399,6 +399,7 @@ export enum Discount {
DISCOUNT_CODE = 'DISCOUNT_CODE',
TALENT_PROTOCOL = 'TALENT_PROTOCOL',
BASE_WORLD = 'BASE_WORLD',
+ DEVCON = 'DEVCON',
}
export function isValidDiscount(key: string): key is keyof typeof Discount {
From 3b63bf4c946069670bb7fcd50d610df4ee3c43f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Galley?=
Date: Tue, 12 Nov 2024 11:22:15 -0500
Subject: [PATCH 20/83] fix wallet avatar dropdown on mobile (#1247)
---
apps/web/app/AppProviders.tsx | 9 ++++++++-
.../ConnectWalletButton/ConnectWalletButton.tsx | 8 +++++---
.../src/components/ConnectWalletButton/UserAvatar.tsx | 11 +++++++++--
3 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/apps/web/app/AppProviders.tsx b/apps/web/app/AppProviders.tsx
index 3f48d3ecb4..438252fee3 100644
--- a/apps/web/app/AppProviders.tsx
+++ b/apps/web/app/AppProviders.tsx
@@ -8,7 +8,7 @@ import {
TrackingCategory,
TrackingPreference,
} from '@coinbase/cookie-manager';
-import { OnchainKitProvider } from '@coinbase/onchainkit';
+import { AppConfig, OnchainKitProvider } from '@coinbase/onchainkit';
import { Provider as TooltipProvider } from '@radix-ui/react-tooltip';
import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit';
import {
@@ -78,6 +78,12 @@ const config = createConfig({
const queryClient = new QueryClient();
const sprigEnvironmentId = process.env.NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID;
+const onchainKitConfig: AppConfig = {
+ appearance: {
+ mode: 'light',
+ },
+};
+
type AppProvidersProps = {
children: React.ReactNode;
};
@@ -141,6 +147,7 @@ export default function AppProviders({ children }: AppProvidersProps) {
diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
index 6e936441ab..56bc3c50f8 100644
--- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
+++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
@@ -23,7 +23,7 @@ import logEvent, {
import sanitizeEventString from 'base-ui/utils/sanitizeEventString';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
-import { useCopyToClipboard } from 'usehooks-ts';
+import { useCopyToClipboard, useMediaQuery } from 'usehooks-ts';
import { useAccount, useSwitchChain } from 'wagmi';
import ChainDropdown from 'apps/web/src/components/ChainDropdown';
import { useSearchParams } from 'next/navigation';
@@ -93,11 +93,13 @@ export function ConnectWalletButton({
);
}, [openConnectModal]);
- const userAddressClasses = classNames('text-lg font-display hidden lg:inline-block', {
+ const userAddressClasses = classNames('text-lg font-display', {
'text-white': connectWalletButtonVariant === ConnectWalletButtonVariants.BaseOrg,
'text-black': connectWalletButtonVariant === ConnectWalletButtonVariants.Basename,
});
+ const isDesktop = useMediaQuery('(min-width: 768px)');
+
if (isConnecting || isReconnecting || !isMounted) {
return ;
}
@@ -141,7 +143,7 @@ export function ConnectWalletButton({
>
-
+ {isDesktop && }
{showChainSwitcher && }
diff --git a/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx b/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx
index 97158ff3d6..db3b99d70c 100644
--- a/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx
+++ b/apps/web/src/components/ConnectWalletButton/UserAvatar.tsx
@@ -4,6 +4,10 @@ import { mainnet } from 'wagmi/chains';
import useBaseEnsName from 'apps/web/src/hooks/useBaseEnsName';
import ImageWithLoading from 'apps/web/src/components/ImageWithLoading';
import BasenameAvatar from 'apps/web/src/components/Basenames/BasenameAvatar';
+import { Basename } from '@coinbase/onchainkit/identity';
+import { getBasenameImage } from 'apps/web/src/utils/usernames';
+import { StaticImageData } from 'next/image';
+import { GetEnsAvatarReturnType } from 'viem';
export function UserAvatar() {
const { address } = useAccount();
@@ -31,7 +35,6 @@ export function UserAvatar() {
});
const isLoading = ensNameIsLoading || ensAvatarIsLoading || baseEnsNameIsLoading;
- const avatar = ensAvatar;
if (baseEnsName) {
return (
@@ -44,7 +47,11 @@ export function UserAvatar() {
);
}
- if (!avatar) return null;
+ let avatar: GetEnsAvatarReturnType | undefined | StaticImageData = ensAvatar;
+ // Default to basename avatar if none exists
+ if (!avatar) {
+ avatar = getBasenameImage(address as Basename);
+ }
return (
Date: Wed, 13 Nov 2024 11:42:24 -0500
Subject: [PATCH 21/83] Fix back button on registration page (#1251)
---
apps/web/src/components/Basenames/RegistrationFlow.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/components/Basenames/RegistrationFlow.tsx b/apps/web/src/components/Basenames/RegistrationFlow.tsx
index 6954b9fd03..fccbd299c6 100644
--- a/apps/web/src/components/Basenames/RegistrationFlow.tsx
+++ b/apps/web/src/components/Basenames/RegistrationFlow.tsx
@@ -105,7 +105,8 @@ export function RegistrationFlow() {
const onBackArrowClick = useCallback(() => {
setRegistrationStep(RegistrationSteps.Search);
- }, [setRegistrationStep]);
+ setSelectedName('');
+ }, [setRegistrationStep, setSelectedName]);
useEffect(() => {
const claimQuery = searchParams?.get(claimQueryKey);
From d11bf629caf9e9eeea2347a17551de08c17248a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=A9o=20Galley?=
Date: Wed, 13 Nov 2024 12:50:37 -0500
Subject: [PATCH 22/83] fix zindex overlay (#1250)
---
.../src/components/ConnectWalletButton/ConnectWalletButton.tsx | 2 +-
apps/web/src/components/Layout/UsernameNav/index.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
index 56bc3c50f8..5c9996f05a 100644
--- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
+++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
@@ -148,7 +148,7 @@ export function ConnectWalletButton({
-
+
+
{showDevelopmentWarning && (
From 0c7febfbaab2953ff9d556203d35e6134d3e6781 Mon Sep 17 00:00:00 2001
From: Brendan from DeFi
Date: Wed, 13 Nov 2024 11:10:57 -0800
Subject: [PATCH 23/83] fix: dynamic crypto providers (#1248)
* dynamic crypto providers
* fixed typo
---
.../builder-anniversary-nft/page.tsx | 5 +-
apps/web/app/(basenames)/layout.tsx | 11 ++-
apps/web/app/AppProviders.tsx | 87 ++-----------------
apps/web/app/CryptoProviders.dynamic.tsx | 22 +++++
apps/web/app/CryptoProviders.tsx | 78 +++++++++++++++++
apps/web/app/global.css | 3 +
.../ConnectWalletButton.tsx | 11 +++
.../shared/TopNavigation/GasPriceDropdown.tsx | 13 ++-
.../base-org/shared/TopNavigation/index.tsx | 35 +++++---
9 files changed, 165 insertions(+), 100 deletions(-)
create mode 100644 apps/web/app/CryptoProviders.dynamic.tsx
create mode 100644 apps/web/app/CryptoProviders.tsx
diff --git a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
index 80efceb319..355dfc619e 100644
--- a/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
+++ b/apps/web/app/(base-org)/builder-anniversary-nft/page.tsx
@@ -1,3 +1,4 @@
+import CryptoProviders from 'apps/web/app/CryptoProviders';
import { BuilderNftHero } from 'apps/web/src/components/BuilderNft/BuilderNftHero';
import type { Metadata } from 'next';
@@ -13,7 +14,9 @@ export const metadata: Metadata = {
export default async function About() {
return (
-
+
+
+
);
}
diff --git a/apps/web/app/(basenames)/layout.tsx b/apps/web/app/(basenames)/layout.tsx
index 493d746931..87db756f35 100644
--- a/apps/web/app/(basenames)/layout.tsx
+++ b/apps/web/app/(basenames)/layout.tsx
@@ -1,3 +1,4 @@
+import CryptoProviders from 'apps/web/app/CryptoProviders';
import ErrorsProvider from 'apps/web/contexts/Errors';
import UsernameNav from 'apps/web/src/components/Layout/UsernameNav';
@@ -27,10 +28,12 @@ export default async function BasenameLayout({
}) {
return (
-
-
- {children}
-
+
+
+
+ {children}
+
+
);
}
diff --git a/apps/web/app/AppProviders.tsx b/apps/web/app/AppProviders.tsx
index 438252fee3..b720ff7c13 100644
--- a/apps/web/app/AppProviders.tsx
+++ b/apps/web/app/AppProviders.tsx
@@ -1,6 +1,4 @@
'use client';
-import '@rainbow-me/rainbowkit/styles.css';
-import '@coinbase/onchainkit/styles.css';
import {
Provider as CookieManagerProvider,
@@ -8,28 +6,14 @@ import {
TrackingCategory,
TrackingPreference,
} from '@coinbase/cookie-manager';
-import { AppConfig, OnchainKitProvider } from '@coinbase/onchainkit';
import { Provider as TooltipProvider } from '@radix-ui/react-tooltip';
-import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit';
-import {
- coinbaseWallet,
- metaMaskWallet,
- phantomWallet,
- rainbowWallet,
- uniswapWallet,
- walletConnectWallet,
-} from '@rainbow-me/rainbowkit/wallets';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ExperimentsProvider from 'base-ui/contexts/Experiments';
import useSprig from 'base-ui/hooks/useSprig';
import { useCallback, useRef } from 'react';
-import { createConfig, http, WagmiProvider } from 'wagmi';
-import { base, baseSepolia, mainnet } from 'wagmi/chains';
import { cookieManagerConfig } from '../src/utils/cookieManagerConfig';
import ClientAnalyticsScript from 'apps/web/src/components/ClientAnalyticsScript/ClientAnalyticsScript';
import dynamic from 'next/dynamic';
import ErrorsProvider from 'apps/web/contexts/Errors';
-import { isDevelopment } from 'apps/web/src/constants';
import { logger } from 'apps/web/src/utils/logger';
const DynamicCookieBannerWrapper = dynamic(
@@ -39,51 +23,8 @@ const DynamicCookieBannerWrapper = dynamic(
},
);
-coinbaseWallet.preference = 'all';
-
-const connectors = connectorsForWallets(
- [
- {
- groupName: 'Recommended',
- wallets: [
- coinbaseWallet,
- metaMaskWallet,
- uniswapWallet,
- rainbowWallet,
- phantomWallet,
- walletConnectWallet,
- ],
- },
- ],
- {
- projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id',
- walletConnectParameters: {},
- appName: 'Base.org',
- appDescription: '',
- appUrl: 'https://www.base.org/',
- appIcon: '',
- },
-);
-
-const config = createConfig({
- connectors,
- chains: [base, baseSepolia, mainnet],
- transports: {
- [base.id]: http(),
- [baseSepolia.id]: http(),
- [mainnet.id]: http(),
- },
- ssr: true,
-});
-const queryClient = new QueryClient();
const sprigEnvironmentId = process.env.NEXT_PUBLIC_SPRIG_ENVIRONMENT_ID;
-const onchainKitConfig: AppConfig = {
- appearance: {
- mode: 'light',
- },
-};
-
type AppProvidersProps = {
children: React.ReactNode;
};
@@ -142,26 +83,14 @@ export default function AppProviders({ children }: AppProvidersProps) {
config={cookieManagerConfig}
>
-
-
-
-
-
-
- <>
- {children}
-
- >
-
-
-
-
-
-
+
+
+ <>
+ {children}
+
+ >
+
+
);
diff --git a/apps/web/app/CryptoProviders.dynamic.tsx b/apps/web/app/CryptoProviders.dynamic.tsx
new file mode 100644
index 0000000000..f0049a96fe
--- /dev/null
+++ b/apps/web/app/CryptoProviders.dynamic.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useErrors } from 'apps/web/contexts/Errors';
+
+export function DynamicCryptoProviders({ children }: { children: React.ReactNode }) {
+ const [CryptoProvidersDynamic, setCryptoProvidersDynamic] =
+ useState>();
+ const { logError } = useErrors();
+
+ useEffect(() => {
+ import('apps/web/app/CryptoProviders')
+ .then((mod) => {
+ setCryptoProvidersDynamic(() => mod.default);
+ })
+ .catch((error) => logError(error, 'Failed to load CryptoProviders'));
+ }, [logError]);
+
+ if (!CryptoProvidersDynamic) return null;
+
+ return {children} ;
+}
diff --git a/apps/web/app/CryptoProviders.tsx b/apps/web/app/CryptoProviders.tsx
new file mode 100644
index 0000000000..f25e43e6ec
--- /dev/null
+++ b/apps/web/app/CryptoProviders.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { AppConfig, OnchainKitProvider } from '@coinbase/onchainkit';
+import { connectorsForWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { isDevelopment } from 'apps/web/src/constants';
+import { createConfig, http, WagmiProvider } from 'wagmi';
+import { base, baseSepolia, mainnet } from 'wagmi/chains';
+import {
+ coinbaseWallet,
+ metaMaskWallet,
+ phantomWallet,
+ rainbowWallet,
+ uniswapWallet,
+ walletConnectWallet,
+} from '@rainbow-me/rainbowkit/wallets';
+
+const connectors = connectorsForWallets(
+ [
+ {
+ groupName: 'Recommended',
+ wallets: [
+ coinbaseWallet,
+ metaMaskWallet,
+ uniswapWallet,
+ rainbowWallet,
+ phantomWallet,
+ walletConnectWallet,
+ ],
+ },
+ ],
+ {
+ projectId: process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? 'dummy-id',
+ walletConnectParameters: {},
+ appName: 'Base.org',
+ appDescription: '',
+ appUrl: 'https://www.base.org/',
+ appIcon: '',
+ },
+);
+
+const config = createConfig({
+ connectors,
+ chains: [base, baseSepolia, mainnet],
+ transports: {
+ [base.id]: http(),
+ [baseSepolia.id]: http(),
+ [mainnet.id]: http(),
+ },
+ ssr: true,
+});
+const queryClient = new QueryClient();
+
+type CryptoProvidersProps = {
+ children: React.ReactNode;
+};
+
+const onchainKitConfig: AppConfig = {
+ appearance: {
+ mode: 'light',
+ },
+};
+
+export default function CryptoProviders({ children }: CryptoProvidersProps) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/apps/web/app/global.css b/apps/web/app/global.css
index 4a2dc90d14..ddfedc28f4 100644
--- a/apps/web/app/global.css
+++ b/apps/web/app/global.css
@@ -2,6 +2,9 @@
@tailwind components;
@tailwind utilities;
+@import '@rainbow-me/rainbowkit/styles.css';
+@import '@coinbase/onchainkit/styles.css';
+
/* For Webkit-based browsers (Chrome, Safari and Opera) */
:not(.scrollbar)::-webkit-scrollbar {
display: none;
diff --git a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
index 5c9996f05a..ff7db6a95c 100644
--- a/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
+++ b/apps/web/src/components/ConnectWalletButton/ConnectWalletButton.tsx
@@ -27,6 +27,7 @@ import { useCopyToClipboard, useMediaQuery } from 'usehooks-ts';
import { useAccount, useSwitchChain } from 'wagmi';
import ChainDropdown from 'apps/web/src/components/ChainDropdown';
import { useSearchParams } from 'next/navigation';
+import { DynamicCryptoProviders } from 'apps/web/app/CryptoProviders.dynamic';
export enum ConnectWalletButtonVariants {
BaseOrg,
@@ -37,6 +38,16 @@ type ConnectWalletButtonProps = {
connectWalletButtonVariant: ConnectWalletButtonVariants;
};
+export function DynamicWrappedConnectWalletButton({
+ connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg,
+}: ConnectWalletButtonProps) {
+ return (
+
+
+
+ )
+}
+
export function ConnectWalletButton({
connectWalletButtonVariant = ConnectWalletButtonVariants.BaseOrg,
}: ConnectWalletButtonProps) {
diff --git a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
index 27543de352..042c810a4c 100644
--- a/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
+++ b/apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown.tsx
@@ -2,6 +2,7 @@ import Card from 'apps/web/src/components/base-org/Card';
import { Icon } from 'apps/web/src/components/Icon/Icon';
import { base, mainnet } from 'viem/chains';
import { useGasPrice } from 'wagmi';
+import { DynamicCryptoProviders } from 'apps/web/app/CryptoProviders.dynamic';
const convertWeiToMwei = (weiValue: bigint): number => {
// 1 mwei = 10^6 wei
@@ -9,7 +10,15 @@ const convertWeiToMwei = (weiValue: bigint): number => {
return Number(mweiValue.toFixed(2)); // Round to 2 decimal places
};
-export default function GasPriceDropdown() {
+export function DynamicWrappedGasPriceDropdown() {
+ return (
+
+
+
+ );
+}
+
+export function GasPriceDropdown() {
const { data: baseGasPriceInWei } = useGasPrice({
chainId: base.id,
query: {
@@ -26,7 +35,7 @@ export default function GasPriceDropdown() {
return (
-
+
diff --git a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
index b7160b2cb8..795c703805 100644
--- a/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
+++ b/apps/web/src/components/base-org/shared/TopNavigation/index.tsx
@@ -1,17 +1,18 @@
'use client';
-import AnalyticsProvider from 'apps/web/contexts/Analytics';
-import Link from 'next/link';
-import logo from './assets/logo.svg';
+import { Suspense } from 'react';
import Image, { StaticImageData } from 'next/image';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import AnalyticsProvider from 'apps/web/contexts/Analytics';
+import logo from 'apps/web/src/components/base-org/shared/TopNavigation/assets/logo.svg';
+import MenuDesktop from 'apps/web/src/components/base-org/shared/TopNavigation/MenuDesktop';
+import MenuMobile from 'apps/web/src/components/base-org/shared/TopNavigation/MenuMobile';
+import { DynamicWrappedGasPriceDropdown } from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown';
import {
- ConnectWalletButton,
ConnectWalletButtonVariants,
+ DynamicWrappedConnectWalletButton,
} from 'apps/web/src/components/ConnectWalletButton/ConnectWalletButton';
-import MenuDesktop from 'apps/web/src/components/base-org/shared/TopNavigation/MenuDesktop';
-import MenuMobile from 'apps/web/src/components/base-org/shared/TopNavigation/MenuMobile';
-import GasPriceDropdown from 'apps/web/src/components/base-org/shared/TopNavigation/GasPriceDropdown';
-import { Suspense } from 'react';
export type SubItem = {
name: string;
@@ -91,7 +92,11 @@ const links: TopNavigationLink[] = [
},
];
+const cryptoExcludedPaths = ['/jobs', '/about', '/ecosystem', '/getstarted'];
+
export default function TopNavigation() {
+ const pathname = usePathname();
+ const showGasDropdownAndConnectWallet = !cryptoExcludedPaths.includes(pathname ?? '');
return (
@@ -101,7 +106,7 @@ export default function TopNavigation() {
-
+ {showGasDropdownAndConnectWallet && }
@@ -114,11 +119,13 @@ export default function TopNavigation() {
{/* Connect Wallet button */}
-
-
-
+ {showGasDropdownAndConnectWallet && (
+
+
+
+ )}
From 474a4e0eda738ba459b0f2c0f595501c1d9816a6 Mon Sep 17 00:00:00 2001
From: NikolaiKryshnev <63440682+NikolaiKryshnev@users.noreply.github.com>
Date: Thu, 14 Nov 2024 22:57:33 +0300
Subject: [PATCH 24/83] chore: update Twitter link and labels to X branding
(#1233)
1. Changed `href` in GetConnectedButton from Twitter to X URL (https://x.com/base)
2. Updated `title` and `aria-label` to reflect "Join us on X" instead of "Join us on Twitter"
3. Kept `iconName` and `eventName` as "twitter" to ensure compatibility with existing icons and events
---
apps/web/src/components/GetConnected/GetConnected.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/apps/web/src/components/GetConnected/GetConnected.tsx b/apps/web/src/components/GetConnected/GetConnected.tsx
index ac80bb90b9..5dc78976b4 100644
--- a/apps/web/src/components/GetConnected/GetConnected.tsx
+++ b/apps/web/src/components/GetConnected/GetConnected.tsx
@@ -25,10 +25,10 @@ export async function GetConnected() {
/>
Date: Thu, 14 Nov 2024 11:57:50 -0800
Subject: [PATCH 25/83] change /registry redirect link (#1249)
---
apps/web/next.config.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index 6c3a807f5f..e2c5b7c675 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -270,7 +270,7 @@ module.exports = extendBaseConfig(
},
{
source: '/registry',
- destination: 'https://buildonbase.deform.cc/registry/',
+ destination: 'https://buildonbase.deform.cc/getstarted/',
permanent: true,
},
{
From 3ddbdc5a5bb12da8d3bdfe5b68cab4ca1c0d399a Mon Sep 17 00:00:00 2001
From: Pat
Date: Fri, 15 Nov 2024 02:58:37 +0700
Subject: [PATCH 26/83] docs(tutorial): Create tutorials for Paymaster/Wagmi +
OCK Theme component (#1225)
* docstutorial): update 'Pay' component to 'Checkout' for consistency
* docs(tutorial): update reference link and objectives
* create OCK theme tutorial using swap component
* add paymaster tutorial and supporting images
---------
Co-authored-by: Eric Brown
---
.../swapped-theme-before.png | Bin 0 -> 239608 bytes
.../swapped-theme-final.png | Bin 0 -> 222110 bytes
.../connect-wallet-mint-page.png | Bin 0 -> 46091 bytes
.../sponsored_mint_nft.png | Bin 0 -> 143705 bytes
.../paymaster-tutorials/wallet-home.png | Bin 0 -> 1433315 bytes
.../paymaster-tutorials/wallet-nft-page.png | Bin 0 -> 454374 bytes
.../tutorials/docs/1_ock-swap-theme.md | 196 +++++++++
.../tutorials/docs/2_ock-fund-tutorial.md | 11 +-
.../docs/2_paymaster-sponsor-using-wagi.md | 383 ++++++++++++++++++
9 files changed, 585 insertions(+), 5 deletions(-)
create mode 100644 apps/base-docs/assets/images/onchainkit-tutorials/swapped-theme-before.png
create mode 100644 apps/base-docs/assets/images/onchainkit-tutorials/swapped-theme-final.png
create mode 100644 apps/base-docs/assets/images/paymaster-tutorials/connect-wallet-mint-page.png
create mode 100644 apps/base-docs/assets/images/paymaster-tutorials/sponsored_mint_nft.png
create mode 100644 apps/base-docs/assets/images/paymaster-tutorials/wallet-home.png
create mode 100644 apps/base-docs/assets/images/paymaster-tutorials/wallet-nft-page.png
create mode 100644 apps/base-docs/tutorials/docs/1_ock-swap-theme.md
create mode 100644 apps/base-docs/tutorials/docs/2_paymaster-sponsor-using-wagi.md
diff --git a/apps/base-docs/assets/images/onchainkit-tutorials/swapped-theme-before.png b/apps/base-docs/assets/images/onchainkit-tutorials/swapped-theme-before.png
new file mode 100644
index 0000000000000000000000000000000000000000..39599d6680e13ae065f0178a637b4a97a469b350
GIT binary patch
literal 239608
zcmeFZcRZVa+c&PNwo~WizSn(Uuix+K)$9J_`R~r_75O-Ge%7&%_whON^1hBb4J9ij5fKrM#@#!5
zL`0M!L_}o$w8V-OO_^T0BqUf%`DysvCAO7(!RW`{!qFGP
z6Fl84*4k6(l#>GI{jq`Wi;aZTqs3Hr`;GeZMD_1P6Ba%t6U}Mt*p_}fE&TbDUq}y>
zNzC1Ifw^aaYEDnb!$an|Z8hHCH}Eo7`+jn$-f9M(fOMcxeRhuMuH^US+cjLYGtER(
z!o+z(oJ60l>@1Y;WNVs}mc~-ck-Npv+vmGR)8`g!B|uURjYYoe5^)t^kIL>5Ngi{d
z1?ZmK=8${ED5DYfnBsd79d2&Ei|%$E`C~SR^NXJa_ePejmYSqSBR71?^okmY)U=H`~BTqja%I{u;C9{^3Y+r8EOreCx@@KA4JI9eR>`nY4`%`
zyG^$WG3R+p)=D#y`nKx@NBV(X#H$XL8Pri3gfsp)gF%*pJpr7Pc4LNS*(NsJWLtwy
z|JucybbM;J3(vWIR^StH`oW77i)NE`;?e&uU{m_snD;FSu3)!xhuY-++H3rV!jbFR
zaGsF3=reMwE_4*_gLz7S?kU=1TAOdjcj$GRN@DMVWdmdwKi=fJ&mgbEs6jvTP@#yg
z>mbe`re*ze%`cVx>c*QO1S~`uIx#zJ=#v
zcZCo=I(MOP`4Q<4(wY6qR!g7U6$;`2uJa$1N!TOaPhZjqRiyWcIUhdd{o;-?_*OT(
z#E68Fi}3VH!Q6MA)Rh&YTM@_c0#YK*GcKmzqt0D>yxm{bKI1rk4)Y1x0P4rZgJBx4a1ky
z>E+1d$U)?3Ve)+~s$~x>)t>8o;gpQe*e%9}nnARU#yJlnR_?Rd&yapS=N*i7b+wWy
z7kpBq5+qpO#F~w-B5&}!t>Sfsk5ZFGWjky5)zv)f+-bQ_pE~!3jvgE(gWtk7?xL5D
zYOHLn$j3^4B@R~&Ut3e~Vnw|`K8krBp7uStM0%~7KCZBuFI3RQr*aqqf0o(3B!!*-
zDIme`o$tQlcKl_fgQ=_A$VF{~dYS!$jD%oDORyF7rD(oOGO;$Y^0FwR%#+hEmayW-
zq>X~)cb4EmUx)-Ub*>O$b=_FInQq`pfm}NYEEDx4sh?8
z-smH1{mMT4)h9KWZHqXFi!LnWCLQhDZ<0%ajOL5C#Hc#YM?=JKUMTqHcb}p%WIdB~
z?E)Eu$KhP=x40?#YGRk?DAzrY;KfYZI#NzLl55u_!-AK1cwgLj94Eu$^qq|2PSYFX
z>ns{7w=Pk9QeL>G^;++Vu-;226~{N{e>|9ze0FJBd4r4g&DA;GImM`tD_<@;hM9hx
zeIVTqY3}jy5J-O6eTV8-_L61=F+%MilV<7ZF1_FR^*b3^c;>JDV!U=yvX7izMXOr#
z8F+jr7?Ah_wp@@a`Kk2XH7aymzrd&lWNI}U*6$<=b-Ho=`a4_y>_aW
zIb{6axVnPs8W)&%|J~i|fo~Omrd&~-dAFm{@;TO@so1!4!oQarA_~t3XtoM~07TA*-Mnzk)iy
zGA~ezsC+eU8vZ8Jw$yuV#dvkgqs&v&bAP2`b$8iuwRVYjrF#(3t5`Jl^ZR?ldsiir
zYNC8!`@TC0di9IBoB7xKd7G6sa}KN;tS~(z{ciCcSjHqx{lT#iUK!7hmpYC)KA>?u
zFCWSrYC>v8-OaKaJHWCkWhhqpWbIpNBlD*SuZYqJcbY1;N|t>#7Pbp)8ZpwWY2qtV
zcC0uSd$uH&46zMy>-UH6Uud`A(@8#Q?X`6?t|_D(G%?J``}p)lUXw@4Q`9n<1hkx_
zoVz^G?w~7g!3j%^y@fTzf~hV?lSlh9dNMW|>j{k{nk2Fbq0Jgf%PKX?Ozj372AmQ0
zUF9MbMdc0lD&^bA4^Q=--6s+qIx4$LC!G9SE+XBL9c2Y&D5tnl)JXI?Xw3T+&nql5
zAB&OVMq+5~`L>T=KXy-leDr12ZQMFG>FkxDc{$%@~`H>d|fTx*&7_Iy4>bBkMEov%bHQ{zOqj@@d=Wwn@9m
zfKBJgG_X*HWX5KoT%dN~QNut(+({=sEJz-o?&c&rlB$@>bgFZDi@1a&MDgv%Uts?XvyKhuBiMShQc!BWP(v
zY_Fucq`gFa+H1OOYk^#nTk^5R_xigXcQu~aTzN60iiob|bmmZ$I5wsh=atJ5Sa`Y?
zHtbuvILqwTH|y0_AA1mI#Q+C=k3G
zh5)R6C|K#(nUbvq6cu>^e8J!X%>G8VoTM4F@`=|NJ4&`=Q9+Pe;KW)4Y$R6YnZ8`t
zg1y?k*omnmGI`yQ>G|=fDVqw#ctJGZ
zk#Ux>VF2{QP#FR*@gW`Ahm^v)lt6VZ&?mI0Nk{3ISI+VKY
zwZ}`h7_YLf6PlJSueAPlZKd!fLe^~6(hN1`dXO(yq4QvgcL1*PTuHF>iROTPx~iWhj=
z*yGk6#hvryiu?lxC{|u(^O5Q>rmrd8P#o}=!|u)wE+f3r_rUIWr_Pamck4xw6XxC7
z57<;JpQw+OL(P)!_LA;e2CHa|g`S0Ii0f^>dmdlok!nGAMld*uG%HpGe2@}hgqPr(Mr3F
zgrYrM4wJO#MZ)xfx*2A!p(v>=={&s1C!63nWF-Pe?yDat`J^2jH>bR@$)w74WXgOx?0L=hHQ(HG
z%jl*@rf@%WmwlI~FjodX>v~!lp&et|Jf$%2fibM=&NXG0H69c&und7+#O&Zl5!`izy%7|yC&{LM2t7i
z-sd#*u5SV34?lfq>|v~ZPtFGBEMolx_Sja$$JzC493llDIiTrm>tW61O^nC^y$%w|GDmxsA2&bE&|d*>XvXh>3`CgDJVVxD=i}v6IugqxN@q;6Fuf
z2M-TdIZ;t>Z*LKA2@%*cdr@&&Sy@rBTcWpa2?KWsyZgF$So;XOxbysDkbjPI$JX8E
z*;7}Kr!W_;vvI8-!#q6{xw+3K`mfhN)@keW^uK3vasPW;zy?Lnz7Z7{5flBdv4O4%
zXIJI!KlQP7GP(2A8K4<32l$qln54pA9sYk`{r8mr($)B1x=PE6{cG2M`Rf1M)xh2M
znF`DqnA8LO-wgY^^S^%icSi-$v%UWdE&f61zpet522(1C{ukH4lyC!DGk}roPw(hH
z1U>;VJA0j90sh?l$0yJ}_xbbd^U8pLC=+Sifjsm%w>Cjh!ETVIw51jHI)*F4g7GC4
zS9ECerw~$zD}?TnE7=wD=vd;}o0P=o9*i{4+^Rk|)3ncY>9y+D=d+|gxXCXP3y@n=
z+`UUqas31ncux_xXIO>vJXl>h?UyahhZ#)ZP#I=;yY6ZK{3Ji?M^w_pB;<5l%Fl_;
z{RfTUCJLb&_Kixghkp!Z6{(rEh;1Ejk5Jr#bchWv+|{9lIruZH~5viz^f`X30%|C+4-fzSPa=R>rqgp3c(
zjjNZAN|tICo3YVJVK=la9co2E^jtmfzrGjO3eUE>#rw*_Zn3hl5PR!-~dg00V@f!DL{@hM#%;~;&Q>v^xr2*wofpstoTNA3gAKoFqQi^GPJL7I&MA^mbB)B={b?QY9T$5vVLQ
zSw{vwAgZ7%$o}EN_#(K+CQgxet+egxgn7>BC@{|`=|c)?#f_0B~Z0$EthU4f8AZ~YwrBO$Aoa&i&a*p^C0nY
zx$&os;JLTH>!ZW$(=GZ%`=oS<6?w$0$A`Dd=J`D41NKK(ZKhAKqD$?gS5z~Xh*6sj
zW-_aVn+$=hE4o+cNVLPAvHU5pSTZ^XBVHd$0Bu@cT51J_`dxM?RJ=#^!
zH#$lJF_-2~p~H2j=81Jh?@3{NPde&&y5Uw{1|5R{(PB48sZMQJ)x9
z>6Sc$VL{kc>o~>zrM;*7_!ANc?(`DTpD00+PbK6#9WI93uCf04mMPyVGl3|$iTo8y
z`9ovRmY>$0dRj4#UG9tBG;B^K58g}IcYhhQb|uDhi`llvyDp}|so(MGZY1#^%qv`g
z;GT_WC;!PW%u#iw9XP$0Cybm>SwNmEUnAkwWoP2mWd-FUyWhKc$ZS;4sv}85FE4?D
z(SaM~(?59Pma3i}3rYq)$hFPhJJxZADjMpEUUWPc}{&OH2sx
zgS8Q?bPLnVJb&VM7nQ>C_de@i?$IfmU(-m)J+i+J+gj2a28}$+(i^LIY8J@`t9$ye
zRr+Rd5YwM6y1TEhYX(U8@69EUeeY
z^`3R&EC!M^F>Af5zAtggx>O3-&bst;DzCQIAn)p#zfVm
z|4E;BgMnpm3YGEiBY*}*r2geW4ba#G6NO~fqblwGEHJ>Xos+v`NNO$5+-%a_V3JX8
zU|EcVMN1FqDg+ZDm;UT`fkYPtReY$DnE=oN(cjM$JVOp-vi1m81Te+}@u5E@&B0(g
zfcT-R(k%@PKw~T$v<+rDV+R?qDDR=c&OaK5K450;zM416LsaA;UU+)`*UDzVvc{T5
z8PX_2yjpJE4EYl+J(2**c_Shn$t(1LX%mub~cwbsxn8lv1Z?INuCVib$4?n-px{`TkPbl>Ol3k5j8hkJk=@UzJCYU9CuN2GQ}%%
zAS#)Qm>3P^a!4`ajrgR-dRl1k>Ar*ThYc6_su_lL#{B`ze>$9LCCy7jos0BoIcTj_
zYt}nkAMzu$*VIq0TjDhE=h)1N{3VfCv}ZtrepOxVu`P#wy}9~tjpfpr^5(Y0sAswZ
zD@rSurVnR!TtIos}N0}F=?0+Vv
zv!gWilp&Qq43D4ZS-^<81f?SCXBeBUD>ndra7K^zQ_&
z9+f?k(+JLL>+IMZRAI*~Fhu-pBunPk>rIbc>#SIQtwx{3=_?^(?qzuo_7F&B<_
zEC0;8LUJ6}Qlk(nSd)TTYV>Jth>EKGT2ctkUY6r|t0mxp;1FE=**u50Y~+r9tfOr46;M=AONf+IF(g@P^5%;-P@
z4TP@ZCsLb&X<${3wDG9nQKm*7II-A(sI1#J(@FBlE>{
zSmLhEb%8=1J$uAG$L-qCtBr4t=Jpt%PM;f{bbhbi!9iy>JkmhAE$D3Wv-m#fFB{GW
z5auOPJJ80$cgW*v5Vl1TkeL8i9B0
zLYh}6xa)FA6nq>hg~s$bz>&v4SE6xhIHCF>@!#rLW)fi5{Cz#U5E!6=!&8V6{#wsO
z0uzbzV`u%4fcc5aW9=~h>6{(bFi3~0+M;_q_T6#$P?kjK2B(yNw99zgBKUN*MR;#h
zxgaDqZ`O7Y!3-*zzB?$My
zTEDeo4w1mX@(=ekGUd-4unosUNamRvOk>;}_X)|QG0r84(nPI3tp4(=))ep6+bb*I
z4KSn&^h`BbdBq$486K_I_A~$LvX6-pVmP;dQhTrRarg1>!Y2+m
zi&-~??Xp>D{O!LcON1GGuqhb!)>saX7CCFazq#sIaa)!D
zTyvos>_XRdxkgsN-}oWM?4d|^T@M!Z7n48B_Jcb?X|23IxC)!2p4K6+o1XeAV}Y1
z*cd<2BZx>{_Hdfp)JS!!I@Q(?cWljdFL1j4TdI9sqS(DnCrK)`S${COHFsGzf-l|Y
z@l=;vM?`|5rFdaQuVyHLS2vxV^~kelXW;M`E&NKu)=_hNN^=ovomEF>`!LFjhNMclvsPUy}hB;)D%ut9=FfI_+lCz$r*I>JCYycHNLGleY{#M3fr%_
z14s)=R@=o3kjspcH}1xJN{j}7QgdA2HG|lc2)T>|Z8d)-d?zwF0hyXTRm^THO)o5R
z!bGu}agcO5cJG_Slr9(!rtPJai6XLht9K`V6&6>-dT}ImqjqIthw^K3gD@#asQ8^!
zJ1;lz_NE5Ng_iT03a~Agii}=4IXeo=wQ1qr4ePW*Q3~F7o8cD^=sq+yT65r6d|i
zm4i#BBiLx6*pKeS8K=9sO~2I9U*x9pa?=@OWoCZlFG@Dt1Y&BGZU~n{(`XLK%g3Mz
zdR|Q$*ZJ_O_Gak7m&csQN*}^27Kz)MN~LkbLlsZ^ZtuDr3_3WMrW+0JXF6i0Z~S_Tba57iQ(?fI89Gq$;W7
zD*kyEVUF@itf`;Hikrmh{nhmNy%*rU^k`%6$)o<7n%LjQ#rLlVbCW{;Qn|IHSenHk
z0vV0I?uT19QsL9S3?GaAIO?$+o0Q%dPw$o+tB6L+!y|?yqY5Zjk1${#o(gUcHBK@a
z8e4~3Gf{ggPX5W%5{ZRZO
zIO1rfzUI~@%5weGkokMi?D%ZM8w3ZWCw{TKqSdR;r22!WL&Zj~>>Nkc?B;{X_uaow
zN@^jQ4v-!)&H%AB6nZ!RTjv$@%gcNbDX^Nn8T-uD2{t$*_*5a_$gBSqPjkBey2<34
zL~s$1f@+&lLV6@~bu9NHKu(v6>UN^?RT^*;^5JYQ)gM=6V?==h-~PrsK!D0C^B@Jo
z+Q|D)bPk+l)97DxkQOn`Xi^v^bk{HhR~Hsl!)y=p-*^fku5DI-VD1);T4HS=@
zPW+o#*f99h1;{~0&{t)drS=@z1EoiVtr``ju^xP!!p_~pnQ8T~g5p}gr3UW?EXVY4
zF}v9|~u@SfbFkP1$OPevTI_m#k}3Nz@7os_D$4ALPb(K{^p1aoWajC}jx*$9L;M*)@lq9XUyKIv
zJ#A`(jY~!ArmNRF3Gxme>-vLo#b`C+7wgX)=1epZ*Rs+54Zs<{WeHT}uRxyF*L!I@
z_3cH@9+>VLY18P{-<$5VD>wD%c8d=PQgxoI&iOlq$U)QTK-31klRL76c>Nr
zys*?#Oyq_!!{UU)^TG!yb}Ghsq%ni~tH=Fvs21ep=uol^YN$3~T+Q*G$!6W@PQ&I1
ze%(V^d)!Wr|NdvLrSS8x
z5L_|(VMCXD0SLy>eK@*oeKem`ta(t?cq(k}*1|9iAkzmWXh|PqxM2rxGg8
z?|8MT^>gmA9JAA=13}0ZkQQG=CRmj}`T6z?>ZPOwZ+)Fg(PI)arW-W%Ul;RC^K9xj
z(&5+xrM6Zfz(;dRkt=az@AyRjVG*6@ITz3wN)AS&mf!=QqH+7Cux;z?1Vh%^^`pfM
zS~$CLWFvlkGgJ+5K<2}To@|kOLsm+!*g!5zwVybETn1sB8=AQ-oFu
zZ#nx6b5yZtZL}bqZWeO_`*UeHqP=$NytG^Sjg*Z#V8|+2(&@y^2Uy8<3pUYFpV=5P
zLgEupJld^(Z;)mT));6EtKVxHg%&fU`^>y_T}4!dWAGc^)1by9XX82_H!tYNeL(t6
ziz1>JND$2ltglxFU*C5W%QyUSYwuJpOjsTXAQ+Lx_U(!EtJDtbN
zyhFzNjxS_IQ{^QhE3FGH6pAk^kIkd)N^AJi`ex+NMKsIj!@A>v;)_Q3nRZA<
z27b*x^xSMC0lzsvK^rvbgzak5;LBhhGTqs}Anosvz$pd25zUBlYONLmmpW5MWu98$?
z1c#T8_u)YCslBloO6gvjoF~_?ultUe_?Oz1`~p4;PL)j1_}1f~8>s2`HVu*`q&-Hkg>oeeTGlJv-PG2z|O7GlDid&cXy%7B%
zR;3I{cROa8{(M=tXsY2bSBat+S2bi%0UC9kyQCiO-VNqEs@zP&Pu$;Zgcz(YrTcGC
z_y>5P#cOAVVNWwiny{AHJmHL_xU7CPA0efN*xxCE)`{F8uf@4+(t=J;TdgN@j!zb<8n2Q+Pp9uz
zB7|5U7~$wJU;XwE(3IO<;i^E9tO|u^L5sWJDJ5Q>(b=XQX%@;p+flh0+JJu#64;@e_P8u}}w(Y=;y?xBo
z#t-+>E7V*SY;4>$myg8`UW0LHK{dr!c26`{-S7Q2Z&&Xj@-N*))19fo2fg+0fq!V`%B`ewcn$=_6qYq9CnutHmsW%1;gzuxnPPhRp
zNY6@QC{~TmBQ0;WKTyjN>HEE^5O5HTwwHnEG0%$(;yv!xwuRr!NMlzJD)s{sHwplS
zd*F-hSxj*`4txQt;aDD|h4kFeN(?)MEoUd`EJQWB;&MRFEI{Ugdb`Q8PIUpI?tftv
z)Bq^eSpQc`OXGrWI%mZM*8O~j5Y05W^h;CxlSSydeu(Kv{az4v-J2k_GP}Ile%7o9
zLu$8SAN0p_2U&_a@}T8V&bC;yD2;kn8%}<-f4Z$Dk$9|r?SScQFEpPcw-Yp(WCUAb
zO)Aoj()aE1^^CVt>P7pm_S{wESIY+TASUp?MT3Ja>CeKjv*02HR;y}odRSSnPa4GN
zJD*@wl7|39y@!Fa525q3w
zBGkt5^h>_Yee01)3j@&egaZk27(V0C^1wp}92Ulam>-#h0?vFJ*P9BW`cj1L49P6Y#{BFe$N(n3ydNhap_esrOyY
zPjcB?Ks2?1D~)`*YKtl2l=csmZ?1sO!w9I7g)xB?ija-+%}MJN=PHU~zk!0Lce)BJ
zA~kbww~!JImdJFLwoY>Ywe4T|i=zWFqmZ5~J9ZXn%wz0USyH{w$-L;nMX*hhg`dg5
z^6AWIL9?LMQG5PkE#k~+sgAOc)UTCggd-|P^Bu1xS)F2CH)<9yp6=pIp$G=R4xuB{
zyeDf`QdD4d+d08Ki(U-)@c?X0C}uV4@_Cx1iwezW;SZ3Vlab;kf^h3AU=+YtVTAvH
zff0*O`7*SrKTp3|(6ZEP*ncxF67b0lyKj5i0c9Recke!E2C`M`)>2kn_q+7ZDHkTIe%mSlajV{|1cJHPJ>^QF*IkHL@xjY1W+yU
z#3`6d4f}q-q#mKKtBCmY6}Z67{_Mv$hIMhHl2J!dPU_ose4vph@3uOEL5GR3?a@%u
zN#%e+6Z?b=$~I28`7o?;nPI)ux*{}lUbO`j?o#p%8YxvJKwj%Q-2@cKV2Hz-0|p~=
z@o{%_M)P}dn}`@TC8yFBwx$VhLd~aoHZ}l5bu%#4e+O92wmLCAFDSrGuP%o*{Sdd(
zBGZrg{h6i(P1gLg1FTTVCd;nDlxJ4vNrvHldaVUghV2p$S0`m6`whXzD
zph;UKpbwl)MX0spK=x4(JxO3kMHRpbS?JPaZ|I?P9jM;ABdT@=UJ!k4NXG>z>?=Am&@(~h@bzau5goBl{!Ri
z%s$FTKc
z-iSl@oo)Mhttz#U%-pmf$6>cz4lNj`*GZ|OlI3>U2cyR0f$4lJbJ?|8FZh`j#efLl
z?;xVV(Tn)CkJEM7?qZq1p$x*6))+RUUz5&fx+AD3%*+DU32deT>8xh98Vp%@Yy03=
zt*qR<+Bh(miyBXlqxK7}`?NtEhc$-<<#8&wF&C6@-Mp|FZD7OP+h{W4?7j%FJ(+KC
zcX803;Pmt0D&8cR??HkUW&^$M$K!C4Qn%Ay5OKJGaU6?OI{CP@R}4qY^q-xbQ|FM%
z`oAho0kOXc95I?pGCO08yluckK_@K{Y1hWqm1$(zHQ<%Cx<4
z;tA9#FV#QJtGqgbcgxsGD`x5xS{Iy`nK$_YvTPsC>HoDPwdd$qQS3Ohh)|i7OWgnP
zxW9eVYbU3RbJ%?}ld5
zf)9ZVC=r&obh;~{L$x`%J;AV^O*0VkB8CJ4!_6YBcjkU(TU?X}pOlD3aHiryt({lz
zS@4<5(>+?9lV*kVFw-?G(}D)m7Q&g#nNL?GG7!}ZN##XU(8kB>QTr~?Gf?o2*9FLC
z?W(Tz-hdIC;hhtW9p>AGRV^7e;5$EM8Ilxsdz!8BdxSmxWe_I7#%G|wQfjT1`5Az|
zBt#+;;6Af#7`meBnZ^08K&)C%ic2lO6KdUy0gGmp{>vg-S*8H`jRuY~a;@@^%aP1H
zdI(g6R!efjDrVM0GSvqj+*2BGdQ>oi>lG{rv~8ErI}P|e-q-%fR8zSuVRm3JU*Vme
znU|5}p=fq-9e9WMMQP&BUgrwbE@MZipUNUf0oT=^@Jv^bqrDACtLr?4)Ca^$jTmR=d1VGKTXi
zSURo4!r9NFgENJZ&{5(^^_cmYt^M&0*AgC%f(_Ttb2jYE7e`1CaGlm&czTkFvk;rf
z+~ZqGO%l8sIGfls$vK9qll@$1(W0Hz1Hh~ilSN!hN~hb(?L?PZ*3iT$+(OmWz(O*W
z@XNZ*SN+!v#0iDg>p(7Eg$aW0yE%xG%iWoNbOoqTtxo+#^L6Sw1N@=~4>iR^yz~S~
z5^fRvt|7;XJ476$OA_?#QG+*G7oEi`3~RId$20-(I&)CszNvRXtVB7Bgk9L0O!j8o
zmQ}j~_V@=CSF4=akx?I8Hu4E$#W(-v1aSFb)F7S)_{NzOc+=wi1{s!
z--95orzhzO0PxI+Z^}!;RpW1^4A$USlvSjG5rUxB(3_&hN>aaTpA%Sv|%wdP)BCxMV4mD+t5rX0%`c(vspB9!tC@Tz+VfmAgh~JtNod4@|dI8
ztvy&!ed?3pMKf59RGs&tMFg^i_MZk%a1@1roJDr|7X-|<`?lhgP92QxKG;SuoIw#`
z+8_icQ5@)GS=XC73~U8no7|}iG+b3eZsex>Ju}8^t2CZSo(whY?`?&PvKs3gH9s%}*n<)%jHW|L$-&PCq7&yR)K-W186bVM$Mx6ZJenb@zvuAAjr
z=o7ykFjv3T*6UwhVo%M|DvUGt({6VIkH*ii-%l9C+Gix_F-u~kxCMkaN-m(ue
zwtP=lTxgvKRa}8!XhWDsUdi(6%3@&rHwDvIIm*QsAX)P>Ez!;1jo68;d7DPPg#8{g
zTx6|^5M+QhfU-ZO+ui99q0NxlddFa4Xh
zPRQ>s|IRqW#;8^GFauOKO*5^&h%eb4jG9exspDU(T-{&*)ScmaGb`^s=2ON_Yd+ML
z3gE4QbFX)q<-OKQSO~M61abi5jA$Os4?M(XY+fZVEWWkY|4B8vNmCGC;CvlVz-H)%
z?GRirqL7|uTEb(Qz$xbrB#@$;-Wo+gfSO*nz~?i&vKM0NSqsDE#S=Xg`eaBhV0((7
zHqPlcLk#W^X7w+ct*7rm<&J9liuk7+3|U%AXQX?379a8165cU2-TPo~$?1#Mo5b7R
zSPZ)7%eaCM)Hv#%V|dW8AG2>#+)!5vO@qzLeeTR1&os0c{k@^b^Y%nTdYovW^)Io3
zs0PtgpI(X)+Zr5TSZ3J-XHx__s@&l|F2m0{KnWmPT)=2vE=|L9MTNN3QuW9}N;p$m4xD=_xky0IsGBv%(}OB3qG61$kNw#7=C;%TRXIE@pKAj-GQhq
zbl9vUh5ggEH{gnZAhj6-K3OZLY8EJs#~)aWdrcl
zb~rd__$L7;<~H+vt4D>_Fm)w~Yw2TT5I6v^^o8%?gMj7I81_Cpsg-7+K|$9thLXAy
zPk*bx2;Pp*;vmtcAb5@)dyFC{)Jh3nBuX8P$-dV*<9!$Qezg-l_Qel?a}}ueLK5@f
zS@|&>V~&9!y_NRkPKmA}=o3u4=Uwsq`JTI7MZZ4^zWYg86d(wIWm}>qNf|o!b3pat
zCsXQ5UmS`D#Co}wnrRi;CwE)hd!~Nore2vub@)SRR2u1mPO>;@QM^)1#=Bfg
zWJ#Y1k8TR5lhYn@-1WeO>e7kkg;kn_G_vjV8NjXThZ?6WjyxMA{yH~kZD
zp#dM+AmMuea(n~$$K#;ZQoY-;NXK?FtU`2e5cXK0UV2^zwx%|Ct>w$WUT)_vso!Vo
z+~m8@=l~zN5hFb;)0v?JAJ%iK!h213!te(rm42LnWk&2Rv^!%eoe%fD8w}i+j0s~^
z{_53>>HZNG?^r#A^yoZrRrU3c)3+O0f;QJSFQ)h}M?!<#w1@3C1GQ2}BB7TXS#91@A3?L<1n8DvCl(K;7_#
z>yK+>Miu@%OVQUS$xXK%9G18NVyCwJpt{~N#zt{to_eUak004~4jL}2f7=nc;cUm<
zMN*kullx)R)K~W}6P*XC9ySs@pSg{a0LnvjL*kphUAXk@PKn~UH-kM(?r(R5DtO3~
z%O?5Lv#|&x`(;o5GrP=v8gp0t8O`s(+RJBPE%@|s*1Cnk=$>5-t_amEkJ&DYS|wm5
z2rS?u&Wyk1++pn?2Iiyx9`8EOcItANvD(ZEEqvg)L^v8CgvA9MW-%-Rwiy_$RoR}}
zE74_pu>e+gbYOjQxSib<+7`_yOu6ZUlRv8lShQGwKiA
zrEqK!F=Yv9=jUJc&^+Y#dMHYlcKRw(JZkBBFPOX79W*?K^NCwb7URLt49#k0l`-sr*
zK_&o|H3GPgHOje)R(!k~NJzrhF%1^RK}YjYk)J@M7Uz0~^0C3;HZ4-y!tb&P9OW}1
zWf6uX5M86sJorF-i%w<-%jithB)?=-i7pt!k0Uy
z%`nGa;pmSU1OOW#-~~n-k78gEvkiU$s{T%Q_kOy7H^OF{fE+C`kbPX#xP&XB@>O{Nt$eRWm+sbh*t
zTa%lE^nzCU^QIHcT#BltrB9~Fl&HzB$|!7s?K-gNY=Ho&>S)g+9a-mqU$1r}`N)-qm^&$H-8`m+Iiu6g4)u&sDX!X
zQku`ogM$EaSF$4@7|C3bUpe51dQ#^#Fkr}Wc(kP047I|IelyzyQr+O5r6I?`jK55q
zWYN%cWMC<)#o<&Wn7MMoE2X4YQ!u>ks=v_5Zf;UohQ&^K{!nY{!;IdGlZ4ZW=0r1R
z7DE%;gl}6wA=QfREbHjiiim}Kjoh#XGG+GT@h-A6;7MPYoORQ@?1_dc*?FT0|IKa3sJRWKkr(?w9rpl`br?^NOX`k&){c@BIP99LDc
z-O3j3ITWZp9E7Vf3UjcDSdJ1bqV}HfGy<%y3q>PtVzVhL4s7bFV@4B@D~ej{jdDi{
zl>@-O1O?6oIE&n6k`V7|!h#gI^v~Y{SmQobdb3AtC)-?c;$it(fUU&xhtLfoULcal
z*_2;T-k~0%YRV>A*0kqrq;iW6v}+5cktMu4=v)YF(_8p3)OHv8v(`zt1)U#ny<4{cM|tD-GE7&5Kazg33R%*bg$I
z7NZx)x(>3m+RU9xSuUP90Kx%^AU;vmn!0rwbbFF631dGCkq0!8ra=Y(wor#$M|A&8^zUpLMk2lQP`
z;{@YI;PnV2x{Ad2``p0EOx{Av70!2l!H6(xQ!fq8Eq=B_%hwV&J{xYHI=$)-)SQ?P
zMgSEnAZb807+MTH9lDbBY{;pIANtmUEzL&B4bz!;px9y7b{3b3_p-94dAFC}-`vz(
zIiBm1FW%^PD+^i~NVU+1orPq1^@CWeVztHJouxiUjOXgnh4j$E=bERr+OFz*n*8&a
zLqR$;XM1oAmUp;(e0Uj)+B`nJbV1ujI1kbW)U0Of8t{nCFq=jXCxv^Hl_Lv?P?&;H
z{k4JI-SqfbW<%Uac2d}Yp_TCbPEoZQVD(r=IY?q)HFdx*CElh6OV%ogJ!nSMt{{RS=kP{n7}tcs&nhtA_g6NDBx
z|Hvz3r)ezcCDB`AaGPHvX)gRrna)m=E;I1`e(0@{ic|Hxmizsq#Zt{KQCeR_2YiZ45y8M
zb1oSuG-=XFHsoOuX0X01fz)i;7#OC6a1Iv$J|*8X
zUfKl*6oiIt6Da^|#*p2m$VOsO;qXA_FjJ*zKFI>?)PMvBZ1kWbm^UVTnulmm*QLm7
z&%o_)G+6$S@tZZr!y`JCOeRYY$Y0f|XO;dbK#(<Kn4rXI
zRnUG0%&k@Qbx&syPV5m{5u-qETwQ_*>Wc&q$saE3!dWM==?4$Zxyg^YUuHxU`$$T9
zyaBvN)8o%1onU|_D;{at?k~}5Cq}U7
zkJW%fm$QJiW6ipxY!zoxP4_zEBu|oVp{p;JPMVJ0g?<@;{pCN>fx4c78OBs<0E)))
z*Lz>LS%`TD6dpW3Gsk=$>@8*gH*Q}0%*`*It|>c+lHM$?ooROk>XhL}*u^1>Sm3ag
zV8C1!=hYMbLydR51ND`_b1lZ-v1lbu;CaB^U|Dn4jmFK>s9Nc9*XFHY+6H*Z#`y!n
zooe9JPUKm<`wG$vgD;vjZwM1ykDum7zSEVnHOBI$IMJVCT9{~{t8>8Nf;z`3RT=g0
zk@HGO3uphKi<^`{0%kSrFdIXv3;g&aCna#W1{Nuzz2}Q5+N9!3^$DmB8r++H)rTer
zO?dnt_TD?53cd{(FDXY#N+Gf;NoJ9eb<|D6ES0@NWMuD^T^S{!j3^;n_Esbbkv&58
z-h02-QRfu#bU*L-eSYuzasTn$=l1YD-|w~eH6cOSAh2h@oi${Ia;6(uvvod~BzETy
z?i8rdu*Dl5p&
zboV~oE?YlZ$~qxi9}t%boeXRSXRl$&$wM!BYe2h4CBgnrC#fg
zAr3DQ@o$K5SGZeU%3D_L+w46qf8B~=Jm387iq?#J2Rqc2pJraHd&oE59Vm6-n2~2)
zICzRLnjyq-B*J^(U{Oh`Wr{}K^;L7hUA9Y7mub`Xj_p{2M38Ar{$>Fp)X_eY3(BX)
zH0p>M%-LgDp^>{$E86A8O&J=7oXN_rG~?zTi-~UMtdUGBs*tHsw%Z#V_5Xl`+m@kpq2R*fS&2RrI
zkFep+wsO_~SSLoQWd+^T`5>l{=DiT+IK3_NBdNC2**aNPcS?TSUVl{Gl>Z?Htm`n*?lE>1ot_P0q@`eB~n{b5qrV^RzZ|
zbHt=yrTCrTDbIktI$<%Q(L1agc*e1`$1)8=LFCO|x4IqhT&}yamrs3YqLyVf`c%d>
zvMkgSgo?aMhNl<$N5$%errfTJ@O-YIw7jS>G&PMfwk_rQB=r=(Y~FJE@PtGsmFCx&
zfJn1~y1UoMx~ncMPkbMx44!{Kas>-#>!L254LjlZXZO!DD5}G{XApCgq_rR;x@Df2RL7b_|LTwJ!?{mIq~{bNdd$7n-z`=B1N?`CYeVZPJl*j^z@
z-2?aettjl4vyI&>0{n6()x$(R@fcv_&G*6*It2nYoHvukS1MvA=f#}et~rQ>nS|{+
z3c1-xo2Q;i1DxsQJXK;K@VsZZq`YSS+a)S`n%A|T-`#qs8#^!QcxURoNlK|!
zN{@3*yG@Ptxgq~slEeOqM6tUC!`%-tPJ9IC5Q2Hy{$_SDNnVVgsAo#U8=uDO9Xnf_
z$!;<@xNJG`DXw^xXjeO#3O8?`S?y%{64L^m1qGJzGIsh2vnO(AhURleebchJVp8X|
zP*QU*;RzAMX@+wPUJY#@TU$wTjZ5F6szeSI=Ai51ple|Y6^>s>o{yo@j26_MfdnHz
zo}6w<&G((+JY)Z)4W4-xX=7sc%S5uO}
z7oa<0RO%P?qDq
z%H>b4t|@bmnIW2}SN)kDrYc0r$t!J!>lj;XD-
z`MfxAIKo=v7Gu{yfUMGI6N}qnHm_^F>NKE~dtCklsiZc2&?D7NJ6n+PV4<7dcAjtu
zp}R3SI!vB_CnA^>rHKtQw68$+OPJ-H{z*jR_RbZuqs$2ZYsF9iLTS6$F>#H_L{tEN
z0WVQP)Q1kyh%K8*dQ0iH-<E
zt^f_z6S0VvuZ;5*FRavO6#;3BmdYW6`TtFH8T)Kr5
z^_pc}brI=)Es}bC4JNUQuG!0p1|2h0aR!ST|6wcv0^0Xj#ot4oa>TlaJHMD!9KYkE
z$AHLtUiODnni|B=Ty4~H4AgHX#_t9D%VNTVuhowcvVu+*}VspMq$P#_>P_1jamFIy?Jn-DC@oeF1f9)$l
zUt=0`QW(e})kkZ~@k%?EaaA|%+%+*Ae*+>62RkgwuYEQ<;o1C&v?eM=(MpS4hSI{0
zLpBHBVoBXZeIflz7>rD)I^WWIzjEL|WC<;yw+Ki$V|K2vZMH)kA*EzvlD?9=;EkgI
zUSj@x9Lj^Kb_>(RGwBJauI$R!L5vO|L)GLhGn0J_F6GwUtSV1$+@!EQ+EWF#b6(p8
z%6a`+FczK|g;fcHr_9(M6;{Il_3GCrqJ(xxoaNRkctCRd;r4`a3CVs5I_i{cq*TB3
zSmwbJAEewB{+j-WVy8!+>Z01KdEB=b2&HC|g7TJhK8O^9MFmpFhHB0$=kfB?Rr*Ii{J6)u-VPmjeo*P|kFI4&LL#I+`Z-+`m
zXR9~Lt=dLki0d7XDK2`u6;Q627bQH8
zD0;)q_n1l(?~^2!CL>AsOQvy$n{+{8*jbsU5u`ormEWZvil=B_!%_`06}8~gt_ixP
zeb!Z|seN|LBicePr{|pdi>cv9s%hu$J>v*I*!l`unHPn5wRnw&q+Pw1dEZEB_XYk3
zGr$rh@ak?>TO$^Qkpz=UM4pIAZ@AV-Tq<0u&h)iP=*_23h)spbX+ej@f@fDG7JpGo
zItJ{L?K!e35#Ivld1^A5$hZr`k{^xt@PyTlTU}XjD{=_wI_hC_camF*zO&KO
zi%612a?9EOU}0IJT7HS8Y6212(B9R5nW`cfST84Q*dc^bu}R2LBK8QM%ePy~bi;CQ
z!>}hZxe%m#&!td!U>AZC1&Wfg*9
zKz`6O99|Gp2(TOokWvlpk?i)Fk|{|dA9$?wwB}~!$ZMs(^#4!>5y}YDiyG;bt+9p(
zp^rQ`_64qS5`c>%{>vvVLTvKqN@ZCW51fl?xZ^>1TI7)96@%8#j5HNBLy)%*4H~%0
z{Kn5iDHC!aSCD6zdu#FYv{b|E)qK0C^L;FQTfL{A4mIsZmd?keia*!O>7h4Kh?0?t
zj<}&~0R?K;qZvn*QKbTTlbkitlP}C6-9N5vy?F=q#mt&bHQHqAi`*<
zo*yuFTl!#kY^o$lc6uyRlnGwA7MxS%fgig#*C^3nUx
zh+k=|kICdr&5LA&w=9MA++v|;4m{LDx#FQ#^6}1zkfz-$2a|lXukB5otIf*YUl)@q
zg!Qt)KWvv@2g%fYdYvIR|(=C|e9v5L(#
z!;Cq(lj)5qZyCG%Wr2n?{diT!(i@4I!$kwwPubS=f8Bfp$X~}Ghzv90QK&Brek2id
zm)k>G$hgZZF?NzuJBcy_X%P}rOF9Z2ndN$YtlyZjboOXVWbBgNJbs0ZNCuWoR!E$u
zVVMU$3C-Jeii(94_335H^V2&8xI1(XZmREp7B@(o1QYB$qu(uDw&MVE0zWt0Np~Qd
zat8Inf3OAUFR}e){0yAq5b|O>Rt`P=OG?zfdt@z;n&1LhC9Ny2d
zB2ki93e<4Kp^^{;7CEZVoztRd*Fo$dk`(*@1OwFdkOp9WxFy{@MN@&ATpD^Sj<$jf
zB{|-D*4(B>$chU7hY=D#0MTm%aNx7sCXyF{;t@*=QBR~O{K%ipnIBG=rD5ANLW{0(
zhZKWAMQuR87cvccnN3Tsym3Toz+v12&Bb5gUy-^Df5ip?9O1y=a|V|U1q*C{ImFMY
zKDRvR0IK)CZeE@Gv(0?fuS3lZvQj`vcB%SB#C6mVgW*cNSb#{v!|qKdE}$_j3+`Rx
zEs5s+t0_h%**#$dy&(WsoVVOOK2(~n@Aq!r{rK}14?*q9I^DQ-%)T2?6JTUSZ2t=E
zOCp<{+I7qb*`|+&{|1T*%)l4)2$)MD58)Drd?@Z5z#j5)X50JMBTzkrH?{iRkY+%V
zrgfHYT#ylP9d4TDIRp5Y-raxxdc?0`MCkcM5}I)8d1j^N_MHL))5>3D_u>ZHDj?y{l8D4(iRsOw(tcipt^=;v2VK7M!M
z5${=oddrutx7AL;t{(f-s=m9UcU=jrC8mUi+W{^zNwy9D-DI%EiN2jGEv$H$$zZQ>OI-Yz
zIBc(LarZ(@__@Xlu2ZUHKFB;gkzhXF(m)HtJl4s<#hn6Hrguc${{|yA<^<(7Bb6`w
zJ(wr_8lPtRzhivbyHD8Y*<;5BJ~YF0Gmq7gjWtrTw$}$_jLK39png#3=Wsbxjs%Bd
zRxVF8E54f^(I{1-o49EW!%tB|rJy{Vtx@o8t~|W;`xO{&CaXOD!9ZpFwLz$Chx_4+
zu24l_dtIA_)83YF_FE4hnJ3f<*any?l|jxW-nU8Zuc9~e-4kL(L=t82Kx{~#THNu9
z6s$4bkmxv0E_WHN$g8mi5Qs#D^~P^jTvgcVIOC19ChH9{TRtuFf+r(5yxGhMrh8@4
zaH#x^j~+9z*u8%>+WAeRQcs@-HEJ#)zY|xbefF)w)h4c{!MC3Z=9CeB^w`TRXM0|%Hlt3F
z60axNPC%+Q9O{~h>uqM5^JO$_;(Mzk>`-RdQJ<2{u8vkLK=2w~rBmWMY;{I}U#
zf~;XUehGnLH(Q-x&
z`3N-pTsk_WXLpe*ne;I`mI?N)ZN@Md0Uu@rNIc`QFbtv(>c0W#Vf!-++>ed2u#^)2t*yUV<
zOdBI_G!H~<%`V@J^!^-nvo)FdPnc}_ldZ|jXrBJ7Jl?i7-T!Yg`8P8Azj)ok_mw2G
z9u>ozCa;RbD}ziV1d!ld{Ie568rxmB??}HVbid~8hZ@BN&B4KlU{nm
z^w3VPgky;hLkKP&Bq(;4Z@7#!XCioFT$$D~9hn+EFyx&bgl|zEDb6c?O`+%djo_Jy
z0scJkLu-I~{1g?osIk81f3-luVV>+a$p6hhKx}=-UqOVi{Xu`%_rwD^AJMkA4gD_>>f(aalU?^=z8)&X!`~zj^)W*HTH@$Hjep
z868b;Y-Z;C^;Ly@5znhS8f@~bU#2+*?l=YWx{kFpu?d|%PXZ0G}w@aQca_=K@GM%T)C#Yk_Cday+UQ#Ia
zj#)N2eMI}a>jH)1U!#y1K<>gjs6@;#{d0
zqq)J$ct^_cAisZ0?e9MAKBz><4{!3aksR@Lx<_D4qAHuNZy^1xiq&1p68GN&hYJg^
zRFH0WsHI~KXpo-gCxH&S
zOm~fj&6Bq4-!(<}6Sh)$j$B{w<01r(`Q&)SJKMF?OBIs^D{qrqVsU-I?Qa&A;=swOZ;|DzJs$$UuBq=M*Y7R
z>{CP}yq3n#I923x_VpPT5D_>C^}eucE39a0BKt2Nb~RMU+3p!^~y+Z~6DJJ7Dnn)f;?o0FfuKJ-+{I(A%qlY%O+=8W(U5
z*?)#w6T66Lb9AqT;PGSNp5MOybl)j#kHArbziZ%$EO-$LnT(B4An-~HPyE?uuB
z|CCZ&T>4iPx+Qr3ZuKCybm?D#w53beS|u9^r!DhnGll;PT3hDPCKl&!+HK1``v1y2
z%8}HKLeB!&a%eVI0$Uur#jzOOw`D5*J#4XMDq+ZF(+^vw(w3=&;iN4ebz{x@yA||*
zM(`HzcBkMyHMvA~_LS4dypJn?pL!ex@$Z^q
z#)WU^EAD%(xm?IJDifLC`!yGlJMCzQT!&G(dzi3QLHi4u;=nqAU)lRT2MHYTt3M5B
zP;08aSQBTqm&KbsULpIasBK-0$KGkD^%>3l0wd#}-+^?6^uxr`4tU;e`Zdpc6N?+*9uf?(AI9HjMkS~*2&DsrO8VbSNrIra;@ZnJ?sg|{BDbct)-CE##{9vL
z56E3uIUd^5$!};K=?!o(|7Xx~D(o_|#`6`QbRg#cTaqE=VDB&~c_!(~w{h
z2TKw!h<614!ifEK$Q?wAM9p)-Ijn1P2S~=9)})$@MeP!pul;!^7c9O*Yy~ef(Q6)&
z-HD?7~
z7pwEsn*WHcKW}*WV_p8E=@gg)@BYV$4Z9g+8fPtMDU|jF58YEB`ANo9tU&p8HR+vV5t@vJ44Q?X72u%Kd|}fN=RB72ZL<(@n~(s(-=_9K{|XQ
zD3*ZjjNDqN4Pk%u9Kt_S(dbdQzVFW7edj}FXC**p2fgtf%u!rG8bhpb^-puYlO9E%sT-t55YzY`$dMf`g9p&;`y9TB^{`s8Izm8L!e97&
zMYxj{nw#to0@2cm6dy+a^3VI(e}kWAh*nRbLn7mc2pqb(0YPeTmEZDA_Kk2~ZH@yt
z7?JuD-sg}=n9y*+^a1)r5jgSrp2W^GP9ZcW%V?-ICG#<@-_x?{le;{)<8
z{QTRAeoHiT{sXNzg`|)@_=Aqgsi?4r?onr<+v|`g;6#yKGKPoGR~+!^enOB;*M^x{
zLbVexxLeXt2n`-I)pNkl?-H8uqTgZ-)G_x3IP^J??Qc2F5K}n4(c{A(RH3DADwo0o6KihU@@RAZJnYyY%yA
zpZ