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) && ( -
    -

    Qualified

    -
    - )} -
  • -
  • - -
    - -

    Coinbase One verification

    - -
    -
    - {allActiveDiscounts.has(Discount.CB1) && ( -
    -

    Qualified

    -
    - )} -
  • -
  • - -
    - -

    A cb.id username

    - -
    -
    - {allActiveDiscounts.has(Discount.CBID) && ( -
    -

    Qualified

    -
    - )} -
  • -
  • - -
    - -

    Base buildathon participant

    - -
    -
    - {allActiveDiscounts.has(Discount.BASE_BUILDATHON_PARTICIPANT) && ( -
    -

    Qualified

    -
    - )} -
  • -
  • - -
    - -

    Builder score 50+

    - -
    -
    - {allActiveDiscounts.has(Discount.TALENT_PROTOCOL) && ( -
    -

    Qualified

    -
    - )} -
  • -
  • - -
    - -

    Summer Pass Level 3

    - -
    -
    - {allActiveDiscounts.has(Discount.SUMMER_PASS_LVL_3) && ( -
    -

    Qualified

    -
    - )} -
  • -
  • - -
    - -

    BNS username

    - -
    -
    - {allActiveDiscounts.has(Discount.BNS_NAME) && ( -
    -

    Qualified

    -
    - )} -
  • -
  • - -
    - -

    Base.eth NFT

    - -
    -
    - {allActiveDiscounts.has(Discount.BASE_DOT_ETH_NFT) && ( -
    -

    Qualified

    -
    - )} -
  • + {DISCOUNT_ITEMS.map(({ discount, icon, alt, label, tooltipContent }) => ( +
  • + +
    + +

    + {label} +

    + +
    +
    + {allActiveDiscounts.has(discount) && ( +
    +

    Qualified

    +
    + )} +
  • + ))}
{!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 ( +